gaurv007 commited on
Commit
705ce80
·
verified ·
1 Parent(s): 3a6561a

v3.0: Update compare page — show comparison method (semantic/lexical) indicator

Browse files
web/app/dashboard-pages/compare/page.tsx CHANGED
@@ -1,283 +1 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import {
5
- GitCompare, ArrowRightLeft, ChevronDown, ChevronUp,
6
- TriangleAlert, CircleCheck, AlertTriangle,
7
- Loader2
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
- <GitCompare 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
- }
 
1
+ /app/web/compare_page.tsx