Files
LexiChain/features/contracts/components/list/contracts-list.tsx

1631 lines
60 KiB
TypeScript
Raw Normal View History

2026-03-28 23:46:45 +01:00
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import type { ReactNode } from "react";
2026-04-12 19:24:24 +01:00
import type { Prisma } from "@prisma/client";
2026-03-28 23:46:45 +01:00
import {
Download,
Trash2,
Eye,
MoreVertical,
Loader2,
FileText,
2026-04-12 19:24:24 +01:00
FileSpreadsheet,
2026-03-28 23:46:45 +01:00
MessageSquare,
AlertTriangle,
X,
Search,
Info,
2026-04-12 19:24:24 +01:00
Network,
2026-03-28 23:46:45 +01:00
} 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,
2026-04-12 19:24:24 +01:00
deleteAllContractsAction,
2026-03-28 23:46:45 +01:00
} 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";
2026-04-19 01:42:00 +01:00
import {
stripMarkdown,
exportToCSV,
exportToPDF,
} from "@/features/contracts/utils/export.utils";
2026-03-28 23:46:45 +01:00
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;
2026-04-12 19:24:24 +01:00
keyPoints?: Prisma.JsonValue | null;
2026-03-28 23:46:45 +01:00
extractedText?: string | null;
2026-04-12 19:24:24 +01:00
ragChunkCount?: number;
isRagged?: boolean;
2026-03-28 23:46:45 +01:00
}
interface ExplainabilityEntry {
field: string;
why: string;
sourceSnippet: string;
sourceHints?: {
page?: string | null;
section?: string | null;
confidence?: number | null;
};
}
2026-04-12 19:24:24 +01:00
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);
};
2026-03-28 23:46:45 +01:00
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<Contract[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [deletingId, setDeletingId] = useState<string | null>(null);
2026-04-12 19:24:24 +01:00
const [isDeletingAll, setIsDeletingAll] = useState(false);
2026-03-28 23:46:45 +01:00
const [detailsOpen, setDetailsOpen] = useState(false);
const [selectedContract, setSelectedContract] = useState<Contract | null>(
null,
);
const [askOpen, setAskOpen] = useState(false);
const [chatContract, setChatContract] = useState<Contract | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [contractToDelete, setContractToDelete] = useState<Contract | null>(
null,
);
2026-04-12 19:24:24 +01:00
const [deleteAllDialogOpen, setDeleteAllDialogOpen] = useState(false);
2026-03-28 23:46:45 +01:00
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(
<strong key={tokenKey} className="font-semibold text-foreground">
{token.slice(2, -2)}
</strong>,
);
} else if (match[2]) {
nodes.push(
<span
key={tokenKey}
className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-1 py-0.5 font-medium text-emerald-700 dark:text-emerald-300"
>
{token}
</span>,
);
} else if (match[3]) {
nodes.push(
<span
key={tokenKey}
className="rounded-md border border-cyan-500/30 bg-cyan-500/10 px-1 py-0.5 font-medium text-cyan-700 dark:text-cyan-300"
>
{token}
</span>,
);
} else if (match[4] || match[5]) {
nodes.push(
<span
key={tokenKey}
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 font-medium text-amber-700 dark:text-amber-300"
>
{token}
</span>,
);
} else if (match[6] || match[7]) {
nodes.push(
<span
key={tokenKey}
className="rounded-md border border-fuchsia-500/30 bg-fuchsia-500/10 px-1 py-0.5 font-medium text-fuchsia-700 dark:text-fuchsia-300"
>
{token}
</span>,
);
} else {
nodes.push(
<span key={tokenKey} className="font-medium text-foreground">
{token}
</span>,
);
}
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 (
<div
key={`${keyPrefix}-line-${idx}`}
className={isList ? "flex items-start gap-2" : ""}
>
{isList && (
<span className="mt-1 h-1.5 w-1.5 rounded-full bg-primary/80" />
)}
<p className="leading-7 text-muted-foreground">
{renderHighlightedText(content, `${keyPrefix}-${idx}`)}
</p>
</div>
);
});
};
// 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[] => {
2026-04-12 19:24:24 +01:00
const raw = isContractKeyPoints(contract?.keyPoints)
? contract.keyPoints.explainability
: undefined;
2026-03-28 23:46:45 +01:00
if (!Array.isArray(raw)) return [];
return raw
2026-04-12 19:24:24 +01:00
.map((item) => ({
2026-03-28 23:46:45 +01:00
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<string, string[]> = {
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(
2026-04-12 19:24:24 +01:00
(isContractKeyPoints(contract.keyPoints)
? contract.keyPoints.aiMeta?.premiumCurrency
: null) ?? "",
2026-03-28 23:46:45 +01:00
).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);
};
2026-04-12 19:24:24 +01:00
const handleDeleteAll = async () => {
setIsDeletingAll(true);
2026-03-28 23:46:45 +01:00
try {
2026-04-12 19:24:24 +01:00
const result = await deleteAllContractsAction();
2026-03-28 23:46:45 +01:00
if (result.success) {
2026-04-12 19:24:24 +01:00
setContracts([]);
toast.success(
`Deleted ${result.deletedCount ?? 0} contract${(result.deletedCount ?? 0) === 1 ? "" : "s"}.`,
);
2026-03-28 23:46:45 +01:00
emitNotificationRefresh();
} else {
2026-04-12 19:24:24 +01:00
toast.error(result.error || "Failed to delete all contracts");
2026-03-28 23:46:45 +01:00
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Unknown error occurred",
);
} finally {
2026-04-12 19:24:24 +01:00
setIsDeletingAll(false);
setDeleteAllDialogOpen(false);
2026-03-28 23:46:45 +01:00
}
};
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 (
<Card className="border-border/50">
<div className="p-12 flex items-center justify-center">
<Loader2 className="w-6 h-6 animate-spin text-primary mr-3" />
<span className="text-muted-foreground">Loading contracts...</span>
</div>
</Card>
);
}
if (contracts.length === 0 && !debouncedSearchQuery) {
return null;
}
return (
<>
{invalidContractReason && (
<Card className="mb-4 border border-destructive/30 bg-destructive/5">
<div className="flex items-start justify-between gap-3 p-4">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 text-destructive" />
<div>
<p className="text-sm font-semibold text-foreground">
Invalid contract upload detected
</p>
<p className="mt-1 text-xs text-muted-foreground">
{invalidContractFileName
? `${invalidContractFileName}: `
: ""}
{invalidContractReason}
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => {
setInvalidContractReason("");
setInvalidContractFileName("");
}}
>
<X className="h-4 w-4" />
</Button>
</div>
</Card>
)}
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="relative w-full sm:max-w-md">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search by contract title or provider..."
className="pl-9"
/>
</div>
2026-04-12 19:24:24 +01:00
<div className="flex items-center gap-2">
{debouncedSearchQuery && (
<p className="text-xs text-muted-foreground">
Showing results for: &quot;{debouncedSearchQuery}&quot;
</p>
)}
<Button
type="button"
variant="destructive"
size="sm"
disabled={contracts.length === 0 || isDeletingAll}
onClick={() => setDeleteAllDialogOpen(true)}
className="gap-2"
>
{isDeletingAll ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
Delete All
</Button>
</div>
2026-03-28 23:46:45 +01:00
</div>
<Card className="border-border/50 overflow-hidden">
<div className="divide-y divide-border/50">
{contracts.map((contract) => (
<div
key={contract.id}
className="p-4 md:p-6 hover:bg-primary/2 dark:hover:bg-primary/5 transition-colors duration-200 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"
>
<div className="flex items-center gap-4 flex-1 min-w-0 w-full sm:w-auto">
<div className="text-2xl flex-shrink-0">
{getFileIcon(contract.mimeType)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-medium text-foreground truncate">
{contract.fileName}
</h3>
<span
className={`text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${getStatusColor(contract.status)}`}
>
{contract.status}
</span>
2026-04-12 19:24:24 +01:00
{contract.isRagged && (
<span className="inline-flex items-center gap-1 rounded-full border border-cyan-500/30 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-cyan-700 dark:text-cyan-300">
<Network className="h-3 w-3" />
RAG {contract.ragChunkCount ?? 0}
</span>
)}
2026-03-28 23:46:45 +01:00
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground flex-wrap">
<span>{formatFileSize(contract.fileSize)}</span>
<span></span>
<span>{formatDate(contract.createdAt)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0 w-full sm:w-auto justify-end">
<Button
variant="ghost"
size="icon"
className="hover:bg-primary/10"
title="View contract"
onClick={() => {
if (contract.fileUrl) {
window.open(contract.fileUrl, "_blank");
}
}}
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="hover:bg-primary/10"
title="Download contract"
onClick={() => {
if (contract.fileUrl) {
const link = document.createElement("a");
2026-04-19 01:42:00 +01:00
link.href = contract.fileUrl;
2026-03-28 23:46:45 +01:00
link.download =
contract.fileUrl.split("/").pop() || "contract";
2026-04-19 01:42:00 +01:00
link.target = "_blank";
link.rel = "noopener noreferrer";
2026-03-28 23:46:45 +01:00
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
>
<Download className="w-4 h-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hover:bg-primary/10"
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleOpenAsk(contract)}
className="cursor-pointer"
>
<MessageSquare className="w-4 h-4 mr-2" />
Ask about this file
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleOpenDetails(contract)}
className="cursor-pointer"
>
<FileText className="w-4 h-4 mr-2" />
Details
</DropdownMenuItem>
2026-04-12 19:24:24 +01:00
<DropdownMenuItem
onClick={() => exportToPDF(contract as any)}
className="cursor-pointer"
>
<FileText className="w-4 h-4 mr-2" />
Export Analysis (PDF)
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => exportToCSV(contract as any)}
className="cursor-pointer"
>
<FileSpreadsheet className="w-4 h-4 mr-2" />
Export Analysis (CSV)
</DropdownMenuItem>
2026-03-28 23:46:45 +01:00
<DropdownMenuItem
onClick={() => requestDeleteContract(contract)}
disabled={deletingId === contract.id}
className="text-destructive focus:text-destructive cursor-pointer"
>
{deletingId === contract.id ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
{contracts.length === 0 && debouncedSearchQuery && (
<div className="p-10 text-center">
<p className="text-sm font-medium text-foreground">
No contracts found
</p>
<p className="mt-1 text-xs text-muted-foreground">
Try different keywords from the title or provider name.
</p>
</div>
)}
</div>
</Card>
{/* Details Modal */}
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
<DialogContent className="max-h-[92vh] max-w-6xl overflow-y-auto border-border/60 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.14),transparent_38%),radial-gradient(circle_at_bottom_left,hsl(var(--secondary)/0.12),transparent_42%)]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Contract Details
</DialogTitle>
<DialogClose />
</DialogHeader>
{selectedContract && (
<div className="space-y-6 py-4">
<div className="rounded-3xl border border-white/20 dark:border-white/10 bg-background/40 p-5 shadow-2xl backdrop-blur-2xl ring-1 ring-black/5 dark:ring-white/5 md:p-6 transition-all duration-500 hover:shadow-primary/5 hover:border-primary/20">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">
Document Profile
</p>
<p className="mt-1 truncate text-base font-semibold text-foreground">
{selectedContract.fileName}
</p>
</div>
<span
className={`text-xs px-2.5 py-1 rounded-full font-medium whitespace-nowrap ${getStatusColor(selectedContract.status)}`}
>
{selectedContract.status}
</span>
</div>
<div className="mt-5 grid auto-rows-fr gap-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div className="min-h-[94px] rounded-xl border border-border/30 bg-muted/20 px-3 py-2 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-md hover:-translate-y-0.5">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
File Size
</p>
<p className="mt-1 font-medium text-foreground">
{formatFileSize(selectedContract.fileSize)}
</p>
</div>
<div className="min-h-[94px] rounded-xl border border-border/30 bg-muted/20 px-3 py-2 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-md hover:-translate-y-0.5">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Mime Type
</p>
<p className="mt-1 font-medium text-foreground truncate">
{selectedContract.mimeType}
</p>
</div>
<div className="min-h-[94px] rounded-xl border border-border/30 bg-muted/20 px-3 py-2 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-md hover:-translate-y-0.5">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Uploaded
</p>
<p className="mt-1 font-medium text-foreground">
{formatDate(selectedContract.createdAt)}
</p>
</div>
<div className="min-h-[94px] rounded-xl border border-border/30 bg-muted/20 px-3 py-2 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-md hover:-translate-y-0.5">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Category
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.type || "Pending analysis"}
</p>
</div>
</div>
</div>
{/* AI Analysis Results */}
{selectedContract.status === "COMPLETED" && (
<>
<div className="space-y-4 rounded-3xl border border-border/60 bg-background/85 p-5 shadow-sm backdrop-blur-sm md:p-6">
<h3 className="text-base font-semibold text-foreground">
Extracted Contract Information
</h3>
<div className="grid auto-rows-fr gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Title
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof("title", "Title")
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show title proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
2026-04-12 19:24:24 +01:00
{stripMarkdown(selectedContract.title) || "N/A"}
2026-03-28 23:46:45 +01:00
</p>
</div>
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Provider
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof("provider", "Provider")
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show provider proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
2026-04-12 19:24:24 +01:00
{stripMarkdown(selectedContract.provider) || "N/A"}
2026-03-28 23:46:45 +01:00
</p>
</div>
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Policy Number
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof(
"policyNumber",
"Policy Number",
)
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show policy number proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
2026-04-19 01:42:00 +01:00
{stripMarkdown(selectedContract.policyNumber) ||
"N/A"}
2026-03-28 23:46:45 +01:00
</p>
</div>
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Start Date
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof("startDate", "Start Date")
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show start date proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
{selectedContract.startDate
? formatDate(selectedContract.startDate)
: "N/A"}
</p>
</div>
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
End Date
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof("endDate", "End Date")
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show end date proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
{selectedContract.endDate
? formatDate(selectedContract.endDate)
: "N/A"}
</p>
</div>
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Premium
</p>
<button
type="button"
onClick={() =>
handleOpenFieldProof("premium", "Premium")
}
className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
aria-label="Show premium proof"
title="Show proof"
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
{formatPremiumWithSourceCurrency(selectedContract)}
</p>
</div>
</div>
</div>
{selectedContract.summary && (
<div className="space-y-2 rounded-3xl border border-border/60 bg-background/80 p-5 shadow-sm backdrop-blur-sm md:p-6">
<h3 className="text-base font-semibold text-foreground">
Summary
</h3>
<div className="space-y-2 rounded-xl border border-border/50 bg-muted/20 p-3 text-sm whitespace-pre-wrap break-words">
{renderRichParagraphs(
selectedContract.summary,
`summary-${selectedContract.id}`,
)}
</div>
</div>
)}
{selectedContract.keyPoints && (
<div className="space-y-2 rounded-3xl border border-border/60 bg-background/80 p-5 shadow-sm backdrop-blur-sm md:p-6">
<h3 className="text-base font-semibold text-foreground">
Key Points
</h3>
<div className="space-y-3 text-sm">
2026-04-12 19:24:24 +01:00
{isContractKeyPoints(selectedContract.keyPoints) &&
selectedContract.keyPoints.guarantees &&
2026-03-28 23:46:45 +01:00
Array.isArray(
2026-04-12 19:24:24 +01:00
selectedContract.keyPoints.guarantees,
2026-03-28 23:46:45 +01:00
) && (
<div>
<p className="text-muted-foreground font-medium">
Guarantees:
</p>
<ul className="ml-1 space-y-2">
{(
2026-04-12 19:24:24 +01:00
selectedContract.keyPoints.guarantees ?? []
).map((guarantee, idx: number) => (
2026-03-28 23:46:45 +01:00
<li
key={idx}
className="rounded-lg border border-border/50 bg-muted/20 px-3 py-2"
>
{renderRichParagraphs(
guarantee,
`guarantee-${selectedContract.id}-${idx}`,
)}
</li>
))}
</ul>
</div>
)}
2026-04-12 19:24:24 +01:00
{isContractKeyPoints(selectedContract.keyPoints) &&
selectedContract.keyPoints.exclusions &&
2026-03-28 23:46:45 +01:00
Array.isArray(
2026-04-12 19:24:24 +01:00
selectedContract.keyPoints.exclusions,
2026-03-28 23:46:45 +01:00
) && (
<div>
<p className="text-muted-foreground font-medium">
Exclusions:
</p>
<ul className="ml-1 space-y-2">
{(
2026-04-12 19:24:24 +01:00
selectedContract.keyPoints.exclusions ?? []
).map((exclusion, idx: number) => (
2026-03-28 23:46:45 +01:00
<li
key={idx}
className="rounded-lg border border-border/50 bg-muted/20 px-3 py-2"
>
{renderRichParagraphs(
exclusion,
`exclusion-${selectedContract.id}-${idx}`,
)}
</li>
))}
</ul>
</div>
)}
2026-04-12 19:24:24 +01:00
{isContractKeyPoints(selectedContract.keyPoints) &&
selectedContract.keyPoints.franchise && (
<div>
<p className="text-muted-foreground font-medium">
Deductible:
</p>
<div className="rounded-lg border border-border/50 bg-muted/20 px-3 py-2 whitespace-pre-wrap break-words">
{renderRichParagraphs(
String(selectedContract.keyPoints.franchise),
`franchise-${selectedContract.id}`,
)}
</div>
2026-03-28 23:46:45 +01:00
</div>
2026-04-12 19:24:24 +01:00
)}
2026-03-28 23:46:45 +01:00
</div>
</div>
)}
</>
)}
{selectedContract.status === "PROCESSING" && (
<div className="flex items-center gap-2 rounded-xl border border-blue-200/40 bg-blue-50/60 p-4 dark:border-blue-800/40 dark:bg-blue-950/30">
<Loader2 className="w-5 h-5 animate-spin text-blue-500" />
<p className="text-sm text-blue-700 dark:text-blue-300">
AI analysis is in progress...
</p>
</div>
)}
{selectedContract.status === "UPLOADED" && (
<div className="flex items-center gap-2 rounded-xl border border-amber-200/40 bg-amber-50/60 p-4 dark:border-amber-800/40 dark:bg-amber-950/30">
2026-04-12 19:24:24 +01:00
<Loader2 className="w-5 h-5 text-amber-500 animate-spin" />
2026-03-28 23:46:45 +01:00
<p className="text-sm text-amber-700 dark:text-amber-300">
2026-04-12 19:24:24 +01:00
Contract uploaded. AI analysis will start automatically.
2026-03-28 23:46:45 +01:00
</p>
</div>
)}
{selectedContract.status === "FAILED" && (
<div className="space-y-2 rounded-xl border border-red-200/40 bg-red-50/60 p-4 dark:border-red-800/40 dark:bg-red-950/30">
<p className="text-sm font-semibold text-red-700 dark:text-red-300">
Analysis failed
</p>
<p className="text-sm text-red-700/90 dark:text-red-300/90 leading-relaxed">
{selectedContract.summary ||
"The uploaded file could not be processed as a valid contract. Please upload a clearer contract document and try again."}
</p>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
<ContractProofModal
isOpen={proofModalOpen}
onOpenChange={setProofModalOpen}
proofData={proofData}
/>
<ContractChatModal
isOpen={askOpen}
onOpenChange={setAskOpen}
contract={chatContract}
renderRichParagraphs={renderRichParagraphs}
/>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this contract?</AlertDialogTitle>
<AlertDialogDescription>
This action permanently removes the selected contract and its
associated file.
{contractToDelete ? `\n\nFile: ${contractToDelete.fileName}` : ""}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={() => {
setContractToDelete(null);
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => void confirmDeleteContract()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{contractToDelete && deletingId === contractToDelete.id
? "Deleting..."
: "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
2026-04-12 19:24:24 +01:00
<AlertDialog
open={deleteAllDialogOpen}
onOpenChange={setDeleteAllDialogOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete all contracts?</AlertDialogTitle>
<AlertDialogDescription>
This action permanently removes all contracts and related files
for your account. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => void handleDeleteAll()}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeletingAll ? "Deleting..." : "Delete All"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
2026-03-28 23:46:45 +01:00
<Dialog
open={invalidContractDialogOpen}
onOpenChange={setInvalidContractDialogOpen}
>
<DialogContent className="max-w-md border-border/70">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertTriangle className="h-5 w-5" />
Invalid Contract File
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
The AI could not validate this file as a real contract.
</p>
<div className="rounded-xl border border-destructive/25 bg-destructive/5 p-3">
<p className="text-xs font-semibold text-foreground">Reason</p>
<p className="mt-1 text-sm text-muted-foreground">
{invalidContractReason ||
"This uploaded file does not appear to be a valid contract."}
</p>
</div>
{invalidContractFileName && (
<p className="text-xs text-muted-foreground">
File:{" "}
<span className="font-medium text-foreground">
{invalidContractFileName}
</span>
</p>
)}
<p className="text-xs text-muted-foreground">
Please upload a contract or policy document with readable legal
terms and agreement details.
</p>
</div>
<div className="mt-2 flex justify-end">
<Button
onClick={() => setInvalidContractDialogOpen(false)}
className="rounded-xl"
>
Got it
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}