diff --git a/frontend/src/lib/components/ask/AskAnswer.svelte b/frontend/src/lib/components/ask/AskAnswer.svelte new file mode 100644 index 0000000..82d7dcd --- /dev/null +++ b/frontend/src/lib/components/ask/AskAnswer.svelte @@ -0,0 +1,144 @@ + + + +
+ +
+
+

+ AI Answer +

+

근거 기반 답변

+
+ + {#if data && !loading} +
+ + 신뢰도 {confidenceLabel(data.confidence)} + + + {STATUS_LABEL[data.synthesis_status]} + + {#if data.synthesis_ms > 0} + + {Math.round(data.synthesis_ms)}ms + + {/if} +
+ {/if} +
+ + + {#if loading} +
+ + + +

+ + 근거 기반 답변 생성 중… 약 15초 소요 +

+
+ {:else if showAnswer && data} +
+ {#each tokens as tok} + {#if tok.type === 'cite'} + + {:else} + {tok.value} + {/if} + {/each} +
+ {:else if showWarning && data} + + + + {/if} +
diff --git a/frontend/src/lib/components/ask/AskEvidence.svelte b/frontend/src/lib/components/ask/AskEvidence.svelte new file mode 100644 index 0000000..55733c4 --- /dev/null +++ b/frontend/src/lib/components/ask/AskEvidence.svelte @@ -0,0 +1,91 @@ + + + +
+
+
+

+ Evidence Highlights +

+

인용 근거

+
+ {#if data && !loading} + {citations.length}개 + {/if} +
+ + {#if loading} +
+ {#each Array(2) as _} +
+ + + + +
+ {/each} +
+ {:else if citations.length === 0} + + {:else} +
+ {#each citations as citation (citation.n)} + {@const isActive = activeCitation === citation.n} +
+
+ [{citation.n}] +
+ + {citation.title ?? `문서 ${citation.doc_id}`} + + {#if citation.section_title} +

{citation.section_title}

+ {/if} +
+
+ + +

+ {citation.span_text} +

+ +
+ relevance {citation.relevance.toFixed(2)} + rerank {citation.rerank_score.toFixed(2)} +
+
+ {/each} +
+ {/if} +
diff --git a/frontend/src/lib/components/ask/AskResults.svelte b/frontend/src/lib/components/ask/AskResults.svelte new file mode 100644 index 0000000..1272988 --- /dev/null +++ b/frontend/src/lib/components/ask/AskResults.svelte @@ -0,0 +1,78 @@ + + + +
+
+
+

+ Search Results +

+

검색 결과

+
+ {#if data && !loading} + {data.total}개 + {/if} +
+ + {#if loading} +
+ {#each Array(5) as _} +
+ + + +
+ {/each} +
+ {:else if results.length === 0} + + {:else} + + {/if} +
diff --git a/frontend/src/lib/types/ask.ts b/frontend/src/lib/types/ask.ts new file mode 100644 index 0000000..4302ef1 --- /dev/null +++ b/frontend/src/lib/types/ask.ts @@ -0,0 +1,64 @@ +/** + * Phase 3.4: `/api/search/ask` 응답 타입. + * + * Backend Pydantic 모델 (`app/api/search.py::AskResponse`) 과 1:1 매칭. + * 필드 변경 시 양쪽 동시 수정 필수. + */ + +export type SynthesisStatus = + | 'completed' + | 'timeout' + | 'skipped' + | 'no_evidence' + | 'parse_failed' + | 'llm_error'; + +export type Confidence = 'high' | 'medium' | 'low'; + +export interface Citation { + n: number; + chunk_id: number | null; + doc_id: number; + title: string | null; + section_title: string | null; + /** LLM이 추출한 50~300자 핵심 span. UI에서 이것만 노출. */ + span_text: string; + /** + * 원본 800자 window. + * + * ⚠ UI 기본 경로에서 절대 렌더 금지. debug 모드에서 hover tooltip 용도로만 + * 조건부 노출 가능. full_snippet을 보여주면 backend span-precision UX + * 가치가 사라진다 (plan §Evidence 표시 규칙). + */ + full_snippet: string; + relevance: number; + rerank_score: number; +} + +export interface SearchResult { + id: number; + title: string | null; + ai_domain: string | null; + ai_summary: string | null; + file_format: string; + score: number; + snippet: string | null; + match_reason: string | null; + chunk_id: number | null; + chunk_index: number | null; + section_title: string | null; + rerank_score: number | null; +} + +export interface AskResponse { + results: SearchResult[]; + ai_answer: string | null; + citations: Citation[]; + synthesis_status: SynthesisStatus; + synthesis_ms: number; + confidence: Confidence | null; + refused: boolean; + no_results_reason: string | null; + query: string; + total: number; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 2356889..6ca056d 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -80,6 +80,7 @@
+ diff --git a/frontend/src/routes/ask/+page.svelte b/frontend/src/routes/ask/+page.svelte new file mode 100644 index 0000000..730e5c9 --- /dev/null +++ b/frontend/src/routes/ask/+page.svelte @@ -0,0 +1,150 @@ + + + + + 질문 - PKM + + +
+ +
+
+
+ + +
+
+
+ + +
+ {#if !queryInput && !loading && !data} +
+ +
+ {:else} +
+ +
+ + +
+ + +
+ +
+
+ {/if} +
+
diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index 5076069..b4d7f46 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation'; import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; - import { Info, List, LayoutGrid, ChevronLeft, X, Plus, Trash2, Tag, FolderTree, Rows3, Rows2 } from 'lucide-svelte'; + import { Info, List, LayoutGrid, ChevronLeft, X, Plus, Trash2, Tag, FolderTree, Rows3, Rows2, Sparkles } from 'lucide-svelte'; import DocumentCard from '$lib/components/DocumentCard.svelte'; import DocumentTable from '$lib/components/DocumentTable.svelte'; import DocumentViewer from '$lib/components/DocumentViewer.svelte'; @@ -441,6 +441,15 @@ + {#if searchQuery.trim()} + + AI 답변 + + {/if}