diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 887f41c..707698c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -84,20 +84,31 @@ export type ApiError = { detail: string; errorCode?: string; errorMessage?: string; + /** + * PR-DocSrv-Web-Ask-Selector-1: backend_unavailable 503 path 가 + * body level 에 `error_reason` (예: `macbook_unavailable`) 을 둠. 본 필드는 + * 그 raw 값. 다른 endpoint 응답에 `error_reason` 이 없으면 undefined. + */ + errorReason?: string; }; function parseDetail(body: unknown, fallback: string): { detail: string; errorCode?: string; errorMessage?: string; + errorReason?: string; } { - if (body && typeof body === 'object' && 'detail' in body) { - const d = (body as { detail: unknown }).detail; - if (typeof d === 'string' && d) return { detail: d }; - if (d && typeof d === 'object') { - const obj = d as { error_code?: string; message?: string }; - const message = obj.message || fallback; - return { detail: message, errorCode: obj.error_code, errorMessage: obj.message }; + if (body && typeof body === 'object') { + const top = body as { detail?: unknown; error_reason?: unknown }; + const errorReason = typeof top.error_reason === 'string' ? top.error_reason : undefined; + if ('detail' in body) { + const d = top.detail; + if (typeof d === 'string' && d) return { detail: d, errorReason }; + if (d && typeof d === 'object') { + const obj = d as { error_code?: string; message?: string }; + const message = obj.message || fallback; + return { detail: message, errorCode: obj.error_code, errorMessage: obj.message, errorReason }; + } } } return { detail: fallback }; diff --git a/frontend/src/lib/components/ask/AskAnswer.svelte b/frontend/src/lib/components/ask/AskAnswer.svelte index e94eed6..d125a63 100644 --- a/frontend/src/lib/components/ask/AskAnswer.svelte +++ b/frontend/src/lib/components/ask/AskAnswer.svelte @@ -60,8 +60,20 @@ no_evidence: '근거 없음', parse_failed: '형식 오류', llm_error: 'AI 오류', + backend_unavailable: 'Backend 비가용', }; + /** + * backend chip label — `backend_requested` 가 명시 opt-in 인 경우만 표시. + * 미지정 (null/undefined) default 호출은 chip 없음 (시각 noise 회피). + */ + function backendChipLabel(backend: string | null | undefined): string | null { + if (!backend) return null; + if (backend === 'qwen-macbook') return 'Qwen 27B (MacBook)'; + if (backend === 'gemma-macmini') return 'Gemma 26B (Mac mini)'; + return backend; + } + let tokens = $derived(data?.ai_answer ? splitAnswer(data.ai_answer) : []); let showFullAnswer = $derived( !!data && !!data.ai_answer && data.completeness === 'full' @@ -88,6 +100,13 @@ 신뢰도 {confidenceLabel(data.confidence)} + {#if backendChipLabel(data.backend_requested)} + + + {backendChipLabel(data.backend_requested)} + + + {/if} {STATUS_LABEL[data.synthesis_status]} diff --git a/frontend/src/lib/types/ask.ts b/frontend/src/lib/types/ask.ts index 4a6cd89..826d976 100644 --- a/frontend/src/lib/types/ask.ts +++ b/frontend/src/lib/types/ask.ts @@ -11,7 +11,8 @@ export type SynthesisStatus = | 'skipped' | 'no_evidence' | 'parse_failed' - | 'llm_error'; + | 'llm_error' + | 'backend_unavailable'; export type Confidence = 'high' | 'medium' | 'low'; @@ -74,4 +75,10 @@ export interface AskResponse { covered_aspects: string[] | null; missing_aspects: string[] | null; confirmed_items: ConfirmedItem[] | null; + /** + * PR-MacBook-RAG-Backend-1: backend dispatcher metadata. + * backend 미지정 호출은 둘 다 null (기존 호출자 호환). 명시 opt-in 시만 채워짐. + */ + backend_requested?: string | null; + backend_used?: string | null; } diff --git a/frontend/src/routes/ask/+page.svelte b/frontend/src/routes/ask/+page.svelte index 730e5c9..17cb790 100644 --- a/frontend/src/routes/ask/+page.svelte +++ b/frontend/src/routes/ask/+page.svelte @@ -1,10 +1,18 @@ @@ -114,12 +161,32 @@ class="w-full pl-9 pr-3 py-2 bg-surface border border-default rounded-lg text-text text-sm focus:border-accent outline-none" /> +
- {#if !queryInput && !loading && !data} + {#if backendUnavailable} +
+ + + +
+ {:else if !queryInput && !loading && !data}