Files
hyungi_document_server/frontend/src/lib/components/ask/AskResults.svelte
Hyungi Ahn bfdf33b442 feat(frontend): Phase 3.4 Ask pipeline UI (/ask 3-panel)
- 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>
2026-04-09 08:45:24 +09:00

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>