- 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>
79 lines
2.7 KiB
Svelte
79 lines
2.7 KiB
Svelte
<!--
|
|
AskResults.svelte — /ask 페이지 하단 패널.
|
|
|
|
검색 결과 리스트. DocumentCard 재사용 X — SearchResult 필드 셋이 달라서
|
|
의존성 리스크 회피. inline 간단 카드로 title/score/snippet/section_title 표시.
|
|
클릭 시 `/documents/{id}` 로 이동.
|
|
-->
|
|
<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 { FileText } from 'lucide-svelte';
|
|
import type { AskResponse } from '$lib/types/ask';
|
|
|
|
interface Props {
|
|
data: AskResponse | null;
|
|
loading: boolean;
|
|
}
|
|
|
|
let { data, loading }: Props = $props();
|
|
|
|
let results = $derived(data?.results ?? []);
|
|
</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">
|
|
<FileText size={12} /> Search Results
|
|
</p>
|
|
<h3 class="mt-1 text-sm font-semibold text-text">검색 결과</h3>
|
|
</div>
|
|
{#if data && !loading}
|
|
<Badge tone="neutral" size="sm">{data.total}개</Badge>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="space-y-3">
|
|
{#each Array(5) as _}
|
|
<div class="border border-default rounded-card p-4 space-y-2">
|
|
<Skeleton w="w-2/3" h="h-4" />
|
|
<Skeleton w="w-full" h="h-3" />
|
|
<Skeleton w="w-4/5" h="h-3" />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else if results.length === 0}
|
|
<EmptyState title="검색 결과가 없습니다." class="py-6" />
|
|
{:else}
|
|
<div class="space-y-3">
|
|
{#each results as result (result.id)}
|
|
<a
|
|
href={`/documents/${result.id}`}
|
|
class="block border border-default rounded-card p-4 hover:border-accent hover:bg-surface-hover transition-colors"
|
|
>
|
|
<div class="flex items-start justify-between gap-3">
|
|
<strong class="text-sm text-text flex-1 min-w-0 truncate">
|
|
{result.title ?? `문서 ${result.id}`}
|
|
</strong>
|
|
<div class="flex gap-1.5 text-[10px] text-dim shrink-0">
|
|
<span>score {result.score.toFixed(2)}</span>
|
|
{#if result.rerank_score != null}
|
|
<span>rerank {result.rerank_score.toFixed(2)}</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{#if result.section_title}
|
|
<p class="mt-1 text-xs text-dim truncate">{result.section_title}</p>
|
|
{/if}
|
|
{#if result.snippet}
|
|
<p class="mt-2 text-xs text-dim line-clamp-2">{result.snippet}</p>
|
|
{/if}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|