"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import type { ReactNode } from "react"; import type { Prisma } from "@prisma/client"; import { Download, Trash2, Eye, MoreVertical, Loader2, FileText, FileSpreadsheet, MessageSquare, AlertTriangle, X, Search, Info, Network, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogClose, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { deleteContract, getContracts, deleteAllContractsAction, } from "@/features/contracts/api/contract.action"; import { toast } from "sonner"; import { ContractChatModal } from "@/features/contracts/components/modals/contract-chat-modal"; import { ContractProofModal } from "@/features/contracts/components/modals/contract-proof-modal"; import { stripMarkdown, exportToCSV, exportToPDF, } from "@/features/contracts/utils/export.utils"; interface Contract { id: string; fileName: string; fileSize: number; mimeType: string; status: string; createdAt: string; // ISO string fileUrl: string; title?: string | null; type?: string | null; provider?: string | null; policyNumber?: string | null; startDate?: string | null; endDate?: string | null; premium?: number | null; summary?: string | null; keyPoints?: Prisma.JsonValue | null; extractedText?: string | null; ragChunkCount?: number; isRagged?: boolean; } interface ExplainabilityEntry { field: string; why: string; sourceSnippet: string; sourceHints?: { page?: string | null; section?: string | null; confidence?: number | null; }; } interface ContractKeyPoints { guarantees?: string[]; exclusions?: string[]; franchise?: string | null; importantDates?: string[]; explainability?: ExplainabilityEntry[]; aiMeta?: { language?: string | null; premiumCurrency?: string | null; }; } const isContractKeyPoints = (value: unknown): value is ContractKeyPoints => { return typeof value === "object" && value !== null && !Array.isArray(value); }; export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) { const emitNotificationRefresh = () => { window.dispatchEvent(new Event("notifications:refresh")); const channel = new BroadcastChannel("notifications-channel"); channel.postMessage({ type: "notifications:refresh" }); channel.close(); }; const [contracts, setContracts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); const [isDeletingAll, setIsDeletingAll] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); const [selectedContract, setSelectedContract] = useState( null, ); const [askOpen, setAskOpen] = useState(false); const [chatContract, setChatContract] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [contractToDelete, setContractToDelete] = useState( null, ); const [deleteAllDialogOpen, setDeleteAllDialogOpen] = useState(false); const [invalidContractDialogOpen, setInvalidContractDialogOpen] = useState(false); const [invalidContractReason, setInvalidContractReason] = useState(""); const [invalidContractFileName, setInvalidContractFileName] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); const [proofModalOpen, setProofModalOpen] = useState(false); const [proofData, setProofData] = useState<{ fieldKey: string; field: string; sourceSnippet: string; confidence: number | null; page: string | null; section: string | null; lineNumber: number | null; contextStartLine: number | null; context: string[]; resolutionMode: "exact" | "fuzzy" | "fallback"; } | null>(null); const ENTITY_REGEX = /(\*\*[^*]+\*\*)|([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})|(\+?\d[\d\s().-]{7,}\d)|(\b\d{4}-\d{2}-\d{2}\b)|(\b\d{1,2}[\/.-]\d{1,2}[\/.-]\d{2,4}\b)|((?:€|\$|£)\s?\d[\d\s.,]*\d|\b\d[\d\s.,]*\d\s?(?:EUR|USD|TND|MAD|DZD|GBP)\b)|(\b\d+(?:[.,]\d+)?\s?%\b)|(\b\d{2,}(?:[.,]\d+)?\b)/g; const renderHighlightedText = ( text: string, keyPrefix: string, ): ReactNode[] => { const nodes: ReactNode[] = []; let lastIndex = 0; let match: RegExpExecArray | null; let index = 0; while ((match = ENTITY_REGEX.exec(text)) !== null) { const [token] = match; const start = match.index; if (start > lastIndex) { nodes.push(text.slice(lastIndex, start)); } const tokenKey = `${keyPrefix}-token-${index}`; index += 1; if (token.startsWith("**") && token.endsWith("**")) { nodes.push( {token.slice(2, -2)} , ); } else if (match[2]) { nodes.push( {token} , ); } else if (match[3]) { nodes.push( {token} , ); } else if (match[4] || match[5]) { nodes.push( {token} , ); } else if (match[6] || match[7]) { nodes.push( {token} , ); } else { nodes.push( {token} , ); } lastIndex = start + token.length; } if (lastIndex < text.length) { nodes.push(text.slice(lastIndex)); } return nodes; }; const renderRichParagraphs = ( text: string, keyPrefix: string, ): ReactNode[] => { const cleaned = text.replace(/\u2022/g, "•").trim(); if (!cleaned) return []; const lines = cleaned .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0); return lines.map((line, idx) => { const isList = /^[-*•]\s+/.test(line); const content = line.replace(/^[-*•]\s+/, ""); return (
{isList && ( )}

