import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal } from '@angular/core'; import { DatePipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Subscription } from 'rxjs'; export interface ModelRequest { request_id: string; attempt_id: string; model?: string; reasoning_effort?: string; role: string; is_fallback: boolean; phase: string; active: boolean; attempt: number; max_retries: number; timeout_seconds: number; elapsed_seconds: number; started_at: number; headers_at?: number; first_output_at?: number; last_output_at?: number; ended_at?: number; retry_at?: number; http_status?: number; server_request_id?: string; error?: string; error_type?: string; failure_phase?: string; output_chunks?: number; tool_argument_chars?: number; max_output_tokens?: number | null; tools?: { name: string | null; argument_chars: number }[]; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number } | null; images: { width: number | null; height: number | null }[]; final_review?: FinalReview; category?: string; } export interface FinalReview { review_id: string; attempt_id: string; status: string; stage: string; active: boolean; started_at: number; deadline_at: number; ended_at?: number; total_seconds: number; elapsed_seconds: number; remaining_seconds: number; request_count: number; retry_count: number; request_attempt?: number; max_retries: number; request_phase?: string; last_failure_category?: string; } export function reviewStage(review: FinalReview): string { if (review.status !== 'running') { return ({ completed: '验收完成', budget_exhausted: '验收总预算已用尽', request_retries_exhausted: '验收请求重试已用尽', cancelled: '验收已中断', failed: '验收未完成' } as Record)[review.status] || '验收状态待核实'; } return ({ preparing_evidence: '正在准备验收证据', reviewing_evidence: '正在核对证据与结论', reading_evidence: '正在读取验收证据', structuring_verdict: '正在整理验收结论', validating_verdict: '正在校验完整结论' } as Record)[review.stage] || '正在进行最终验收'; } export function requestPhase(request: ModelRequest | string): string { const phase = typeof request === 'string' ? request : request.phase; return ({ waiting_response: '等待模型响应', waiting_output: '等待模型输出', receiving_output: '正在接收模型输出', generating_tool_arguments: '正在生成工具参数', retry_wait: '等待重试', completed: '请求完成', failed: '请求失败', cancelled: '请求已取消' } as Record)[phase] || '请求状态待核实'; } export function requestHint(request: ModelRequest, now: number): string { if (request.phase === 'retry_wait') { const remaining = Math.max(0, Math.ceil((request.retry_at || now) - now)); return remaining ? `${remaining} 秒后进行第 ${request.attempt}/${request.max_retries} 次重试。` : '重试等待已结束,正在等待下一次请求状态。'; } if (request.phase === 'waiting_output') return '已收到服务响应,尚未收到有效模型输出。'; if (request.phase === 'generating_tool_arguments') { const names = (request.tools || []).map(tool => tool.name).filter(Boolean).join('、'); return `${names ? names + ' · ' : ''}已接收 ${(request.tool_argument_chars || 0).toLocaleString()} 个参数字符,最近输出在 ${Math.max(0, Math.floor(now - (request.last_output_at || now)))} 秒前;本次工具尚未执行。`; } if (request.phase === 'receiving_output') return `最近一次输出在 ${Math.max(0, Math.floor(now - (request.last_output_at || now)))} 秒前;完整响应仍在接收中。`; return '正在等待服务响应;尚不能确认模型是否开始生成。'; } @Component({ selector: 'app-llm-request-status', standalone: true, imports: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './llm-request-status.component.html', styleUrl: './llm-request-status.component.scss' }) export class LlmRequestStatusComponent { private http = inject(HttpClient); sessionId = input(null); running = input(false); activeChange = output(); requests = signal([]); reviews = signal([]); now = signal(Date.now() / 1000); unavailable = signal(false); loading = signal(false); active = computed(() => this.running() ? this.requests().filter(r => r.active) : []); activeReviews = computed(() => this.running() ? this.reviews().filter(r => r.active) : []); downloadUrl = computed(() => `/api/sessions/${encodeURIComponent(this.sessionId() || '')}/request-diagnostics?download=true`); phase = requestPhase; hint = requestHint; reviewStage = reviewStage; constructor() { effect(() => this.activeChange.emit(this.active().length > 0 || this.activeReviews().length > 0)); effect(onCleanup => { const id = this.sessionId(); const running = this.running(); this.requests.set([]); this.reviews.set([]); this.unavailable.set(false); this.loading.set(!!id); if (!id) return; let stopped = false; let timer: ReturnType | undefined; let subscription: Subscription | undefined; const schedule = () => { if (running && !stopped) timer = setTimeout(load, 3000); }; const load = () => { subscription = this.http.get<{ requests: ModelRequest[]; final_reviews?: FinalReview[] }>(`/api/sessions/${encodeURIComponent(id)}/request-diagnostics`).subscribe({ next: data => { this.requests.set(data.requests || []); this.reviews.set(data.final_reviews || []); this.unavailable.set(false); this.loading.set(false); schedule(); }, error: () => { this.unavailable.set(true); this.loading.set(false); schedule(); } }); }; load(); const clock = running ? setInterval(() => this.now.set(Date.now() / 1000), 1000) : undefined; onCleanup(() => { stopped = true; clearTimeout(timer); clearInterval(clock); subscription?.unsubscribe(); }); }); } elapsed(request: ModelRequest): number { return Math.max(0, Math.floor(request.ended_at ? request.elapsed_seconds : this.now() - request.started_at)); } reviewElapsed(review: FinalReview): number { return Math.max(0, Math.floor(review.ended_at ? review.elapsed_seconds : this.now() - review.started_at)); } reviewRemaining(review: FinalReview): number { return Math.max(0, Math.ceil(review.deadline_at - this.now())); } role(request: ModelRequest): string { return ({ FlashRunner: '任务执行', operator: '任务执行', planner: '规划', checker: '结果检查', outputter: '报告整理', step_summarizer: '步骤整理' } as Record)[request.role] || request.role; } }