gaurv007 commited on
Commit
5786572
·
verified ·
1 Parent(s): 5575628

feat: redesigned score card (circular gauge), multi-format export dropdown, better loading state, taller results panel

Browse files
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -3,7 +3,7 @@
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, Layers, Landmark, Briefcase,
9
  AlertTriangle, Tag, BookOpen, ClipboardList, DollarSign,
@@ -12,6 +12,7 @@ import {
12
  ShieldOff, CircleSlash, MessageSquareWarning, Construction,
13
  MessageSquare, Send, Loader2
14
  } from "lucide-react";
 
15
 
16
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
17
  interface Clause { text: string; categories: Cat[]; }
@@ -233,17 +234,6 @@ export default function AnalyzePage() {
233
  if (fileInputRef.current) fileInputRef.current.value = "";
234
  }
235
 
236
- async function handleDownloadPDF() {
237
- if (!results) return;
238
- try {
239
- const res = await fetch("/api/pdf/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(results) });
240
- const blob = await res.blob();
241
- const url = URL.createObjectURL(blob);
242
- const a = document.createElement("a"); a.href = url; a.download = "clauseguard-report.pdf"; a.click();
243
- URL.revokeObjectURL(url);
244
- } catch {}
245
- }
246
-
247
  function handleCopy() {
248
  if (!results) return;
249
  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` +
@@ -369,41 +359,51 @@ export default function AnalyzePage() {
369
  <div className="lg:col-span-3">
370
  {results ? (
371
  <div className="space-y-3 sm:space-y-4">
372
- {/* Score Card */}
373
- <div className="bg-white border border-zinc-200 rounded-xl p-4 sm:p-5">
374
- <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
375
- <div>
376
- <div className="flex items-baseline gap-2">
377
- <span className="text-3xl sm:text-4xl font-semibold tracking-tight">{results.risk_score}</span>
378
- <span className="text-sm text-zinc-400">/100 risk</span>
379
- </div>
380
- <div className="mt-2 h-1.5 w-full sm:w-48 bg-zinc-100 rounded-full overflow-hidden">
381
- <div className={`h-full rounded-full transition-all duration-700 ${
382
- results.risk_score >= 60 ? "bg-red-500" : results.risk_score >= 30 ? "bg-amber-400" : "bg-emerald-500"
383
- }`} style={{ width: `${results.risk_score}%` }} />
 
 
 
 
384
  </div>
385
  </div>
386
- <span className={`self-start text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
387
- Grade {results.grade}
388
- </span>
389
- </div>
390
 
391
- {/* Severity breakdown grid */}
392
- <div className="mt-4 grid grid-cols-4 gap-2">
393
- {(["CRITICAL", "HIGH", "MEDIUM", "LOW"] as const).map(sev => {
394
- const c = SEV_CONFIG[sev];
395
- return (
396
- <div key={sev} className={`text-center p-2 rounded-lg ${c.bg} border ${c.border}`}>
397
- <span className={`text-lg font-bold ${c.text}`}>{sevCounts[sev]}</span>
398
- <p className={`text-[10px] ${c.text} opacity-70`}>{c.label}</p>
399
- </div>
400
- );
401
- })}
402
- </div>
 
 
 
 
 
 
 
 
 
 
403
 
404
- {/* Meta stats */}
405
- <div className="mt-3 flex items-center gap-2 sm:gap-3 text-[11px] text-zinc-400 flex-wrap">
406
- <span className="flex items-center gap-1"><Layers className="w-3 h-3" />{results.total_clauses} clauses</span>
407
  <span className="w-px h-3 bg-zinc-200" />
408
  <span className="flex items-center gap-1"><Tag className="w-3 h-3" />{results.entities.length} entities</span>
409
  <span className="w-px h-3 bg-zinc-200" />
@@ -411,9 +411,11 @@ export default function AnalyzePage() {
411
  <span className="w-px h-3 bg-zinc-200" />
412
  <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{results.latency_ms}ms</span>
413
  <span className="w-px h-3 bg-zinc-200" />
414
- <span className="flex items-center gap-1">
415
- {results.model !== "regex" ? <><Cpu className="w-3 h-3" /> ML Models</> : <><FileSearch className="w-3 h-3" /> Pattern fallback</>}
416
- </span>
 
 
417
  </div>
418
  </div>
419
 
@@ -433,11 +435,11 @@ export default function AnalyzePage() {
433
  </button>
434
  ))}
435
  </div>
436
- <div className="flex gap-1.5 self-end sm:self-auto">
437
  <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
438
  {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
439
  </button>
440
- <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>
441
  </div>
442
  </div>
443
 
@@ -457,7 +459,7 @@ export default function AnalyzePage() {
457
  </div>
458
 
459
  {/* Tab Content */}
460
- <div className="max-h-[350px] sm:max-h-[420px] overflow-y-auto pr-1">
461
 
462
  {/* Clauses */}
463
  {activeTab === "clauses" && (
@@ -847,8 +849,18 @@ export default function AnalyzePage() {
847
  )}
848
  </div>
849
  </div>
 
 
 
 
 
 
 
 
 
 
850
  ) : (
851
- <div className="bg-white border border-dashed border-zinc-200 rounded-xl h-[300px] sm:h-[420px] flex flex-col items-center justify-center">
852
  <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
853
  <p className="text-sm text-zinc-300">Paste text and analyze to see results</p>
854
  </div>
 
3
  import { useState, useRef, useEffect } from "react";
4
  import {
5
  ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
6
+ 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,
 
12
  ShieldOff, CircleSlash, MessageSquareWarning, Construction,
13
  MessageSquare, Send, Loader2
14
  } from "lucide-react";
15
+ import { ExportDropdown } from "@/components/export-dropdown";
16
 
17
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
18
  interface Clause { text: string; categories: Cat[]; }
 
234
  if (fileInputRef.current) fileInputRef.current.value = "";
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
 
237
  function handleCopy() {
238
  if (!results) return;
239
  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` +
 
359
  <div className="lg:col-span-3">
360
  {results ? (
361
  <div className="space-y-3 sm:space-y-4">
362
+ {/* Score Card — redesigned with circular gauge */}
363
+ <div className="bg-white border border-zinc-200 rounded-2xl p-5 sm:p-6 shadow-sm">
364
+ <div className="flex items-center gap-5 sm:gap-6">
365
+ {/* Circular score gauge */}
366
+ <div className="relative w-20 h-20 sm:w-24 sm:h-24 shrink-0">
367
+ <svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
368
+ <circle cx="50" cy="50" r="42" fill="none" stroke="#f4f4f5" strokeWidth="8" />
369
+ <circle cx="50" cy="50" r="42" fill="none"
370
+ stroke={results.risk_score >= 60 ? "#ef4444" : results.risk_score >= 30 ? "#f59e0b" : "#22c55e"}
371
+ strokeWidth="8" strokeLinecap="round"
372
+ strokeDasharray={`${results.risk_score * 2.64} 264`}
373
+ className="transition-all duration-1000 ease-out" />
374
+ </svg>
375
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
376
+ <span className="text-xl sm:text-2xl font-bold tracking-tight">{results.risk_score}</span>
377
+ <span className="text-[9px] text-zinc-400 -mt-0.5">/ 100</span>
378
  </div>
379
  </div>
 
 
 
 
380
 
381
+ <div className="flex-1 min-w-0">
382
+ <div className="flex items-center gap-2 mb-2">
383
+ <span className={`text-sm font-bold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
384
+ Grade {results.grade}
385
+ </span>
386
+ <span className="text-xs text-zinc-400">
387
+ {results.risk_score < 20 ? "Low Risk" : results.risk_score < 40 ? "Moderate Risk" : results.risk_score < 60 ? "Elevated Risk" : results.risk_score < 80 ? "High Risk" : "Critical Risk"}
388
+ </span>
389
+ </div>
390
+
391
+ {/* Severity breakdown — compact horizontal */}
392
+ <div className="grid grid-cols-4 gap-1.5">
393
+ {(["CRITICAL", "HIGH", "MEDIUM", "LOW"] as const).map(sev => {
394
+ const c = SEV_CONFIG[sev];
395
+ return (
396
+ <div key={sev} className={`text-center py-1.5 px-1 rounded-lg ${c.bg} border ${c.border}`}>
397
+ <span className={`text-sm font-bold ${c.text}`}>{sevCounts[sev]}</span>
398
+ <p className={`text-[9px] ${c.text} opacity-70`}>{c.label}</p>
399
+ </div>
400
+ );
401
+ })}
402
+ </div>
403
 
404
+ {/* Meta stats */}
405
+ <div className="mt-2.5 flex items-center gap-2 text-[10px] text-zinc-400 flex-wrap">
406
+ <span className="flex items-center gap-1"><Layers className="w-3 h-3" />{results.total_clauses} clauses</span>
407
  <span className="w-px h-3 bg-zinc-200" />
408
  <span className="flex items-center gap-1"><Tag className="w-3 h-3" />{results.entities.length} entities</span>
409
  <span className="w-px h-3 bg-zinc-200" />
 
411
  <span className="w-px h-3 bg-zinc-200" />
412
  <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{results.latency_ms}ms</span>
413
  <span className="w-px h-3 bg-zinc-200" />
414
+ <span className="flex items-center gap-1">
415
+ {results.model !== "regex" ? <><Cpu className="w-3 h-3" /> ML Models</> : <><FileSearch className="w-3 h-3" /> Pattern fallback</>}
416
+ </span>
417
+ </div>
418
+ </div>
419
  </div>
420
  </div>
421
 
 
435
  </button>
436
  ))}
437
  </div>
438
+ <div className="flex gap-1.5 self-end sm:self-auto items-center">
439
  <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
440
  {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
441
  </button>
442
+ <ExportDropdown results={results} />
443
  </div>
444
  </div>
445
 
 
459
  </div>
460
 
461
  {/* Tab Content */}
462
+ <div className="max-h-[450px] sm:max-h-[560px] overflow-y-auto pr-1 scroll-smooth">
463
 
464
  {/* Clauses */}
465
  {activeTab === "clauses" && (
 
849
  )}
850
  </div>
851
  </div>
852
+ ) : loading ? (
853
+ <div className="bg-white border border-zinc-200 rounded-2xl h-[300px] sm:h-[420px] flex flex-col items-center justify-center shadow-sm">
854
+ <div className="relative w-16 h-16 mb-4">
855
+ <div className="absolute inset-0 rounded-full border-2 border-zinc-100" />
856
+ <div className="absolute inset-0 rounded-full border-2 border-t-zinc-900 animate-spin" />
857
+ <ScanLine className="absolute inset-0 m-auto w-6 h-6 text-zinc-400" />
858
+ </div>
859
+ <p className="text-sm font-medium text-zinc-700">Analyzing contract...</p>
860
+ <p className="text-xs text-zinc-400 mt-1">Running 6 ML models · This may take 30-60 seconds</p>
861
+ </div>
862
  ) : (
863
+ <div className="bg-white border border-dashed border-zinc-200 rounded-2xl h-[300px] sm:h-[420px] flex flex-col items-center justify-center">
864
  <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
865
  <p className="text-sm text-zinc-300">Paste text and analyze to see results</p>
866
  </div>