From c086c9f85d89743945cd7691fe372a0c5d2624e8 Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 22 May 2026 13:40:11 +0000 Subject: [PATCH] feat(ask): /ask backend selector + 503 macbook_unavailable UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 선행 PR-MacBook-RAG-Backend-1 (main a7b8f15) backend dispatcher 의 frontend 소비. /ask 페이지에 backend selector (default | qwen-macbook) + URL ?backend=qwen-macbook 지원 + 503 friendly empty state + "Default 로 재요청" 버튼 (backend param 명시 제거 → 무한 루프 0). 정책 (선행 PR 그대로 유지): - default / backend 미지정 = Gemma Mac mini (현 path 변동 0, 기존 호출자 호환) - backend=qwen-macbook = MacBook 명시 opt-in. unavailable 시 HTTP 503 + error_reason=macbook_unavailable. Gemma 자동 fallback 0. 변경 4 파일: - types/ask.ts: AskResponse 에 backend_requested / backend_used 필드 + SynthesisStatus 에 backend_unavailable literal 추가 - api.ts: ApiError 에 errorReason 추가, parseDetail 이 503 body 의 error_reason 흡수 (다른 endpoint 영향 0) - AskAnswer.svelte: backend_requested 명시 시 muted chip 표시 (default 호출은 미표시, 시각 noise 회피) - routes/ask/+page.svelte: selector dropdown + URL state + 503 분기 Non-Goals (별 PR): - localStorage / Settings preference (PR-DocSrv-Ask-Default-Pref-1) - SSE streaming, Tool-calling ReAct - shared secret / MacBook auth (Tailscale ACL only) 검증: docker compose build frontend 통과 (svelte-check + vite build). lint:tokens 본 PR 변경 위반 0 (기존 62 건은 baseline stale debt, settings/login). Spec: ~/.claude/plans/document-buzzing-codd.md Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/api.ts | 25 +++-- .../src/lib/components/ask/AskAnswer.svelte | 19 ++++ frontend/src/lib/types/ask.ts | 9 +- frontend/src/routes/ask/+page.svelte | 101 +++++++++++++++--- 4 files changed, 129 insertions(+), 25 deletions(-) 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}