gaurv007 commited on
Commit
a9c4f8f
·
verified ·
1 Parent(s): ea2f207

Upload web/app/dashboard-pages/analyze/page.tsx

Browse files
Files changed (1) hide show
  1. web/app/dashboard-pages/analyze/page.tsx +306 -121
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -1,18 +1,38 @@
1
  "use client";
2
 
3
- import { useState, useRef, useEffect } 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
 
 
9
  } from "lucide-react";
10
 
11
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
12
  interface Clause { text: string; categories: Cat[]; }
13
- interface Result { risk_score: number; grade: string; total_clauses: number; flagged_count: number; results: Clause[]; model: string; latency_ms: number; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string }> = {
 
16
  HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200" },
17
  MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200" },
18
  LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200" },
@@ -20,16 +40,42 @@ const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: s
20
 
21
  const GRADE_STYLE: Record<string, string> = {
22
  A: "bg-emerald-50 text-emerald-700 border-emerald-200",
23
- B: "bg-emerald-50 text-emerald-700 border-emerald-200",
24
  C: "bg-amber-50 text-amber-700 border-amber-200",
25
- D: "bg-red-50 text-red-700 border-red-200",
26
  F: "bg-red-50 text-red-700 border-red-200",
27
  };
28
 
29
  const CATEGORY_ICONS: Record<string, any> = {
30
  "Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
31
  "Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
32
- "Choice of law": Gavel, "Contract by using": Stamp,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  };
34
 
35
  const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
@@ -48,11 +94,12 @@ Any dispute shall be finally settled by arbitration in New York County.`;
48
 
49
  export default function AnalyzePage() {
50
  const [text, setText] = useState("");
51
- const [results, setResults] = useState<Result | null>(null);
52
  const [loading, setLoading] = useState(false);
53
  const [error, setError] = useState("");
54
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
55
  const [filter, setFilter] = useState<string>("all");
 
56
  const [copied, setCopied] = useState(false);
57
  const [scanCount, setScanCount] = useState(0);
58
  const [userPlan, setUserPlan] = useState("free");
@@ -64,7 +111,6 @@ export default function AnalyzePage() {
64
 
65
  async function handleAnalyze() {
66
  if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
67
-
68
  if (!canScan) { setShowUpgrade(true); return; }
69
 
70
  setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
@@ -81,27 +127,17 @@ export default function AnalyzePage() {
81
  async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
82
  const file = e.target.files?.[0];
83
  if (!file) return;
84
-
85
  if (userPlan === "free") { setShowUpgrade(true); return; }
86
 
87
- setLoading(true);
88
- setError("");
89
-
90
  try {
91
  const formData = new FormData();
92
  formData.append("file", file);
93
-
94
  const res = await fetch("/api/parse-upload", { method: "POST", body: formData });
95
- if (!res.ok) {
96
- const err = await res.json();
97
- throw new Error(err.error || "Failed to parse file");
98
- }
99
-
100
  const { text: extractedText } = await res.json();
101
  setText(extractedText);
102
- } catch (e: any) {
103
- setError(e.message || "Could not read file.");
104
- }
105
  setLoading(false);
106
  if (fileInputRef.current) fileInputRef.current.value = "";
107
  }
@@ -119,7 +155,7 @@ export default function AnalyzePage() {
119
 
120
  function handleCopy() {
121
  if (!results) return;
122
- const summary = `ClauseGuard Report\nRisk: ${results.risk_score}/100 (Grade ${results.grade})\n${results.flagged_count} of ${results.total_clauses} clauses flagged\n\n` +
123
  results.results.filter(r => r.categories.length > 0).map((r, i) =>
124
  `${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
125
  ).join("\n");
@@ -130,82 +166,87 @@ export default function AnalyzePage() {
130
  const flagged = results?.results.filter(r => r.categories.length > 0) || [];
131
  const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
132
 
133
- const sevCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
134
  flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  return (
137
  <div className="min-h-screen bg-white">
138
- {/* Upgrade modal */}
139
  {showUpgrade && (
140
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
141
  <div className="bg-white rounded-2xl p-6 max-w-sm mx-4 shadow-xl">
142
  <div className="flex justify-between items-start">
143
- <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center">
144
- <Lock className="w-5 h-5 text-amber-600" />
145
- </div>
146
  <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
147
  </div>
148
- <h3 className="mt-4 text-lg font-semibold">
149
- {userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}
150
- </h3>
151
  <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
152
  {userPlan === "free" && scanCount >= FREE_LIMIT
153
- ? `You have used all ${FREE_LIMIT} free scans this month. Upgrade to Pro for unlimited scans, file uploads, and AI explanations.`
154
  : "File upload is available on the Pro plan. Upgrade to scan contracts and leases directly."}
155
  </p>
156
  <div className="mt-5 flex gap-2">
157
- <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">
158
- View plans
159
- </a>
160
- <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">
161
- Not now
162
- </button>
163
  </div>
164
  </div>
165
  </div>
166
  )}
167
 
168
- <div className="max-w-6xl mx-auto px-5 py-10">
169
  <div className="mb-8 flex items-start justify-between">
170
  <div>
171
  <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
172
  <ScanText className="w-6 h-6 text-zinc-400" />
173
  Scan a document
174
  </h1>
175
- <p className="mt-1 text-sm text-zinc-500">Paste text or upload a file (.pdf, .docx, .txt).</p>
176
  </div>
177
  {userPlan === "free" && (
178
- <span className="text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md">
179
- {scanCount}/{FREE_LIMIT} free scans
180
- </span>
181
  )}
182
  </div>
183
 
184
  <div className="grid lg:grid-cols-5 gap-6">
185
- {/* Input — 2 cols */}
186
  <div className="lg:col-span-2">
187
  <textarea value={text} onChange={(e) => setText(e.target.value)}
188
- placeholder="Paste your document text here..."
189
  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" />
190
  <div className="mt-3 flex gap-2">
191
  <button onClick={handleAnalyze} disabled={loading}
192
  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">
193
- {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Scanning...</> : <><ScanText className="w-4 h-4" /> Scan</>}
194
- </button>
195
- <button onClick={() => setText(EXAMPLE)}
196
- className="px-3 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">
197
- Example
198
  </button>
 
199
  <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
200
- <button onClick={() => fileInputRef.current?.click()}
201
- className="px-3 border border-zinc-200 rounded-lg text-zinc-500 hover:bg-zinc-50 transition-colors" title="Upload file">
202
- <Upload className="w-4 h-4" />
203
- </button>
204
  </div>
205
  {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>}
206
  </div>
207
 
208
- {/* Results — 3 cols */}
209
  <div className="lg:col-span-3">
210
  {results ? (
211
  <div className="space-y-4">
@@ -228,32 +269,27 @@ export default function AnalyzePage() {
228
  </span>
229
  </div>
230
  <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
231
- <span>{results.total_clauses} clauses</span>
232
- <span className="w-px h-3 bg-zinc-200" />
233
- <span>{results.flagged_count} flagged</span>
234
- <span className="w-px h-3 bg-zinc-200" />
235
- <span>{results.latency_ms}ms</span>
236
- <span className="w-px h-3 bg-zinc-200" />
237
- <span className="flex items-center gap-1">
238
- {results.model === "ml" && <SparklesIcon className="w-3 h-3" />}
239
- {results.model === "ml" ? "Legal-BERT" : "Pattern matching"}
240
- </span>
241
  </div>
242
  </div>
243
 
244
- {/* Actions bar */}
245
  <div className="flex items-center justify-between">
246
  <div className="flex gap-1">
247
  {[
248
  { key: "all", label: "All", count: flagged.length },
 
249
  { key: "HIGH", label: "High", count: sevCounts.HIGH },
250
  { key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
251
  { key: "LOW", label: "Low", count: sevCounts.LOW },
252
  ].map((f) => (
253
  <button key={f.key} onClick={() => setFilter(f.key)}
254
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
255
- filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"
256
- }`}>
257
  {f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
258
  </button>
259
  ))}
@@ -262,74 +298,223 @@ export default function AnalyzePage() {
262
  <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
263
  {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
264
  </button>
265
- <button onClick={handleDownloadPDF} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Download PDF">
266
- <FileDown className="w-4 h-4" />
267
- </button>
268
  </div>
269
  </div>
270
 
271
- {/* Clause list */}
272
- <div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
273
- {filtered.length === 0 ? (
274
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
275
- <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
276
- <p className="text-sm text-zinc-500">{filter === "all" ? "No unfair clauses found." : "No clauses at this severity."}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  </div>
278
- ) : filtered.map((clause, i) => {
279
- const maxSev = clause.categories.reduce((m, c) => {
280
- const order: Record<string, number> = { HIGH: 3, MEDIUM: 2, LOW: 1 };
281
- return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
282
- }, "LOW");
283
- const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
284
- const isExpanded = expandedIdx === i;
285
- const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || TriangleAlert;
286
-
287
- return (
288
- <div key={i} className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
289
- <button onClick={() => setExpandedIdx(isExpanded ? null : i)}
290
- className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
291
- <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
292
- <CatIcon className={`w-4 h-4 ${conf.text}`} />
 
 
 
 
 
 
 
 
 
 
 
 
293
  </div>
294
- <div className="flex-1 min-w-0">
295
- <div className="flex items-center gap-2 flex-wrap">
296
- {clause.categories.map((cat, j) => {
297
- const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
298
- return (
299
- <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
300
- {cat.name}{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
301
- </span>
302
- );
303
- })}
 
 
 
 
 
 
 
 
 
 
304
  </div>
305
- <p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
306
  </div>
307
- <div className="shrink-0 mt-1">
308
- {isExpanded ? <ChevronUp className="w-4 h-4 text-zinc-400" /> : <ChevronDown className="w-4 h-4 text-zinc-400" />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </div>
310
- </button>
311
- {isExpanded && (
312
- <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
313
- <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">{clause.text}</p>
314
- {clause.categories.map((cat, j) => (
315
- <div key={j} className="mt-3 flex items-start gap-2">
316
- <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
317
- <p className="text-[13px] text-zinc-500 leading-relaxed">
318
- <span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may be unfair under consumer protection law."}
319
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  </div>
323
- )}
324
- </div>
325
- );
326
- })}
327
  </div>
328
  </div>
329
  ) : (
330
  <div className="border border-dashed border-zinc-200 rounded-xl h-[420px] flex flex-col items-center justify-center">
331
  <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
332
- <p className="text-sm text-zinc-300">Paste text and scan to see results</p>
333
  </div>
334
  )}
335
  </div>
 
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, FileCompare, 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" },
 
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.
 
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");
 
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);
 
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
  }
 
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");
 
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">
 
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
  ))}
 
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>