gaurv007 commited on
Commit
922c4c8
·
verified ·
1 Parent(s): e1126b7

Fix: 7-day session config, plan gating on scanner (10 free/month), file upload button, upgrade modal when limit hit

Browse files
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -1,20 +1,21 @@
1
  "use client";
2
 
3
- import { useState } from "react";
4
  import {
5
  ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
6
- Download, FileDown, ChevronDown, ChevronUp, Highlighter, Copy, Check,
7
- ShieldCheck, ShieldAlert, Scale, Gavel, Ban, Globe, Eye, Stamp, FileX
 
8
  } from "lucide-react";
9
 
10
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
11
  interface Clause { text: string; categories: Cat[]; }
12
  interface Result { risk_score: number; grade: string; total_clauses: number; flagged_count: number; results: Clause[]; model: string; latency_ms: number; }
13
 
14
- const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string; dot: string }> = {
15
- HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200", dot: "bg-red-500" },
16
- MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200", dot: "bg-amber-500" },
17
- LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200", dot: "bg-blue-500" },
18
  };
19
 
20
  const GRADE_STYLE: Record<string, string> = {
@@ -53,18 +54,59 @@ export default function AnalyzePage() {
53
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
54
  const [filter, setFilter] = useState<string>("all");
55
  const [copied, setCopied] = useState(false);
 
 
 
 
 
 
 
56
 
57
  async function handleAnalyze() {
58
  if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
 
 
 
59
  setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
60
  try {
61
  const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
62
  if (!res.ok) throw new Error((await res.json()).error || "Failed");
63
- setResults(await res.json());
 
 
64
  } catch (e: any) { setError(e.message); }
65
  finally { setLoading(false); }
66
  }
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  async function handleDownloadPDF() {
69
  if (!results) return;
70
  try {
@@ -94,13 +136,50 @@ export default function AnalyzePage() {
94
 
95
  return (
96
  <div className="min-h-screen bg-white">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  <div className="max-w-6xl mx-auto px-5 py-10">
98
- <div className="mb-8">
99
- <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
100
- <ScanText className="w-6 h-6 text-zinc-400" />
101
- Scan a document
102
- </h1>
103
- <p className="mt-1 text-sm text-zinc-500">Paste Terms of Service, a contract, or a lease agreement.</p>
 
 
 
 
 
 
 
104
  </div>
105
 
106
  <div className="grid lg:grid-cols-5 gap-6">
@@ -108,16 +187,21 @@ export default function AnalyzePage() {
108
  <div className="lg:col-span-2">
109
  <textarea value={text} onChange={(e) => setText(e.target.value)}
110
  placeholder="Paste your document text here..."
111
- className="w-full h-[420px] 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" />
112
  <div className="mt-3 flex gap-2">
113
  <button onClick={handleAnalyze} disabled={loading}
114
  className="flex-1 inline-flex items-center justify-center gap-2 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
115
  {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Scanning...</> : <><ScanText className="w-4 h-4" /> Scan</>}
116
  </button>
117
  <button onClick={() => setText(EXAMPLE)}
118
- className="px-4 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">
119
  Example
120
  </button>
 
 
 
 
 
121
  </div>
122
  {error && <p className="mt-2 text-sm text-red-600 flex items-center gap-1.5"><TriangleAlert className="w-3.5 h-3.5" />{error}</p>}
123
  </div>
@@ -140,11 +224,9 @@ export default function AnalyzePage() {
140
  }`} style={{ width: `${results.risk_score}%` }} />
141
  </div>
142
  </div>
143
- <div className="flex items-center gap-2">
144
- <span className={`text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
145
- Grade {results.grade}
146
- </span>
147
- </div>
148
  </div>
149
  <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
150
  <span>{results.total_clauses} clauses</span>
@@ -154,7 +236,7 @@ export default function AnalyzePage() {
154
  <span>{results.latency_ms}ms</span>
155
  <span className="w-px h-3 bg-zinc-200" />
156
  <span className="flex items-center gap-1">
157
- {results.model === "ml" ? <Sparkles className="w-3 h-3" /> : null}
158
  {results.model === "ml" ? "Legal-BERT" : "Pattern matching"}
159
  </span>
160
  </div>
@@ -188,11 +270,11 @@ export default function AnalyzePage() {
188
  </div>
189
 
190
  {/* Clause list */}
191
- <div className="space-y-2 max-h-[400px] overflow-y-auto pr-1">
192
  {filtered.length === 0 ? (
193
  <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
194
  <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
195
- <p className="text-sm text-zinc-500">{filter === "all" ? "No unfair clauses found." : "No clauses at this severity level."}</p>
196
  </div>
197
  ) : filtered.map((clause, i) => {
198
  const maxSev = clause.categories.reduce((m, c) => {
@@ -204,8 +286,7 @@ export default function AnalyzePage() {
204
  const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || TriangleAlert;
205
 
206
  return (
207
- <div key={i}
208
- className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
209
  <button onClick={() => setExpandedIdx(isExpanded ? null : i)}
210
  className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
211
  <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
@@ -217,8 +298,7 @@ export default function AnalyzePage() {
217
  const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
218
  return (
219
  <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
220
- {cat.name}
221
- {cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
222
  </span>
223
  );
224
  })}
@@ -231,9 +311,7 @@ export default function AnalyzePage() {
231
  </button>
232
  {isExpanded && (
233
  <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
234
- <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">
235
- {clause.text}
236
- </p>
237
  {clause.categories.map((cat, j) => (
238
  <div key={j} className="mt-3 flex items-start gap-2">
239
  <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
@@ -261,7 +339,3 @@ export default function AnalyzePage() {
261
  </div>
262
  );
263
  }
264
-
265
- function Sparkles({ className }: { className?: string }) {
266
- return <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/></svg>;
267
- }
 
1
  "use client";
2
 
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
9
  } from "lucide-react";
10
 
11
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
12
  interface Clause { text: string; categories: Cat[]; }
13
  interface Result { risk_score: number; grade: string; total_clauses: number; flagged_count: number; results: Clause[]; model: string; latency_ms: number; }
14
 
15
+ const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string }> = {
16
+ HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200" },
17
+ MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200" },
18
+ LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200" },
19
  };
20
 
21
  const GRADE_STYLE: Record<string, string> = {
 
54
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
55
  const [filter, setFilter] = useState<string>("all");
56
  const [copied, setCopied] = useState(false);
57
+ const [scanCount, setScanCount] = useState(0);
58
+ const [userPlan, setUserPlan] = useState("free");
59
+ const [showUpgrade, setShowUpgrade] = useState(false);
60
+ const fileInputRef = useRef<HTMLInputElement>(null);
61
+
62
+ const FREE_LIMIT = 10;
63
+ const canScan = userPlan !== "free" || scanCount < FREE_LIMIT;
64
 
65
  async function handleAnalyze() {
66
  if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
67
+
68
+ if (!canScan) { setShowUpgrade(true); return; }
69
+
70
  setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
71
  try {
72
  const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
73
  if (!res.ok) throw new Error((await res.json()).error || "Failed");
74
+ const data = await res.json();
75
+ setResults(data);
76
+ setScanCount(prev => prev + 1);
77
  } catch (e: any) { setError(e.message); }
78
  finally { setLoading(false); }
79
  }
80
 
81
+ async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
82
+ const file = e.target.files?.[0];
83
+ if (!file) return;
84
+
85
+ if (userPlan === "free") { setShowUpgrade(true); return; }
86
+
87
+ setLoading(true);
88
+ setError("");
89
+
90
+ try {
91
+ // Read file as text
92
+ if (file.name.endsWith(".txt") || file.name.endsWith(".md")) {
93
+ const content = await file.text();
94
+ setText(content);
95
+ } else if (file.name.endsWith(".pdf")) {
96
+ setError("PDF parsing requires the Pro plan backend. Paste text for now.");
97
+ } else if (file.name.endsWith(".docx")) {
98
+ setError("DOCX parsing requires the Pro plan backend. Paste text for now.");
99
+ } else {
100
+ const content = await file.text();
101
+ setText(content);
102
+ }
103
+ } catch {
104
+ setError("Could not read file.");
105
+ }
106
+ setLoading(false);
107
+ if (fileInputRef.current) fileInputRef.current.value = "";
108
+ }
109
+
110
  async function handleDownloadPDF() {
111
  if (!results) return;
112
  try {
 
136
 
137
  return (
138
  <div className="min-h-screen bg-white">
139
+ {/* Upgrade modal */}
140
+ {showUpgrade && (
141
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
142
+ <div className="bg-white rounded-2xl p-6 max-w-sm mx-4 shadow-xl">
143
+ <div className="flex justify-between items-start">
144
+ <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center">
145
+ <Lock className="w-5 h-5 text-amber-600" />
146
+ </div>
147
+ <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
148
+ </div>
149
+ <h3 className="mt-4 text-lg font-semibold">
150
+ {userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}
151
+ </h3>
152
+ <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
153
+ {userPlan === "free" && scanCount >= FREE_LIMIT
154
+ ? `You have used all ${FREE_LIMIT} free scans this month. Upgrade to Pro for unlimited scans, file uploads, and AI explanations.`
155
+ : "File upload is available on the Pro plan. Upgrade to scan contracts and leases directly."}
156
+ </p>
157
+ <div className="mt-5 flex gap-2">
158
+ <a href="/#pricing" className="flex-1 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium text-center hover:bg-zinc-800 transition-colors">
159
+ View plans
160
+ </a>
161
+ <button onClick={() => setShowUpgrade(false)} className="flex-1 border border-zinc-200 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">
162
+ Not now
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ )}
168
+
169
  <div className="max-w-6xl mx-auto px-5 py-10">
170
+ <div className="mb-8 flex items-start justify-between">
171
+ <div>
172
+ <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
173
+ <ScanText className="w-6 h-6 text-zinc-400" />
174
+ Scan a document
175
+ </h1>
176
+ <p className="mt-1 text-sm text-zinc-500">Paste text or upload a file (.txt, .md).</p>
177
+ </div>
178
+ {userPlan === "free" && (
179
+ <span className="text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md">
180
+ {scanCount}/{FREE_LIMIT} free scans
181
+ </span>
182
+ )}
183
  </div>
184
 
185
  <div className="grid lg:grid-cols-5 gap-6">
 
187
  <div className="lg:col-span-2">
188
  <textarea value={text} onChange={(e) => setText(e.target.value)}
189
  placeholder="Paste your document text here..."
190
+ className="w-full h-[380px] 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" />
191
  <div className="mt-3 flex gap-2">
192
  <button onClick={handleAnalyze} disabled={loading}
193
  className="flex-1 inline-flex items-center justify-center gap-2 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
194
  {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Scanning...</> : <><ScanText className="w-4 h-4" /> Scan</>}
195
  </button>
196
  <button onClick={() => setText(EXAMPLE)}
197
+ className="px-3 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">
198
  Example
199
  </button>
200
+ <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
201
+ <button onClick={() => fileInputRef.current?.click()}
202
+ className="px-3 border border-zinc-200 rounded-lg text-zinc-500 hover:bg-zinc-50 transition-colors" title="Upload file">
203
+ <Upload className="w-4 h-4" />
204
+ </button>
205
  </div>
206
  {error && <p className="mt-2 text-sm text-red-600 flex items-center gap-1.5"><TriangleAlert className="w-3.5 h-3.5" />{error}</p>}
207
  </div>
 
224
  }`} style={{ width: `${results.risk_score}%` }} />
225
  </div>
226
  </div>
227
+ <span className={`text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
228
+ Grade {results.grade}
229
+ </span>
 
 
230
  </div>
231
  <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
232
  <span>{results.total_clauses} clauses</span>
 
236
  <span>{results.latency_ms}ms</span>
237
  <span className="w-px h-3 bg-zinc-200" />
238
  <span className="flex items-center gap-1">
239
+ {results.model === "ml" && <SparklesIcon className="w-3 h-3" />}
240
  {results.model === "ml" ? "Legal-BERT" : "Pattern matching"}
241
  </span>
242
  </div>
 
270
  </div>
271
 
272
  {/* Clause list */}
273
+ <div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
274
  {filtered.length === 0 ? (
275
  <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
276
  <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
277
+ <p className="text-sm text-zinc-500">{filter === "all" ? "No unfair clauses found." : "No clauses at this severity."}</p>
278
  </div>
279
  ) : filtered.map((clause, i) => {
280
  const maxSev = clause.categories.reduce((m, c) => {
 
286
  const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || TriangleAlert;
287
 
288
  return (
289
+ <div key={i} className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
 
290
  <button onClick={() => setExpandedIdx(isExpanded ? null : i)}
291
  className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
292
  <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
 
298
  const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
299
  return (
300
  <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
301
+ {cat.name}{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
 
302
  </span>
303
  );
304
  })}
 
311
  </button>
312
  {isExpanded && (
313
  <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
314
+ <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">{clause.text}</p>
 
 
315
  {clause.categories.map((cat, j) => (
316
  <div key={j} className="mt-3 flex items-start gap-2">
317
  <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
 
339
  </div>
340
  );
341
  }
 
 
 
 
web/lib/supabase/client.ts CHANGED
@@ -3,6 +3,15 @@ import { createBrowserClient } from "@supabase/ssr";
3
  export function createClient() {
4
  return createBrowserClient(
5
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
6
- process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
 
 
 
 
 
 
 
 
 
7
  );
8
  }
 
3
  export function createClient() {
4
  return createBrowserClient(
5
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
6
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
7
+ {
8
+ auth: {
9
+ autoRefreshToken: true,
10
+ persistSession: true,
11
+ // 7 day session — Supabase default is 3600s (1 hour)
12
+ // This must also be set in Supabase Dashboard → Auth → Settings → JWT Expiry
13
+ // Set to 604800 (7 days) in the dashboard
14
+ },
15
+ }
16
  );
17
  }