bfdf33b442
- routes/ask/+page.svelte: URL-driven orchestrator, lastQuery guard (hydration 중복 호출 방지), citation scroll 연동 - lib/components/ask/AskAnswer: answer body + clickable [n] + confidence/status Badge + warning EmptyState (no_results_reason + /documents?q=<same> 역링크) - lib/components/ask/AskEvidence: span_text ONLY 렌더 (full_snippet 금지 룰 컴포넌트 주석에 박음) + active highlight + doc-group ordering 유지 - lib/components/ask/AskResults: inline 카드 (DocumentCard 의존 회피) - lib/types/ask.ts: backend AskResponse 스키마 1:1 매칭 - +layout.svelte: 탑 nav 질문 버튼 추가 - documents/+page.svelte: 검색바 옆 AI 답변 링크 (searchQuery 있을 때만) plan: ~/.claude/plans/quiet-meandering-nova.md (Phase 3.4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
92 lines
3.3 KiB
Svelte
92 lines
3.3 KiB
Svelte
<!--
|
|
AskEvidence.svelte — /ask 페이지 우측 sticky 패널.
|
|
|
|
⚠ 영구 룰 (Phase 3.4 plan):
|
|
`citation.full_snippet` 은 UI 에 직접 렌더 금지. debug 모드(`?debug=1`)
|
|
에서 hover tooltip 으로만 조건부 노출 가능.
|
|
|
|
이 규칙이 깨지면 backend span-precision UX 가치가 사라진다. 코드 리뷰에서
|
|
반드시 reject. span_text 만 본문으로 노출한다.
|
|
-->
|
|
<script lang="ts">
|
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
|
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
|
import { BookOpen } from 'lucide-svelte';
|
|
import type { AskResponse } from '$lib/types/ask';
|
|
|
|
interface Props {
|
|
data: AskResponse | null;
|
|
loading: boolean;
|
|
activeCitation: number | null;
|
|
registerCitation: (n: number, node: HTMLElement) => { destroy: () => void };
|
|
}
|
|
|
|
let { data, loading, activeCitation, registerCitation }: Props = $props();
|
|
|
|
let citations = $derived(data?.citations ?? []);
|
|
</script>
|
|
|
|
<section class="bg-surface border border-default rounded-card p-5">
|
|
<div class="flex items-start justify-between gap-3 mb-4">
|
|
<div>
|
|
<p class="text-[10px] font-semibold tracking-wider uppercase text-dim flex items-center gap-1.5">
|
|
<BookOpen size={12} /> Evidence Highlights
|
|
</p>
|
|
<h3 class="mt-1 text-sm font-semibold text-text">인용 근거</h3>
|
|
</div>
|
|
{#if data && !loading}
|
|
<Badge tone="neutral" size="sm">{citations.length}개</Badge>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="space-y-3">
|
|
{#each Array(2) as _}
|
|
<div class="border border-default rounded-card p-4 space-y-2">
|
|
<Skeleton w="w-24" h="h-3" />
|
|
<Skeleton w="w-full" h="h-3" />
|
|
<Skeleton w="w-5/6" h="h-3" />
|
|
<Skeleton w="w-3/4" h="h-3" />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else if citations.length === 0}
|
|
<EmptyState title="표시할 근거가 없습니다." class="py-6" />
|
|
{:else}
|
|
<div class="space-y-3">
|
|
{#each citations as citation (citation.n)}
|
|
{@const isActive = activeCitation === citation.n}
|
|
<article
|
|
class="border rounded-card p-4 transition-colors {isActive
|
|
? 'border-accent ring-2 ring-accent/20 bg-accent/5'
|
|
: 'border-default'}"
|
|
use:registerCitation={citation.n}
|
|
>
|
|
<div class="flex items-start gap-2">
|
|
<span class="text-accent font-bold text-sm shrink-0">[{citation.n}]</span>
|
|
<div class="flex-1 min-w-0">
|
|
<strong class="block text-sm text-text truncate">
|
|
{citation.title ?? `문서 ${citation.doc_id}`}
|
|
</strong>
|
|
{#if citation.section_title}
|
|
<p class="mt-0.5 text-xs text-dim truncate">{citation.section_title}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ⚠ span_text 만 렌더. full_snippet 금지 -->
|
|
<p class="mt-3 text-sm leading-relaxed text-text whitespace-pre-wrap">
|
|
{citation.span_text}
|
|
</p>
|
|
|
|
<div class="mt-3 flex gap-2 text-[10px] text-dim">
|
|
<span>relevance {citation.relevance.toFixed(2)}</span>
|
|
<span>rerank {citation.rerank_score.toFixed(2)}</span>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|