{renderHighlightedText(content, `${keyPrefix}-${idx}`)}

); }); }; // Helper: Find source snippet in extracted text and get line number + context const findSourceSnippetInText = ( sourceSnippet: string, extractedText: string | null | undefined, fieldKey?: string, fallbackNeedles?: string[], ): { lineNumber: number | null; contextStartLine: number | null; context: string[]; resolutionMode: "exact" | "fuzzy" | "fallback"; } => { if (!extractedText || !sourceSnippet) { return { lineNumber: null, contextStartLine: null, context: [], resolutionMode: "fallback", }; } const normalizeForMatch = (value: string) => value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim() .replace(/\s+/g, " "); const buildDateNeedles = (isoDate: string): string[] => { if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return [isoDate]; const [year, month, day] = isoDate.split("-"); const monthIndex = Number(month) - 1; const frMonths = [ "janvier", "fevrier", "mars", "avril", "mai", "juin", "juillet", "aout", "septembre", "octobre", "novembre", "decembre", ]; const enMonths = [ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", ]; const numericDay = String(Number(day)); const numericMonth = String(Number(month)); return [ isoDate, `${day}/${month}/${year}`, `${day}-${month}-${year}`, `${numericDay}/${numericMonth}/${year}`, `${numericDay}-${numericMonth}-${year}`, `${numericDay} ${frMonths[monthIndex]} ${year}`, `${numericDay} ${enMonths[monthIndex]} ${year}`, ]; }; const tokenize = (value: string) => normalizeForMatch(value) .split(" ") .filter((token) => token.length >= 3 || /^\d+$/.test(token)); const lines = extractedText.split(/\r?\n/); const normalizedLines = lines.map((line) => normalizeForMatch(line)); const snippetNormalized = normalizeForMatch(sourceSnippet); const snippetTokens = tokenize(sourceSnippet); const resolvedFallbackNeedles = [...(fallbackNeedles ?? [])]; if ( (fieldKey === "startDate" || fieldKey === "endDate") && fallbackNeedles?.[0] ) { resolvedFallbackNeedles.push(...buildDateNeedles(fallbackNeedles[0])); } const needleTokens = resolvedFallbackNeedles.flatMap((needle) => tokenize(needle), ); const snippetNeedle = snippetNormalized.slice(0, 90); const isDateField = fieldKey === "startDate" || fieldKey === "endDate"; // Stage 1: strict exact matching to avoid false positives. const exactIndex = normalizedLines.findIndex((line) => snippetNeedle.length >= 25 ? line.includes(snippetNeedle) : false, ); if (exactIndex >= 0) { const contextStart = Math.max(0, exactIndex - 3); const contextEnd = Math.min(lines.length, exactIndex + 4); return { lineNumber: exactIndex + 1, contextStartLine: contextStart + 1, context: lines.slice(contextStart, contextEnd), resolutionMode: "exact", }; } // Stage 2: deterministic date/value lookup for date fields. if (isDateField && resolvedFallbackNeedles.length > 0) { const normalizedNeedles = resolvedFallbackNeedles .map((value) => normalizeForMatch(value)) .filter((value) => value.length >= 6); const dateCandidates = normalizedLines .map((line, idx) => { const needleHits = normalizedNeedles.filter((needle) => line.includes(needle), ).length; if (needleHits === 0) return null; const genericTemporalBoost = /(effet|debut|start|commence|expiration|expire|echeance|fin|periode|garantie|validite|prend effet)/.test( line, ) ? 15 : 0; const fieldSpecificBoost = fieldKey === "endDate" ? /(expiration|expire|echeance|fin|expire le|date d expiration)/.test( line, ) ? 28 : 0 : /(prend effet|debut|commence|start date|date d effet)/.test( line, ) ? 24 : 0; const score = needleHits * 40 + genericTemporalBoost + fieldSpecificBoost; return { idx, score }; }) .filter((item): item is { idx: number; score: number } => item !== null) .sort((a, b) => b.score - a.score); const bestDateCandidate = dateCandidates[0]; if (bestDateCandidate) { const contextStart = Math.max(0, bestDateCandidate.idx - 3); const contextEnd = Math.min(lines.length, bestDateCandidate.idx + 4); return { lineNumber: bestDateCandidate.idx + 1, contextStartLine: contextStart + 1, context: lines.slice(contextStart, contextEnd), resolutionMode: "exact", }; } } const windows: { start: number; end: number; text: string }[] = []; for (let start = 0; start < normalizedLines.length; start++) { for (let length = 1; length <= 4; length++) { const end = start + length; if (end > normalizedLines.length) break; windows.push({ start, end, text: normalizedLines.slice(start, end).join(" "), }); } } let bestMatch: { start: number; score: number } | null = null; const hasDateHint = isDateField; const hasPremiumHint = fieldKey === "premium"; for (const window of windows) { const windowTokens = window.text.split(" ").filter(Boolean); const tokenSet = new Set(windowTokens); let score = 0; if ( snippetNormalized && window.text.includes(snippetNormalized.slice(0, 60)) ) { score += 45; } if (snippetTokens.length > 0) { const overlap = snippetTokens.filter((token) => tokenSet.has(token), ).length; score += (overlap / snippetTokens.length) * 40; } if (needleTokens.length > 0) { const overlap = needleTokens.filter((token) => tokenSet.has(token), ).length; score += (overlap / needleTokens.length) * 35; } if (hasDateHint) { const containsYear = /\b(19|20)\d{2}\b/.test(window.text); const temporalKeywords = /(effet|debut|start|commence|expiration|expire|echeance|fin|periode|garantie|validite|prend effet)/.test( window.text, ); if (containsYear) score += 12; if (temporalKeywords) score += 20; if ( fieldKey === "endDate" && /(expiration|expire|echeance|fin|date d effet|expire le)/.test( window.text, ) ) { score += 22; } } if (hasPremiumHint) { if ( /(prime|cotisation|premium|montant|cout|cost|total)/.test(window.text) ) { score += 18; } if (/(eur|usd|tnd|mad|dzd|gbp|€|\$|£)/.test(window.text)) { score += 12; } } if (!bestMatch || score > bestMatch.score) { bestMatch = { start: window.start, score }; } } if (bestMatch && bestMatch.score >= 32) { const contextStart = Math.max(0, bestMatch.start - 3); const contextEnd = Math.min(lines.length, bestMatch.start + 4); const context = lines.slice(contextStart, contextEnd); return { lineNumber: bestMatch.start + 1, contextStartLine: contextStart + 1, context, resolutionMode: "fuzzy", }; } return { lineNumber: null, contextStartLine: null, context: [], resolutionMode: "fallback", }; }; // Handler: Open proof modal when clicking info icon const handleProofClick = ( fieldKey: string, entry: ExplainabilityEntry, fieldValueFallback?: string, sectionHint?: string | null, ) => { const proofHints = [fieldValueFallback, sectionHint] .filter((hint): hint is string => Boolean(hint && hint.trim())) .map((hint) => hint.trim()); const { lineNumber, contextStartLine, context, resolutionMode } = findSourceSnippetInText( entry.sourceSnippet, selectedContract?.extractedText, fieldKey, proofHints, ); setProofData({ fieldKey, field: entry.field, sourceSnippet: entry.sourceSnippet, confidence: entry.sourceHints?.confidence ?? null, page: entry.sourceHints?.page ?? null, section: entry.sourceHints?.section ?? null, lineNumber, contextStartLine, context, resolutionMode, }); setProofModalOpen(true); }; const getExplainabilityItems = ( contract: Contract | null, ): ExplainabilityEntry[] => { const raw = isContractKeyPoints(contract?.keyPoints) ? contract.keyPoints.explainability : undefined; if (!Array.isArray(raw)) return []; return raw .map((item) => ({ field: String(item?.field ?? "").trim(), why: String(item?.why ?? "").trim(), sourceSnippet: String(item?.sourceSnippet ?? "").trim(), sourceHints: { page: item?.sourceHints?.page === null || item?.sourceHints?.page === undefined ? null : String(item?.sourceHints?.page), section: item?.sourceHints?.section === null || item?.sourceHints?.section === undefined ? null : String(item?.sourceHints?.section), confidence: typeof item?.sourceHints?.confidence === "number" ? item.sourceHints.confidence : null, }, })) .filter((item: ExplainabilityEntry) => { return item.field && item.why && item.sourceSnippet; }) .slice(0, 24); }; const explainabilityItems = useMemo( () => getExplainabilityItems(selectedContract), [selectedContract], ); const getProofForField = useCallback( (fieldKey: string): ExplainabilityEntry | null => { const normalize = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, ""); const aliases: Record = { title: ["title", "contracttitle"], provider: ["provider", "institution", "insurer", "company"], policyNumber: [ "policynumber", "policyno", "contractnumber", "reference", ], startDate: ["startdate", "effectivedate", "start"], endDate: ["enddate", "expirationdate", "expirydate", "maturitydate"], premium: ["premium", "amount", "totalcost", "fee", "price"], }; const normalizedTargets = new Set( (aliases[fieldKey] ?? [fieldKey]).map((item) => normalize(item)), ); const ranked = explainabilityItems .map((entry) => { const normalizedField = normalize(entry.field); const isExact = normalizedTargets.has(normalizedField); const isPartial = Array.from(normalizedTargets).some( (target) => normalizedField.includes(target) || target.includes(normalizedField), ); const confidence = entry.sourceHints?.confidence ?? 0; const score = (isExact ? 100 : 0) + (isPartial ? 40 : 0) + confidence; return { entry, score }; }) .filter((item) => item.score > 0) .sort((a, b) => b.score - a.score); const match = ranked[0]?.entry; return match ?? null; }, [explainabilityItems], ); const getFieldValueForProof = useCallback( (fieldKey: string): string => { if (!selectedContract) return ""; switch (fieldKey) { case "title": return selectedContract.title ?? ""; case "provider": return selectedContract.provider ?? ""; case "policyNumber": return selectedContract.policyNumber ?? ""; case "startDate": return selectedContract.startDate ?? ""; case "endDate": return selectedContract.endDate ?? ""; case "premium": return selectedContract.premium ? selectedContract.premium.toString() : ""; default: return ""; } }, [selectedContract], ); const resolvePremiumCurrency = useCallback((contract: Contract | null) => { if (!contract) return null; const fromMeta = String( (isContractKeyPoints(contract.keyPoints) ? contract.keyPoints.aiMeta?.premiumCurrency : null) ?? "", ).trim(); if (fromMeta) return fromMeta; const explainability = getExplainabilityItems(contract); const premiumEvidence = explainability.find((item) => { const normalized = item.field.toLowerCase(); return normalized.includes("premium") || normalized.includes("prime"); }); const currencyRegex = /(EUR|USD|TND|MAD|DZD|GBP|CHF|CAD|AUD|€|\$|£)/i; const snippetCurrency = premiumEvidence?.sourceSnippet.match(currencyRegex)?.[0]; if (snippetCurrency) return snippetCurrency.toUpperCase(); const textCurrency = contract.extractedText?.match(currencyRegex)?.[0]; if (textCurrency) return textCurrency.toUpperCase(); return null; }, []); const formatPremiumWithSourceCurrency = useCallback( (contract: Contract | null) => { if ( !contract || contract.premium === null || contract.premium === undefined ) { return "N/A"; } const currency = resolvePremiumCurrency(contract) ?? "EUR"; const numeric = new Intl.NumberFormat("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(contract.premium); if (currency === "€" || currency === "$" || currency === "£") { return `${currency}${numeric}`; } return `${numeric} ${currency}`; }, [resolvePremiumCurrency], ); const handleOpenFieldProof = (fieldKey: string, label: string) => { const fieldValueFallback = getFieldValueForProof(fieldKey); const proof = getProofForField(fieldKey); if (!proof) { if (fieldValueFallback) { handleProofClick( fieldKey, { field: label, why: "Resolved using extracted field value fallback when explicit explainability evidence is missing.", sourceSnippet: fieldValueFallback, sourceHints: { confidence: null, page: null, section: null, }, }, fieldValueFallback, null, ); return; } toast.error(`No proof snippet is available for ${label}`); return; } handleProofClick( fieldKey, proof, fieldValueFallback, proof.sourceHints?.section ?? null, ); }; const loadContracts = useCallback( async (options?: { silent?: boolean; search?: string }) => { const isSilentRefresh = options?.silent ?? false; if (!isSilentRefresh) { setIsLoading(true); } try { const result = await getContracts({ search: options?.search?.trim() || undefined, }); if (result.success && Array.isArray(result.contracts)) { setContracts(result.contracts); } } catch (error) { console.error("Failed to load contracts:", error); toast.error("Failed to load contracts"); } finally { if (!isSilentRefresh) { setIsLoading(false); } } }, [], ); useEffect(() => { const timeoutId = window.setTimeout(() => { setDebouncedSearchQuery(searchQuery.trim()); }, 300); return () => window.clearTimeout(timeoutId); }, [searchQuery]); useEffect(() => { void loadContracts({ search: debouncedSearchQuery }); }, [loadContracts, refreshTrigger, debouncedSearchQuery]); useEffect(() => { const hasPendingContracts = contracts.some( (contract) => contract.status === "PROCESSING" || contract.status === "UPLOADED", ); if (!hasPendingContracts) { return; } const intervalId = window.setInterval(() => { void loadContracts({ silent: true, search: debouncedSearchQuery, }); }, 7000); return () => window.clearInterval(intervalId); }, [contracts, loadContracts, debouncedSearchQuery]); const handleDelete = async (id: string) => { setDeletingId(id); try { const result = await deleteContract(id); if (result.success) { setContracts(contracts.filter((c) => c.id !== id)); toast.success("Contract deleted successfully"); emitNotificationRefresh(); } else { toast.error(result.error || "Failed to delete contract"); } } catch (error) { toast.error( error instanceof Error ? error.message : "Unknown error occurred", ); } finally { setDeletingId(null); } }; const requestDeleteContract = (contract: Contract) => { setContractToDelete(contract); setDeleteDialogOpen(true); }; const confirmDeleteContract = async () => { if (!contractToDelete) return; await handleDelete(contractToDelete.id); setDeleteDialogOpen(false); setContractToDelete(null); }; const handleDeleteAll = async () => { setIsDeletingAll(true); try { const result = await deleteAllContractsAction(); if (result.success) { setContracts([]); toast.success( `Deleted ${result.deletedCount ?? 0} contract${(result.deletedCount ?? 0) === 1 ? "" : "s"}.`, ); emitNotificationRefresh(); } else { toast.error(result.error || "Failed to delete all contracts"); } } catch (error) { toast.error( error instanceof Error ? error.message : "Unknown error occurred", ); } finally { setIsDeletingAll(false); setDeleteAllDialogOpen(false); } }; const handleOpenDetails = (contract: Contract) => { setSelectedContract(contract); setDetailsOpen(true); }; const handleOpenAsk = (contract: Contract) => { setChatContract(contract); setAskOpen(true); }; const formatDate = (date: Date | string) => { const dateObj = typeof date === "string" ? new Date(date) : date; return dateObj.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); }; const formatFileSize = (bytes: number) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; }; const getFileIcon = (mimeType: string) => { if (mimeType.startsWith("image/")) { return "🖼️"; } if (mimeType === "application/pdf") { return "📄"; } return "📋"; }; const getStatusColor = (status: string) => { switch (status) { case "COMPLETED": return "text-green-500 dark:text-green-400 bg-green-50 dark:bg-green-950/30"; case "PROCESSING": return "text-blue-500 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/30"; case "UPLOADED": return "text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30"; case "FAILED": return "text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-950/30"; default: return "text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-950/30"; } }; if (isLoading) { return (
Loading contracts...
); } if (contracts.length === 0 && !debouncedSearchQuery) { return null; } return ( <> {invalidContractReason && (

Invalid contract upload detected

{invalidContractFileName ? `${invalidContractFileName}: ` : ""} {invalidContractReason}

)}
setSearchQuery(event.target.value)} placeholder="Search by contract title or provider..." className="pl-9" />
{debouncedSearchQuery && (

Showing results for: "{debouncedSearchQuery}"

)}
{contracts.map((contract) => (
{getFileIcon(contract.mimeType)}

{contract.fileName}

{contract.status} {contract.isRagged && ( RAG {contract.ragChunkCount ?? 0} )}
{formatFileSize(contract.fileSize)} {formatDate(contract.createdAt)}
handleOpenAsk(contract)} className="cursor-pointer" > Ask about this file handleOpenDetails(contract)} className="cursor-pointer" > Details exportToPDF(contract as any)} className="cursor-pointer" > Export Analysis (PDF) exportToCSV(contract as any)} className="cursor-pointer" > Export Analysis (CSV) requestDeleteContract(contract)} disabled={deletingId === contract.id} className="text-destructive focus:text-destructive cursor-pointer" > {deletingId === contract.id ? ( <> Deleting... ) : ( <> Delete )}
))} {contracts.length === 0 && debouncedSearchQuery && (

No contracts found

Try different keywords from the title or provider name.

)}
{/* Details Modal */} Contract Details {selectedContract && (

Document Profile

{selectedContract.fileName}

{selectedContract.status}

File Size

{formatFileSize(selectedContract.fileSize)}

Mime Type

{selectedContract.mimeType}

Uploaded

{formatDate(selectedContract.createdAt)}

Category

{selectedContract.type || "Pending analysis"}

{/* AI Analysis Results */} {selectedContract.status === "COMPLETED" && ( <>

Extracted Contract Information

Title

{stripMarkdown(selectedContract.title) || "N/A"}

Provider

{stripMarkdown(selectedContract.provider) || "N/A"}

Policy Number

{stripMarkdown(selectedContract.policyNumber) || "N/A"}

Start Date

{selectedContract.startDate ? formatDate(selectedContract.startDate) : "N/A"}

End Date

{selectedContract.endDate ? formatDate(selectedContract.endDate) : "N/A"}

Premium

{formatPremiumWithSourceCurrency(selectedContract)}

{selectedContract.summary && (

Summary

{renderRichParagraphs( selectedContract.summary, `summary-${selectedContract.id}`, )}
)} {selectedContract.keyPoints && (

Key Points

{isContractKeyPoints(selectedContract.keyPoints) && selectedContract.keyPoints.guarantees && Array.isArray( selectedContract.keyPoints.guarantees, ) && (

Guarantees:

    {( selectedContract.keyPoints.guarantees ?? [] ).map((guarantee, idx: number) => (
  • {renderRichParagraphs( guarantee, `guarantee-${selectedContract.id}-${idx}`, )}
  • ))}
)} {isContractKeyPoints(selectedContract.keyPoints) && selectedContract.keyPoints.exclusions && Array.isArray( selectedContract.keyPoints.exclusions, ) && (

Exclusions:

    {( selectedContract.keyPoints.exclusions ?? [] ).map((exclusion, idx: number) => (
  • {renderRichParagraphs( exclusion, `exclusion-${selectedContract.id}-${idx}`, )}
  • ))}
)} {isContractKeyPoints(selectedContract.keyPoints) && selectedContract.keyPoints.franchise && (

Deductible:

{renderRichParagraphs( String(selectedContract.keyPoints.franchise), `franchise-${selectedContract.id}`, )}
)}
)} )} {selectedContract.status === "PROCESSING" && (

AI analysis is in progress...

)} {selectedContract.status === "UPLOADED" && (

Contract uploaded. AI analysis will start automatically.

)} {selectedContract.status === "FAILED" && (

Analysis failed

{selectedContract.summary || "The uploaded file could not be processed as a valid contract. Please upload a clearer contract document and try again."}

)}
)}
Delete this contract? This action permanently removes the selected contract and its associated file. {contractToDelete ? `\n\nFile: ${contractToDelete.fileName}` : ""} { setContractToDelete(null); }} > Cancel void confirmDeleteContract()} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {contractToDelete && deletingId === contractToDelete.id ? "Deleting..." : "Delete"} Delete all contracts? This action permanently removes all contracts and related files for your account. This cannot be undone. Cancel void handleDeleteAll()} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {isDeletingAll ? "Deleting..." : "Delete All"} Invalid Contract File

The AI could not validate this file as a real contract.

Reason

{invalidContractReason || "This uploaded file does not appear to be a valid contract."}

{invalidContractFileName && (

File:{" "} {invalidContractFileName}

)}

Please upload a contract or policy document with readable legal terms and agreement details.

); }