gaurv007 commited on
Commit
32e255a
·
verified ·
1 Parent(s): 0473fc1

v3.0: Upload web/app/dashboard-pages/analyze/page.tsx — zero emojis, Lucide icons, responsive

Browse files
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -1 +1,667 @@
1
- /app/web_final/analyze_page.tsx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useRef } from "react";
4
+ import {
5
+ ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
6
+ FileDown, ChevronDown, ChevronUp, Copy, Check, Upload, FileText,
7
+ ShieldCheck, ShieldAlert, Scale, Gavel, Ban, Globe, Eye, Stamp, FileX,
8
+ Lock, Sparkles as SparklesIcon, X, Layers, Landmark, Briefcase,
9
+ AlertTriangle, Tag, BookOpen, ClipboardList, DollarSign,
10
+ Calendar, Building, MapPin, Hash, Bot, FileSearch, Percent, Clock,
11
+ User, BookMarked, ShieldX, HelpCircle, Cpu, PenTool, Zap,
12
+ ShieldOff, CircleSlash, MessageSquareWarning, Construction
13
+ } from "lucide-react";
14
+
15
+ interface Cat { name: string; severity: string; description?: string; confidence?: number; }
16
+ interface Clause { text: string; categories: Cat[]; }
17
+ interface Entity { text: string; type: string; score?: number; source?: string; }
18
+ interface Contradiction { type: string; explanation: string; severity: string; confidence?: number; source?: string; }
19
+ interface Obligation { type: string; party: string; description: string; deadline: string; priority?: number; }
20
+ interface ComplianceCheck { requirement: string; description: string; severity: string; status: string; matched_keywords: string[]; context?: string[]; }
21
+ interface ComplianceReg { description: string; compliance_rate: number; checks: ComplianceCheck[]; overall_status: string; negated_count?: number; ambiguous_count?: number; }
22
+ interface AnalysisResult {
23
+ risk_score: number;
24
+ grade: string;
25
+ total_clauses: number;
26
+ flagged_count: number;
27
+ results: Clause[];
28
+ entities: Entity[];
29
+ contradictions: Contradiction[];
30
+ obligations: Obligation[];
31
+ compliance: Record<string, ComplianceReg>;
32
+ model: string;
33
+ latency_ms: number;
34
+ }
35
+
36
+ const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string; ring: string }> = {
37
+ CRITICAL: { icon: AlertTriangle, label: "Critical", text: "text-red-700", bg: "bg-red-50", border: "border-red-300", ring: "ring-red-200" },
38
+ HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200", ring: "ring-red-100" },
39
+ MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200", ring: "ring-amber-100" },
40
+ LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200", ring: "ring-blue-100" },
41
+ };
42
+
43
+ const GRADE_STYLE: Record<string, string> = {
44
+ A: "bg-emerald-50 text-emerald-700 border-emerald-200",
45
+ B: "bg-emerald-50 text-emerald-600 border-emerald-200",
46
+ C: "bg-amber-50 text-amber-700 border-amber-200",
47
+ D: "bg-orange-50 text-orange-700 border-orange-200",
48
+ F: "bg-red-50 text-red-700 border-red-200",
49
+ };
50
+
51
+ const CATEGORY_ICONS: Record<string, any> = {
52
+ "Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
53
+ "Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
54
+ "Choice of law": Gavel, "Contract by using": Stamp, "Uncapped Liability": AlertTriangle,
55
+ "IP Ownership Assignment": Lock, "Non-Compete": Ban, "Governing Law": Gavel,
56
+ "Termination for Convenience": Ban, "Indemnification": ShieldCheck, "Confidentiality": Lock,
57
+ "Notice Period to Terminate Renewal": Clock, "Cap on Liability": ShieldCheck,
58
+ "Liquidated Damages": DollarSign, "Force Majeure": Zap,
59
+ };
60
+
61
+ const ENTITY_COLORS: Record<string, { bg: string; text: string; border: string; icon: any }> = {
62
+ DATE: { bg: "bg-blue-50", text: "text-blue-700", border: "border-blue-200", icon: Calendar },
63
+ DATE_REF: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200", icon: Calendar },
64
+ MONEY: { bg: "bg-emerald-50", text: "text-emerald-700", border: "border-emerald-200", icon: DollarSign },
65
+ PERCENTAGE: { bg: "bg-teal-50", text: "text-teal-700", border: "border-teal-200", icon: Percent },
66
+ DURATION: { bg: "bg-indigo-50", text: "text-indigo-700", border: "border-indigo-200", icon: Clock },
67
+ PARTY: { bg: "bg-purple-50", text: "text-purple-700", border: "border-purple-200", icon: Building },
68
+ PARTY_ROLE: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200", icon: Briefcase },
69
+ PERSON: { bg: "bg-pink-50", text: "text-pink-700", border: "border-pink-200", icon: User },
70
+ JURISDICTION: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200", icon: MapPin },
71
+ DEFINED_TERM: { bg: "bg-pink-50", text: "text-pink-700", border: "border-pink-200", icon: Hash },
72
+ LEGAL_REF: { bg: "bg-zinc-100", text: "text-zinc-700", border: "border-zinc-200", icon: BookMarked },
73
+ MISC: { bg: "bg-zinc-50", text: "text-zinc-600", border: "border-zinc-200", icon: Tag },
74
+ };
75
+
76
+ const OBLIGATION_COLORS: Record<string, { bg: string; text: string; border: string; icon: any }> = {
77
+ monetary: { bg: "bg-emerald-50", text: "text-emerald-700", border: "border-emerald-200", icon: DollarSign },
78
+ compliance: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200", icon: ShieldCheck },
79
+ reporting: { bg: "bg-blue-50", text: "text-blue-700", border: "border-blue-200", icon: ClipboardList },
80
+ delivery: { bg: "bg-purple-50", text: "text-purple-700", border: "border-purple-200", icon: FileText },
81
+ termination: { bg: "bg-red-50", text: "text-red-700", border: "border-red-200", icon: Ban },
82
+ };
83
+
84
+ const COMPLIANCE_STATUS: Record<string, { bg: string; text: string; border: string }> = {
85
+ COMPLIANT: { bg: "bg-emerald-50", text: "text-emerald-700", border: "border-emerald-200" },
86
+ PARTIAL: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200" },
87
+ "NON-COMPLIANT": { bg: "bg-red-50", text: "text-red-700", border: "border-red-200" },
88
+ WARNING: { bg: "bg-orange-50", text: "text-orange-700", border: "border-orange-200" },
89
+ };
90
+
91
+ function SourceBadge({ isML, confidence }: { isML: boolean; confidence?: number | null }) {
92
+ if (isML) {
93
+ return (
94
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium bg-indigo-50 text-indigo-600 border border-indigo-200 px-1.5 py-0.5 rounded">
95
+ <Cpu className="w-2.5 h-2.5" />
96
+ ML {confidence != null ? `${Math.round(confidence * 100)}%` : ""}
97
+ </span>
98
+ );
99
+ }
100
+ return (
101
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium bg-zinc-50 text-zinc-500 border border-zinc-200 px-1.5 py-0.5 rounded">
102
+ <PenTool className="w-2.5 h-2.5" />
103
+ Pattern
104
+ </span>
105
+ );
106
+ }
107
+
108
+ function CheckStatusIcon({ status }: { status: string }) {
109
+ switch (status) {
110
+ case "PASS": return <CircleCheck className="w-4 h-4 text-emerald-500" />;
111
+ case "MISSING": return <X className="w-4 h-4 text-red-500" />;
112
+ case "NEGATED": return <ShieldOff className="w-4 h-4 text-orange-500" />;
113
+ case "AMBIGUOUS": return <HelpCircle className="w-4 h-4 text-amber-500" />;
114
+ default: return <CircleAlert className="w-4 h-4 text-zinc-400" />;
115
+ }
116
+ }
117
+
118
+ function ContradictionSourceBadge({ source, confidence }: { source?: string; confidence?: number }) {
119
+ if (source === "nli_model") {
120
+ return (
121
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium bg-indigo-50 text-indigo-600 border border-indigo-200 px-1.5 py-0.5 rounded">
122
+ <Cpu className="w-2.5 h-2.5" />NLI {confidence != null ? `${Math.round(confidence * 100)}%` : ""}
123
+ </span>
124
+ );
125
+ }
126
+ if (source === "heuristic") {
127
+ return (
128
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium bg-amber-50 text-amber-600 border border-amber-200 px-1.5 py-0.5 rounded">
129
+ <PenTool className="w-2.5 h-2.5" />Heuristic
130
+ </span>
131
+ );
132
+ }
133
+ if (source === "structural") {
134
+ return (
135
+ <span className="inline-flex items-center gap-1 text-[10px] font-medium bg-zinc-50 text-zinc-500 border border-zinc-200 px-1.5 py-0.5 rounded">
136
+ <Construction className="w-2.5 h-2.5" />Structural
137
+ </span>
138
+ );
139
+ }
140
+ return null;
141
+ }
142
+
143
+ const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
144
+
145
+ Spotify may, in its sole discretion, modify or update these Terms of Service at any time without prior notice. Your continued use of the Service after any such changes constitutes your acceptance of the new Terms of Service.
146
+
147
+ In no event will Spotify be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly.
148
+
149
+ Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
150
+
151
+ Spotify may terminate your account or suspend your access at any time, with or without cause, with or without notice, effective immediately.
152
+
153
+ These Terms will be governed by and construed in accordance with the laws of the State of New York.
154
+
155
+ Any dispute shall be finally settled by arbitration in New York County.`;
156
+
157
+ export default function AnalyzePage() {
158
+ const [text, setText] = useState("");
159
+ const [results, setResults] = useState<AnalysisResult | null>(null);
160
+ const [loading, setLoading] = useState(false);
161
+ const [error, setError] = useState("");
162
+ const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
163
+ const [filter, setFilter] = useState<string>("all");
164
+ const [activeTab, setActiveTab] = useState<string>("clauses");
165
+ const [copied, setCopied] = useState(false);
166
+ const [scanCount, setScanCount] = useState(0);
167
+ const [userPlan, setUserPlan] = useState("free");
168
+ const [showUpgrade, setShowUpgrade] = useState(false);
169
+ const fileInputRef = useRef<HTMLInputElement>(null);
170
+
171
+ const FREE_LIMIT = 10;
172
+ const canScan = userPlan !== "free" || scanCount < FREE_LIMIT;
173
+
174
+ async function handleAnalyze() {
175
+ if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
176
+ if (!canScan) { setShowUpgrade(true); return; }
177
+ setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
178
+ try {
179
+ const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
180
+ if (!res.ok) throw new Error((await res.json()).error || "Failed");
181
+ setResults(await res.json());
182
+ setScanCount(prev => prev + 1);
183
+ } catch (e: any) { setError(e.message); }
184
+ finally { setLoading(false); }
185
+ }
186
+
187
+ async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
188
+ const file = e.target.files?.[0];
189
+ if (!file) return;
190
+ if (userPlan === "free") { setShowUpgrade(true); return; }
191
+ setLoading(true); setError("");
192
+ try {
193
+ const formData = new FormData(); formData.append("file", file);
194
+ const res = await fetch("/api/parse-upload", { method: "POST", body: formData });
195
+ if (!res.ok) throw new Error((await res.json()).error || "Failed to parse file");
196
+ setText((await res.json()).text);
197
+ } catch (e: any) { setError(e.message || "Could not read file."); }
198
+ setLoading(false);
199
+ if (fileInputRef.current) fileInputRef.current.value = "";
200
+ }
201
+
202
+ async function handleDownloadPDF() {
203
+ if (!results) return;
204
+ try {
205
+ const res = await fetch("/api/pdf/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(results) });
206
+ const blob = await res.blob();
207
+ const url = URL.createObjectURL(blob);
208
+ const a = document.createElement("a"); a.href = url; a.download = "clauseguard-report.pdf"; a.click();
209
+ URL.revokeObjectURL(url);
210
+ } catch {}
211
+ }
212
+
213
+ function handleCopy() {
214
+ if (!results) return;
215
+ const summary = `ClauseGuard Report\nRisk: ${results.risk_score}/100 (Grade ${results.grade})\n${results.flagged_count} of ${results.total_clauses} clauses flagged\nEntities: ${results.entities.length}\nContradictions: ${results.contradictions.length}\nObligations: ${results.obligations.length}\n\n` +
216
+ results.results.filter(r => r.categories.length > 0).map((r, i) =>
217
+ `${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
218
+ ).join("\n");
219
+ navigator.clipboard.writeText(summary);
220
+ setCopied(true); setTimeout(() => setCopied(false), 2000);
221
+ }
222
+
223
+ const flagged = results?.results.filter(r => r.categories.length > 0) || [];
224
+ const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
225
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
226
+ flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
227
+
228
+ const entityGroups: Record<string, Entity[]> = {};
229
+ results?.entities.forEach(e => {
230
+ if (!entityGroups[e.type]) entityGroups[e.type] = [];
231
+ if (!entityGroups[e.type].find(x => x.text === e.text)) entityGroups[e.type].push(e);
232
+ });
233
+
234
+ const obligationGroups: Record<string, Obligation[]> = {};
235
+ results?.obligations.forEach(o => {
236
+ if (!obligationGroups[o.type]) obligationGroups[o.type] = [];
237
+ obligationGroups[o.type].push(o);
238
+ });
239
+
240
+ const tabs = [
241
+ { key: "clauses", label: "Clauses", icon: Layers, count: flagged.length },
242
+ { key: "entities", label: "Entities", icon: Tag, count: results?.entities.length || 0 },
243
+ { key: "contradictions", label: "Issues", icon: AlertTriangle, count: results?.contradictions.length || 0 },
244
+ { key: "obligations", label: "Obligations", icon: ClipboardList, count: results?.obligations.length || 0 },
245
+ { key: "compliance", label: "Compliance", icon: ShieldCheck, count: Object.keys(results?.compliance || {}).length },
246
+ ];
247
+
248
+ return (
249
+ <div className="min-h-screen bg-zinc-50/30">
250
+ {/* Upgrade Modal */}
251
+ {showUpgrade && (
252
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4">
253
+ <div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl">
254
+ <div className="flex justify-between items-start">
255
+ <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center"><Lock className="w-5 h-5 text-amber-600" /></div>
256
+ <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
257
+ </div>
258
+ <h3 className="mt-4 text-lg font-semibold">{userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}</h3>
259
+ <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
260
+ {userPlan === "free" && scanCount >= FREE_LIMIT
261
+ ? `You have used all ${FREE_LIMIT} free scans. Upgrade to Pro for unlimited scans and full analysis.`
262
+ : "File upload is available on the Pro plan."}
263
+ </p>
264
+ <div className="mt-5 flex gap-2">
265
+ <a href="/#pricing" className="flex-1 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium text-center hover:bg-zinc-800 transition-colors">View plans</a>
266
+ <button onClick={() => setShowUpgrade(false)} className="flex-1 border border-zinc-200 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">Not now</button>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ )}
271
+
272
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
273
+ {/* Header */}
274
+ <div className="mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
275
+ <div>
276
+ <h1 className="text-xl sm:text-2xl font-semibold tracking-tight flex items-center gap-2">
277
+ <ScanText className="w-5 h-5 sm:w-6 sm:h-6 text-zinc-400" />
278
+ Scan a document
279
+ </h1>
280
+ <p className="mt-1 text-xs sm:text-sm text-zinc-500 max-w-xl">Paste text or upload a file. Get 41-category clause detection, risk scoring, ML NER, NLI contradictions, compliance checks, and obligation tracking.</p>
281
+ </div>
282
+ {userPlan === "free" && (
283
+ <span className="self-start text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md whitespace-nowrap">{scanCount}/{FREE_LIMIT} free scans</span>
284
+ )}
285
+ </div>
286
+
287
+ <div className="grid lg:grid-cols-5 gap-4 sm:gap-6">
288
+ {/* Input Panel */}
289
+ <div className="lg:col-span-2">
290
+ <div className="bg-white border border-zinc-200 rounded-xl p-3 sm:p-4">
291
+ <textarea value={text} onChange={(e) => setText(e.target.value)}
292
+ placeholder="Paste your contract or terms text here..."
293
+ className="w-full h-[260px] sm:h-[360px] p-3 border border-zinc-100 rounded-lg text-sm leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-zinc-900/10 focus:border-zinc-300 placeholder:text-zinc-300 font-mono bg-zinc-50/50" />
294
+ <div className="mt-3 flex gap-2">
295
+ <button onClick={handleAnalyze} disabled={loading}
296
+ className="flex-1 inline-flex items-center justify-center gap-2 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
297
+ {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Analyzing...</> : <><ScanText className="w-4 h-4" /> Analyze</>}
298
+ </button>
299
+ <button onClick={() => setText(EXAMPLE)} className="px-3 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">Example</button>
300
+ <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
301
+ <button onClick={() => fileInputRef.current?.click()} className="px-3 border border-zinc-200 rounded-lg text-zinc-500 hover:bg-zinc-50 transition-colors" title="Upload file"><Upload className="w-4 h-4" /></button>
302
+ </div>
303
+ </div>
304
+ {error && <p className="mt-2 text-sm text-red-600 flex items-center gap-1.5"><TriangleAlert className="w-3.5 h-3.5" />{error}</p>}
305
+ </div>
306
+
307
+ {/* Results Panel */}
308
+ <div className="lg:col-span-3">
309
+ {results ? (
310
+ <div className="space-y-3 sm:space-y-4">
311
+ {/* Score Card */}
312
+ <div className="bg-white border border-zinc-200 rounded-xl p-4 sm:p-5">
313
+ <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
314
+ <div>
315
+ <div className="flex items-baseline gap-2">
316
+ <span className="text-3xl sm:text-4xl font-semibold tracking-tight">{results.risk_score}</span>
317
+ <span className="text-sm text-zinc-400">/100 risk</span>
318
+ </div>
319
+ <div className="mt-2 h-1.5 w-full sm:w-48 bg-zinc-100 rounded-full overflow-hidden">
320
+ <div className={`h-full rounded-full transition-all duration-700 ${
321
+ results.risk_score >= 60 ? "bg-red-500" : results.risk_score >= 30 ? "bg-amber-400" : "bg-emerald-500"
322
+ }`} style={{ width: `${results.risk_score}%` }} />
323
+ </div>
324
+ </div>
325
+ <span className={`self-start text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
326
+ Grade {results.grade}
327
+ </span>
328
+ </div>
329
+
330
+ {/* Severity breakdown grid */}
331
+ <div className="mt-4 grid grid-cols-4 gap-2">
332
+ {(["CRITICAL", "HIGH", "MEDIUM", "LOW"] as const).map(sev => {
333
+ const c = SEV_CONFIG[sev];
334
+ return (
335
+ <div key={sev} className={`text-center p-2 rounded-lg ${c.bg} border ${c.border}`}>
336
+ <span className={`text-lg font-bold ${c.text}`}>{sevCounts[sev]}</span>
337
+ <p className={`text-[10px] ${c.text} opacity-70`}>{c.label}</p>
338
+ </div>
339
+ );
340
+ })}
341
+ </div>
342
+
343
+ {/* Meta stats */}
344
+ <div className="mt-3 flex items-center gap-2 sm:gap-3 text-[11px] text-zinc-400 flex-wrap">
345
+ <span className="flex items-center gap-1"><Layers className="w-3 h-3" />{results.total_clauses} clauses</span>
346
+ <span className="w-px h-3 bg-zinc-200" />
347
+ <span className="flex items-center gap-1"><Tag className="w-3 h-3" />{results.entities.length} entities</span>
348
+ <span className="w-px h-3 bg-zinc-200" />
349
+ <span className="flex items-center gap-1"><ClipboardList className="w-3 h-3" />{results.obligations.length} obligations</span>
350
+ <span className="w-px h-3 bg-zinc-200" />
351
+ <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{results.latency_ms}ms</span>
352
+ <span className="w-px h-3 bg-zinc-200" />
353
+ <span className="flex items-center gap-1">
354
+ {results.model !== "regex" ? <><Cpu className="w-3 h-3" /> ML Models</> : <><FileSearch className="w-3 h-3" /> Pattern fallback</>}
355
+ </span>
356
+ </div>
357
+ </div>
358
+
359
+ {/* Filter + Actions bar */}
360
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
361
+ <div className="flex gap-1 overflow-x-auto pb-1">
362
+ {[
363
+ { key: "all", label: "All", count: flagged.length },
364
+ { key: "CRITICAL", label: "Critical", count: sevCounts.CRITICAL },
365
+ { key: "HIGH", label: "High", count: sevCounts.HIGH },
366
+ { key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
367
+ { key: "LOW", label: "Low", count: sevCounts.LOW },
368
+ ].map((f) => (
369
+ <button key={f.key} onClick={() => setFilter(f.key)}
370
+ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors whitespace-nowrap ${filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"}`}>
371
+ {f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
372
+ </button>
373
+ ))}
374
+ </div>
375
+ <div className="flex gap-1.5 self-end sm:self-auto">
376
+ <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
377
+ {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
378
+ </button>
379
+ <button onClick={handleDownloadPDF} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Download PDF"><FileDown className="w-4 h-4" /></button>
380
+ </div>
381
+ </div>
382
+
383
+ {/* Tabs */}
384
+ <div className="border-b border-zinc-200 overflow-x-auto">
385
+ <div className="flex gap-0.5 min-w-max">
386
+ {tabs.map((t) => (
387
+ <button key={t.key} onClick={() => setActiveTab(t.key)}
388
+ className={`flex items-center gap-1.5 px-3 py-2 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
389
+ activeTab === t.key ? "border-zinc-900 text-zinc-900" : "border-transparent text-zinc-400 hover:text-zinc-600"
390
+ }`}>
391
+ <t.icon className="w-3.5 h-3.5" />{t.label}
392
+ {t.count > 0 && <span className="text-[10px] bg-zinc-100 text-zinc-500 px-1.5 py-0.5 rounded-full">{t.count}</span>}
393
+ </button>
394
+ ))}
395
+ </div>
396
+ </div>
397
+
398
+ {/* Tab Content */}
399
+ <div className="max-h-[350px] sm:max-h-[420px] overflow-y-auto pr-1">
400
+
401
+ {/* Clauses */}
402
+ {activeTab === "clauses" && (
403
+ <div className="space-y-2">
404
+ {filtered.length === 0 ? (
405
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
406
+ <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
407
+ <p className="text-sm text-zinc-500">{filter === "all" ? "No flagged clauses found." : "No clauses at this severity."}</p>
408
+ </div>
409
+ ) : filtered.map((clause, i) => {
410
+ const maxSev = clause.categories.reduce((m, c) => {
411
+ const order: Record<string, number> = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
412
+ return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
413
+ }, "LOW");
414
+ const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
415
+ const isExpanded = expandedIdx === i;
416
+ const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || Layers;
417
+ return (
418
+ <div key={i} className={`bg-white border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-md ring-1 " + conf.ring : "hover:shadow-sm"}`}>
419
+ <button onClick={() => setExpandedIdx(isExpanded ? null : i)} className="w-full text-left p-3 sm:p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
420
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
421
+ <CatIcon className={`w-4 h-4 ${conf.text}`} />
422
+ </div>
423
+ <div className="flex-1 min-w-0">
424
+ <div className="flex items-center gap-1.5 flex-wrap">
425
+ {clause.categories.map((cat, j) => {
426
+ const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
427
+ return (
428
+ <span key={j} className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
429
+ {cat.name}
430
+ </span>
431
+ );
432
+ })}
433
+ <SourceBadge isML={clause.categories[0]?.confidence != null} confidence={clause.categories[0]?.confidence} />
434
+ </div>
435
+ <p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
436
+ </div>
437
+ <div className="shrink-0 mt-1">{isExpanded ? <ChevronUp className="w-4 h-4 text-zinc-400" /> : <ChevronDown className="w-4 h-4 text-zinc-400" />}</div>
438
+ </button>
439
+ {isExpanded && (
440
+ <div className="px-3 sm:px-4 pb-4 pt-0 border-t border-zinc-100">
441
+ <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3 break-words">{clause.text}</p>
442
+ {clause.categories.map((cat, j) => (
443
+ <div key={j} className="mt-3 flex items-start gap-2">
444
+ <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
445
+ <div className="text-[13px] text-zinc-500 leading-relaxed">
446
+ <span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may contain risks. Review carefully."}
447
+ <div className="mt-1">
448
+ <SourceBadge isML={cat.confidence != null} confidence={cat.confidence} />
449
+ </div>
450
+ </div>
451
+ </div>
452
+ ))}
453
+ </div>
454
+ )}
455
+ </div>
456
+ );
457
+ })}
458
+ </div>
459
+ )}
460
+
461
+ {/* Entities */}
462
+ {activeTab === "entities" && (
463
+ <div className="space-y-4">
464
+ {Object.keys(entityGroups).length === 0 ? (
465
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
466
+ <Tag className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
467
+ <p className="text-sm text-zinc-500">No entities detected.</p>
468
+ </div>
469
+ ) : Object.entries(entityGroups).map(([type, items]) => {
470
+ const cfg = ENTITY_COLORS[type] || ENTITY_COLORS.MISC;
471
+ const Icon = cfg.icon;
472
+ const hasML = items.some(e => e.source === "ml");
473
+ return (
474
+ <div key={type} className="bg-white border border-zinc-200 rounded-xl p-3 sm:p-4">
475
+ <div className="flex items-center gap-2 mb-2.5">
476
+ <div className={`w-6 h-6 rounded flex items-center justify-center ${cfg.bg}`}>
477
+ <Icon className={`w-3.5 h-3.5 ${cfg.text}`} />
478
+ </div>
479
+ <span className="text-sm font-medium text-zinc-700">{type.replace(/_/g, " ")}</span>
480
+ <span className="text-[11px] text-zinc-400 bg-zinc-100 px-1.5 py-0.5 rounded">{items.length}</span>
481
+ {hasML && <SourceBadge isML={true} />}
482
+ </div>
483
+ <div className="flex flex-wrap gap-1.5">
484
+ {items.slice(0, 25).map((item, i) => (
485
+ <span key={i} className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${cfg.bg} ${cfg.text} border ${cfg.border}`}
486
+ title={item.score ? `Confidence: ${Math.round(item.score * 100)}%` : item.source || ""}>
487
+ {item.text}
488
+ </span>
489
+ ))}
490
+ </div>
491
+ </div>
492
+ );
493
+ })}
494
+ </div>
495
+ )}
496
+
497
+ {/* Contradictions */}
498
+ {activeTab === "contradictions" && (
499
+ <div className="space-y-2">
500
+ {results.contradictions.length === 0 ? (
501
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
502
+ <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
503
+ <p className="text-sm text-zinc-500">No contradictions or missing clauses detected.</p>
504
+ </div>
505
+ ) : results.contradictions.map((c, i) => {
506
+ const conf = SEV_CONFIG[c.severity] || SEV_CONFIG.MEDIUM;
507
+ return (
508
+ <div key={i} className={`bg-white border rounded-xl p-3 sm:p-4 ${conf.border}`}>
509
+ <div className="flex items-center gap-2 mb-2 flex-wrap">
510
+ <conf.icon className={`w-4 h-4 ${conf.text}`} />
511
+ <span className={`text-xs font-semibold uppercase ${conf.text}`}>{c.type}</span>
512
+ <ContradictionSourceBadge source={c.source} confidence={c.confidence} />
513
+ </div>
514
+ <p className="text-sm text-zinc-700 leading-relaxed">{c.explanation}</p>
515
+ </div>
516
+ );
517
+ })}
518
+ </div>
519
+ )}
520
+
521
+ {/* Obligations */}
522
+ {activeTab === "obligations" && (
523
+ <div className="space-y-4">
524
+ {Object.keys(obligationGroups).length === 0 ? (
525
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
526
+ <ClipboardList className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
527
+ <p className="text-sm text-zinc-500">No obligations detected.</p>
528
+ </div>
529
+ ) : (
530
+ <>
531
+ {/* Summary cards */}
532
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2">
533
+ {Object.entries(obligationGroups).map(([type, items]) => {
534
+ const cfg = OBLIGATION_COLORS[type] || OBLIGATION_COLORS.compliance;
535
+ const Icon = cfg.icon;
536
+ return (
537
+ <div key={type} className={`text-center p-3 rounded-xl ${cfg.bg} border ${cfg.border}`}>
538
+ <Icon className={`w-5 h-5 mx-auto ${cfg.text}`} />
539
+ <span className={`text-lg font-bold ${cfg.text}`}>{items.length}</span>
540
+ <p className={`text-[10px] capitalize ${cfg.text} opacity-70`}>{type}</p>
541
+ </div>
542
+ );
543
+ })}
544
+ </div>
545
+ {/* Individual obligations */}
546
+ {Object.entries(obligationGroups).map(([type, items]) => {
547
+ const cfg = OBLIGATION_COLORS[type] || OBLIGATION_COLORS.compliance;
548
+ const Icon = cfg.icon;
549
+ return (
550
+ <div key={type}>
551
+ <div className="flex items-center gap-2 mb-2">
552
+ <Icon className={`w-4 h-4 ${cfg.text}`} />
553
+ <span className="text-sm font-medium capitalize text-zinc-700">{type}</span>
554
+ <span className="text-[11px] text-zinc-400 bg-zinc-100 px-1.5 py-0.5 rounded">{items.length}</span>
555
+ </div>
556
+ <div className="space-y-2">
557
+ {items.map((o, i) => (
558
+ <div key={i} className="bg-white border border-zinc-200 rounded-lg p-3">
559
+ <div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
560
+ <div className="flex items-center gap-2">
561
+ <span className="text-xs font-medium text-zinc-600">{o.party}</span>
562
+ {o.priority != null && o.priority >= 3 && (
563
+ <span className="inline-flex items-center gap-1 text-[9px] bg-red-50 text-red-600 border border-red-200 px-1.5 py-0.5 rounded font-semibold">
564
+ <AlertTriangle className="w-2.5 h-2.5" />HIGH
565
+ </span>
566
+ )}
567
+ {o.priority != null && o.priority === 2 && (
568
+ <span className="inline-flex items-center gap-1 text-[9px] bg-amber-50 text-amber-600 border border-amber-200 px-1.5 py-0.5 rounded font-semibold">
569
+ <CircleAlert className="w-2.5 h-2.5" />MED
570
+ </span>
571
+ )}
572
+ </div>
573
+ <span className="text-[11px] text-zinc-400 bg-zinc-100 px-2 py-0.5 rounded flex items-center gap-1">
574
+ <Clock className="w-3 h-3" />{o.deadline}
575
+ </span>
576
+ </div>
577
+ <p className="text-sm text-zinc-600 leading-relaxed">{o.description}</p>
578
+ </div>
579
+ ))}
580
+ </div>
581
+ </div>
582
+ );
583
+ })}
584
+ </>
585
+ )}
586
+ </div>
587
+ )}
588
+
589
+ {/* Compliance */}
590
+ {activeTab === "compliance" && (
591
+ <div className="space-y-4">
592
+ {Object.keys(results.compliance).length === 0 ? (
593
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
594
+ <ShieldCheck className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
595
+ <p className="text-sm text-zinc-500">No compliance data available.</p>
596
+ </div>
597
+ ) : Object.entries(results.compliance).map(([regName, reg]) => {
598
+ const status = COMPLIANCE_STATUS[reg.overall_status] || COMPLIANCE_STATUS.PARTIAL;
599
+ return (
600
+ <div key={regName} className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
601
+ <div className={`flex flex-col sm:flex-row sm:items-center justify-between p-4 border-b ${status.bg} ${status.border}`}>
602
+ <div>
603
+ <div className="flex items-center gap-2 flex-wrap">
604
+ <span className="text-sm font-semibold text-zinc-900">{regName}</span>
605
+ {(reg.negated_count ?? 0) > 0 && (
606
+ <span className="inline-flex items-center gap-1 text-[9px] bg-orange-50 text-orange-600 border border-orange-200 px-1.5 py-0.5 rounded font-medium">
607
+ <ShieldOff className="w-2.5 h-2.5" />{reg.negated_count} negated
608
+ </span>
609
+ )}
610
+ {(reg.ambiguous_count ?? 0) > 0 && (
611
+ <span className="inline-flex items-center gap-1 text-[9px] bg-amber-50 text-amber-600 border border-amber-200 px-1.5 py-0.5 rounded font-medium">
612
+ <HelpCircle className="w-2.5 h-2.5" />{reg.ambiguous_count} ambiguous
613
+ </span>
614
+ )}
615
+ </div>
616
+ <p className="text-[11px] text-zinc-500 mt-0.5">{reg.description}</p>
617
+ </div>
618
+ <div className="text-left sm:text-right mt-2 sm:mt-0">
619
+ <span className={`text-lg font-bold ${status.text}`}>{reg.compliance_rate}%</span>
620
+ <span className={`text-[11px] font-medium block ${status.text}`}>{reg.overall_status}</span>
621
+ </div>
622
+ </div>
623
+ <div className="p-3 space-y-0.5">
624
+ {reg.checks.map((check, i) => {
625
+ const sev = SEV_CONFIG[check.severity] || SEV_CONFIG.MEDIUM;
626
+ return (
627
+ <div key={i} className="py-2.5 px-2 hover:bg-zinc-50 rounded-lg transition-colors">
628
+ <div className="flex items-start justify-between gap-2">
629
+ <div className="flex-1 min-w-0">
630
+ <p className="text-xs text-zinc-600 leading-relaxed">{check.description}</p>
631
+ {check.matched_keywords.length > 0 && (
632
+ <p className="text-[10px] text-zinc-400 mt-0.5">Matched: {check.matched_keywords.slice(0, 3).join(", ")}</p>
633
+ )}
634
+ {check.context && check.context.length > 0 && (
635
+ <p className="text-[10px] text-zinc-400 mt-1 italic border-l-2 border-zinc-200 pl-2 line-clamp-2">
636
+ {check.context[0].slice(0, 120)}
637
+ </p>
638
+ )}
639
+ </div>
640
+ <div className="flex items-center gap-2 ml-2 shrink-0">
641
+ <span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${sev.bg} ${sev.text}`}>{check.severity}</span>
642
+ <CheckStatusIcon status={check.status} />
643
+ </div>
644
+ </div>
645
+ </div>
646
+ );
647
+ })}
648
+ </div>
649
+ </div>
650
+ );
651
+ })}
652
+ </div>
653
+ )}
654
+ </div>
655
+ </div>
656
+ ) : (
657
+ <div className="bg-white border border-dashed border-zinc-200 rounded-xl h-[300px] sm:h-[420px] flex flex-col items-center justify-center">
658
+ <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
659
+ <p className="text-sm text-zinc-300">Paste text and analyze to see results</p>
660
+ </div>
661
+ )}
662
+ </div>
663
+ </div>
664
+ </div>
665
+ </div>
666
+ );
667
+ }