import * as React from "react"; import { Building2, Info } from "lucide-react"; import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PolicyVerdictBadge } from "./policy-ai-primitives"; import type { PolicyBankVerdict, PolicyCitationItem, PolicyQueryContext, PolicyRankedBankItem, PolicyVerdict, } from "./policy-ai-primitives"; // --------------------------------------------------------------------------- // PolicySingleBankAnswer props // --------------------------------------------------------------------------- export interface PolicySingleBankAnswerProps { bankName: string; verdict: PolicyVerdict; /** Prose answer text. May contain inline [N] citation markers. */ answer: string; citations: PolicyCitationItem[]; /** * Optional query context — when provided, policy type and categories are * rendered as an Info icon tooltip inside the bank header. */ queryContext?: PolicyQueryContext; className?: string; } // --------------------------------------------------------------------------- // PolicyComparisonTable props // --------------------------------------------------------------------------- export interface PolicySummaryCounts { yes: number; softNo: number; no: number; total: number; } export interface PolicyComparisonTableProps { /** Column headers — one per policy category queried. */ categories: string[]; banks: PolicyBankVerdict[]; summaryCounts: PolicySummaryCounts; citations: PolicyCitationItem[]; /** Optional query context — shown as Info icon tooltip in the summary header. */ queryContext?: PolicyQueryContext; className?: string; } // --------------------------------------------------------------------------- // PolicyRankedList props // --------------------------------------------------------------------------- export interface PolicyRankedListProps { /** Categories used in the TOPSIS ranking (shown in header). */ categories: string[]; banks: PolicyRankedBankItem[]; citations: PolicyCitationItem[]; /** Optional query context — shown as Info icon tooltip in the ranking header. */ queryContext?: PolicyQueryContext; className?: string; } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- /** Bank initial avatar — consistent across all response formats. */ function BankAvatar({ name }: { name: string }) { return ( ); } /** Info icon with a tooltip. Pass `label` as the tooltip text. */ function QueryContextInfo({ label }: { label: string }) { return ( ); } /** Format a PolicyQueryContext into a human-readable tooltip label. */ function queryContextLabel(context: PolicyQueryContext): string { return [ context.policyType, ...context.categories, context.queryType.replace(/_/g, " "), ].join(" · "); } /** Inline [N] badge that opens a Popover with the source citation details. */ function CitationBadge({ citation }: { citation: PolicyCitationItem }) { return ( {citation.index} } />
{citation.bankName} {citation.category}

{citation.excerpt}

); } /** Renders answer prose with [N] markers replaced by inline CitationBadge. */ function AnswerWithCitations({ answer, citations, }: { answer: string; citations: PolicyCitationItem[]; }) { const citationMap = new Map(citations.map((c) => [c.index, c])); const parts = answer.split(/(\[\d+\])/g); return (

{parts.map((part, i) => { const match = part.match(/^\[(\d+)\]$/); if (match) { const citation = citationMap.get(parseInt(match[1], 10)); if (citation) return ; return ( {part} ); } return {part}; })}

); } // --------------------------------------------------------------------------- // PolicySingleBankAnswer (Molecule) — Type A response // --------------------------------------------------------------------------- /** * Displays a single-bank policy answer (Type A RAG retrieval). * Shows the bank name, verdict badge, AI-generated prose with inline citation * markers, and a collapsible citation panel listing source excerpts. * * @example * */ export function PolicySingleBankAnswer({ bankName, verdict, answer, citations, queryContext, className, }: PolicySingleBankAnswerProps) { return (
{/* Bank header */}
{bankName} {queryContext && ( )}
{/* Answer prose */}
); } // --------------------------------------------------------------------------- // PolicyComparisonTable (Molecule) — Type B/C cross-bank response // --------------------------------------------------------------------------- type VerdictFilter = "all" | PolicyVerdict; /** * Displays a cross-bank policy comparison matrix (Type B/C retrieval). * Shows a summary count row, search input, verdict filter tabs, a table * with one row per bank and one column per policy category, and a * collapsible citation panel at the bottom. * * @example * */ export function PolicyComparisonTable({ categories, banks, summaryCounts, citations, queryContext, className, }: PolicyComparisonTableProps) { const [search, setSearch] = React.useState(""); const [filter, setFilter] = React.useState("all"); const filtered = React.useMemo(() => { let result = banks; if (search.trim()) { const q = search.toLowerCase(); result = result.filter((b) => b.bankName.toLowerCase().includes(q)); } if (filter !== "all") { result = result.filter((b) => b.verdict === filter); } return result; }, [banks, search, filter]); return (
{/* Summary counts */}
{/* Search input */}
setSearch(e.target.value)} className="h-8 text-sm" />
{/* Verdict filter tabs */}
setFilter(v as VerdictFilter)} > All Yes Soft No No
{/* Bank table */}
{categories.map((cat) => ( ))} {filtered.length === 0 ? ( ) : ( filtered.map((bank) => ( {categories.map((cat) => ( ))} )) )}
Bank {cat}
No banks match your search.
{bank.bankName} {bank.details && ( {bank.details} )}
); } // --------------------------------------------------------------------------- // Internal: verdict → progress bar colour map (track + indicator) // --------------------------------------------------------------------------- const VERDICT_SCORE_COLORS: Record< string, { track: string; indicator: string } > = { yes: { track: "bg-success/20", indicator: "bg-success" }, soft_no: { track: "bg-warning/20", indicator: "bg-warning" }, no: { track: "bg-destructive/20", indicator: "bg-destructive" }, unknown: { track: "bg-muted", indicator: "bg-muted-foreground" }, }; // --------------------------------------------------------------------------- // PolicyRankedList (Molecule) — Type C ranking response // --------------------------------------------------------------------------- /** * Displays a TOPSIS-ranked list of banks (Type C retrieval). * Each bank row shows rank number, name, a colour-coded score progress bar, * percentage, verdict badge, and key policy highlights as bullet points. * A collapsible citation panel appears at the bottom. * * @example * */ export function PolicyRankedList({ categories, banks, citations, queryContext, className, }: PolicyRankedListProps) { return (
{/* Header */}
Ranked:
{categories.map((cat, i) => ( {i > 0 && ( + )} {cat} ))}
{banks.length} banks {queryContext && ( )}
{/* Ranked rows */}
{banks.map((bank) => { const scorePercent = Math.round(bank.score * 100); return (
#{bank.rank}
{bank.bankName}
{scorePercent}%
{bank.highlights.length > 0 && (
    {bank.highlights.map((h, i) => (
  • {h}
  • ))}
)}
); })}
); }