Spaces:
Running
Running
import { Component, Inject, OnInit } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray, FormsModule } from '@angular/forms'; | |
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule, MatDialog } from '@angular/material/dialog'; | |
import { MatTabsModule } from '@angular/material/tabs'; | |
import { MatFormFieldModule } from '@angular/material/form-field'; | |
import { MatInputModule } from '@angular/material/input'; | |
import { MatSelectModule } from '@angular/material/select'; | |
import { MatCheckboxModule } from '@angular/material/checkbox'; | |
import { MatButtonModule } from '@angular/material/button'; | |
import { MatIconModule } from '@angular/material/icon'; | |
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; | |
import { MatTableModule } from '@angular/material/table'; | |
import { MatChipsModule } from '@angular/material/chips'; | |
import { MatExpansionModule } from '@angular/material/expansion'; | |
import { MatDividerModule } from '@angular/material/divider'; | |
import { MatProgressBarModule } from '@angular/material/progress-bar'; | |
import { MatListModule } from '@angular/material/list'; | |
import { ApiService, Project, Version } from '../../services/api.service'; | |
import ConfirmDialogComponent from '../confirm-dialog/confirm-dialog.component'; | |
({ | |
selector: 'app-version-edit-dialog', | |
standalone: true, | |
imports: [ | |
CommonModule, | |
ReactiveFormsModule, | |
FormsModule, // BU SATIRI EKLE | |
MatDialogModule, | |
MatTabsModule, | |
MatFormFieldModule, | |
MatInputModule, | |
MatSelectModule, | |
MatCheckboxModule, | |
MatButtonModule, | |
MatIconModule, | |
MatSnackBarModule, | |
MatTableModule, | |
MatChipsModule, | |
MatExpansionModule, | |
MatDividerModule, | |
MatProgressBarModule, | |
MatListModule | |
], | |
templateUrl: './version-edit-dialog.component.html', | |
styleUrls: ['./version-edit-dialog.component.scss'] | |
}) | |
export default class VersionEditDialogComponent implements OnInit { | |
project: Project; | |
versions: Version[] = []; | |
selectedVersion: Version | null = null; | |
versionForm!: FormGroup; | |
loading = false; | |
saving = false; | |
publishing = false; | |
creating = false; | |
selectedTabIndex = 0; | |
testUserMessage = ''; | |
testResult: any = null; | |
testing = false; | |
constructor( | |
private fb: FormBuilder, | |
private apiService: ApiService, | |
private snackBar: MatSnackBar, | |
private dialog: MatDialog, | |
public dialogRef: MatDialogRef<VersionEditDialogComponent>, | |
public data: any (MAT_DIALOG_DATA) | |
) { | |
this.project = data.project; | |
this.versions = [...this.project.versions].sort((a, b) => b.id - a.id); | |
} | |
ngOnInit() { | |
this.initializeForm(); | |
// Select the latest unpublished version or the latest version | |
const unpublished = this.versions.find(v => !v.published); | |
this.selectedVersion = unpublished || this.versions[0] || null; | |
if (this.selectedVersion) { | |
this.loadVersion(this.selectedVersion); | |
} | |
} | |
initializeForm() { | |
this.versionForm = this.fb.group({ | |
id: [{value: '', disabled: true}], | |
caption: ['', Validators.required], | |
published: [{value: false, disabled: true}], | |
general_prompt: ['', Validators.required], | |
llm: this.fb.group({ | |
repo_id: ['', Validators.required], | |
generation_config: this.fb.group({ | |
max_new_tokens: [256, [Validators.required, Validators.min(1), Validators.max(2048)]], | |
temperature: [0.2, [Validators.required, Validators.min(0), Validators.max(2)]], | |
top_p: [0.8, [Validators.required, Validators.min(0), Validators.max(1)]], | |
repetition_penalty: [1.1, [Validators.required, Validators.min(1), Validators.max(2)]] | |
}), | |
use_fine_tune: [false], | |
fine_tune_zip: [''] | |
}), | |
intents: this.fb.array([]), | |
last_update_date: [''] | |
}); | |
// Watch for fine-tune toggle | |
this.versionForm.get('llm.use_fine_tune')?.valueChanges.subscribe(useFineTune => { | |
const fineTuneControl = this.versionForm.get('llm.fine_tune_zip'); | |
if (useFineTune) { | |
fineTuneControl?.setValidators([Validators.required]); | |
} else { | |
fineTuneControl?.clearValidators(); | |
fineTuneControl?.setValue(''); | |
} | |
fineTuneControl?.updateValueAndValidity(); | |
}); | |
} | |
loadVersion(version: Version) { | |
this.selectedVersion = version; | |
// Reset form to initial state | |
this.initializeForm(); | |
// Populate basic fields | |
this.versionForm.patchValue({ | |
id: version.id, | |
caption: version.caption, | |
published: version.published, | |
general_prompt: version.general_prompt || '', | |
llm: version.llm || { | |
repo_id: '', | |
generation_config: { | |
max_new_tokens: 256, | |
temperature: 0.2, | |
top_p: 0.8, | |
repetition_penalty: 1.1 | |
}, | |
use_fine_tune: false, | |
fine_tune_zip: '' | |
}, | |
last_update_date: version.last_update_date || '' | |
}); | |
// Populate intents | |
const intentsArray = this.versionForm.get('intents') as FormArray; | |
if (version.intents) { | |
version.intents.forEach(intent => { | |
const intentFormGroup = this.createIntentFormGroup(intent); | |
// Parameters'ı ayrı olarak ekle | |
if (intent.parameters) { | |
this.populateIntentParameters(intentFormGroup, intent.parameters); | |
} | |
intentsArray.push(intentFormGroup); | |
}); | |
} | |
// Enable/disable form based on published status | |
if (version.published) { | |
this.versionForm.disable(); | |
this.snackBar.open('This version is published and cannot be edited', 'OK', { duration: 3000 }); | |
} else { | |
this.versionForm.enable(); | |
this.versionForm.get('id')?.disable(); | |
this.versionForm.get('published')?.disable(); | |
} | |
} | |
createIntentFormGroup(intent: any = {}): FormGroup { | |
return this.fb.group({ | |
name: [intent.name || '', [Validators.required, Validators.pattern(/^[a-zA-Z0-9-]+$/)]], | |
caption: [intent.caption || ''], | |
locale: [intent.locale || 'tr-TR'], | |
detection_prompt: [intent.detection_prompt || '', Validators.required], | |
examples: this.fb.array(intent.examples || []), | |
parameters: this.fb.array([]), // Boş array ile başla | |
action: [intent.action || '', Validators.required], | |
fallback_timeout_prompt: [intent.fallback_timeout_prompt || ''], | |
fallback_error_prompt: [intent.fallback_error_prompt || ''] | |
}); | |
} | |
private populateIntentParameters(intentFormGroup: FormGroup, parameters: any[]) { | |
const parametersArray = intentFormGroup.get('parameters') as FormArray; | |
parameters.forEach(param => { | |
parametersArray.push(this.createParameterFormGroup(param)); | |
}); | |
} | |
createParameterFormGroup(param: any = {}): FormGroup { | |
return this.fb.group({ | |
name: [param.name || '', [Validators.required, Validators.pattern(/^[a-zA-Z0-9_]+$/)]], | |
caption: [param.caption || ''], | |
type: [param.type || 'str', Validators.required], | |
required: [param.required !== false], | |
variable_name: [param.variable_name || '', Validators.required], | |
extraction_prompt: [param.extraction_prompt || ''], | |
validation_regex: [param.validation_regex || ''], | |
invalid_prompt: [param.invalid_prompt || ''], | |
type_error_prompt: [param.type_error_prompt || ''] | |
}); | |
} | |
get intents() { | |
return this.versionForm.get('intents') as FormArray; | |
} | |
getIntentParameters(intentIndex: number): FormArray { | |
return this.intents.at(intentIndex).get('parameters') as FormArray; | |
} | |
getIntentExamples(intentIndex: number): FormArray { | |
return this.intents.at(intentIndex).get('examples') as FormArray; | |
} | |
addIntent() { | |
this.intents.push(this.createIntentFormGroup()); | |
} | |
removeIntent(index: number) { | |
const intent = this.intents.at(index).value; | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '400px', | |
data: { | |
title: 'Delete Intent', | |
message: `Are you sure you want to delete intent "${intent.name}"?`, | |
confirmText: 'Delete', | |
confirmColor: 'warn' | |
} | |
}); | |
dialogRef.afterClosed().subscribe(confirmed => { | |
if (confirmed) { | |
this.intents.removeAt(index); | |
} | |
}); | |
} | |
async editIntent(intentIndex: number) { | |
const { default: IntentEditDialogComponent } = await import('../intent-edit-dialog/intent-edit-dialog.component'); | |
const intent = this.intents.at(intentIndex); | |
const dialogRef = this.dialog.open(IntentEditDialogComponent, { | |
width: '90vw', | |
maxWidth: '1000px', | |
data: { | |
intent: intent.value, | |
project: this.project, | |
apis: await this.getAvailableAPIs() | |
} | |
}); | |
dialogRef.afterClosed().subscribe(result => { | |
if (result) { | |
// Update the intent in the form array | |
intent.patchValue(result); | |
} | |
}); | |
} | |
addParameter(intentIndex: number) { | |
const parameters = this.getIntentParameters(intentIndex); | |
parameters.push(this.createParameterFormGroup()); | |
} | |
removeParameter(intentIndex: number, paramIndex: number) { | |
const parameters = this.getIntentParameters(intentIndex); | |
parameters.removeAt(paramIndex); | |
} | |
addExample(intentIndex: number, example: string) { | |
if (example.trim()) { | |
const examples = this.getIntentExamples(intentIndex); | |
examples.push(this.fb.control(example)); | |
} | |
} | |
removeExample(intentIndex: number, exampleIndex: number) { | |
const examples = this.getIntentExamples(intentIndex); | |
examples.removeAt(exampleIndex); | |
} | |
async createVersion() { | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '500px', | |
data: { | |
title: 'Create New Version', | |
message: 'Which version would you like to use as a base for the new version?', | |
confirmText: 'Create', | |
showVersionSelect: true, | |
versions: this.versions | |
} | |
}); | |
dialogRef.afterClosed().subscribe(async (sourceVersionId) => { | |
if (sourceVersionId) { | |
this.creating = true; | |
try { | |
const result = await this.apiService.createVersion(this.project.id, { | |
source_version_id: sourceVersionId, | |
caption: 'New Version' | |
}).toPromise(); | |
this.snackBar.open('Version created successfully', 'Close', { duration: 3000 }); | |
// Reload project to get new version | |
await this.reloadProject(); | |
// Select the new version | |
const newVersion = this.versions.find(v => v.id === result.id); | |
if (newVersion) { | |
this.loadVersion(newVersion); | |
} | |
} catch (error: any) { | |
this.snackBar.open(error.error?.detail || 'Failed to create version', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} finally { | |
this.creating = false; | |
} | |
} | |
}); | |
} | |
async saveVersion() { | |
if (this.versionForm.invalid || !this.selectedVersion) { | |
this.snackBar.open('Please fix all validation errors', 'Close', { duration: 3000 }); | |
return; | |
} | |
this.saving = true; | |
// updateData'yı try bloğunun dışında tanımla | |
const formValue = this.versionForm.getRawValue(); | |
const intents = formValue.intents.map((intent: any) => ({ | |
...intent, | |
examples: intent.examples || [] | |
})); | |
const updateData = { | |
caption: formValue.caption, | |
general_prompt: formValue.general_prompt, | |
llm: formValue.llm, | |
intents: intents, | |
last_update_date: formValue.last_update_date | |
}; | |
try { | |
await this.apiService.updateVersion( | |
this.project.id, | |
this.selectedVersion.id, | |
updateData | |
).toPromise(); | |
this.snackBar.open('Version saved successfully', 'Close', { duration: 3000 }); | |
// Update last_update_date for next save | |
this.versionForm.patchValue({ | |
last_update_date: new Date().toISOString() | |
}); | |
} catch (error: any) { | |
if (error.status === 409 || error.requiresReload) { | |
// Race condition detected | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '500px', | |
data: { | |
title: 'Version Modified', | |
message: 'This version was modified by another user. Do you want to reload and lose your changes, or save anyway?', | |
confirmText: 'Reload', | |
cancelText: 'Save Anyway', | |
confirmColor: 'primary' | |
} | |
}); | |
dialogRef.afterClosed().subscribe(async (shouldReload) => { | |
if (shouldReload) { | |
// Reload version | |
await this.reloadProject(); | |
if (this.selectedVersion) { | |
const updated = this.versions.find(v => v.id === this.selectedVersion!.id); | |
if (updated) { | |
this.loadVersion(updated); | |
} | |
} | |
} else { | |
// Force save by removing last_update_date check | |
const forceUpdateData = { ...updateData }; // updateData artık erişilebilir | |
delete forceUpdateData.last_update_date; | |
try { | |
await this.apiService.updateVersion( | |
this.project.id, | |
this.selectedVersion!.id, | |
forceUpdateData, | |
true // force parameter | |
).toPromise(); | |
this.snackBar.open('Version saved (forced)', 'Close', { duration: 3000 }); | |
await this.reloadProject(); | |
} catch (err: any) { | |
this.snackBar.open(err.error?.detail || 'Failed to save version', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
}); | |
} else { | |
this.snackBar.open(error.error?.detail || 'Failed to save version', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} finally { | |
this.saving = false; | |
} | |
} | |
async publishVersion() { | |
if (!this.selectedVersion) return; | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '500px', | |
data: { | |
title: 'Publish Version', | |
message: `Are you sure you want to publish version "${this.selectedVersion.caption}"? This will unpublish all other versions.`, | |
confirmText: 'Publish', | |
confirmColor: 'primary' | |
} | |
}); | |
dialogRef.afterClosed().subscribe(async (confirmed) => { | |
if (confirmed && this.selectedVersion) { | |
this.publishing = true; | |
try { | |
await this.apiService.publishVersion( | |
this.project.id, | |
this.selectedVersion.id | |
).toPromise(); | |
this.snackBar.open('Version published successfully', 'Close', { duration: 3000 }); | |
// Reload to get updated data | |
await this.reloadProject(); | |
} catch (error: any) { | |
this.snackBar.open(error.error?.detail || 'Failed to publish version', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} finally { | |
this.publishing = false; | |
} | |
} | |
}); | |
} | |
async deleteVersion() { | |
if (!this.selectedVersion || this.selectedVersion.published) return; | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '400px', | |
data: { | |
title: 'Delete Version', | |
message: `Are you sure you want to delete version "${this.selectedVersion.caption}"?`, | |
confirmText: 'Delete', | |
confirmColor: 'warn' | |
} | |
}); | |
dialogRef.afterClosed().subscribe(async (confirmed) => { | |
if (confirmed && this.selectedVersion) { | |
try { | |
await this.apiService.deleteVersion( | |
this.project.id, | |
this.selectedVersion.id | |
).toPromise(); | |
this.snackBar.open('Version deleted successfully', 'Close', { duration: 3000 }); | |
// Reload and select another version | |
await this.reloadProject(); | |
if (this.versions.length > 0) { | |
this.loadVersion(this.versions[0]); | |
} else { | |
this.selectedVersion = null; | |
} | |
} catch (error: any) { | |
this.snackBar.open(error.error?.detail || 'Failed to delete version', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
}); | |
} | |
async testIntentDetection() { | |
if (!this.testUserMessage.trim()) { | |
this.snackBar.open('Please enter a test message', 'Close', { duration: 3000 }); | |
return; | |
} | |
this.testing = true; | |
this.testResult = null; | |
// Simulate intent detection test | |
setTimeout(() => { | |
// This is a mock - in real implementation, this would call the Spark service | |
const intents = this.versionForm.get('intents')?.value || []; | |
// Simple matching for demo | |
let detectedIntent = null; | |
let confidence = 0; | |
for (const intent of intents) { | |
for (const example of intent.examples || []) { | |
if (this.testUserMessage.toLowerCase().includes(example.toLowerCase())) { | |
detectedIntent = intent.name; | |
confidence = 0.95; | |
break; | |
} | |
} | |
if (detectedIntent) break; | |
} | |
// Random detection for demo | |
if (!detectedIntent && intents.length > 0) { | |
const randomIntent = intents[Math.floor(Math.random() * intents.length)]; | |
detectedIntent = randomIntent.name; | |
confidence = 0.65; | |
} | |
this.testResult = { | |
success: true, | |
intent: detectedIntent, | |
confidence: confidence, | |
parameters: detectedIntent ? this.extractTestParameters(detectedIntent) : [] | |
}; | |
this.testing = false; | |
}, 1500); | |
} | |
private extractTestParameters(intentName: string): any[] { | |
// Mock parameter extraction | |
const intent = this.intents.value.find((i: any) => i.name === intentName); | |
if (!intent) return []; | |
return intent.parameters.map((param: any) => ({ | |
name: param.name, | |
value: param.type === 'date' ? '2025-06-15' : 'test_value', | |
extracted: Math.random() > 0.3 | |
})); | |
} | |
async getAvailableAPIs(): Promise<any[]> { | |
try { | |
return await this.apiService.getAPIs().toPromise() || []; | |
} catch { | |
return []; | |
} | |
} | |
private async reloadProject() { | |
this.loading = true; | |
try { | |
const projects = await this.apiService.getProjects().toPromise() || []; | |
const updatedProject = projects.find(p => p.id === this.project.id); | |
if (updatedProject) { | |
this.project = updatedProject; | |
this.versions = [...updatedProject.versions].sort((a, b) => b.id - a.id); | |
} | |
} catch (error) { | |
console.error('Failed to reload project:', error); | |
} finally { | |
this.loading = false; | |
} | |
} | |
compareVersions() { | |
// TODO: Implement version comparison | |
this.snackBar.open('Version comparison coming soon', 'Close', { duration: 3000 }); | |
} | |
close() { | |
this.dialogRef.close(true); | |
} | |
} |