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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatBadgeModule } from '@angular/material/badge'; import { ApiService, Project, Version } from '../../services/api.service'; import { LocaleManagerService } from '../../services/locale-manager.service'; import ConfirmDialogComponent from '../confirm-dialog/confirm-dialog.component'; // Interfaces for multi-language support interface LocalizedExample { locale_code: string; example: string; } interface LocalizedCaption { locale_code: string; caption: string; } interface Locale { code: string; name: string; } @Component({ selector: 'app-version-edit-dialog', standalone: true, imports: [ CommonModule, ReactiveFormsModule, FormsModule, MatDialogModule, MatTabsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatCheckboxModule, MatButtonModule, MatIconModule, MatSnackBarModule, MatTableModule, MatChipsModule, MatExpansionModule, MatDividerModule, MatProgressBarModule, MatListModule, MatProgressSpinnerModule, MatBadgeModule ], 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; isDirty = false; testing = false; selectedTabIndex = 0; testUserMessage = ''; testResult: any = null; // Multi-language support selectedExampleLocale: string = 'tr'; availableLocales: Locale[] = []; constructor( private fb: FormBuilder, private apiService: ApiService, private localeService: LocaleManagerService, private snackBar: MatSnackBar, private dialog: MatDialog, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any ) { this.project = data.project; this.versions = [...this.project.versions].sort((a, b) => b.no - a.no); this.selectedExampleLocale = this.project.default_locale || 'tr'; } ngOnInit() { this.initializeForm(); this.loadAvailableLocales(); // 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); } this.versionForm.valueChanges.subscribe(() => { this.isDirty = true; }); } initializeForm() { this.versionForm = this.fb.group({ no: [{value: '', disabled: true}], caption: ['', Validators.required], published: [{value: false, disabled: true}], general_prompt: ['', Validators.required], welcome_prompt: [''], // Added welcome_prompt field 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(); }); } async loadAvailableLocales() { // Get supported locales from project const supportedCodes = [ this.project.default_locale, ...(this.project.supported_locales || []) ].filter(Boolean); // Get locale details for (const code of supportedCodes) { if (!code) continue; // Skip undefined/null values try { const localeInfo = await this.localeService.getLocaleDetails(code).toPromise(); if (localeInfo) { this.availableLocales.push({ code: localeInfo.code, name: localeInfo.name }); } } catch (error) { // Use fallback for known locales const fallbackNames: { [key: string]: string } = { 'tr': 'Türkçe', 'en': 'English', 'de': 'Deutsch', 'fr': 'Français', 'es': 'Español' }; if (code && fallbackNames[code]) { this.availableLocales.push({ code: code, name: fallbackNames[code] }); } } } } getAvailableLocales(): Locale[] { return this.availableLocales; } getLocaleName(localeCode: string): string { const locale = this.availableLocales.find(l => l.code === localeCode); return locale?.name || localeCode; } loadVersion(version: Version) { this.selectedVersion = version; // Debug published status console.log('Loading version:', version.no, 'Published:', version.published); // Form değerlerini set et this.versionForm.patchValue({ no: version.no, caption: version.caption || '', published: version.published || false, general_prompt: (version as any).general_prompt || '', welcome_prompt: (version as any).welcome_prompt || '', // Added welcome_prompt last_update_date: version.last_update_date || '' }); // LLM config'i ayrı set et if ((version as any).llm) { this.versionForm.patchValue({ llm: { repo_id: (version as any).llm.repo_id || '', generation_config: (version as any).llm.generation_config || { max_new_tokens: 512, temperature: 0.7, top_p: 0.95, repetition_penalty: 1.1 }, use_fine_tune: (version as any).llm.use_fine_tune || false, fine_tune_zip: (version as any).llm.fine_tune_zip || '' } }); } // Clear and rebuild intents this.intents.clear(); ((version as any).intents || []).forEach((intent: any) => { this.intents.push(this.createIntentFormGroup(intent)); }); this.isDirty = false; } async loadVersions() { this.loading = true; try { const project = await this.apiService.getProject(this.project.id).toPromise(); if (project) { this.project = project; this.versions = [...project.versions].sort((a, b) => b.no - a.no); // Re-select current version if it still exists if (this.selectedVersion) { const currentVersion = this.versions.find(v => v.no === this.selectedVersion!.no); if (currentVersion) { this.loadVersion(currentVersion); } else if (this.versions.length > 0) { this.loadVersion(this.versions[0]); } } else if (this.versions.length > 0) { this.loadVersion(this.versions[0]); } } } catch (error) { this.snackBar.open('Failed to reload versions', 'Close', { duration: 3000 }); } finally { this.loading = false; } } createIntentFormGroup(intent: any = {}): FormGroup { const group = this.fb.group({ name: [intent.name || '', [Validators.required, Validators.pattern(/^[a-zA-Z0-9-]+$/)]], caption: [intent.caption || ''], detection_prompt: [intent.detection_prompt || '', Validators.required], examples: [intent.examples || []], // Store as array, not FormArray parameters: this.fb.array([]), action: [intent.action || '', Validators.required], fallback_timeout_prompt: [intent.fallback_timeout_prompt || ''], fallback_error_prompt: [intent.fallback_error_prompt || ''] }); // Parameters'ı ayrı olarak ekle if (intent.parameters && Array.isArray(intent.parameters)) { const parametersArray = group.get('parameters') as FormArray; intent.parameters.forEach((param: any) => { parametersArray.push(this.createParameterFormGroup(param)); }); } return group; } 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; } // LocalizedExample support methods getLocalizedExamples(examples: any[], locale: string): LocalizedExample[] { if (!examples || !Array.isArray(examples)) return []; // Check if examples are in new format if (examples.length > 0 && typeof examples[0] === 'object' && 'locale_code' in examples[0]) { return examples.filter(ex => ex.locale_code === locale); } // Old format - convert to new if (typeof examples[0] === 'string') { return examples.map(ex => ({ locale_code: locale, example: ex })); } return []; } getParameterCaptionDisplay(captions: LocalizedCaption[]): string { if (!captions || !Array.isArray(captions) || captions.length === 0) { return '(No caption)'; } // Try to find caption for selected locale const selectedCaption = captions.find(c => c.locale_code === this.selectedExampleLocale); if (selectedCaption) return selectedCaption.caption; // Try default locale const defaultCaption = captions.find(c => c.locale_code === this.project.default_locale); if (defaultCaption) return defaultCaption.caption; // Return first available caption return captions[0].caption; } addLocalizedExample(intentIndex: number, example: string) { if (!example.trim()) return; const intent = this.intents.at(intentIndex); const currentExamples = intent.get('examples')?.value || []; // Check if already exists const exists = currentExamples.some((ex: any) => ex.locale_code === this.selectedExampleLocale && ex.example === example.trim() ); if (!exists) { const newExamples = [...currentExamples, { locale_code: this.selectedExampleLocale, example: example.trim() }]; intent.patchValue({ examples: newExamples }); this.isDirty = true; } } removeLocalizedExample(intentIndex: number, exampleToRemove: LocalizedExample) { const intent = this.intents.at(intentIndex); const currentExamples = intent.get('examples')?.value || []; const newExamples = currentExamples.filter((ex: any) => !(ex.locale_code === exampleToRemove.locale_code && ex.example === exampleToRemove.example) ); intent.patchValue({ examples: newExamples }); this.isDirty = true; } addParameter(intentIndex: number) { const parameters = this.getIntentParameters(intentIndex); parameters.push(this.createParameterFormGroup()); this.isDirty = true; } removeParameter(intentIndex: number, paramIndex: number) { const parameters = this.getIntentParameters(intentIndex); parameters.removeAt(paramIndex); this.isDirty = true; } // Check if version can be edited get canEdit(): boolean { const canEditResult = !this.selectedVersion?.published; console.log('Can edit check:', 'Version:', this.selectedVersion?.no, 'Published:', this.selectedVersion?.published, 'Result:', canEditResult); return canEditResult; } addIntent() { this.intents.push(this.createIntentFormGroup()); this.isDirty = true; } 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); this.isDirty = true; } }); } async editIntent(intentIndex: number) { const { default: IntentEditDialogComponent } = await import('../intent-edit-dialog/intent-edit-dialog.component'); const intent = this.intents.at(intentIndex); const currentValue = intent.value; // Intent verilerini dialog'a gönder const dialogRef = this.dialog.open(IntentEditDialogComponent, { width: '90vw', maxWidth: '1000px', data: { intent: { ...currentValue, examples: currentValue.examples || [], parameters: currentValue.parameters || [] }, project: this.project, apis: await this.getAvailableAPIs() } }); dialogRef.afterClosed().subscribe(result => { if (result) { // Update intent with result intent.patchValue({ name: result.name, caption: result.caption, detection_prompt: result.detection_prompt, examples: result.examples || [], action: result.action, fallback_timeout_prompt: result.fallback_timeout_prompt, fallback_error_prompt: result.fallback_error_prompt }); // Update parameters const parametersArray = intent.get('parameters') as FormArray; parametersArray.clear(); (result.parameters || []).forEach((param: any) => { parametersArray.push(this.createParameterFormGroup(param)); }); this.isDirty = true; } }); } async getAvailableAPIs(): Promise { try { return await this.apiService.getAPIs().toPromise() || []; } catch { return []; } } async createVersion() { const publishedVersions = this.versions.filter(v => v.published); const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '500px', data: { title: 'Create New Version', message: 'Which published version would you like to use as a base for the new version?', showDropdown: true, dropdownOptions: publishedVersions.map(v => ({ value: v.no, label: `Version ${v.no} - ${v.caption || 'No description'}` })), dropdownPlaceholder: 'Select published version (or leave empty for blank)', confirmText: 'Create', cancelText: 'Cancel' } }); dialogRef.afterClosed().subscribe(async (result) => { if (result?.confirmed) { this.creating = true; try { let newVersionData; if (result.selectedValue) { // Copy from selected version - we need to get the full version data const sourceVersion = this.versions.find(v => v.no === result.selectedValue); if (sourceVersion) { // Load the full version data from the current form if it's the selected version if (sourceVersion.no === this.selectedVersion?.no) { const formValue = this.versionForm.getRawValue(); newVersionData = { ...formValue, no: undefined, published: false, last_update_date: undefined, caption: `Copy of ${sourceVersion.caption || `version ${sourceVersion.no}`}` }; } else { // For other versions, we only have basic info, so create minimal copy newVersionData = { caption: `Copy of ${sourceVersion.caption || `version ${sourceVersion.no}`}`, general_prompt: '', llm: { repo_id: '', generation_config: { max_new_tokens: 512, temperature: 0.7, top_p: 0.95, repetition_penalty: 1.1 }, use_fine_tune: false, fine_tune_zip: '' }, intents: [] }; } } } else { // Create blank version newVersionData = { caption: `Version ${this.versions.length + 1}`, general_prompt: '', llm: { repo_id: '', generation_config: { max_new_tokens: 512, temperature: 0.7, top_p: 0.95, repetition_penalty: 1.1 }, use_fine_tune: false, fine_tune_zip: '' }, intents: [] }; } if (newVersionData) { await this.apiService.createVersion(this.project.id, newVersionData).toPromise(); await this.loadVersions(); this.snackBar.open('Version created successfully!', 'Close', { duration: 3000 }); } } catch (error) { this.snackBar.open('Failed to create version', 'Close', { duration: 3000 }); } finally { this.creating = false; } } }); } async saveVersion() { console.log('Save button clicked - canEdit:', this.canEdit, 'published:', this.selectedVersion?.published); if (!this.selectedVersion || !this.canEdit) { this.snackBar.open('Cannot save published version', 'Close', { duration: 3000 }); return; } if (this.versionForm.invalid) { const invalidFields: string[] = []; Object.keys(this.versionForm.controls).forEach(key => { const control = this.versionForm.get(key); if (control && control.invalid) { invalidFields.push(key); } }); this.intents.controls.forEach((intent, index) => { if (intent.invalid) { invalidFields.push(`Intent ${index + 1}`); } }); this.snackBar.open(`Please fix validation errors in: ${invalidFields.join(', ')}`, 'Close', { duration: 5000 }); return; } const currentVersion = this.selectedVersion!; this.saving = true; try { const formValue = this.versionForm.getRawValue(); // updateData'yı backend'in beklediği formatta hazırla const updateData = { caption: formValue.caption, general_prompt: formValue.general_prompt || '', welcome_prompt: formValue.welcome_prompt || '', // Added welcome_prompt llm: formValue.llm, intents: formValue.intents.map((intent: any) => ({ name: intent.name, caption: intent.caption, detection_prompt: intent.detection_prompt, examples: Array.isArray(intent.examples) ? intent.examples : [], parameters: Array.isArray(intent.parameters) ? intent.parameters.map((param: any) => ({ name: param.name, caption: param.caption, type: param.type, required: param.required, variable_name: param.variable_name, extraction_prompt: param.extraction_prompt, validation_regex: param.validation_regex, invalid_prompt: param.invalid_prompt, type_error_prompt: param.type_error_prompt })) : [], action: intent.action, fallback_timeout_prompt: intent.fallback_timeout_prompt, fallback_error_prompt: intent.fallback_error_prompt })), last_update_date: currentVersion.last_update_date || '' }; console.log('Saving version data:', JSON.stringify(updateData, null, 2)); const result = await this.apiService.updateVersion( this.project.id, currentVersion.no, updateData ).toPromise(); this.snackBar.open('Version saved successfully', 'Close', { duration: 3000 }); this.isDirty = false; if (result) { this.selectedVersion = result; this.versionForm.patchValue({ last_update_date: result.last_update_date }); } await this.loadVersions(); } catch (error: any) { console.error('Save error:', error); if (error.status === 409) { // Race condition handling await this.handleRaceCondition(currentVersion); } else if (error.status === 400 && error.error?.detail?.includes('Published versions')) { this.snackBar.open('Published versions cannot be modified. Create a new version instead.', 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } else { const errorMessage = error.error?.detail || error.message || 'Failed to save version'; this.snackBar.open(errorMessage, 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } } finally { this.saving = false; } } // Race condition handling private async handleRaceCondition(currentVersion: Version) { const formValue = this.versionForm.getRawValue(); const retryUpdateData = { caption: formValue.caption, general_prompt: formValue.general_prompt || '', welcome_prompt: formValue.welcome_prompt || '', llm: formValue.llm, intents: formValue.intents.map((intent: any) => ({ name: intent.name, caption: intent.caption, detection_prompt: intent.detection_prompt, examples: Array.isArray(intent.examples) ? intent.examples : [], parameters: Array.isArray(intent.parameters) ? intent.parameters : [], action: intent.action, fallback_timeout_prompt: intent.fallback_timeout_prompt, fallback_error_prompt: intent.fallback_error_prompt })), last_update_date: currentVersion.last_update_date || '' }; 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 force save?', confirmText: 'Force Save', cancelText: 'Reload', confirmColor: 'warn' } }); dialogRef.afterClosed().subscribe(async (forceSave) => { if (forceSave) { try { await this.apiService.updateVersion( this.project.id, currentVersion.no, retryUpdateData, true ).toPromise(); this.snackBar.open('Version force saved', 'Close', { duration: 3000 }); await this.loadVersions(); } catch (err: any) { this.snackBar.open(err.error?.detail || 'Force save failed', 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } } else { await this.loadVersions(); } }); } 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.no ).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.no ).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) { // Check examples in all locales const allExamples = intent.examples || []; for (const example of allExamples) { const exampleText = typeof example === 'string' ? example : example.example; if (this.testUserMessage.toLowerCase().includes(exampleText.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 })); } 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.no - a.no); } } catch (error) { console.error('Failed to reload project:', error); } finally { this.loading = false; } } async compareVersions() { if (this.versions.length < 2) { this.snackBar.open('Need at least 2 versions to compare', 'Close', { duration: 3000 }); return; } const { default: VersionCompareDialogComponent } = await import('../version-compare-dialog/version-compare-dialog.component'); this.dialog.open(VersionCompareDialogComponent, { width: '90vw', maxWidth: '1000px', maxHeight: '80vh', data: { versions: this.versions, selectedVersion: this.selectedVersion } }); } close() { if (this.isDirty) { const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '400px', data: { title: 'Unsaved Changes', message: 'You have unsaved changes. Do you want to save before closing?', confirmText: 'Save & Close', cancelText: 'Discard Changes', showThirdOption: true, thirdOptionText: 'Cancel' } }); dialogRef.afterClosed().subscribe(result => { if (result === 'confirm') { this.saveVersion(); this.dialogRef.close(); } else if (result === 'cancel') { this.dialogRef.close(); } // If result is null or 'third', don't close }); } else { this.dialogRef.close(); } } }