import { Component, OnInit, inject, signal, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { finalize } from 'rxjs';
interface ModelSettings {
base_url: string;
model: string;
api_key_configured: boolean;
timeout_seconds: number;
max_retries: number;
retry_interval_seconds: number;
rate_limit_retry_seconds: number;
tool_fallback_model: string;
tool_fallback_reasoning_effort: string;
tool_fallback_max_retries: number;
tool_fallback_no_progress_attempts: number;
reasoning_effort: string;
model_presets: string[];
env_path: string;
message?: string;
}
@Component({
selector: 'app-model-settings',
standalone: true,
imports: [FormsModule],
template: `
`,
styles: [`
.model-settings { display: grid; gap: 10px; color: #1e293b; }
p, small { margin: 0; line-height: 1.6; color: #64748b; }
label { font-weight: 600; } label span { font-weight: 400; font-size: 12px; }
input, select { box-sizing: border-box; width: 100%; padding: 12px; border-radius: 10px; border: 1px solid #64748b66;
background: #f8fafc; color: inherit; font: inherit; }
input:focus, select:focus { outline: 2px solid #38bdf8; }
.number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }
.number-fields input { margin-top: 8px; }
.recovery-settings { border: 1px solid #cbd5e1; border-radius: 10px; background: #f8fafc; padding: 10px 14px; }
.recovery-settings summary { cursor: pointer; font-weight: 600; color: #334155; }
.recovery-content { display: grid; gap: 10px; padding-top: 12px; }
.recovery-content label input, .recovery-content label select { margin-top: 8px; }
.actions { display: flex; gap: 12px; margin-top: 8px; }
button { padding: 11px 20px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; cursor: pointer; font: inherit; }
button:disabled { opacity: .5; cursor: default; } .success { color: #047857; } .error { color: #be123c; }
.model-presets { display: flex; flex-wrap: wrap; gap: 8px; }
.model-presets button { padding: 8px 12px; background: #f8fafc; color: #475569; border-color: #cbd5e1; font-size: 14px; }
.model-presets button:hover:not(:disabled) { background: #e0f2fe; border-color: #38bdf8; }
.model-presets button[aria-pressed="true"] { background: #e0f2fe; border-color: #0284c7; color: #075985; }
button:focus-visible { outline: 2px solid #0284c7; outline-offset: 2px; }
small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }
`]
})
export class ModelSettingsComponent implements OnInit {
private http = inject(HttpClient);
saved = output();
modelPresets: string[] = [];
baseUrl = ''; model = ''; apiKey = ''; timeout = 300; retries = 5; retryInterval = 10; reasoning = 'high';
rateLimitRetry = 60; toolFallbackModel = 'gpt-6-luna'; toolFallbackReasoning = 'high';
toolFallbackRetries = 3; toolFallbackNoProgress = 3;
busy = signal<'save' | 'test' | null>(null);
loading = signal(true); keyConfigured = signal(false); envPath = signal('');
message = signal(''); error = signal('');
ngOnInit(): void {
this.http.get('/api/system/model-settings').pipe(finalize(() => this.loading.set(false))).subscribe({
next: config => this.apply(config),
error: () => this.error.set('配置加载失败,请刷新页面重试。')
});
}
valid(): boolean {
return !!this.baseUrl.trim() && !!this.model.trim() && (!!this.apiKey.trim() || this.keyConfigured())
&& this.timeout > 0 && this.timeout <= 3600 && Number.isInteger(this.retries) && this.retries >= 0 && this.retries <= 10 && this.retryInterval >= 0 && this.retryInterval <= 300
&& this.rateLimitRetry >= 0 && this.rateLimitRetry <= 3600 && !!this.toolFallbackModel.trim()
&& Number.isInteger(this.toolFallbackRetries) && this.toolFallbackRetries >= 0 && this.toolFallbackRetries <= 10
&& Number.isInteger(this.toolFallbackNoProgress) && this.toolFallbackNoProgress >= 1 && this.toolFallbackNoProgress <= 10;
}
private payload() {
return { base_url: this.baseUrl.trim(), model: this.model.trim(), api_key: this.apiKey.trim() || undefined,
timeout_seconds: this.timeout, max_retries: this.retries, retry_interval_seconds: this.retryInterval, reasoning_effort: this.reasoning,
rate_limit_retry_seconds: this.rateLimitRetry, tool_fallback_model: this.toolFallbackModel.trim(),
tool_fallback_reasoning_effort: this.toolFallbackReasoning, tool_fallback_max_retries: this.toolFallbackRetries,
tool_fallback_no_progress_attempts: this.toolFallbackNoProgress };
}
private apply(config: ModelSettings): void {
this.baseUrl = config.base_url; this.model = config.model; this.timeout = config.timeout_seconds;
this.modelPresets = config.model_presets;
this.retries = config.max_retries; this.retryInterval = config.retry_interval_seconds ?? 10; this.reasoning = config.reasoning_effort ?? 'high'; this.keyConfigured.set(config.api_key_configured); this.envPath.set(config.env_path);
this.rateLimitRetry = config.rate_limit_retry_seconds ?? 60;
this.toolFallbackModel = config.tool_fallback_model ?? 'gpt-6-luna';
this.toolFallbackReasoning = config.tool_fallback_reasoning_effort ?? 'high';
this.toolFallbackRetries = config.tool_fallback_max_retries ?? 3;
this.toolFallbackNoProgress = config.tool_fallback_no_progress_attempts ?? 3;
}
save(): void {
if (this.busy() || !this.valid()) return;
this.busy.set('save'); this.message.set(''); this.error.set('');
this.http.post('/api/system/model-settings', this.payload()).pipe(finalize(() => this.busy.set(null))).subscribe({
next: config => { this.apply(config); this.apiKey = ''; this.message.set(config.message || '配置已保存。'); this.saved.emit(); },
error: err => this.error.set(typeof err.error?.detail === 'string' ? err.error.detail : '保存失败,请检查配置内容。')
});
}
test(): void {
if (this.busy() || !this.valid()) return;
this.busy.set('test'); this.message.set(''); this.error.set('');
this.http.post<{ message: string }>('/api/system/model-settings/test', this.payload()).pipe(finalize(() => this.busy.set(null))).subscribe({
next: result => this.message.set(result.message),
error: err => this.error.set(typeof err.error?.detail === 'string' ? err.error.detail : '连接测试失败,请检查配置。')
});
}
}