Spaces:
Sleeping
Sleeping
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 |
-
|
| 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-
|
| 374 |
-
<div className="flex
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
<
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
results.risk_score
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
<
|
| 397 |
-
<
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 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 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
| 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 |
-
<
|
| 441 |
</div>
|
| 442 |
</div>
|
| 443 |
|
|
@@ -457,7 +459,7 @@ export default function AnalyzePage() {
|
|
| 457 |
</div>
|
| 458 |
|
| 459 |
{/* Tab Content */}
|
| 460 |
-
<div className="max-h-[
|
| 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-
|
| 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>
|