import { Component, OnInit, OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { ApiService } from '../../services/api.service'; import { EnvironmentService } from '../../services/environment.service'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatSliderModule } from '@angular/material/slider'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatDividerModule } from '@angular/material/divider'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDialogModule } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; // Provider interfaces interface ProviderConfig { type: string; name: string; display_name: string; requires_endpoint: boolean; requires_api_key: boolean; requires_repo_info: boolean; description?: string; } interface ProviderSettings { name: string; api_key?: string; endpoint?: string; settings: any; } interface EnvironmentConfig { llm_provider: ProviderSettings; tts_provider: ProviderSettings; stt_provider: ProviderSettings; providers: ProviderConfig[]; } @Component({ selector: 'app-environment', standalone: true, imports: [ CommonModule, ReactiveFormsModule, FormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatSliderModule, MatSlideToggleModule, MatExpansionModule, MatDividerModule, MatProgressSpinnerModule, MatSnackBarModule, MatTooltipModule, MatDialogModule ], templateUrl: './environment.component.html', styleUrls: ['./environment.component.scss'] }) export class EnvironmentComponent implements OnInit, OnDestroy { form: FormGroup; loading = false; saving = false; isLoading = false; // Provider lists llmProviders: ProviderConfig[] = []; ttsProviders: ProviderConfig[] = []; sttProviders: ProviderConfig[] = []; // Current provider configurations currentLLMProvider?: ProviderConfig; currentTTSProvider?: ProviderConfig; currentSTTProvider?: ProviderConfig; // Settings for LLM internalPrompt: string = ''; parameterCollectionConfig: any = { enabled: false, max_params_per_question: 1, show_all_required: false, ask_optional_params: false, group_related_params: false, min_confidence_score: 0.7, collection_prompt: 'Please provide the following information:' }; hideSTTKey = true; sttLanguages = [ { code: 'tr-TR', name: 'Türkçe' }, { code: 'en-US', name: 'English (US)' }, { code: 'en-GB', name: 'English (UK)' }, { code: 'de-DE', name: 'Deutsch' }, { code: 'fr-FR', name: 'Français' }, { code: 'es-ES', name: 'Español' }, { code: 'it-IT', name: 'Italiano' }, { code: 'pt-BR', name: 'Português (BR)' }, { code: 'ja-JP', name: '日本語' }, { code: 'ko-KR', name: '한국어' }, { code: 'zh-CN', name: '中文' } ]; sttModels = [ { value: 'default', name: 'Default' }, { value: 'latest_short', name: 'Latest Short (Optimized for short audio)' }, { value: 'latest_long', name: 'Latest Long (Best accuracy)' }, { value: 'command_and_search', name: 'Command and Search' }, { value: 'phone_call', name: 'Phone Call (Optimized for telephony)' } ]; // API key visibility tracking showApiKeys: { [key: string]: boolean } = {}; // Memory leak prevention private destroyed$ = new Subject(); constructor( private fb: FormBuilder, private apiService: ApiService, private environmentService: EnvironmentService, private snackBar: MatSnackBar ) { this.form = this.fb.group({ // LLM Provider llm_provider_name: ['', Validators.required], llm_provider_api_key: [''], llm_provider_endpoint: [''], // TTS Provider tts_provider_name: ['no_tts', Validators.required], tts_provider_api_key: [''], tts_provider_endpoint: [''], // STT Provider stt_provider_name: ['no_stt', Validators.required], stt_provider_api_key: [''], stt_provider_endpoint: [''], // STT Settings stt_settings: this.fb.group({ language: ['tr-TR'], speech_timeout_ms: [2000], enable_punctuation: [true], interim_results: [true], use_enhanced: [true], model: ['latest_long'], noise_reduction_level: [2], vad_sensitivity: [0.5] }) }); } ngOnInit() { this.loadEnvironment(); } ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); } // Safe getters for template get currentLLMProviderSafe(): ProviderConfig | null { return this.currentLLMProvider || null; } get currentTTSProviderSafe(): ProviderConfig | null { return this.currentTTSProvider || null; } get currentSTTProviderSafe(): ProviderConfig | null { return this.currentSTTProvider || null; } // API key masking methods maskApiKey(key?: string): string { if (!key) return ''; if (key.length <= 8) return '••••••••'; return key.substring(0, 4) + '••••' + key.substring(key.length - 4); } toggleApiKeyVisibility(fieldName: string): void { this.showApiKeys[fieldName] = !this.showApiKeys[fieldName]; } getApiKeyInputType(fieldName: string): string { return this.showApiKeys[fieldName] ? 'text' : 'password'; } formatApiKeyForDisplay(fieldName: string, value?: string): string { if (this.showApiKeys[fieldName]) { return value || ''; } return this.maskApiKey(value); } loadEnvironment(): void { this.loading = true; this.isLoading = true; this.apiService.getEnvironment() .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (data: any) => { // Check if it's new format or legacy if (data.llm_provider) { this.handleNewFormat(data); } else { this.handleLegacyFormat(data); } this.loading = false; this.isLoading = false; }, error: (err) => { console.error('Failed to load environment:', err); this.snackBar.open('Failed to load environment configuration', 'Close', { duration: 3000, panelClass: ['error-snackbar'] }); this.loading = false; this.isLoading = false; } }); } handleNewFormat(data: EnvironmentConfig): void { // Update provider lists if (data.providers) { this.llmProviders = data.providers.filter(p => p.type === 'llm'); this.ttsProviders = data.providers.filter(p => p.type === 'tts'); this.sttProviders = data.providers.filter(p => p.type === 'stt'); } // Set form values this.form.patchValue({ llm_provider_name: data.llm_provider?.name || '', llm_provider_api_key: data.llm_provider?.api_key || '', llm_provider_endpoint: data.llm_provider?.endpoint || '', tts_provider_name: data.tts_provider?.name || 'no_tts', tts_provider_api_key: data.tts_provider?.api_key || '', tts_provider_endpoint: data.tts_provider?.endpoint || '', stt_provider_name: data.stt_provider?.name || 'no_stt', stt_provider_api_key: data.stt_provider?.api_key || '', stt_provider_endpoint: data.stt_provider?.endpoint || '' }); // Set internal prompt and parameter collection config this.internalPrompt = data.llm_provider?.settings?.internal_prompt || ''; this.parameterCollectionConfig = data.llm_provider?.settings?.parameter_collection_config || this.parameterCollectionConfig; // Update current providers this.updateCurrentProviders(); // Notify environment service if (data.tts_provider?.name !== 'no_tts') { this.environmentService.setTTSEnabled(true); } if (data.stt_provider?.name !== 'no_stt') { this.environmentService.setSTTEnabled(true); } if (data.stt_provider?.settings) { this.form.get('stt_settings')?.patchValue(data.stt_provider.settings); } } handleLegacyFormat(data: any): void { console.warn('Legacy environment format detected, using defaults'); // Set default providers if not present this.llmProviders = this.getDefaultProviders('llm'); this.ttsProviders = this.getDefaultProviders('tts'); this.sttProviders = this.getDefaultProviders('stt'); // Map legacy fields this.form.patchValue({ llm_provider_name: data.work_mode || 'spark', llm_provider_api_key: data.cloud_token || '', llm_provider_endpoint: data.spark_endpoint || '', tts_provider_name: data.tts_engine || 'no_tts', tts_provider_api_key: data.tts_engine_api_key || '', stt_provider_name: data.stt_engine || 'no_stt', stt_provider_api_key: data.stt_engine_api_key || '' }); this.internalPrompt = data.internal_prompt || ''; this.parameterCollectionConfig = data.parameter_collection_config || this.parameterCollectionConfig; this.updateCurrentProviders(); if (data.stt_settings) { this.form.get('stt_settings')?.patchValue(data.stt_settings); } } getDefaultProviders(type: string): ProviderConfig[] { const defaults: { [key: string]: ProviderConfig[] } = { llm: [ { type: 'llm', name: 'spark', display_name: 'Spark (YTU Cosmos)', requires_endpoint: true, requires_api_key: true, requires_repo_info: true, description: 'YTU Cosmos Spark LLM Service' }, { type: 'llm', name: 'gpt-4o', display_name: 'GPT-4o', requires_endpoint: false, requires_api_key: true, requires_repo_info: false, description: 'OpenAI GPT-4o model' }, { type: 'llm', name: 'gpt-4o-mini', display_name: 'GPT-4o Mini', requires_endpoint: false, requires_api_key: true, requires_repo_info: false, description: 'OpenAI GPT-4o Mini model' } ], tts: [ { type: 'tts', name: 'no_tts', display_name: 'No TTS', requires_endpoint: false, requires_api_key: false, requires_repo_info: false, description: 'Disable text-to-speech' }, { type: 'tts', name: 'elevenlabs', display_name: 'ElevenLabs', requires_endpoint: false, requires_api_key: true, requires_repo_info: false, description: 'ElevenLabs TTS service' } ], stt: [ { type: 'stt', name: 'no_stt', display_name: 'No STT', requires_endpoint: false, requires_api_key: false, requires_repo_info: false, description: 'Disable speech-to-text' }, { type: 'stt', name: 'google', display_name: 'Google Cloud Speech', requires_endpoint: false, requires_api_key: true, requires_repo_info: false, description: 'Google Cloud Speech-to-Text API' }, { type: 'stt', name: 'azure', display_name: 'Azure Speech Services', requires_endpoint: false, requires_api_key: true, requires_repo_info: false, description: 'Azure Cognitive Services Speech' }, { type: 'stt', name: 'flicker', display_name: 'Flicker STT', requires_endpoint: true, requires_api_key: true, requires_repo_info: false, description: 'Flicker Speech Recognition Service' } ] }; return defaults[type] || []; } updateCurrentProviders(): void { const llmName = this.form.get('llm_provider_name')?.value; const ttsName = this.form.get('tts_provider_name')?.value; const sttName = this.form.get('stt_provider_name')?.value; this.currentLLMProvider = this.llmProviders.find(p => p.name === llmName); this.currentTTSProvider = this.ttsProviders.find(p => p.name === ttsName); this.currentSTTProvider = this.sttProviders.find(p => p.name === sttName); // Update form validators based on requirements this.updateFormValidators(); } updateFormValidators(): void { // LLM validators if (this.currentLLMProvider?.requires_api_key) { this.form.get('llm_provider_api_key')?.setValidators(Validators.required); } else { this.form.get('llm_provider_api_key')?.clearValidators(); } if (this.currentLLMProvider?.requires_endpoint) { this.form.get('llm_provider_endpoint')?.setValidators(Validators.required); } else { this.form.get('llm_provider_endpoint')?.clearValidators(); } // TTS validators if (this.currentTTSProvider?.requires_api_key) { this.form.get('tts_provider_api_key')?.setValidators(Validators.required); } else { this.form.get('tts_provider_api_key')?.clearValidators(); } // STT validators if (this.currentSTTProvider?.requires_api_key) { this.form.get('stt_provider_api_key')?.setValidators(Validators.required); } else { this.form.get('stt_provider_api_key')?.clearValidators(); } // STT endpoint validator if (this.currentSTTProvider?.requires_endpoint) { this.form.get('stt_provider_endpoint')?.setValidators(Validators.required); } else { this.form.get('stt_provider_endpoint')?.clearValidators(); } // Update validity this.form.get('llm_provider_api_key')?.updateValueAndValidity(); this.form.get('llm_provider_endpoint')?.updateValueAndValidity(); this.form.get('tts_provider_api_key')?.updateValueAndValidity(); this.form.get('stt_provider_api_key')?.updateValueAndValidity(); this.form.get('stt_provider_endpoint')?.updateValueAndValidity(); } onLLMProviderChange(value: string): void { this.currentLLMProvider = this.llmProviders.find(p => p.name === value); this.updateFormValidators(); // Reset fields if provider doesn't require them if (!this.currentLLMProvider?.requires_api_key) { this.form.get('llm_provider_api_key')?.setValue(''); } if (!this.currentLLMProvider?.requires_endpoint) { this.form.get('llm_provider_endpoint')?.setValue(''); } } onTTSProviderChange(value: string): void { this.currentTTSProvider = this.ttsProviders.find(p => p.name === value); this.updateFormValidators(); if (!this.currentTTSProvider?.requires_api_key) { this.form.get('tts_provider_api_key')?.setValue(''); } if (value !== this.form.get('stt_provider_name')?.value) { this.form.get('stt_provider_api_key')?.setValue(''); } // Provider-specific defaults if (value === 'google') { this.form.get('stt_settings')?.patchValue({ model: 'latest_long', use_enhanced: true }); } else if (value === 'azure') { this.form.get('stt_settings')?.patchValue({ model: 'default', use_enhanced: false }); } // STT endpoint validator if (this.currentSTTProvider?.requires_endpoint) { this.form.get('stt_provider_endpoint')?.setValidators(Validators.required); } else { this.form.get('stt_provider_endpoint')?.clearValidators(); } this.form.get('stt_provider_endpoint')?.updateValueAndValidity(); // Notify environment service this.environmentService.setTTSEnabled(value !== 'no_tts'); } onSTTProviderChange(value: string): void { this.currentSTTProvider = this.sttProviders.find(p => p.name === value); this.updateFormValidators(); if (!this.currentSTTProvider?.requires_api_key) { this.form.get('stt_provider_api_key')?.setValue(''); } // Notify environment service this.environmentService.setSTTEnabled(value !== 'no_stt'); } saveEnvironment(): void { if (this.form.invalid || this.saving) { this.snackBar.open('Please fix validation errors', 'Close', { duration: 3000, panelClass: ['error-snackbar'] }); return; } this.saving = true; const formValue = this.form.value; const saveData = { llm_provider: { name: formValue.llm_provider_name, api_key: formValue.llm_provider_api_key, endpoint: formValue.llm_provider_endpoint, settings: { internal_prompt: this.internalPrompt, parameter_collection_config: this.parameterCollectionConfig } }, tts_provider: { name: formValue.tts_provider_name, api_key: formValue.tts_provider_api_key, endpoint: formValue.tts_provider_endpoint, settings: {} }, stt_provider: { name: formValue.stt_provider_name, api_key: formValue.stt_provider_api_key, endpoint: formValue.stt_provider_endpoint, settings: formValue.stt_settings || {} } }; this.apiService.updateEnvironment(saveData as any) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: () => { this.saving = false; this.snackBar.open('Environment configuration saved successfully', 'Close', { duration: 3000, panelClass: ['success-snackbar'] }); // Update environment service this.environmentService.updateEnvironment(saveData as any); // Clear form dirty state this.form.markAsPristine(); }, error: (error) => { this.saving = false; // Race condition handling if (error.status === 409) { const details = error.error?.details || {}; this.snackBar.open( `Settings were modified by ${details.last_update_user || 'another user'}. Please reload.`, 'Reload', { duration: 0 } ).onAction().subscribe(() => { this.loadEnvironment(); }); } else { this.snackBar.open( error.error?.detail || 'Failed to save environment configuration', 'Close', { duration: 5000, panelClass: ['error-snackbar'] } ); } } }); } // Icon helpers getLLMProviderIcon(provider: ProviderConfig | null): string { if (!provider || !provider.name) return 'smart_toy'; switch(provider.name) { case 'gpt-4o': case 'gpt-4o-mini': return 'psychology'; case 'spark': return 'auto_awesome'; default: return 'smart_toy'; } } getTTSProviderIcon(provider: ProviderConfig | null): string { if (!provider || !provider.name) return 'record_voice_over'; switch(provider.name) { case 'elevenlabs': return 'graphic_eq'; case 'blaze': return 'volume_up'; default: return 'record_voice_over'; } } getSTTProviderIcon(provider: ProviderConfig | null): string { if (!provider || !provider.name) return 'mic'; switch(provider.name) { case 'google': return 'g_translate'; case 'azure': return 'cloud'; case 'flicker': return 'mic_none'; default: return 'mic'; } } getProviderIcon(provider: ProviderConfig): string { switch(provider.type) { case 'llm': return this.getLLMProviderIcon(provider); case 'tts': return this.getTTSProviderIcon(provider); case 'stt': return this.getSTTProviderIcon(provider); default: return 'settings'; } } // Helper methods getApiKeyLabel(type: string): string { switch(type) { case 'llm': return this.currentLLMProvider?.name === 'spark' ? 'API Token' : 'API Key'; case 'tts': return 'API Key'; case 'stt': return this.currentSTTProvider?.name === 'google' ? 'Credentials JSON Path' : 'API Key'; default: return 'API Key'; } } getApiKeyPlaceholder(type: string): string { switch(type) { case 'llm': if (this.currentLLMProvider?.name === 'spark') return 'Enter Spark token'; if (this.currentLLMProvider?.name?.includes('gpt')) return 'sk-...'; return 'Enter API key'; case 'tts': return 'Enter TTS API key'; case 'stt': if (this.currentSTTProvider?.name === 'google') return '/path/to/credentials.json'; if (this.currentSTTProvider?.name === 'azure') return 'subscription_key|region'; return 'Enter STT API key'; default: return 'Enter API key'; } } getEndpointPlaceholder(type: string): string { switch(type) { case 'llm': return 'https://spark-api.example.com'; case 'tts': return 'https://tts-api.example.com'; case 'stt': return 'https://stt-api.example.com'; default: return 'https://api.example.com'; } } resetCollectionPrompt(): void { this.parameterCollectionConfig.collection_prompt = 'Please provide the following information:'; } testConnection(): void { const endpoint = this.form.get('llm_provider_endpoint')?.value; if (!endpoint) { this.snackBar.open('Please enter an endpoint URL', 'Close', { duration: 2000 }); return; } this.snackBar.open('Testing connection...', 'Close', { duration: 2000 }); // TODO: Implement actual connection test } }