gaurv007 commited on
Commit
3a6561a
Β·
verified Β·
1 Parent(s): f0fb36b

v3.0: Update analyze page β€” source indicators (πŸ€–/πŸ“), NLI confidence, negation badges, priority, new entity types

Browse files
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -1,525 +1 @@
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
11
- } from "lucide-react";
12
-
13
- interface Cat { name: string; severity: string; description?: string; confidence?: number; }
14
- interface Clause { text: string; categories: Cat[]; }
15
- interface Entity { text: string; type: string; }
16
- interface Contradiction { type: string; explanation: string; severity: string; }
17
- interface Obligation { type: string; party: string; description: string; deadline: string; }
18
- interface ComplianceCheck { requirement: string; description: string; severity: string; status: string; matched_keywords: string[]; }
19
- interface ComplianceReg { description: string; compliance_rate: number; checks: ComplianceCheck[]; overall_status: string; }
20
- interface AnalysisResult {
21
- risk_score: number;
22
- grade: string;
23
- total_clauses: number;
24
- flagged_count: number;
25
- results: Clause[];
26
- entities: Entity[];
27
- contradictions: Contradiction[];
28
- obligations: Obligation[];
29
- compliance: Record<string, ComplianceReg>;
30
- model: string;
31
- latency_ms: number;
32
- }
33
-
34
- const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string }> = {
35
- CRITICAL: { icon: AlertTriangle, label: "Critical", text: "text-red-700", bg: "bg-red-50", border: "border-red-300" },
36
- HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200" },
37
- MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200" },
38
- LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200" },
39
- };
40
-
41
- const GRADE_STYLE: Record<string, string> = {
42
- A: "bg-emerald-50 text-emerald-700 border-emerald-200",
43
- B: "bg-emerald-50 text-emerald-600 border-emerald-200",
44
- C: "bg-amber-50 text-amber-700 border-amber-200",
45
- D: "bg-orange-50 text-orange-700 border-orange-200",
46
- F: "bg-red-50 text-red-700 border-red-200",
47
- };
48
-
49
- const CATEGORY_ICONS: Record<string, any> = {
50
- "Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
51
- "Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
52
- "Choice of law": Gavel, "Contract by using": Stamp, "Uncapped Liability": AlertTriangle,
53
- "IP Ownership Assignment": Lock, "Non-Compete": Ban, "Governing Law": Gavel,
54
- "Termination for Convenience": Ban, "Indemnification": ShieldCheck, "Confidentiality": Lock,
55
- };
56
-
57
- const ENTITY_COLORS: Record<string, { bg: string; text: string; border: string; icon: any }> = {
58
- DATE: { bg: "bg-blue-50", text: "text-blue-700", border: "border-blue-200", icon: Calendar },
59
- DATE_REF: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200", icon: Calendar },
60
- MONEY: { bg: "bg-emerald-50", text: "text-emerald-700", border: "border-emerald-200", icon: DollarSign },
61
- PARTY: { bg: "bg-purple-50", text: "text-purple-700", border: "border-purple-200", icon: Building },
62
- PARTY_ROLE: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200", icon: Briefcase },
63
- JURISDICTION: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200", icon: MapPin },
64
- DEFINED_TERM: { bg: "bg-pink-50", text: "text-pink-700", border: "border-pink-200", icon: Hash },
65
- };
66
-
67
- const OBLIGATION_COLORS: Record<string, { bg: string; text: string; icon: any }> = {
68
- monetary: { bg: "bg-emerald-50", text: "text-emerald-700", icon: DollarSign },
69
- compliance: { bg: "bg-amber-50", text: "text-amber-700", icon: ShieldCheck },
70
- reporting: { bg: "bg-blue-50", text: "text-blue-700", icon: ClipboardList },
71
- delivery: { bg: "bg-purple-50", text: "text-purple-700", icon: FileText },
72
- termination: { bg: "bg-red-50", text: "text-red-700", icon: Ban },
73
- };
74
-
75
- const COMPLIANCE_STATUS: Record<string, { bg: string; text: string }> = {
76
- COMPLIANT: { bg: "bg-emerald-50", text: "text-emerald-700" },
77
- PARTIAL: { bg: "bg-amber-50", text: "text-amber-700" },
78
- "NON-COMPLIANT": { bg: "bg-red-50", text: "text-red-700" },
79
- };
80
-
81
- const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
82
-
83
- 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.
84
-
85
- 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.
86
-
87
- Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
88
-
89
- Spotify may terminate your account or suspend your access at any time, with or without cause, with or without notice, effective immediately.
90
-
91
- These Terms will be governed by and construed in accordance with the laws of the State of New York.
92
-
93
- Any dispute shall be finally settled by arbitration in New York County.`;
94
-
95
- export default function AnalyzePage() {
96
- const [text, setText] = useState("");
97
- const [results, setResults] = useState<AnalysisResult | null>(null);
98
- const [loading, setLoading] = useState(false);
99
- const [error, setError] = useState("");
100
- const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
101
- const [filter, setFilter] = useState<string>("all");
102
- const [activeTab, setActiveTab] = useState<string>("clauses");
103
- const [copied, setCopied] = useState(false);
104
- const [scanCount, setScanCount] = useState(0);
105
- const [userPlan, setUserPlan] = useState("free");
106
- const [showUpgrade, setShowUpgrade] = useState(false);
107
- const fileInputRef = useRef<HTMLInputElement>(null);
108
-
109
- const FREE_LIMIT = 10;
110
- const canScan = userPlan !== "free" || scanCount < FREE_LIMIT;
111
-
112
- async function handleAnalyze() {
113
- if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
114
- if (!canScan) { setShowUpgrade(true); return; }
115
-
116
- setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
117
- try {
118
- const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
119
- if (!res.ok) throw new Error((await res.json()).error || "Failed");
120
- const data = await res.json();
121
- setResults(data);
122
- setScanCount(prev => prev + 1);
123
- } catch (e: any) { setError(e.message); }
124
- finally { setLoading(false); }
125
- }
126
-
127
- async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
128
- const file = e.target.files?.[0];
129
- if (!file) return;
130
- if (userPlan === "free") { setShowUpgrade(true); return; }
131
-
132
- setLoading(true); setError("");
133
- try {
134
- const formData = new FormData();
135
- formData.append("file", file);
136
- const res = await fetch("/api/parse-upload", { method: "POST", body: formData });
137
- if (!res.ok) throw new Error((await res.json()).error || "Failed to parse file");
138
- const { text: extractedText } = await res.json();
139
- setText(extractedText);
140
- } catch (e: any) { setError(e.message || "Could not read file."); }
141
- setLoading(false);
142
- if (fileInputRef.current) fileInputRef.current.value = "";
143
- }
144
-
145
- async function handleDownloadPDF() {
146
- if (!results) return;
147
- try {
148
- const res = await fetch("/api/pdf/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(results) });
149
- const blob = await res.blob();
150
- const url = URL.createObjectURL(blob);
151
- const a = document.createElement("a"); a.href = url; a.download = "clauseguard-report.pdf"; a.click();
152
- URL.revokeObjectURL(url);
153
- } catch {}
154
- }
155
-
156
- function handleCopy() {
157
- if (!results) return;
158
- 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} found\nContradictions: ${results.contradictions.length} detected\n\n` +
159
- results.results.filter(r => r.categories.length > 0).map((r, i) =>
160
- `${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
161
- ).join("\n");
162
- navigator.clipboard.writeText(summary);
163
- setCopied(true); setTimeout(() => setCopied(false), 2000);
164
- }
165
-
166
- const flagged = results?.results.filter(r => r.categories.length > 0) || [];
167
- const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
168
-
169
- const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
170
- flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
171
-
172
- // Group entities by type
173
- const entityGroups: Record<string, string[]> = {};
174
- results?.entities.forEach(e => {
175
- if (!entityGroups[e.type]) entityGroups[e.type] = [];
176
- if (!entityGroups[e.type].includes(e.text)) entityGroups[e.type].push(e.text);
177
- });
178
-
179
- // Group obligations by type
180
- const obligationGroups: Record<string, Obligation[]> = {};
181
- results?.obligations.forEach(o => {
182
- if (!obligationGroups[o.type]) obligationGroups[o.type] = [];
183
- obligationGroups[o.type].push(o);
184
- });
185
-
186
- const tabs = [
187
- { key: "clauses", label: "Clauses", icon: Layers },
188
- { key: "entities", label: "Entities", icon: Tag },
189
- { key: "contradictions", label: "Contradictions", icon: AlertTriangle },
190
- { key: "obligations", label: "Obligations", icon: ClipboardList },
191
- { key: "compliance", label: "Compliance", icon: ShieldCheck },
192
- ];
193
-
194
- return (
195
- <div className="min-h-screen bg-white">
196
- {showUpgrade && (
197
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
198
- <div className="bg-white rounded-2xl p-6 max-w-sm mx-4 shadow-xl">
199
- <div className="flex justify-between items-start">
200
- <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>
201
- <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
202
- </div>
203
- <h3 className="mt-4 text-lg font-semibold">{userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}</h3>
204
- <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
205
- {userPlan === "free" && scanCount >= FREE_LIMIT
206
- ? `You have used all ${FREE_LIMIT} free scans. Upgrade to Pro for unlimited scans, file uploads, and full analysis.`
207
- : "File upload is available on the Pro plan. Upgrade to scan contracts and leases directly."}
208
- </p>
209
- <div className="mt-5 flex gap-2">
210
- <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>
211
- <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>
212
- </div>
213
- </div>
214
- </div>
215
- )}
216
-
217
- <div className="max-w-7xl mx-auto px-5 py-10">
218
- <div className="mb-8 flex items-start justify-between">
219
- <div>
220
- <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
221
- <ScanText className="w-6 h-6 text-zinc-400" />
222
- Scan a document
223
- </h1>
224
- <p className="mt-1 text-sm text-zinc-500">Paste text or upload a file (.pdf, .docx, .txt). Get 41-category clause detection, risk scoring, NER, compliance, and more.</p>
225
- </div>
226
- {userPlan === "free" && (
227
- <span className="text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md">{scanCount}/{FREE_LIMIT} free scans</span>
228
- )}
229
- </div>
230
-
231
- <div className="grid lg:grid-cols-5 gap-6">
232
- {/* Input */}
233
- <div className="lg:col-span-2">
234
- <textarea value={text} onChange={(e) => setText(e.target.value)}
235
- placeholder="Paste your contract or terms text here..."
236
- className="w-full h-[380px] p-4 border border-zinc-200 rounded-xl 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" />
237
- <div className="mt-3 flex gap-2">
238
- <button onClick={handleAnalyze} disabled={loading}
239
- 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">
240
- {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Analyzing...</> : <><ScanText className="w-4 h-4" /> Analyze</>}
241
- </button>
242
- <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>
243
- <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
244
- <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>
245
- </div>
246
- {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>}
247
- </div>
248
-
249
- {/* Results */}
250
- <div className="lg:col-span-3">
251
- {results ? (
252
- <div className="space-y-4">
253
- {/* Score card */}
254
- <div className="border border-zinc-200 rounded-xl p-5">
255
- <div className="flex items-start justify-between">
256
- <div>
257
- <div className="flex items-baseline gap-2">
258
- <span className="text-4xl font-semibold tracking-tight">{results.risk_score}</span>
259
- <span className="text-sm text-zinc-400">/100 risk</span>
260
- </div>
261
- <div className="mt-2 h-1.5 w-48 bg-zinc-100 rounded-full overflow-hidden">
262
- <div className={`h-full rounded-full transition-all duration-700 ${
263
- results.risk_score >= 60 ? "bg-red-500" : results.risk_score >= 30 ? "bg-amber-400" : "bg-emerald-500"
264
- }`} style={{ width: `${results.risk_score}%` }} />
265
- </div>
266
- </div>
267
- <span className={`text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
268
- Grade {results.grade}
269
- </span>
270
- </div>
271
- <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
272
- <span>{results.total_clauses} clauses</span><span className="w-px h-3 bg-zinc-200" />
273
- <span>{results.flagged_count} flagged</span><span className="w-px h-3 bg-zinc-200" />
274
- <span>{results.entities.length} entities</span><span className="w-px h-3 bg-zinc-200" />
275
- <span>{results.contradictions.length} issues</span><span className="w-px h-3 bg-zinc-200" />
276
- <span>{results.latency_ms}ms</span><span className="w-px h-3 bg-zinc-200" />
277
- <span className="flex items-center gap-1">{results.model !== "regex" && <SparklesIcon className="w-3 h-3" />}{results.model !== "regex" ? "Legal-BERT v2" : "Pattern fallback"}</span>
278
- </div>
279
- </div>
280
-
281
- {/* Filter + Actions */}
282
- <div className="flex items-center justify-between">
283
- <div className="flex gap-1">
284
- {[
285
- { key: "all", label: "All", count: flagged.length },
286
- { key: "CRITICAL", label: "Critical", count: sevCounts.CRITICAL },
287
- { key: "HIGH", label: "High", count: sevCounts.HIGH },
288
- { key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
289
- { key: "LOW", label: "Low", count: sevCounts.LOW },
290
- ].map((f) => (
291
- <button key={f.key} onClick={() => setFilter(f.key)}
292
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"}`}>
293
- {f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
294
- </button>
295
- ))}
296
- </div>
297
- <div className="flex gap-1.5">
298
- <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
299
- {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
300
- </button>
301
- <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>
302
- </div>
303
- </div>
304
-
305
- {/* Tabs */}
306
- <div className="border-b border-zinc-200">
307
- <div className="flex gap-1">
308
- {tabs.map((t) => (
309
- <button key={t.key} onClick={() => setActiveTab(t.key)}
310
- className={`flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
311
- activeTab === t.key ? "border-zinc-900 text-zinc-900" : "border-transparent text-zinc-400 hover:text-zinc-600"
312
- }`}>
313
- <t.icon className="w-4 h-4" />{t.label}
314
- </button>
315
- ))}
316
- </div>
317
- </div>
318
-
319
- {/* Tab Content */}
320
- <div className="max-h-[420px] overflow-y-auto pr-1">
321
- {/* Clauses Tab */}
322
- {activeTab === "clauses" && (
323
- <div className="space-y-2">
324
- {filtered.length === 0 ? (
325
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
326
- <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
327
- <p className="text-sm text-zinc-500">{filter === "all" ? "No flagged clauses found." : "No clauses at this severity."}</p>
328
- </div>
329
- ) : filtered.map((clause, i) => {
330
- const maxSev = clause.categories.reduce((m, c) => {
331
- const order: Record<string, number> = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
332
- return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
333
- }, "LOW");
334
- const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
335
- const isExpanded = expandedIdx === i;
336
- const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || Layers;
337
- return (
338
- <div key={i} className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
339
- <button onClick={() => setExpandedIdx(isExpanded ? null : i)} className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
340
- <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
341
- <CatIcon className={`w-4 h-4 ${conf.text}`} />
342
- </div>
343
- <div className="flex-1 min-w-0">
344
- <div className="flex items-center gap-2 flex-wrap">
345
- {clause.categories.map((cat, j) => {
346
- const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
347
- return (
348
- <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
349
- {cat.name}{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
350
- </span>
351
- );
352
- })}
353
- </div>
354
- <p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
355
- </div>
356
- <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>
357
- </button>
358
- {isExpanded && (
359
- <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
360
- <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">{clause.text}</p>
361
- {clause.categories.map((cat, j) => (
362
- <div key={j} className="mt-3 flex items-start gap-2">
363
- <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
364
- <p className="text-[13px] text-zinc-500 leading-relaxed">
365
- <span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may contain risks. Review carefully."}
366
- </p>
367
- </div>
368
- ))}
369
- </div>
370
- )}
371
- </div>
372
- );
373
- })}
374
- </div>
375
- )}
376
-
377
- {/* Entities Tab */}
378
- {activeTab === "entities" && (
379
- <div className="space-y-4">
380
- {Object.keys(entityGroups).length === 0 ? (
381
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
382
- <Tag className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
383
- <p className="text-sm text-zinc-500">No entities detected.</p>
384
- </div>
385
- ) : Object.entries(entityGroups).map(([type, items]) => {
386
- const cfg = ENTITY_COLORS[type] || { bg: "bg-zinc-50", text: "text-zinc-700", border: "border-zinc-200", icon: Tag };
387
- const Icon = cfg.icon;
388
- return (
389
- <div key={type}>
390
- <div className="flex items-center gap-2 mb-2">
391
- <Icon className={`w-4 h-4 ${cfg.text}`} />
392
- <span className="text-sm font-medium text-zinc-700">{type.replace("_", " ")}</span>
393
- <span className="text-xs text-zinc-400">({items.length})</span>
394
- </div>
395
- <div className="flex flex-wrap gap-2">
396
- {items.slice(0, 20).map((item, i) => (
397
- <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}`}>
398
- {item}
399
- </span>
400
- ))}
401
- </div>
402
- </div>
403
- );
404
- })}
405
- </div>
406
- )}
407
-
408
- {/* Contradictions Tab */}
409
- {activeTab === "contradictions" && (
410
- <div className="space-y-2">
411
- {results.contradictions.length === 0 ? (
412
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
413
- <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
414
- <p className="text-sm text-zinc-500">No contradictions or missing clauses detected.</p>
415
- </div>
416
- ) : results.contradictions.map((c, i) => {
417
- const conf = SEV_CONFIG[c.severity] || SEV_CONFIG.MEDIUM;
418
- return (
419
- <div key={i} className={`border rounded-xl p-4 ${conf.border} ${conf.bg}`}>
420
- <div className="flex items-center gap-2 mb-2">
421
- <conf.icon className={`w-4 h-4 ${conf.text}`} />
422
- <span className={`text-xs font-semibold uppercase ${conf.text}`}>{c.type}</span>
423
- </div>
424
- <p className="text-sm text-zinc-700">{c.explanation}</p>
425
- </div>
426
- );
427
- })}
428
- </div>
429
- )}
430
-
431
- {/* Obligations Tab */}
432
- {activeTab === "obligations" && (
433
- <div className="space-y-4">
434
- {Object.keys(obligationGroups).length === 0 ? (
435
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
436
- <ClipboardList className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
437
- <p className="text-sm text-zinc-500">No obligations detected.</p>
438
- </div>
439
- ) : Object.entries(obligationGroups).map(([type, items]) => {
440
- const cfg = OBLIGATION_COLORS[type] || { bg: "bg-zinc-50", text: "text-zinc-700", icon: ClipboardList };
441
- const Icon = cfg.icon;
442
- return (
443
- <div key={type}>
444
- <div className="flex items-center gap-2 mb-2">
445
- <Icon className={`w-4 h-4 ${cfg.text}`} />
446
- <span className="text-sm font-medium capitalize text-zinc-700">{type} Obligations</span>
447
- <span className="text-xs text-zinc-400">({items.length})</span>
448
- </div>
449
- <div className="space-y-2">
450
- {items.map((o, i) => (
451
- <div key={i} className="border border-zinc-200 rounded-lg p-3">
452
- <div className="flex items-center justify-between mb-1">
453
- <span className="text-xs font-medium text-zinc-600">{o.party}</span>
454
- <span className="text-[11px] text-zinc-400 bg-zinc-100 px-2 py-0.5 rounded">{o.deadline}</span>
455
- </div>
456
- <p className="text-sm text-zinc-600">{o.description}</p>
457
- </div>
458
- ))}
459
- </div>
460
- </div>
461
- );
462
- })}
463
- </div>
464
- )}
465
-
466
- {/* Compliance Tab */}
467
- {activeTab === "compliance" && (
468
- <div className="space-y-4">
469
- {Object.keys(results.compliance).length === 0 ? (
470
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
471
- <ShieldCheck className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
472
- <p className="text-sm text-zinc-500">No compliance data available.</p>
473
- </div>
474
- ) : Object.entries(results.compliance).map(([regName, reg]) => {
475
- const status = COMPLIANCE_STATUS[reg.overall_status] || COMPLIANCE_STATUS.PARTIAL;
476
- return (
477
- <div key={regName} className="border border-zinc-200 rounded-xl overflow-hidden">
478
- <div className="flex items-center justify-between p-4 border-b border-zinc-100 bg-zinc-50/50">
479
- <div>
480
- <span className="text-sm font-semibold text-zinc-900">{regName}</span>
481
- <p className="text-[11px] text-zinc-500 mt-0.5">{reg.description}</p>
482
- </div>
483
- <div className="text-right">
484
- <span className={`text-lg font-bold ${status.text}`}>{reg.compliance_rate}%</span>
485
- <span className={`text-[11px] font-medium block ${status.text}`}>{reg.overall_status}</span>
486
- </div>
487
- </div>
488
- <div className="p-3 space-y-1">
489
- {reg.checks.map((check, i) => {
490
- const sev = SEV_CONFIG[check.severity] || SEV_CONFIG.MEDIUM;
491
- return (
492
- <div key={i} className="flex items-center justify-between py-2 px-2 hover:bg-zinc-50 rounded-md">
493
- <div className="flex-1 min-w-0">
494
- <p className="text-xs text-zinc-600">{check.description}</p>
495
- {check.matched_keywords.length > 0 && (
496
- <p className="text-[10px] text-zinc-400 mt-0.5">Matched: {check.matched_keywords.slice(0, 3).join(", ")}</p>
497
- )}
498
- </div>
499
- <div className="flex items-center gap-2 ml-3">
500
- <span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${sev.bg} ${sev.text}`}>{check.severity}</span>
501
- <span className="text-sm">{check.status === "PASS" ? "βœ…" : "❌"}</span>
502
- </div>
503
- </div>
504
- );
505
- })}
506
- </div>
507
- </div>
508
- );
509
- })}
510
- </div>
511
- )}
512
- </div>
513
- </div>
514
- ) : (
515
- <div className="border border-dashed border-zinc-200 rounded-xl h-[420px] flex flex-col items-center justify-center">
516
- <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
517
- <p className="text-sm text-zinc-300">Paste text and analyze to see results</p>
518
- </div>
519
- )}
520
- </div>
521
- </div>
522
- </div>
523
- </div>
524
- );
525
- }
 
1
+ /app/web/analyze_page.tsx