flare / flare-ui /src /app /dialogs /version-edit-dialog /version-edit-dialog.component.ts
ciyidogan's picture
Update flare-ui/src/app/dialogs/version-edit-dialog/version-edit-dialog.component.ts
bb05395 verified
raw
history blame
25.5 kB
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';
@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
],
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;
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>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
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);
}
this.versionForm.valueChanges.subscribe(() => {
this.isDirty = true;
});
}
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;
// Form değerlerini set et
this.versionForm.patchValue({
id: version.id,
caption: version.caption || '',
published: version.published || false,
general_prompt: (version as any).general_prompt || '', // Backend'den gelen general_prompt
last_update_date: version.last_update_date || ''
});
// LLM config'i ayrı set et
if (version.llm) {
this.versionForm.patchValue({
llm: {
repo_id: version.llm.repo_id || '',
generation_config: version.llm.generation_config || {
max_new_tokens: 512,
temperature: 0.7,
top_p: 0.95,
repetition_penalty: 1.1
},
use_fine_tune: version.llm.use_fine_tune || false,
fine_tune_zip: version.llm.fine_tune_zip || ''
}
});
}
// Clear and rebuild intents
this.intents.clear();
(version.intents || []).forEach(intent => {
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.id - a.id);
// Re-select current version if it still exists
if (this.selectedVersion) {
const currentVersion = this.versions.find(v => v.id === this.selectedVersion!.id);
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 || ''],
locale: [intent.locale || 'tr-TR'],
detection_prompt: [intent.detection_prompt || '', Validators.required],
examples: this.fb.array([]),
parameters: this.fb.array([]),
action: [intent.action || '', Validators.required],
fallback_timeout_prompt: [intent.fallback_timeout_prompt || ''],
fallback_error_prompt: [intent.fallback_error_prompt || '']
});
// Examples ve parameters'ı ayrı olarak ekle
if (intent.examples && Array.isArray(intent.examples)) {
const examplesArray = group.get('examples') as FormArray;
intent.examples.forEach((example: string) => {
examplesArray.push(this.fb.control(example));
});
}
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;
}
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 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) {
// FormArray'leri yeniden oluştur
intent.patchValue({
name: result.name,
caption: result.caption,
locale: result.locale,
detection_prompt: result.detection_prompt,
action: result.action,
fallback_timeout_prompt: result.fallback_timeout_prompt,
fallback_error_prompt: result.fallback_error_prompt
});
// Examples'ı güncelle
const examplesArray = intent.get('examples') as FormArray;
examplesArray.clear();
(result.examples || []).forEach((example: string) => {
examplesArray.push(this.fb.control(example));
});
// Parameters'ı güncelle
const parametersArray = intent.get('parameters') as FormArray;
parametersArray.clear();
(result.parameters || []).forEach((param: any) => {
parametersArray.push(this.createParameterFormGroup(param));
});
}
});
}
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 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 => ({ // ✅ publishedVersions kullan
value: v.id,
label: `Version ${v.id} - ${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.loading = true;
try {
let newVersionData;
if (result.selectedValue) {
// Copy from selected version
const sourceVersion = this.versions.find(v => v.id === result.selectedValue);
if (sourceVersion) {
newVersionData = {
...sourceVersion,
id: undefined,
published: false,
last_update_date: undefined,
caption: `Copy of version ${sourceVersion.id}`,
description: sourceVersion.caption ? `Copy of ${sourceVersion.caption}` : `Copy of version ${sourceVersion.id}`
};
}
} else {
// Create blank version
newVersionData = {
caption: `Version ${this.versions.length + 1}`,
description: 'New version',
default_api: '',
published: false,
llm: {
repo_id: '',
generation_config: {
max_new_tokens: 512,
temperature: 0.7,
top_p: 0.95,
top_k: 50,
repetition_penalty: 1.1
},
use_fine_tune: false,
fine_tune_zip: ''
},
intents: [],
parameters: []
};
}
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.loading = false;
}
}
});
}
async saveVersion() {
if (!this.selectedVersion) {
this.snackBar.open('No version selected', '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 || '', // Bu alan eksikti
llm: formValue.llm,
intents: formValue.intents.map((intent: any) => ({
name: intent.name,
caption: intent.caption,
locale: intent.locale,
detection_prompt: intent.detection_prompt,
examples: Array.isArray(intent.examples) ? intent.examples.filter((ex: any) => ex) : [],
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.id,
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 için ayrı metod çağır
await this.handleRaceCondition(currentVersion);
} 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 için ayrı metod
private async handleRaceCondition(currentVersion: Version) {
const formValue = this.versionForm.getRawValue();
const retryUpdateData = {
caption: formValue.caption,
general_prompt: formValue.general_prompt || '',
llm: formValue.llm,
intents: formValue.intents.map((intent: any) => ({
name: intent.name,
caption: intent.caption,
locale: intent.locale,
detection_prompt: intent.detection_prompt,
examples: Array.isArray(intent.examples) ? intent.examples.filter((ex: any) => ex) : [],
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.id,
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.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;
}
}
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() {
this.dialogRef.close(true);
}
}