Files
duocthu/packages/ui/src/CitationCard.tsx
T

163 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React from "react";
import type { Citation } from "@duoc-thu/shared-types";
import { BookOpen, FileText, ChevronRight, AlertTriangle, ExternalLink } from "lucide-react";
import { cn } from "./lib/utils";
interface CitationCardProps {
citation: Citation;
index: number;
isActive?: boolean;
onSelect?: () => void;
className?: string;
}
// Matches the real 19-field section_key enum the corpus is chunked on
// (apps/ai-service/rag/understanding.py SECTION_KEY_HINTS) — the previous
// map here used guessed keys (lieu_dung, tac_dung_phu, duoc_ly, qua_lieu,
// bao_quan) that never matched a real chunk, so every citation silently
// fell back to the raw slug instead of a readable label.
const SECTION_LABELS: Record<string, string> = {
ten_chung_quoc_te: "Tên chung quốc tế",
ten_thuong_mai: "Tên thương mại",
ma_atc: "Mã ATC",
loai_thuoc: "Phân loại thuốc",
dang_thuoc_va_ham_luong: "Dạng thuốc & Hàm lượng",
duoc_ly_va_co_che_tac_dung: "Dược lý & Cơ chế tác dụng",
chi_dinh: "Chỉ định",
chong_chi_dinh: "Chống chỉ định",
than_trong: "Thận trọng",
thoi_ky_mang_thai: "Thời kỳ mang thai",
thoi_ky_cho_con_bu: "Thời kỳ cho con bú",
tac_dung_khong_mong_muon: "Tác dụng không mong muốn (ADR)",
huong_dan_xu_tri_adr: "Hướng dẫn xử trí ADR",
lieu_luong_va_cach_dung: "Liều lượng & Cách dùng",
tuong_tac_thuoc: "Tương tác thuốc",
qua_lieu_va_xu_tri: "Quá liều & Xử trí",
do_on_dinh_va_bao_quan: "Độ ổn định & Bảo quản",
tuong_ky: "Tương kỵ",
thong_tin_quy_che: "Thông tin quy chế",
};
export function citationSectionLabel(sectionType: string): string {
return SECTION_LABELS[sectionType] ?? (sectionType.replace(/_/g, " ") || "Chuyên luận");
}
export function CitationCard({
citation,
index,
isActive = false,
onSelect,
className,
}: CitationCardProps) {
const sectionLabel = citationSectionLabel(citation.sectionType);
const pageRangeText = citation.sourcePageRange
? `Trang ${citation.sourcePageRange[0]}${
citation.sourcePageRange[1] && citation.sourcePageRange[1] !== citation.sourcePageRange[0]
? `${citation.sourcePageRange[1]}`
: ""
}`
: "Dược thư 2018";
return (
<div
onClick={onSelect}
id={`citation-card-${index}`}
tabIndex={0}
role="button"
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelect?.();
}
}}
className={cn(
"group relative flex flex-col gap-2 rounded-2xl border p-3.5 transition-all cursor-pointer select-none",
isActive
? "bg-accent-soft/30 border-border-accent shadow-elevated glass-beam-glow ring-2 ring-accent-primary/20"
: "bg-surface border-border-subtle hover:border-border-active hover:bg-surface-elevated shadow-sm",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span
className={cn(
"flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-[0.7rem] font-extrabold tracking-tight transition-colors",
isActive
? "bg-accent-primary text-txt-inverse shadow-sm"
: "bg-surface-elevated text-txt-secondary border border-border-subtle group-hover:border-border-active"
)}
>
{index}
</span>
<h4 className="m-0 truncate text-xs font-bold text-txt-primary">
{citation.drugName || "Chuyên luận Dược thư"}
</h4>
</div>
<div className="flex items-center gap-1 shrink-0">
<span className="inline-flex items-center gap-1 rounded-full border border-border-subtle bg-surface-elevated px-2 py-0.5 text-[0.65rem] font-semibold text-txt-secondary">
<BookOpen className="h-2.5 w-2.5 text-accent-primary" />
{pageRangeText}
</span>
</div>
</div>
{/* Section Badge */}
<div className="flex items-center gap-1.5">
<span className="inline-flex items-center gap-1 rounded-md bg-accent-soft px-2 py-0.5 text-[0.68rem] font-semibold text-accent-primary">
<FileText className="h-3 w-3" />
{sectionLabel}
</span>
</div>
{/* Snippet / Source Excerpt - the exact retrieved chunk text, verbatim */}
{citation.snippet && (
<div className="relative rounded-xl border border-border-subtle bg-surface-elevated/70 p-2.5 text-[0.75rem] leading-relaxed text-txt-secondary italic font-sans max-h-40 overflow-y-auto">
<span className="not-italic text-accent-primary font-bold mr-1">&ldquo;</span>
{citation.snippet}
<span className="not-italic text-accent-primary font-bold ml-1">&rdquo;</span>
</div>
)}
{/* Quarantine notice - only rendered when the source pipeline actually
flagged this chunk (table/formula lifted out of prose), never a
generic boilerplate line for an ordinary citation. */}
{citation.isQuarantined && (
<div className="rounded-xl border border-status-warning/40 bg-status-warning-bg/50 p-2.5 text-[0.7rem] leading-snug text-txt-primary space-y-1.5">
<p className="m-0 flex items-start gap-1.5">
<AlertTriangle className="h-3.5 w-3.5 text-status-warning shrink-0 mt-0.5" />
<span>{citation.quarantineNotice}</span>
</p>
{citation.sourceCropUrl ? (
<img
src={citation.sourceCropUrl}
alt={`Ảnh chụp bảng/công thức trang in ${citation.sourcePageRange[0]}`}
className="w-full rounded-lg border border-border-subtle"
/>
) : (
<a
href={`/api/pdf#page=${(citation.quarantinePhysicalPage ?? citation.physicalPage) + 1}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1 rounded-lg bg-surface px-2 py-1 font-semibold text-accent-primary hover:underline"
>
<ExternalLink className="h-3 w-3" />
Mở trang PDF gốc đ đi chiếu
</a>
)}
</div>
)}
<div className="flex items-center justify-end text-[0.65rem] font-medium text-accent-primary group-hover:translate-x-0.5 transition-transform">
<span>Xem trích dẫn đy đ</span>
<ChevronRight className="h-3 w-3 ml-0.5" />
</div>
</div>
);
}