gaurv007 commited on
Commit
61c5918
·
verified ·
1 Parent(s): ec0f550

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

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