Spaces:
Building
Building
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[]; | |
} | |
({ | |
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<void>(); | |
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 | |
} | |
} |