Files
LexiChain/components/views/dashboard/contracts-list.tsx
2026-03-25 13:52:45 +01:00

1079 lines
42 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import {
Download,
Trash2,
Eye,
MoreVertical,
Loader2,
Sparkles,
FileText,
MessageSquare,
Send,
Scale,
Briefcase,
User,
Bot,
AlertTriangle,
X,
Search,
} 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,
analyzeContractAction,
askContractQuestionAction,
} from "@/lib/actions/contract.action";
import { toast } from "sonner";
import { Textarea } from "@/components/ui/textarea";
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?: Record<string, unknown> | null;
}
interface ChatMessage {
role: "user" | "assistant";
content: string;
}
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);
const [analyzingId, setAnalyzingId] = useState<string | null>(null);
const [detailsOpen, setDetailsOpen] = useState(false);
const [selectedContract, setSelectedContract] = useState<Contract | null>(
null,
);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [askOpen, setAskOpen] = useState(false);
const [chatContract, setChatContract] = useState<Contract | null>(null);
const [question, setQuestion] = useState("");
const [isAsking, setIsAsking] = useState(false);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [contractToDelete, setContractToDelete] = useState<Contract | null>(
null,
);
const [invalidContractDialogOpen, setInvalidContractDialogOpen] =
useState(false);
const [invalidContractReason, setInvalidContractReason] = useState("");
const [invalidContractFileName, setInvalidContractFileName] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
const quickQuestions = [
"What are the main obligations and deadlines?",
"What are the non-compliance risks under general EU/US principles?",
"What are the most important exclusions and liabilities?",
];
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 handleAnalyze = async (id: string) => {
const selected = contracts.find((contract) => contract.id === id);
setAnalyzingId(id);
setIsAnalyzing(true);
try {
const result = await analyzeContractAction(id);
if (result.success) {
// Reload contracts to get all AI analysis data
await loadContracts();
toast.success("Contract analyzed successfully!");
emitNotificationRefresh();
} else {
const errorCode = (result as { errorCode?: string }).errorCode;
if (errorCode === "INVALID_CONTRACT") {
const reason =
result.error ||
"This uploaded file is not recognized as a valid contract.";
setInvalidContractReason(reason);
setInvalidContractFileName(selected?.fileName || "Unknown file");
setInvalidContractDialogOpen(true);
toast.error("Invalid contract file detected");
} else {
toast.error(result.error || "Failed to analyze contract");
}
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Unknown error occurred",
);
} finally {
setAnalyzingId(null);
setIsAnalyzing(false);
}
};
const handleOpenDetails = (contract: Contract) => {
setSelectedContract(contract);
setDetailsOpen(true);
};
const handleOpenAsk = (contract: Contract) => {
setChatContract(contract);
setMessages([
{
role: "assistant",
content:
"Ask me anything about this contract. I will answer based on the file analysis.",
},
]);
setQuestion("");
setAskOpen(true);
};
const handleAskQuestion = async () => {
if (!chatContract) return;
const trimmedQuestion = question.trim();
if (!trimmedQuestion) return;
setMessages((prev) => [
...prev,
{ role: "user", content: trimmedQuestion },
]);
setQuestion("");
setIsAsking(true);
try {
const result = await askContractQuestionAction(
chatContract.id,
trimmedQuestion,
);
if (result.success && result.answer) {
setMessages((prev) => [
...prev,
{ role: "assistant", content: result.answer as string },
]);
} else {
const errorMessage = result.error || "Failed to get AI response";
setMessages((prev) => [
...prev,
{ role: "assistant", content: `Error: ${errorMessage}` },
]);
}
} catch (error) {
const fallbackMessage =
error instanceof Error ? error.message : "Unknown error occurred";
setMessages((prev) => [
...prev,
{ role: "assistant", content: `Error: ${fallbackMessage}` },
]);
} finally {
setIsAsking(false);
}
};
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>
{debouncedSearchQuery && (
<p className="text-xs text-muted-foreground">
Showing results for: "{debouncedSearchQuery}"
</p>
)}
</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>
</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 downloadUrl = contract.fileUrl + "?download=1";
const link = document.createElement("a");
link.href = downloadUrl;
link.download =
contract.fileUrl.split("/").pop() || "contract";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
>
<Download className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="hover:bg-primary/10"
title="Analyze with AI"
disabled={analyzingId === contract.id}
onClick={() => handleAnalyze(contract.id)}
>
{analyzingId === contract.id ? (
<Loader2 className="w-4 h-4 animate-spin text-primary" />
) : (
<Sparkles 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>
<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-w-3xl max-h-[90vh] overflow-y-auto border-border/60 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.12),transparent_40%),radial-gradient(circle_at_bottom_left,hsl(var(--secondary)/0.1),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-2xl border border-border/60 bg-background/75 p-4">
<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-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4 text-sm">
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<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="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<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="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<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="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<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-3 rounded-2xl border border-border/60 bg-background/75 p-4">
<h3 className="text-base font-semibold text-foreground">
Extracted Contract Information
</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 text-sm">
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Title
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.title || "N/A"}
</p>
</div>
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Provider
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.provider || "N/A"}
</p>
</div>
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Policy Number
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.policyNumber || "N/A"}
</p>
</div>
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Start Date
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.startDate
? formatDate(selectedContract.startDate)
: "N/A"}
</p>
</div>
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
End Date
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.endDate
? formatDate(selectedContract.endDate)
: "N/A"}
</p>
</div>
<div className="rounded-xl border border-border/50 bg-muted/25 px-3 py-2">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
Premium
</p>
<p className="mt-1 font-medium text-foreground">
{selectedContract.premium
? `${selectedContract.premium.toFixed(2)}`
: "N/A"}
</p>
</div>
</div>
</div>
{selectedContract.summary && (
<div className="space-y-2 rounded-2xl border border-border/60 bg-background/75 p-4">
<h3 className="text-base font-semibold text-foreground">
Summary
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{selectedContract.summary}
</p>
</div>
)}
{selectedContract.keyPoints && (
<div className="space-y-2 rounded-2xl border border-border/60 bg-background/75 p-4">
<h3 className="text-base font-semibold text-foreground">
Key Points
</h3>
<div className="space-y-3 text-sm">
{(selectedContract.keyPoints as any)?.guarantees &&
Array.isArray(
(selectedContract.keyPoints as any).guarantees,
) && (
<div>
<p className="text-muted-foreground font-medium">
Guarantees:
</p>
<ul className="list-disc list-inside ml-2 space-y-1">
{(
(selectedContract.keyPoints as any)
.guarantees as string[]
).map((guarantee: string, idx: number) => (
<li
key={idx}
className="text-muted-foreground"
>
{guarantee}
</li>
))}
</ul>
</div>
)}
{(selectedContract.keyPoints as any)?.exclusions &&
Array.isArray(
(selectedContract.keyPoints as any).exclusions,
) && (
<div>
<p className="text-muted-foreground font-medium">
Exclusions:
</p>
<ul className="list-disc list-inside ml-2 space-y-1">
{(
(selectedContract.keyPoints as any)
.exclusions as string[]
).map((exclusion: string, idx: number) => (
<li
key={idx}
className="text-muted-foreground"
>
{exclusion}
</li>
))}
</ul>
</div>
)}
{(selectedContract.keyPoints as any)?.franchise && (
<div>
<p className="text-muted-foreground font-medium">
Deductible:
</p>
<p className="text-muted-foreground">
{String(
(selectedContract.keyPoints as any).franchise,
)}
</p>
</div>
)}
</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">
<Sparkles className="w-5 h-5 text-amber-500" />
<p className="text-sm text-amber-700 dark:text-amber-300">
Click the Sparkles button to analyze this contract
</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>
<Dialog open={askOpen} onOpenChange={setAskOpen}>
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto border-border/60 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.18),transparent_40%),radial-gradient(circle_at_bottom_left,hsl(var(--secondary)/0.12),transparent_45%)]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5" />
Ask About This File
</DialogTitle>
</DialogHeader>
{chatContract && (
<div className="space-y-4">
<div className="rounded-2xl border border-border/70 bg-background/70 p-4 shadow-sm backdrop-blur-sm">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">
Contract Intelligence Assistant
</p>
<p className="text-sm font-medium truncate mt-1">
{chatContract.fileName}
</p>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-muted/30 px-2 py-1">
<Briefcase className="w-3.5 h-3.5" />
Business
</span>
<span className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-muted/30 px-2 py-1">
<Scale className="w-3.5 h-3.5" />
Legal Context
</span>
</div>
</div>
</div>
<div className="space-y-2">
<p className="text-xs text-muted-foreground">Quick prompts</p>
<div className="flex flex-wrap gap-2">
{quickQuestions.map((quickQuestion) => (
<Button
key={quickQuestion}
type="button"
variant="outline"
size="sm"
disabled={isAsking}
onClick={() => setQuestion(quickQuestion)}
className="border-primary/25 bg-background/80 text-xs hover:border-primary/50 hover:bg-primary/10"
>
{quickQuestion}
</Button>
))}
</div>
</div>
<div className="h-80 space-y-3 overflow-y-auto rounded-2xl border border-border/70 bg-background/65 p-4 shadow-inner backdrop-blur-sm">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div className="flex max-w-[88%] items-start gap-2">
{message.role === "assistant" && (
<span className="mt-1 inline-flex h-7 w-7 items-center justify-center rounded-full border border-border/60 bg-muted/40 text-muted-foreground">
<Bot className="h-4 w-4" />
</span>
)}
<div
className={`rounded-2xl px-3 py-2 text-sm whitespace-pre-wrap shadow-sm ${
message.role === "user"
? "bg-gradient-to-r from-primary to-accent text-primary-foreground"
: "border border-border/70 bg-background"
}`}
>
{message.content}
</div>
{message.role === "user" && (
<span className="mt-1 inline-flex h-7 w-7 items-center justify-center rounded-full border border-primary/25 bg-primary/10 text-primary">
<User className="h-4 w-4" />
</span>
)}
</div>
</div>
))}
{isAsking && (
<div className="flex justify-start">
<div className="flex items-center gap-2 rounded-2xl border border-border/70 bg-background px-3 py-2 text-sm shadow-sm">
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-muted-foreground">
<Bot className="h-3.5 w-3.5" />
</span>
<Loader2 className="w-4 h-4 animate-spin" />
Preparing a professional legal-business answer...
</div>
</div>
)}
</div>
<div className="space-y-2">
<Textarea
value={question}
onChange={(event) => setQuestion(event.target.value)}
placeholder="Ask about obligations, liabilities, legal exposure, compliance risks, or business impact..."
rows={3}
disabled={isAsking}
className="rounded-2xl border-border/70 bg-background/80"
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!isAsking &&
question.trim()
) {
event.preventDefault();
void handleAskQuestion();
}
}}
/>
<div className="flex justify-end">
<Button
onClick={handleAskQuestion}
disabled={isAsking || !question.trim()}
className="gap-2 bg-gradient-to-r from-primary to-accent text-white shadow-md hover:from-primary/90 hover:to-accent/90"
>
{isAsking ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Sending...
</>
) : (
<>
<Send className="w-4 h-4" />
Send
</>
)}
</Button>
</div>
</div>
<p className="text-[11px] text-muted-foreground">
Tip: press Enter to send, Shift+Enter for a new line.
</p>
</div>
)}
</DialogContent>
</Dialog>
<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>
<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>
{/* AI Analysis Loading Overlay */}
{isAnalyzing && (
<div className="animate-in fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/55 backdrop-blur-sm duration-200">
<div className="mx-4 max-w-md rounded-3xl border border-border/60 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.22),transparent_45%),radial-gradient(circle_at_bottom_left,hsl(var(--secondary)/0.16),transparent_45%),hsl(var(--background))] p-8 shadow-2xl md:p-10">
<div className="flex flex-col items-center text-center space-y-6">
<div className="relative">
<div className="absolute inset-0 rounded-full bg-primary/30 blur-xl animate-pulse"></div>
<div className="relative rounded-full bg-gradient-to-br from-primary to-accent p-4">
<Sparkles className="h-8 w-8 animate-pulse text-white" />
</div>
</div>
<div className="relative">
<Loader2 className="h-11 w-11 animate-spin text-primary" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-foreground">
Analyzing Contract
</h3>
<p className="text-sm text-muted-foreground">
Our AI is carefully reviewing your document...
</p>
</div>
<div className="w-full space-y-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Processing</span>
<span className="flex items-center gap-1">
<span className="animate-pulse"></span>
<span className="animate-pulse delay-75"></span>
<span className="animate-pulse delay-150"></span>
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div className="h-full animate-pulse rounded-full bg-gradient-to-r from-primary to-accent"></div>
</div>
</div>
<p className="text-xs text-muted-foreground">
This may take up to 10 seconds
</p>
</div>
</div>
</div>
)}
</>
);
}