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

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

Browse files
web/app/dashboard-pages/compare/page.tsx CHANGED
@@ -1 +1,256 @@
1
- /app/web_final/compare_page.tsx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ GitCompare, ArrowRightLeft, ChevronDown, ChevronUp,
6
+ TriangleAlert, CircleCheck, AlertTriangle,
7
+ Loader2, Cpu, FileSearch, Layers, Scale
8
+ } from "lucide-react";
9
+
10
+ interface CompareResult {
11
+ alignment_score: number;
12
+ contract_a_clauses: number;
13
+ contract_b_clauses: number;
14
+ added_clauses: Array<{ text: string; type: string }>;
15
+ removed_clauses: Array<{ text: string; type: string }>;
16
+ modified_clauses: Array<{ type: string; similarity: number; clause_a: string; clause_b: string; clause_type: string }>;
17
+ risk_delta: string;
18
+ risk_winner: string;
19
+ comparison_method?: string;
20
+ type_map_a: Record<string, number>;
21
+ type_map_b: Record<string, number>;
22
+ }
23
+
24
+ const EXAMPLE_A = `This Master Service Agreement ("MSA") is entered into as of March 1, 2024 by and between CloudTech Solutions, Inc. ("Provider") and Global Retail Partners LLC ("Customer").
25
+
26
+ 1. SERVICES. Provider shall provide cloud hosting services as described in Exhibit A.
27
+
28
+ 2. TERM. The initial term is twelve (12) months, automatically renewing for successive one year periods.
29
+
30
+ 3. FEES. Customer shall pay a monthly fee of $25,000 within 30 days of invoice.
31
+
32
+ 4. LIABILITY. Provider's aggregate liability shall not exceed $1,000,000. IN NO EVENT SHALL PROVIDER BE LIABLE FOR LOST PROFITS.
33
+
34
+ 5. TERMINATION. Either party may terminate for convenience with 90 days notice. Provider may terminate immediately for non-payment.
35
+
36
+ 6. GOVERNING LAW. This Agreement is governed by the laws of the State of Delaware.`;
37
+
38
+ const EXAMPLE_B = `This Master Service Agreement ("MSA") is entered into as of April 15, 2024 by and between CloudTech Solutions, Inc. ("Provider") and Global Retail Partners LLC ("Customer").
39
+
40
+ 1. SERVICES. Provider shall provide cloud hosting and data processing services as described in Exhibit A and B.
41
+
42
+ 2. TERM. The initial term is twenty-four (24) months, automatically renewing for successive one year periods unless terminated in accordance with Section 5.
43
+
44
+ 3. FEES. Customer shall pay a monthly fee of $30,000 within 15 days of invoice. Late payments incur a penalty of 2% per month.
45
+
46
+ 4. LIABILITY. Provider's aggregate liability shall not exceed $500,000. IN NO EVENT SHALL PROVIDER BE LIABLE FOR LOST PROFITS OR CONSEQUENTIAL DAMAGES.
47
+
48
+ 5. TERMINATION. Either party may terminate for convenience with 180 days notice. Provider may terminate immediately for non-payment or material breach.
49
+
50
+ 6. GOVERNING LAW. This Agreement is governed by the laws of the State of New York.`;
51
+
52
+ export default function ComparePage() {
53
+ const [textA, setTextA] = useState("");
54
+ const [textB, setTextB] = useState("");
55
+ const [result, setResult] = useState<CompareResult | null>(null);
56
+ const [loading, setLoading] = useState(false);
57
+ const [error, setError] = useState("");
58
+ const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
59
+ const [activeSection, setActiveSection] = useState<string>("summary");
60
+
61
+ async function handleCompare() {
62
+ if (!textA.trim() || textA.trim().length < 50) { setError("Contract A must have at least 50 characters."); return; }
63
+ if (!textB.trim() || textB.trim().length < 50) { setError("Contract B must have at least 50 characters."); return; }
64
+ setLoading(true); setError(""); setResult(null); setExpandedIdx(null);
65
+ try {
66
+ const res = await fetch("/api/compare", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text_a: textA, text_b: textB }) });
67
+ if (!res.ok) throw new Error((await res.json()).error || "Failed");
68
+ setResult(await res.json());
69
+ } catch (e: any) { setError(e.message); }
70
+ finally { setLoading(false); }
71
+ }
72
+
73
+ return (
74
+ <div className="min-h-screen bg-zinc-50/30">
75
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
76
+ <div className="mb-6 sm:mb-8">
77
+ <h1 className="text-xl sm:text-2xl font-semibold tracking-tight flex items-center gap-2">
78
+ <GitCompare className="w-5 h-5 sm:w-6 sm:h-6 text-zinc-400" />
79
+ Compare Contracts
80
+ </h1>
81
+ <p className="mt-1 text-xs sm:text-sm text-zinc-500">Side-by-side semantic diff with clause-level alignment and risk delta.</p>
82
+ </div>
83
+
84
+ {/* Input */}
85
+ <div className="grid md:grid-cols-2 gap-4 mb-6">
86
+ {[
87
+ { label: "A", value: textA, setValue: setTextA },
88
+ { label: "B", value: textB, setValue: setTextB },
89
+ ].map(({ label, value, setValue }) => (
90
+ <div key={label}>
91
+ <label className="text-sm font-medium text-zinc-700 mb-1.5 flex items-center gap-2">
92
+ <span className="w-6 h-6 rounded bg-zinc-100 flex items-center justify-center text-xs font-bold text-zinc-600">{label}</span>
93
+ Contract {label}
94
+ </label>
95
+ <textarea value={value} onChange={(e) => setValue(e.target.value)}
96
+ placeholder={`Paste contract ${label} here...`}
97
+ className="w-full h-[200px] sm:h-[280px] p-3 sm:p-4 bg-white 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" />
98
+ </div>
99
+ ))}
100
+ </div>
101
+
102
+ <div className="flex gap-2 mb-8">
103
+ <button onClick={handleCompare} disabled={loading}
104
+ className="inline-flex items-center gap-2 bg-zinc-900 text-white px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
105
+ {loading ? <><Loader2 className="w-4 h-4 animate-spin" /> Comparing...</> : <><ArrowRightLeft className="w-4 h-4" /> Compare</>}
106
+ </button>
107
+ <button onClick={() => { setTextA(EXAMPLE_A); setTextB(EXAMPLE_B); }} className="px-4 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">Load Example</button>
108
+ </div>
109
+
110
+ {error && <p className="mb-6 text-sm text-red-600 flex items-center gap-1.5"><TriangleAlert className="w-3.5 h-3.5" />{error}</p>}
111
+
112
+ {result && (
113
+ <div className="space-y-4 sm:space-y-6">
114
+ {/* Method indicator */}
115
+ {result.comparison_method && (
116
+ <div className="flex items-center justify-center gap-2 text-xs text-zinc-400">
117
+ {result.comparison_method.includes("semantic") ? <Cpu className="w-3.5 h-3.5" /> : <FileSearch className="w-3.5 h-3.5" />}
118
+ <span>Method: {result.comparison_method}</span>
119
+ </div>
120
+ )}
121
+
122
+ {/* Summary grid */}
123
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
124
+ <div className="bg-white border border-zinc-200 rounded-xl p-4 text-center">
125
+ <Layers className="w-5 h-5 text-blue-500 mx-auto mb-1" />
126
+ <p className="text-2xl font-bold text-zinc-900">{(result.alignment_score * 100).toFixed(1)}%</p>
127
+ <p className="text-[11px] text-zinc-400">Alignment</p>
128
+ </div>
129
+ <div className="bg-white border border-zinc-200 rounded-xl p-4 text-center">
130
+ <p className="text-[11px] text-zinc-400 mb-1">Contract A</p>
131
+ <p className="text-2xl font-bold text-zinc-900">{result.contract_a_clauses}</p>
132
+ <p className="text-[11px] text-zinc-400">clauses</p>
133
+ </div>
134
+ <div className="bg-white border border-zinc-200 rounded-xl p-4 text-center">
135
+ <p className="text-[11px] text-zinc-400 mb-1">Contract B</p>
136
+ <p className="text-2xl font-bold text-zinc-900">{result.contract_b_clauses}</p>
137
+ <p className="text-[11px] text-zinc-400">clauses</p>
138
+ </div>
139
+ <div className={`border rounded-xl p-4 text-center ${result.risk_winner === "tie" ? "border-emerald-200 bg-emerald-50" : "border-red-200 bg-red-50"}`}>
140
+ <Scale className={`w-5 h-5 mx-auto mb-1 ${result.risk_winner === "tie" ? "text-emerald-500" : "text-red-500"}`} />
141
+ <p className={`text-sm font-bold leading-tight ${result.risk_winner === "tie" ? "text-emerald-700" : "text-red-700"}`}>{result.risk_delta}</p>
142
+ </div>
143
+ </div>
144
+
145
+ {/* Tabs */}
146
+ <div className="border-b border-zinc-200 overflow-x-auto">
147
+ <div className="flex gap-0.5 min-w-max">
148
+ {[
149
+ { key: "summary", label: "Summary" },
150
+ { key: "modified", label: "Modified", count: result.modified_clauses.length },
151
+ { key: "added", label: "Added in B", count: result.added_clauses.length },
152
+ { key: "removed", label: "Removed from A", count: result.removed_clauses.length },
153
+ ].map((s) => (
154
+ <button key={s.key} onClick={() => setActiveSection(s.key)}
155
+ className={`px-3 py-2 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeSection === s.key ? "border-zinc-900 text-zinc-900" : "border-transparent text-zinc-400 hover:text-zinc-600"}`}>
156
+ {s.label} {s.count != null && s.count > 0 && <span className="ml-1 text-zinc-400 bg-zinc-100 px-1.5 py-0.5 rounded-full text-[10px]">{s.count}</span>}
157
+ </button>
158
+ ))}
159
+ </div>
160
+ </div>
161
+
162
+ {/* Content */}
163
+ <div className="max-h-[400px] sm:max-h-[500px] overflow-y-auto">
164
+ {activeSection === "modified" && (
165
+ <div className="space-y-3">
166
+ {result.modified_clauses.length === 0 ? (
167
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center bg-white"><CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" /><p className="text-sm text-zinc-500">No modified clauses.</p></div>
168
+ ) : result.modified_clauses.map((m, i) => {
169
+ const isExpanded = expandedIdx === i;
170
+ const simColor = m.similarity >= 0.8 ? "text-emerald-600 bg-emerald-50" : m.similarity >= 0.6 ? "text-amber-600 bg-amber-50" : "text-red-600 bg-red-50";
171
+ return (
172
+ <div key={i} className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
173
+ <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">
174
+ <div className="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center shrink-0"><AlertTriangle className="w-4 h-4 text-amber-600" /></div>
175
+ <div className="flex-1 min-w-0">
176
+ <div className="flex items-center gap-2 flex-wrap">
177
+ <span className="text-xs font-medium text-zinc-500 uppercase">{m.clause_type}</span>
178
+ <span className={`text-xs font-bold px-2 py-0.5 rounded ${simColor}`}>{(m.similarity * 100).toFixed(0)}% similar</span>
179
+ </div>
180
+ <p className="mt-1 text-sm text-zinc-600 line-clamp-2">{m.clause_a}</p>
181
+ </div>
182
+ <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>
183
+ </button>
184
+ {isExpanded && (
185
+ <div className="px-3 sm:px-4 pb-4 pt-0 border-t border-zinc-100">
186
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
187
+ <div className="bg-red-50 rounded-lg p-3 border border-red-100">
188
+ <p className="text-[10px] font-semibold text-red-600 uppercase mb-1.5">Contract A</p>
189
+ <p className="text-sm text-zinc-700 leading-relaxed">{m.clause_a}</p>
190
+ </div>
191
+ <div className="bg-emerald-50 rounded-lg p-3 border border-emerald-100">
192
+ <p className="text-[10px] font-semibold text-emerald-600 uppercase mb-1.5">Contract B</p>
193
+ <p className="text-sm text-zinc-700 leading-relaxed">{m.clause_b}</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ })}
201
+ </div>
202
+ )}
203
+
204
+ {activeSection === "added" && (
205
+ <div className="space-y-2">
206
+ {result.added_clauses.length === 0 ? (
207
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center bg-white"><CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" /><p className="text-sm text-zinc-500">No new clauses in B.</p></div>
208
+ ) : result.added_clauses.map((c, i) => (
209
+ <div key={i} className="bg-white border-l-4 border-emerald-400 border border-zinc-200 rounded-r-xl p-3">
210
+ <span className="text-[10px] font-semibold text-emerald-600 uppercase">{c.type}</span>
211
+ <p className="text-sm text-zinc-700 mt-1 leading-relaxed">{c.text}</p>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ )}
216
+
217
+ {activeSection === "removed" && (
218
+ <div className="space-y-2">
219
+ {result.removed_clauses.length === 0 ? (
220
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center bg-white"><CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" /><p className="text-sm text-zinc-500">No clauses removed.</p></div>
221
+ ) : result.removed_clauses.map((c, i) => (
222
+ <div key={i} className="bg-white border-l-4 border-red-400 border border-zinc-200 rounded-r-xl p-3">
223
+ <span className="text-[10px] font-semibold text-red-600 uppercase">{c.type}</span>
224
+ <p className="text-sm text-zinc-700 mt-1 leading-relaxed">{c.text}</p>
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+
230
+ {activeSection === "summary" && (
231
+ <div className="space-y-4">
232
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
233
+ {[
234
+ { label: "Contract A Clause Types", data: result.type_map_a },
235
+ { label: "Contract B Clause Types", data: result.type_map_b },
236
+ ].map(({ label, data }) => (
237
+ <div key={label} className="bg-white border border-zinc-200 rounded-xl p-4">
238
+ <p className="text-xs font-medium text-zinc-500 mb-2">{label}</p>
239
+ {Object.entries(data).map(([type, count]) => (
240
+ <div key={type} className="flex justify-between text-sm py-1 border-b border-zinc-50 last:border-0">
241
+ <span className="text-zinc-600 capitalize">{type}</span>
242
+ <span className="font-medium text-zinc-900">{count}</span>
243
+ </div>
244
+ ))}
245
+ </div>
246
+ ))}
247
+ </div>
248
+ </div>
249
+ )}
250
+ </div>
251
+ </div>
252
+ )}
253
+ </div>
254
+ </div>
255
+ );
256
+ }