anky2002 commited on
Commit
3c5a789
Β·
2 Parent(s): 51ac51a6277262

Merge branch 'main' of https://huggingface.co/spaces/gaurv007/ClauseGuard

Browse files
api/main.py CHANGED
@@ -1,18 +1,28 @@
1
  """
2
- ClauseGuard β€” FastAPI Backend (Production)
3
- Clause classification + explanations + history + JWT auth.
4
- FastAPI 0.136, Pydantic 2.13, Python 3.12 (April 2026)
 
 
 
 
 
 
 
5
  """
6
 
7
  import os
8
- import time
9
  import re
 
 
10
  from contextlib import asynccontextmanager
11
  from typing import Optional
 
 
12
 
13
  import httpx
14
  import numpy as np
15
- from fastapi import FastAPI, HTTPException, Depends
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from pydantic import BaseModel, Field
18
 
@@ -27,12 +37,50 @@ SUPABASE_SERVICE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
27
  HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
28
  SAULLM_ENDPOINT = os.environ.get("SAULLM_ENDPOINT", "")
29
 
30
- LABEL_NAMES = [
31
- "Limitation of liability", "Unilateral termination", "Unilateral change",
32
- "Content removal", "Contract by using", "Choice of law", "Jurisdiction", "Arbitration",
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  ]
34
 
35
- LABEL_DESCRIPTIONS = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  "Limitation of liability": "Company limits or excludes liability for losses, data breaches, or service failures.",
37
  "Unilateral termination": "Company can terminate your account at any time without reason.",
38
  "Unilateral change": "Company can change terms at any time without your consent.",
@@ -41,79 +89,93 @@ LABEL_DESCRIPTIONS = {
41
  "Choice of law": "Governing law may differ from your country, reducing your legal protections.",
42
  "Jurisdiction": "Disputes must be resolved in a jurisdiction that may disadvantage you.",
43
  "Arbitration": "Forces disputes to arbitration instead of court. You waive your right to sue.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
- SEVERITY_MAP = {
47
- "Limitation of liability": "HIGH", "Unilateral termination": "HIGH", "Arbitration": "HIGH",
48
- "Unilateral change": "MEDIUM", "Content removal": "MEDIUM", "Choice of law": "MEDIUM",
49
- "Jurisdiction": "MEDIUM", "Contract by using": "LOW",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
- LEGAL_BASIS = {
53
- "Arbitration": "EU Directive 93/13/EEC Art. 3; CFPB arbitration rule (US).",
54
- "Unilateral change": "EU Directive 93/13/EEC Annex 1(j) β€” unilateral alteration.",
55
- "Content removal": "EU Digital Services Act Art. 17 β€” statement of reasons required.",
56
- "Jurisdiction": "EU Regulation 1215/2012 Art. 18 β€” consumer domicile prevails.",
57
- "Choice of law": "EU Regulation 593/2008 Art. 6 β€” consumer protection of habitual residence.",
58
- "Limitation of liability": "EU Directive 93/13/EEC Annex 1(a) β€” excluding statutory rights.",
59
- "Unilateral termination": "EU Directive 93/13/EEC Annex 1(f)(g) β€” termination without notice.",
60
- "Contract by using": "EU Directive 2011/83/EU Art. 8 β€” active consent required.",
61
- }
62
 
63
- # ─── Model ───
64
- classifier = None
 
 
 
 
 
65
 
66
  def load_model():
67
- global classifier
 
 
 
68
  try:
69
- if USE_ONNX and os.path.exists(ONNX_MODEL_PATH):
70
- from optimum.onnxruntime import ORTModelForSequenceClassification
71
- from transformers import AutoTokenizer, pipeline
72
- model = ORTModelForSequenceClassification.from_pretrained(ONNX_MODEL_PATH)
73
- tokenizer = AutoTokenizer.from_pretrained(ONNX_MODEL_PATH)
74
- classifier = pipeline("text-classification", model=model, tokenizer=tokenizer, top_k=None)
75
- elif os.path.exists(MODEL_PATH):
76
- from transformers import pipeline
77
- classifier = pipeline("text-classification", model=MODEL_PATH, top_k=None, device=-1)
 
78
  except Exception as e:
79
- print(f"Model load failed: {e}")
80
-
81
- # ─── Regex fallback ───
82
- PATTERNS = {
83
- 0: [r"not liable", r"shall not be (liable|responsible)", r"in no event.*liable", r"limitation of liability", r"without warranty", r"disclaim"],
84
- 1: [r"terminat.*at any time", r"suspend.*account.*without", r"we may (terminat|suspend|discontinu)", r"right to (terminat|suspend)"],
85
- 2: [r"sole discretion", r"reserves? the right to (modify|change|update|amend)", r"at any time.*without (prior )?notice", r"we may (modify|change|update)"],
86
- 3: [r"remove.*content.*without", r"right to remove", r"we may.*remove"],
87
- 4: [r"by (using|accessing).*you agree", r"continued use.*constitutes? acceptance"],
88
- 5: [r"governed by.*laws? of", r"shall be governed", r"laws of the state of"],
89
- 6: [r"exclusive jurisdiction", r"courts? of.*(california|delaware|new york|ireland|england)", r"submit to.*jurisdiction"],
90
- 7: [r"arbitrat", r"binding arbitration", r"waive.*right.*court", r"class action waiver"],
91
- }
92
-
93
- def classify_clause(text: str) -> list[dict]:
94
- if classifier:
95
- try:
96
- preds = classifier(text, truncation=True, max_length=512)
97
- items = preds[0] if isinstance(preds[0], list) else preds
98
- return [
99
- {"name": p["label"], "severity": SEVERITY_MAP.get(p["label"], "MEDIUM"),
100
- "description": LABEL_DESCRIPTIONS.get(p["label"], ""), "confidence": round(p["score"], 3)}
101
- for p in items if p["score"] > 0.5 and p["label"] in LABEL_DESCRIPTIONS
102
- ]
103
- except Exception:
104
- pass
105
-
106
- # Regex fallback
107
- results = []
108
- text_lower = text.lower()
109
- for lid, pats in PATTERNS.items():
110
- for p in pats:
111
- if re.search(p, text_lower):
112
- name = LABEL_NAMES[lid]
113
- results.append({"name": name, "severity": SEVERITY_MAP[name],
114
- "description": LABEL_DESCRIPTIONS[name], "confidence": 0.7})
115
- break
116
- return results
117
 
118
  # ─── Supabase helper ───
119
  async def supabase_insert(table: str, data: dict):
@@ -138,9 +200,348 @@ async def supabase_query(table: str, params: dict, headers_extra: dict = {}):
138
  )
139
  return resp.json() if resp.status_code == 200 else []
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ─── Models ───
142
  class AnalyzeRequest(BaseModel):
143
- clauses: list[str] = Field(..., min_length=1, max_length=500)
144
  source_url: Optional[str] = None
145
 
146
  class AnalyzeResponse(BaseModel):
@@ -149,9 +550,17 @@ class AnalyzeResponse(BaseModel):
149
  total_clauses: int
150
  flagged_count: int
151
  results: list[dict]
 
 
 
 
152
  model: str
153
  latency_ms: int
154
 
 
 
 
 
155
  class ExplainRequest(BaseModel):
156
  clause: str = Field(..., min_length=10, max_length=2000)
157
  category: str
@@ -169,73 +578,72 @@ async def lifespan(app: FastAPI):
169
  load_model()
170
  yield
171
 
172
- app = FastAPI(title="ClauseGuard API", version="1.0.0", lifespan=lifespan)
173
 
174
  app.add_middleware(
175
  CORSMiddleware,
176
- allow_origins=["https://clauseguardweb.netlify.app", "https://clauseguardweb.netlify.app", "chrome-extension://*", "http://localhost:3000"],
177
  allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
178
  )
179
 
180
  @app.get("/health")
181
  async def health():
182
- return {"status": "ok", "model": "ml" if classifier else "regex"}
183
 
184
  @app.post("/api/analyze", response_model=AnalyzeResponse)
185
  async def analyze(req: AnalyzeRequest, user: Optional[dict] = Depends(get_current_user)):
186
  start = time.time()
187
-
188
- results = [{"text": c, "categories": classify_clause(c)} for c in req.clauses]
189
- flagged = [r for r in results if r["categories"]]
190
-
191
- sev = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
192
- for r in flagged:
193
- for c in r["categories"]:
194
- sev[c.get("severity", "LOW")] += 1
195
-
196
- total = len(req.clauses)
197
- risk = min(100, round((sev["HIGH"] * 20 + sev["MEDIUM"] * 10 + sev["LOW"] * 5) / max(1, total) * 100))
198
- grade = "F" if risk >= 60 else "D" if risk >= 40 else "C" if risk >= 20 else "B" if risk >= 10 else "A"
 
 
 
 
199
  latency = int((time.time() - start) * 1000)
200
-
201
- # Save to DB if authenticated
 
202
  if user:
203
  await supabase_insert("analyses", {
204
- "user_id": user["id"], "source_url": req.source_url, "total_clauses": total,
205
- "flagged_count": len(flagged), "risk_score": risk, "grade": grade, "clauses": results,
 
 
206
  })
207
-
208
- return AnalyzeResponse(risk_score=risk, grade=grade, total_clauses=total,
209
- flagged_count=len(flagged), results=results,
210
- model="ml" if classifier else "regex", latency_ms=latency)
 
 
 
 
 
 
 
 
 
211
 
212
  @app.post("/api/explain", response_model=ExplainResponse)
213
  async def explain(req: ExplainRequest, user: dict = Depends(require_auth)):
214
- desc = LABEL_DESCRIPTIONS.get(req.category, "Unknown category.")
215
- legal = LEGAL_BASIS.get(req.category, "Consult local consumer protection laws.")
216
  recommendation = "Review this clause carefully. Consider negotiating or seeking legal advice before agreeing."
217
-
218
- # Try SaulLM-7B if endpoint configured
219
  if SAULLM_ENDPOINT and HF_API_TOKEN:
220
  try:
221
- prompt = f"""You are a consumer protection legal analyst. Analyze this clause and explain why it may be unfair.
222
-
223
- Clause: "{req.clause}"
224
- Category: {req.category}
225
-
226
- Provide:
227
- 1. A plain-English explanation of why this is problematic
228
- 2. The specific legal basis (EU/US consumer protection law)
229
- 3. A practical recommendation for the consumer
230
-
231
- Be concise. 3-4 sentences maximum per section."""
232
-
233
  async with httpx.AsyncClient(timeout=30.0) as client:
234
- resp = await client.post(
235
- SAULLM_ENDPOINT,
236
- json={"inputs": prompt, "parameters": {"max_new_tokens": 300, "temperature": 0.3}},
237
- headers={"Authorization": f"Bearer {HF_API_TOKEN}"},
238
- )
239
  if resp.status_code == 200:
240
  output = resp.json()
241
  generated = output[0]["generated_text"] if isinstance(output, list) else output.get("generated_text", "")
@@ -245,18 +653,13 @@ Be concise. 3-4 sentences maximum per section."""
245
  legal = parts[1] if len(parts) > 1 else legal
246
  recommendation = parts[2] if len(parts) > 2 else recommendation
247
  except Exception:
248
- pass # Fall back to static responses
249
-
250
- return ExplainResponse(clause=req.clause, category=req.category,
251
- explanation=desc, legal_basis=legal, recommendation=recommendation)
252
 
253
  @app.get("/api/history")
254
  async def history(user: dict = Depends(require_auth), limit: int = 20, offset: int = 0):
255
  limit = min(limit, 100)
256
- data = await supabase_query("analyses", {
257
- "user_id": f"eq.{user['id']}", "select": "*",
258
- "order": "created_at.desc", "limit": str(limit), "offset": str(offset),
259
- })
260
  return {"analyses": data, "limit": limit, "offset": offset}
261
 
262
  if __name__ == "__main__":
 
1
  """
2
+ ClauseGuard β€” FastAPI Backend v2.0
3
+ ══════════════════════════════════
4
+ Features:
5
+ β€’ 41 CUAD clause categories via fine-tuned Legal-BERT
6
+ β€’ 4-tier risk scoring (Critical / High / Medium / Low)
7
+ β€’ Legal NER: parties, dates, monetary values, jurisdictions, defined terms
8
+ β€’ NLI contradiction & missing-clause detection
9
+ β€’ Contract comparison engine
10
+ β€’ Obligation tracker
11
+ β€’ Compliance checker (GDPR, CCPA, SOX, HIPAA, FINRA)
12
  """
13
 
14
  import os
 
15
  import re
16
+ import json
17
+ import time
18
  from contextlib import asynccontextmanager
19
  from typing import Optional
20
+ from collections import defaultdict
21
+ from datetime import datetime
22
 
23
  import httpx
24
  import numpy as np
25
+ from fastapi import FastAPI, HTTPException, Depends, Body
26
  from fastapi.middleware.cors import CORSMiddleware
27
  from pydantic import BaseModel, Field
28
 
 
37
  HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
38
  SAULLM_ENDPOINT = os.environ.get("SAULLM_ENDPOINT", "")
39
 
40
+ # ─── CUAD Labels (41 categories) ───
41
+ CUAD_LABELS = [
42
+ "Document Name", "Parties", "Agreement Date", "Effective Date",
43
+ "Expiration Date", "Renewal Term", "Governing Law", "Most Favored Nation",
44
+ "Non-Compete", "Exclusivity", "No-Solicit of Customers",
45
+ "No-Solicit of Employees", "Non-Disparagement",
46
+ "Termination for Convenience", "ROFR/ROFO/ROFN", "Change of Control",
47
+ "Anti-Assignment", "Revenue/Profit Sharing", "Price Restriction",
48
+ "Minimum Commitment", "Volume Restriction", "IP Ownership Assignment",
49
+ "Joint IP Ownership", "License Grant", "Non-Transferable License",
50
+ "Affiliate License-Licensor", "Affiliate License-Licensee",
51
+ "Unlimited/All-You-Can-Eat License", "Irrevocable or Perpetual License",
52
+ "Source Code Escrow", "Post-Termination Services", "Audit Rights",
53
+ "Uncapped Liability", "Cap on Liability", "Liquidated Damages",
54
+ "Warranty Duration", "Insurance", "Covenant Not to Sue",
55
+ "Third Party Beneficiary", "Other"
56
  ]
57
 
58
+ RISK_MAP = {
59
+ "Uncapped Liability": "CRITICAL", "Arbitration": "CRITICAL",
60
+ "IP Ownership Assignment": "CRITICAL", "Termination for Convenience": "CRITICAL",
61
+ "Limitation of liability": "CRITICAL", "Unilateral termination": "CRITICAL",
62
+ "Liquidated Damages": "CRITICAL",
63
+ "Non-Compete": "HIGH", "Exclusivity": "HIGH", "Change of Control": "HIGH",
64
+ "No-Solicit of Customers": "HIGH", "No-Solicit of Employees": "HIGH",
65
+ "Unilateral change": "HIGH", "Content removal": "HIGH", "Anti-Assignment": "HIGH",
66
+ "Governing Law": "MEDIUM", "Jurisdiction": "MEDIUM", "Choice of law": "MEDIUM",
67
+ "Price Restriction": "MEDIUM", "Minimum Commitment": "MEDIUM",
68
+ "Volume Restriction": "MEDIUM", "Non-Disparagement": "MEDIUM",
69
+ "Most Favored Nation": "MEDIUM", "Revenue/Profit Sharing": "MEDIUM",
70
+ "Warranty Duration": "MEDIUM",
71
+ "Document Name": "LOW", "Parties": "LOW", "Agreement Date": "LOW",
72
+ "Effective Date": "LOW", "Expiration Date": "LOW", "Renewal Term": "LOW",
73
+ "Joint IP Ownership": "LOW", "License Grant": "LOW",
74
+ "Non-Transferable License": "LOW", "Affiliate License-Licensor": "LOW",
75
+ "Affiliate License-Licensee": "LOW", "Unlimited/All-You-Can-Eat License": "LOW",
76
+ "Irrevocable or Perpetual License": "LOW", "Source Code Escrow": "LOW",
77
+ "Post-Termination Services": "LOW", "Audit Rights": "LOW",
78
+ "Cap on Liability": "LOW", "Insurance": "LOW",
79
+ "Covenant Not to Sue": "LOW", "Third Party Beneficiary": "LOW",
80
+ "Other": "LOW", "ROFR/ROFO/ROFN": "LOW", "Contract by using": "LOW",
81
+ }
82
+
83
+ DESC_MAP = {
84
  "Limitation of liability": "Company limits or excludes liability for losses, data breaches, or service failures.",
85
  "Unilateral termination": "Company can terminate your account at any time without reason.",
86
  "Unilateral change": "Company can change terms at any time without your consent.",
 
89
  "Choice of law": "Governing law may differ from your country, reducing your legal protections.",
90
  "Jurisdiction": "Disputes must be resolved in a jurisdiction that may disadvantage you.",
91
  "Arbitration": "Forces disputes to arbitration instead of court. You waive your right to sue.",
92
+ "Uncapped Liability": "No financial limit on damages the party may be liable for.",
93
+ "Cap on Liability": "Maximum financial liability is explicitly capped.",
94
+ "Non-Compete": "Restrictions on competing with the counter-party.",
95
+ "Exclusivity": "Obligation to deal exclusively with one party.",
96
+ "IP Ownership Assignment": "Intellectual property rights are transferred entirely.",
97
+ "Termination for Convenience": "Either party may terminate without cause or notice.",
98
+ "Governing Law": "Specifies which jurisdiction's laws apply.",
99
+ "Non-Disparagement": "Agreement not to speak negatively about the other party.",
100
+ "ROFR/ROFO/ROFN": "Right of First Refusal / Offer / Negotiation clause.",
101
+ "Change of Control": "Provisions triggered by ownership or control changes.",
102
+ "Anti-Assignment": "Restrictions on transferring contract rights to third parties.",
103
+ "Liquidated Damages": "Pre-determined damages amount for breach of contract.",
104
+ "Source Code Escrow": "Third-party holds source code for release under defined conditions.",
105
+ "Post-Termination Services": "Services to be provided after the contract ends.",
106
+ "Audit Rights": "Right to inspect records or verify compliance.",
107
+ "Warranty Duration": "Length of time warranties remain in effect.",
108
+ "Covenant Not to Sue": "Agreement not to bring legal action against a party.",
109
+ "Third Party Beneficiary": "Non-party who benefits from the contract terms.",
110
+ "Insurance": "Insurance coverage requirements.",
111
+ "Revenue/Profit Sharing": "Revenue or profit sharing arrangements between parties.",
112
+ "Price Restriction": "Restrictions on pricing or discounting.",
113
+ "Minimum Commitment": "Minimum purchase or usage commitment.",
114
+ "Volume Restriction": "Limits on volume of goods or services.",
115
+ "License Grant": "Permission to use intellectual property.",
116
+ "Non-Transferable License": "License that cannot be transferred to third parties.",
117
+ "Irrevocable or Perpetual License": "License that cannot be revoked or lasts indefinitely.",
118
+ "Unlimited/All-You-Can-Eat License": "License with no usage limits.",
119
  }
120
 
121
+ RISK_WEIGHTS = {"CRITICAL": 40, "HIGH": 20, "MEDIUM": 10, "LOW": 3}
122
+
123
+ # ─── Regex patterns (fallback) ───
124
+ REGEX_PATTERNS = {
125
+ "Limitation of liability": [r"not liable", r"shall not be (liable|responsible)", r"in no event.*liable", r"limitation of liability", r"without warranty", r"disclaim"],
126
+ "Unilateral termination": [r"terminat.*at any time", r"suspend.*account.*without", r"we may (terminat|suspend|discontinu)", r"right to (terminat|suspend)"],
127
+ "Unilateral change": [r"sole discretion", r"reserves? the right to (modify|change|update|amend)", r"at any time.*without (prior )?notice", r"we may (modify|change|update)"],
128
+ "Content removal": [r"remove.*content.*without", r"right to remove", r"we may.*remove"],
129
+ "Contract by using": [r"by (using|accessing).*you agree", r"continued use.*constitutes? acceptance"],
130
+ "Choice of law": [r"governed by.*laws? of", r"shall be governed", r"laws of the state of"],
131
+ "Jurisdiction": [r"exclusive jurisdiction", r"courts? of.*(california|delaware|new york|ireland|england)", r"submit to.*jurisdiction"],
132
+ "Arbitration": [r"arbitrat", r"binding arbitration", r"waive.*right.*court", r"class action waiver"],
133
+ "Governing Law": [r"governed by", r"laws of", r"jurisdiction of"],
134
+ "Termination for Convenience": [r"terminat.*for convenience", r"terminat.*without cause", r"terminat.*at any time"],
135
+ "Non-Compete": [r"non-compete", r"shall not compete", r"competition"],
136
+ "Exclusivity": [r"exclusive", r"exclusivity"],
137
+ "IP Ownership Assignment": [r"assign.*intellectual property", r"ownership of.*ip", r"all rights.*assign"],
138
+ "Uncapped Liability": [r"unlimited liability", r"uncapped", r"no.*limit.*liability"],
139
+ "Cap on Liability": [r"cap on liability", r"maximum liability", r"liability.*shall not exceed"],
140
+ "Indemnification": [r"indemnif", r"hold harmless", r"defend"],
141
+ "Confidentiality": [r"confidential", r"non-disclosure", r"nda"],
142
+ "Force Majeure": [r"force majeure", r"act of god", r"beyond.*control"],
143
+ "Penalties": [r"penalt", r"late fee", r"default charge", r"interest on overdue"],
144
  }
145
 
146
+ # ─── Model Loading ───
147
+ cuad_tokenizer = None
148
+ cuad_model = None
149
+ _HAS_TORCH = False
 
 
 
 
 
 
150
 
151
+ try:
152
+ import torch
153
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
154
+ from peft import PeftModel
155
+ _HAS_TORCH = True
156
+ except Exception:
157
+ pass
158
 
159
  def load_model():
160
+ global cuad_tokenizer, cuad_model, classifier
161
+ if not _HAS_TORCH:
162
+ print("[ClauseGuard] PyTorch not available")
163
+ return
164
  try:
165
+ base = "nlpaueb/legal-bert-base-uncased"
166
+ adapter = "Mokshith31/legalbert-contract-clause-classification"
167
+ print(f"[ClauseGuard] Loading CUAD classifier: {adapter}")
168
+ cuad_tokenizer = AutoTokenizer.from_pretrained(base)
169
+ base_model = AutoModelForSequenceClassification.from_pretrained(
170
+ base, num_labels=41, ignore_mismatched_sizes=True
171
+ )
172
+ cuad_model = PeftModel.from_pretrained(base_model, adapter)
173
+ cuad_model.eval()
174
+ print("[ClauseGuard] CUAD model loaded successfully")
175
  except Exception as e:
176
+ print(f"[ClauseGuard] CUAD model load failed: {e}")
177
+ cuad_tokenizer = None
178
+ cuad_model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  # ─── Supabase helper ───
181
  async def supabase_insert(table: str, data: dict):
 
200
  )
201
  return resp.json() if resp.status_code == 200 else []
202
 
203
+ # ─── Clause Processing ───
204
+ def split_clauses(text):
205
+ text = re.sub(r'\n{3,}', '\n\n', text.strip())
206
+ parts = re.split(r'(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n\n)(?=\d+[.)]\s|\([a-z]\)\s|[A-Z][A-Z\s]{2,})', text)
207
+ return [p.strip() for p in parts if len(p.strip()) > 30]
208
+
209
+ def classify_regex(text):
210
+ text_lower = text.lower()
211
+ results = []
212
+ seen = set()
213
+ for label, patterns in REGEX_PATTERNS.items():
214
+ for pat in patterns:
215
+ if re.search(pat, text_lower):
216
+ if label not in seen:
217
+ risk = RISK_MAP.get(label, "MEDIUM")
218
+ results.append({
219
+ "label": label,
220
+ "confidence": 0.7,
221
+ "risk": risk,
222
+ "description": DESC_MAP.get(label, label),
223
+ })
224
+ seen.add(label)
225
+ break
226
+ return results
227
+
228
+ def classify_cuad(clause_text):
229
+ if cuad_model is None or cuad_tokenizer is None:
230
+ return classify_regex(clause_text)
231
+ try:
232
+ inputs = cuad_tokenizer(clause_text, return_tensors="pt", truncation=True, max_length=256, padding=True)
233
+ with torch.no_grad():
234
+ logits = cuad_model(**inputs).logits
235
+ probs = torch.softmax(logits, dim=-1)[0]
236
+ threshold = 0.15
237
+ results = []
238
+ for i, prob in enumerate(probs):
239
+ if prob > threshold and i < len(CUAD_LABELS):
240
+ label = CUAD_LABELS[i]
241
+ results.append({
242
+ "label": label,
243
+ "confidence": round(float(prob), 3),
244
+ "risk": RISK_MAP.get(label, "LOW"),
245
+ "description": DESC_MAP.get(label, label),
246
+ })
247
+ results.sort(key=lambda x: x["confidence"], reverse=True)
248
+ if not results:
249
+ top_idx = int(probs.argmax())
250
+ label = CUAD_LABELS[top_idx] if top_idx < len(CUAD_LABELS) else "Other"
251
+ results.append({
252
+ "label": label,
253
+ "confidence": round(float(probs[top_idx]), 3),
254
+ "risk": RISK_MAP.get(label, "LOW"),
255
+ "description": DESC_MAP.get(label, label),
256
+ })
257
+ return results
258
+ except Exception:
259
+ return classify_regex(clause_text)
260
+
261
+ # ─── NER ───
262
+ def extract_entities(text):
263
+ entities = []
264
+ # Dates
265
+ for pat, etype in [
266
+ (r'\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b', "DATE"),
267
+ (r'\b\d{1,2}/\d{1,2}/\d{2,4}\b', "DATE"),
268
+ (r'\b\d{1,2}-\d{1,2}-\d{2,4}\b', "DATE"),
269
+ (r'\b(?:Effective|Commencement|Expiration|Termination)\s+Date\b', "DATE_REF"),
270
+ ]:
271
+ for m in re.finditer(pat, text, re.IGNORECASE):
272
+ entities.append({"text": m.group(), "type": etype, "start": m.start(), "end": m.end()})
273
+ # Money
274
+ for pat, etype in [
275
+ (r'\$\d{1,3}(?:,\d{3})*(?:\.\d{2})?(?:\s*(?:million|billion|thousand|M|B|K))?', "MONEY"),
276
+ (r'\b\d{1,3}(?:,\d{3})*(?:\.\d{2})?\s*(?:USD|EUR|GBP|dollars|euros)', "MONEY"),
277
+ ]:
278
+ for m in re.finditer(pat, text, re.IGNORECASE):
279
+ entities.append({"text": m.group(), "type": etype, "start": m.start(), "end": m.end()})
280
+ # Parties
281
+ for pat, etype in [
282
+ (r'\b[A-Z][A-Za-z0-9\s&]+(?:Inc\.|LLC|Ltd\.|Limited|Corp\.|Corporation|PLC|GmbH|AG|S\.A\.|B\.V\.)\b', "PARTY"),
283
+ (r'\b(?:Party A|Party B|Disclosing Party|Receiving Party|Licensor|Licensee|Buyer|Seller|Tenant|Landlord|Employer|Employee|Company|Customer|Vendor|Client)\b', "PARTY_ROLE"),
284
+ ]:
285
+ for m in re.finditer(pat, text):
286
+ entities.append({"text": m.group(), "type": etype, "start": m.start(), "end": m.end()})
287
+ # Jurisdictions
288
+ for pat, etype in [
289
+ (r'\b(?:State|Laws?) of [A-Z][a-zA-Z\s]+', "JURISDICTION"),
290
+ (r'\b(?:California|Delaware|New York|Texas|Florida|England|Ireland|Germany|France|Singapore|Hong Kong)\b', "JURISDICTION"),
291
+ ]:
292
+ for m in re.finditer(pat, text, re.IGNORECASE):
293
+ entities.append({"text": m.group(), "type": etype, "start": m.start(), "end": m.end()})
294
+ # Defined Terms
295
+ for pat, etype in [
296
+ (r'"([A-Z][A-Z\s]+)"', "DEFINED_TERM"),
297
+ (r'\(([A-Z][A-Z\s]+)\)', "DEFINED_TERM"),
298
+ ]:
299
+ for m in re.finditer(pat, text):
300
+ entities.append({"text": m.group(1), "type": etype, "start": m.start(), "end": m.end()})
301
+ # Deduplicate
302
+ entities.sort(key=lambda x: (x["start"], -(x["end"] - x["start"])))
303
+ filtered = []
304
+ last_end = -1
305
+ for e in entities:
306
+ if e["start"] >= last_end:
307
+ filtered.append(e)
308
+ last_end = e["end"]
309
+ return filtered
310
+
311
+ # ─── Contradictions ───
312
+ CONTRADICTION_PAIRS = [
313
+ (["Uncapped Liability", "unlimited liability"], ["Cap on Liability", "cap on liability"],
314
+ "Liability cannot be both uncapped and capped simultaneously."),
315
+ (["Governing Law"], ["Governing Law"],
316
+ "Multiple governing law provisions detected β€” verify consistency."),
317
+ (["Termination for Convenience", "terminat.*convenience"], ["Fixed Term", "fixed term"],
318
+ "Contract has both fixed term and termination for convenience β€” review carefully."),
319
+ (["IP Ownership Assignment", "assign.*ip"], ["Joint IP Ownership", "joint ownership"],
320
+ "IP cannot be both fully assigned and jointly owned."),
321
+ ]
322
+
323
+ def detect_contradictions(clause_results):
324
+ contradictions = []
325
+ labels_found = set()
326
+ for cr in clause_results:
327
+ labels_found.add(cr["label"])
328
+ for group_a, group_b, explanation in CONTRADICTION_PAIRS:
329
+ found_a = any(l in labels_found for l in group_a)
330
+ found_b = any(l in labels_found for l in group_b)
331
+ if found_a and found_b:
332
+ contradictions.append({"type": "CONTRADICTION", "explanation": explanation, "severity": "HIGH", "clauses": list(set(group_a + group_b))})
333
+ for cc in ["Governing Law", "Termination for Convenience", "Limitation of liability", "Arbitration"]:
334
+ if cc not in labels_found:
335
+ contradictions.append({"type": "MISSING", "explanation": f"Critical clause '{cc}' not detected.", "severity": "MEDIUM", "clauses": [cc]})
336
+ return contradictions
337
+
338
+ # ─── Risk Scoring ───
339
+ def compute_risk_score(clause_results, total_clauses):
340
+ sev_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
341
+ for cr in clause_results:
342
+ sev = cr.get("risk", "LOW")
343
+ sev_counts[sev] += 1
344
+ if total_clauses == 0:
345
+ return 0, "A", sev_counts
346
+ weighted = sum(sev_counts[s] * RISK_WEIGHTS[s] for s in sev_counts)
347
+ risk = min(100, round(weighted / max(1, total_clauses) * 10))
348
+ if risk >= 70: grade = "F"
349
+ elif risk >= 50: grade = "D"
350
+ elif risk >= 30: grade = "C"
351
+ elif risk >= 15: grade = "B"
352
+ else: grade = "A"
353
+ return risk, grade, sev_counts
354
+
355
+ # ─── Obligations ───
356
+ OBLIGATION_PATTERNS = {
357
+ "monetary": [r"(?:shall|must|will|agrees? to)\s+pay\s+(?:\$?[\d,]+)", r"(?:fee|payment|compensation|reimburs(?:e|ement))\s+of\s+(?:\$?[\d,]+)", r"(?:shall|must|will)\s+remit\s+(?:\$?[\d,]+)", r"(?:annual|monthly|quarterly)\s+(?:fee|payment)\s+of", r"(?:liquidated damages|penalty)\s+of\s+(?:\$?[\d,]+)"],
358
+ "compliance": [r"(?:shall|must|will)\s+comply\s+with", r"(?:shall|must|will)\s+adhere\s+to", r"(?:shall|must|will)\s+conform\s+to", r"(?:GDPR|CCPA|HIPAA|SOX|PCI-DSS|ISO\s+\d+)", r"(?:confidential|privacy|data protection)", r"(?:shall|must|will)\s+maintain\s+(?:insurance|coverage|bond)"],
359
+ "reporting": [r"(?:shall|must|will)\s+report", r"(?:shall|must|will)\s+provide\s+(?:regular|monthly|quarterly|annual)\s+(?:reports?|updates?|status)", r"(?:shall|must|will)\s+notify", r"(?:shall|must|will)\s+inform"],
360
+ "delivery": [r"(?:shall|must|will)\s+deliver", r"(?:shall|must|will)\s+provide", r"(?:shall|must|will)\s+furnish", r"(?:shall|must|will)\s+supply", r"(?:shall|must|will)\s+submit"],
361
+ "termination": [r"(?:shall|must|will)\s+return", r"(?:shall|must|will)\s+destroy", r"(?:shall|must|will)\s+cease", r"(?:upon|after)\s+termination"],
362
+ }
363
+
364
+ def extract_obligations(text):
365
+ sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', text)
366
+ obligations = []
367
+ for sentence in sentences:
368
+ sentence = sentence.strip()
369
+ if len(sentence) < 30:
370
+ continue
371
+ found_types = set()
372
+ for otype, patterns in OBLIGATION_PATTERNS.items():
373
+ for pat in patterns:
374
+ if re.search(pat, sentence, re.IGNORECASE):
375
+ found_types.add(otype)
376
+ break
377
+ if not found_types:
378
+ continue
379
+ party = "Unknown"
380
+ for pp in [r'\b(?:Party A|Party B|Disclosing Party|Receiving Party|Licensor|Licensee|Buyer|Seller|Tenant|Landlord|Employer|Employee|Company|Customer|Vendor|Client)\b', r'\b[A-Z][A-Za-z0-9\s&]+(?:Inc\.|LLC|Ltd\.|Limited|Corp\.|Corporation|PLC|GmbH|AG|S\.A\.|B\.V\.)\b']:
381
+ m = re.search(pp, sentence)
382
+ if m:
383
+ party = m.group(0)
384
+ break
385
+ deadline = "Not specified"
386
+ for pat, ptype in [
387
+ (r"within\s+(\d+)\s+(day|week|month|year)s?", "relative"),
388
+ (r"no\s+later\s+than\s+(\d+)\s+(day|week|month|year)s?", "relative"),
389
+ (r"within\s+(\d+)\s+business\s+days?", "business_days"),
390
+ (r"by\s+([A-Z][a-z]+\s+\d{1,2},?\s+\d{4})", "absolute"),
391
+ (r"on\s+or\s+before\s+([A-Z][a-z]+\s+\d{1,2},?\s+\d{4})", "absolute"),
392
+ ]:
393
+ m = re.search(pat, sentence, re.IGNORECASE)
394
+ if m:
395
+ deadline = m.group(0)
396
+ break
397
+ for otype in found_types:
398
+ obligations.append({"type": otype, "party": party, "description": sentence[:250] + ("..." if len(sentence) > 250 else ""), "deadline": deadline})
399
+ return obligations
400
+
401
+ # ─── Compliance ───
402
+ REGULATIONS = {
403
+ "GDPR": {
404
+ "description": "EU General Data Protection Regulation (Regulation 2016/679)",
405
+ "requirements": {
406
+ "lawful_basis": {"keywords": ["lawful basis", "legal basis", "legitimate interest", "consent", "performance of contract", "legal obligation"], "description": "Must specify lawful basis for data processing (Art. 6)", "severity": "HIGH"},
407
+ "data_subject_rights": {"keywords": ["right to access", "right to erasure", "right to be forgotten", "data portability", "rectification", "object to processing"], "description": "Must acknowledge data subject rights (Arts. 15-22)", "severity": "HIGH"},
408
+ "data_breach_notification": {"keywords": ["data breach", "breach notification", "notify supervisory authority", "72 hours"], "description": "Must include data breach notification obligations (Art. 33)", "severity": "MEDIUM"},
409
+ "cross_border_transfer": {"keywords": ["standard contractual clauses", "SCCs", "adequacy decision", "transfer mechanism", "third country"], "description": "Must specify transfer safeguards for cross-border data (Arts. 44-49)", "severity": "HIGH"},
410
+ },
411
+ },
412
+ "CCPA": {
413
+ "description": "California Consumer Privacy Act (Cal. Civ. Code Β§ 1798.100 et seq.)",
414
+ "requirements": {
415
+ "consumer_rights": {"keywords": ["right to know", "right to delete", "right to opt out", "right to non-discrimination", "consumer rights"], "description": "Must acknowledge California consumer rights", "severity": "HIGH"},
416
+ "data_categories": {"keywords": ["categories of personal information", "personal information categories", "identifiers", "commercial information"], "description": "Must disclose categories of personal information collected", "severity": "HIGH"},
417
+ "sale_of_data": {"keywords": ["do not sell my personal information", "opt-out of sale", "sale of personal information"], "description": "Must provide opt-out mechanism for data sales", "severity": "HIGH"},
418
+ },
419
+ },
420
+ "SOX": {
421
+ "description": "Sarbanes-Oxley Act (US, 2002)",
422
+ "requirements": {
423
+ "internal_controls": {"keywords": ["internal controls", "internal control over financial reporting", "ICFR"], "description": "Must reference internal controls over financial reporting (Β§ 404)", "severity": "HIGH"},
424
+ "whistleblower": {"keywords": ["whistleblower", "anonymous reporting", "reporting hotline", "retaliation"], "description": "Should protect whistleblower provisions (Β§ 806)", "severity": "HIGH"},
425
+ "document_retention": {"keywords": ["document retention", "record retention", "retention policy", "preserve records"], "description": "Must include document retention obligations (Β§ 802)", "severity": "HIGH"},
426
+ },
427
+ },
428
+ "HIPAA": {
429
+ "description": "Health Insurance Portability and Accountability Act (US, 1996)",
430
+ "requirements": {
431
+ "phi_protection": {"keywords": ["protected health information", "PHI", "health information", "ePHI"], "description": "Must protect PHI and limit uses/disclosures", "severity": "CRITICAL"},
432
+ "security_safeguards": {"keywords": ["administrative safeguards", "technical safeguards", "physical safeguards", "encryption", "access controls"], "description": "Must implement security safeguards (Β§ 164.308-312)", "severity": "HIGH"},
433
+ "breach_notification": {"keywords": ["breach notification", "notification of breach", "unauthorized access"], "description": "Must include breach notification obligations (Β§ 164.400-414)", "severity": "HIGH"},
434
+ },
435
+ },
436
+ "FINRA": {
437
+ "description": "Financial Industry Regulatory Authority (US)",
438
+ "requirements": {
439
+ "recordkeeping": {"keywords": ["recordkeeping", "books and records", "retain records", "SEC Rule 17a-4"], "description": "Must comply with recordkeeping rules (FINRA Rule 4511)", "severity": "HIGH"},
440
+ "anti_money_laundering": {"keywords": ["anti-money laundering", "AML", "suspicious activity", "SAR", "OFAC"], "description": "Must reference AML compliance (FINRA Rule 3310)", "severity": "HIGH"},
441
+ "privacy": {"keywords": ["privacy policy", "customer information", "Regulation S-P", "nonpublic personal information"], "description": "Must protect customer information (Regulation S-P)", "severity": "HIGH"},
442
+ },
443
+ },
444
+ }
445
+
446
+ def check_compliance(text):
447
+ text_lower = text.lower()
448
+ results = {}
449
+ for reg_name, reg_data in REGULATIONS.items():
450
+ checks = []
451
+ for req_name, req_data in reg_data["requirements"].items():
452
+ matched = False
453
+ matched_keywords = []
454
+ for kw in req_data["keywords"]:
455
+ if kw.lower() in text_lower:
456
+ matched = True
457
+ matched_keywords.append(kw)
458
+ checks.append({"requirement": req_name, "description": req_data["description"], "severity": req_data["severity"], "status": "PASS" if matched else "MISSING", "matched_keywords": matched_keywords})
459
+ passed = sum(1 for c in checks if c["status"] == "PASS")
460
+ total = len(checks)
461
+ compliance_rate = round(passed / total * 100) if total > 0 else 0
462
+ results[reg_name] = {"description": reg_data["description"], "compliance_rate": compliance_rate, "checks": checks, "overall_status": "COMPLIANT" if compliance_rate >= 80 else "PARTIAL" if compliance_rate >= 40 else "NON-COMPLIANT"}
463
+ return results
464
+
465
+ # ─── Comparison ───
466
+ from difflib import SequenceMatcher
467
+
468
+ def _normalize(text):
469
+ text = text.lower()
470
+ text = re.sub(r'[^a-z0-9\s]', ' ', text)
471
+ text = re.sub(r'\s+', ' ', text).strip()
472
+ return text
473
+
474
+ def _clause_type(text):
475
+ text_lower = text.lower()
476
+ type_keywords = {
477
+ "governing law": ["govern", "law", "jurisdiction"],
478
+ "termination": ["terminat", "cancel", "end"],
479
+ "indemnification": ["indemnif", "hold harmless"],
480
+ "confidentiality": ["confidential", "non-disclosure"],
481
+ "liability": ["liability", "liable", "damages"],
482
+ "payment": ["payment", "fee", "price", "compensat"],
483
+ "intellectual property": ["intellectual", "ip", "copyright", "patent"],
484
+ "warranty": ["warrant", "guarantee"],
485
+ "force majeure": ["force majeure", "act of god"],
486
+ "arbitration": ["arbitrat", "mediation"],
487
+ "assignment": ["assign", "transfer"],
488
+ "non-compete": ["compete", "competition"],
489
+ "renewal": ["renew", "extend"],
490
+ }
491
+ for ctype, keywords in type_keywords.items():
492
+ if any(kw in text_lower for kw in keywords):
493
+ return ctype
494
+ return "general"
495
+
496
+ def compare_contracts(text_a, text_b):
497
+ clauses_a = split_clauses(text_a)
498
+ clauses_b = split_clauses(text_b)
499
+ matched_a = set()
500
+ matched_b = set()
501
+ modified = []
502
+ for i, ca in enumerate(clauses_a):
503
+ best_sim, best_j = 0, -1
504
+ for j, cb in enumerate(clauses_b):
505
+ if j in matched_b:
506
+ continue
507
+ sim = SequenceMatcher(None, _normalize(ca), _normalize(cb)).ratio()
508
+ if sim > best_sim:
509
+ best_sim = sim
510
+ best_j = j
511
+ if best_sim >= 0.75:
512
+ matched_a.add(i)
513
+ matched_b.add(best_j)
514
+ if best_sim < 0.95:
515
+ modified.append({"type": "modified", "similarity": round(best_sim, 3), "clause_a": ca[:200], "clause_b": clauses_b[best_j][:200], "clause_type": _clause_type(ca)})
516
+ elif best_sim >= 0.45:
517
+ modified.append({"type": "partial", "similarity": round(best_sim, 3), "clause_a": ca[:200], "clause_b": clauses_b[best_j][:200] if best_j >= 0 else "", "clause_type": _clause_type(ca)})
518
+ removed = [clauses_a[i] for i in range(len(clauses_a)) if i not in matched_a]
519
+ added = [clauses_b[j] for j in range(len(clauses_b)) if j not in matched_b]
520
+ total_pairs = max(len(clauses_a), len(clauses_b))
521
+ alignment = len(matched_a) / total_pairs if total_pairs > 0 else 0.0
522
+ risk_keywords = ["unlimited", "unilateral", "waive", "arbitration", "indemnif", "not liable", "no warranty", "sole discretion"]
523
+ risk_a = sum(1 for kw in risk_keywords if kw in text_a.lower())
524
+ risk_b = sum(1 for kw in risk_keywords if kw in text_b.lower())
525
+ if risk_a > risk_b + 2:
526
+ risk_delta, risk_winner = "Contract A is significantly riskier", "B"
527
+ elif risk_b > risk_a + 2:
528
+ risk_delta, risk_winner = "Contract B is significantly riskier", "A"
529
+ else:
530
+ risk_delta, risk_winner = "Similar risk profiles", "tie"
531
+ return {
532
+ "alignment_score": round(alignment, 3),
533
+ "contract_a_clauses": len(clauses_a), "contract_b_clauses": len(clauses_b),
534
+ "added_clauses": [{"text": c[:200], "type": _clause_type(c)} for c in added[:50]],
535
+ "removed_clauses": [{"text": c[:200], "type": _clause_type(c)} for c in removed[:50]],
536
+ "modified_clauses": modified[:50],
537
+ "risk_delta": risk_delta, "risk_winner": risk_winner,
538
+ "type_map_a": {k: len(v) for k, v in defaultdict(list, [("general", [])]).items()},
539
+ "type_map_b": {k: len(v) for k, v in defaultdict(list, [("general", [])]).items()},
540
+ }
541
+
542
  # ─── Models ───
543
  class AnalyzeRequest(BaseModel):
544
+ text: str = Field(..., min_length=50)
545
  source_url: Optional[str] = None
546
 
547
  class AnalyzeResponse(BaseModel):
 
550
  total_clauses: int
551
  flagged_count: int
552
  results: list[dict]
553
+ entities: list[dict]
554
+ contradictions: list[dict]
555
+ obligations: list[dict]
556
+ compliance: dict
557
  model: str
558
  latency_ms: int
559
 
560
+ class CompareRequest(BaseModel):
561
+ text_a: str = Field(..., min_length=50)
562
+ text_b: str = Field(..., min_length=50)
563
+
564
  class ExplainRequest(BaseModel):
565
  clause: str = Field(..., min_length=10, max_length=2000)
566
  category: str
 
578
  load_model()
579
  yield
580
 
581
+ app = FastAPI(title="ClauseGuard API", version="2.0.0", lifespan=lifespan)
582
 
583
  app.add_middleware(
584
  CORSMiddleware,
585
+ allow_origins=["https://clauseguardweb.netlify.app", "https://clauseguardweb.netlify.app", "chrome-extension://*", "http://localhost:3000", "*"],
586
  allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
587
  )
588
 
589
  @app.get("/health")
590
  async def health():
591
+ return {"status": "ok", "model": "ml" if cuad_model else "regex", "version": "2.0.0"}
592
 
593
  @app.post("/api/analyze", response_model=AnalyzeResponse)
594
  async def analyze(req: AnalyzeRequest, user: Optional[dict] = Depends(get_current_user)):
595
  start = time.time()
596
+ clauses = split_clauses(req.text)
597
+ if not clauses:
598
+ raise HTTPException(status_code=400, detail="No clauses detected in document")
599
+
600
+ clause_results = []
601
+ for clause in clauses:
602
+ predictions = classify_cuad(clause)
603
+ if predictions:
604
+ for pred in predictions:
605
+ clause_results.append({"text": clause, "label": pred["label"], "confidence": pred["confidence"], "risk": pred["risk"], "description": pred["description"]})
606
+
607
+ entities = extract_entities(req.text)
608
+ contradictions = detect_contradictions(clause_results)
609
+ risk, grade, sev_counts = compute_risk_score(clause_results, len(clauses))
610
+ obligations = extract_obligations(req.text)
611
+ compliance = check_compliance(req.text)
612
  latency = int((time.time() - start) * 1000)
613
+
614
+ results_for_db = [{"text": cr["text"], "categories": [{"name": cr["label"], "severity": cr["risk"], "confidence": cr["confidence"], "description": cr["description"]}]} for cr in clause_results]
615
+
616
  if user:
617
  await supabase_insert("analyses", {
618
+ "user_id": user["id"], "source_url": req.source_url, "total_clauses": len(clauses),
619
+ "flagged_count": len(set(cr["text"] for cr in clause_results)), "risk_score": risk, "grade": grade,
620
+ "clauses": results_for_db, "entities": entities, "contradictions": contradictions,
621
+ "obligations": obligations, "compliance": compliance,
622
  })
623
+
624
+ return AnalyzeResponse(
625
+ risk_score=risk, grade=grade, total_clauses=len(clauses),
626
+ flagged_count=len(set(cr["text"] for cr in clause_results)),
627
+ results=results_for_db, entities=entities, contradictions=contradictions,
628
+ obligations=obligations, compliance=compliance,
629
+ model="ml" if cuad_model else "regex", latency_ms=latency,
630
+ )
631
+
632
+ @app.post("/api/compare")
633
+ async def compare(req: CompareRequest):
634
+ result = compare_contracts(req.text_a, req.text_b)
635
+ return result
636
 
637
  @app.post("/api/explain", response_model=ExplainResponse)
638
  async def explain(req: ExplainRequest, user: dict = Depends(require_auth)):
639
+ desc = DESC_MAP.get(req.category, "Unknown category.")
640
+ legal = "Consult local consumer protection laws."
641
  recommendation = "Review this clause carefully. Consider negotiating or seeking legal advice before agreeing."
 
 
642
  if SAULLM_ENDPOINT and HF_API_TOKEN:
643
  try:
644
+ prompt = f"You are a consumer protection legal analyst. Analyze this clause and explain why it may be unfair.\n\nClause: \"{req.clause}\"\nCategory: {req.category}\n\nProvide:\n1. A plain-English explanation\n2. The specific legal basis\n3. A practical recommendation\n\nBe concise. 3-4 sentences per section."
 
 
 
 
 
 
 
 
 
 
 
645
  async with httpx.AsyncClient(timeout=30.0) as client:
646
+ resp = await client.post(SAULLM_ENDPOINT, json={"inputs": prompt, "parameters": {"max_new_tokens": 300, "temperature": 0.3}}, headers={"Authorization": f"Bearer {HF_API_TOKEN}"})
 
 
 
 
647
  if resp.status_code == 200:
648
  output = resp.json()
649
  generated = output[0]["generated_text"] if isinstance(output, list) else output.get("generated_text", "")
 
653
  legal = parts[1] if len(parts) > 1 else legal
654
  recommendation = parts[2] if len(parts) > 2 else recommendation
655
  except Exception:
656
+ pass
657
+ return ExplainResponse(clause=req.clause, category=req.category, explanation=desc, legal_basis=legal, recommendation=recommendation)
 
 
658
 
659
  @app.get("/api/history")
660
  async def history(user: dict = Depends(require_auth), limit: int = 20, offset: int = 0):
661
  limit = min(limit, 100)
662
+ data = await supabase_query("analyses", {"user_id": f"eq.{user['id']}", "select": "*", "order": "created_at.desc", "limit": str(limit), "offset": str(offset)})
 
 
 
663
  return {"analyses": data, "limit": limit, "offset": offset}
664
 
665
  if __name__ == "__main__":
api/requirements.txt CHANGED
@@ -6,3 +6,5 @@ optimum[onnxruntime]>=1.24.0
6
  numpy>=2.0.0
7
  python-jose[cryptography]>=3.3.0
8
  httpx>=0.28.0
 
 
 
6
  numpy>=2.0.0
7
  python-jose[cryptography]>=3.3.0
8
  httpx>=0.28.0
9
+ peft>=0.15.0
10
+ torch>=2.5.0
web/app/api/analyze/route.ts CHANGED
@@ -14,26 +14,11 @@ export async function POST(req: NextRequest) {
14
  );
15
  }
16
 
17
- // Split text into clauses
18
- const clauses = text
19
- .replace(/\n{2,}/g, "\n")
20
- .trim()
21
- .split(/(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[.)]\s|\([a-z]\)\s|β€’\s|-\s)/)
22
- .map((c: string) => c.trim())
23
- .filter((c: string) => c.length > 30);
24
-
25
- if (clauses.length === 0) {
26
- return NextResponse.json(
27
- { error: "Could not extract clauses from the provided text." },
28
- { status: 400 }
29
- );
30
- }
31
-
32
- // Forward to backend API
33
  const response = await fetch(`${API_URL}/api/analyze`, {
34
  method: "POST",
35
  headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify({ clauses, source_url }),
37
  });
38
 
39
  if (!response.ok) {
 
14
  );
15
  }
16
 
17
+ // Forward to backend API v2.0 (full text, clauses split server-side)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const response = await fetch(`${API_URL}/api/analyze`, {
19
  method: "POST",
20
  headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({ text, source_url }),
22
  });
23
 
24
  if (!response.ok) {
web/app/api/compare/route.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const API_URL = process.env.CLAUSEGUARD_API_URL || "https://gaurv007-clauseguard-api.hf.space";
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const body = await req.json();
8
+ const { text_a, text_b } = body;
9
+
10
+ if (!text_a || !text_b || text_a.trim().length < 50 || text_b.trim().length < 50) {
11
+ return NextResponse.json(
12
+ { error: "Both contracts must have at least 50 characters." },
13
+ { status: 400 }
14
+ );
15
+ }
16
+
17
+ const response = await fetch(`${API_URL}/api/compare`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ text_a, text_b }),
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`Backend error: ${response.status}`);
25
+ }
26
+
27
+ const result = await response.json();
28
+ return NextResponse.json(result);
29
+ } catch (error: any) {
30
+ console.error("Compare error:", error.message);
31
+ return NextResponse.json({ error: error.message || "Comparison failed" }, { status: 500 });
32
+ }
33
+ }
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -1,18 +1,38 @@
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" },
@@ -20,16 +40,42 @@ const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: s
20
 
21
  const GRADE_STYLE: Record<string, string> = {
22
  A: "bg-emerald-50 text-emerald-700 border-emerald-200",
23
- B: "bg-emerald-50 text-emerald-700 border-emerald-200",
24
  C: "bg-amber-50 text-amber-700 border-amber-200",
25
- D: "bg-red-50 text-red-700 border-red-200",
26
  F: "bg-red-50 text-red-700 border-red-200",
27
  };
28
 
29
  const CATEGORY_ICONS: Record<string, any> = {
30
  "Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
31
  "Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
32
- "Choice of law": Gavel, "Contract by using": Stamp,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  };
34
 
35
  const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
@@ -48,11 +94,12 @@ Any dispute shall be finally settled by arbitration in New York County.`;
48
 
49
  export default function AnalyzePage() {
50
  const [text, setText] = useState("");
51
- const [results, setResults] = useState<Result | null>(null);
52
  const [loading, setLoading] = useState(false);
53
  const [error, setError] = useState("");
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");
@@ -64,7 +111,6 @@ export default function AnalyzePage() {
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);
@@ -81,27 +127,17 @@ export default function AnalyzePage() {
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
  const formData = new FormData();
92
  formData.append("file", file);
93
-
94
  const res = await fetch("/api/parse-upload", { method: "POST", body: formData });
95
- if (!res.ok) {
96
- const err = await res.json();
97
- throw new Error(err.error || "Failed to parse file");
98
- }
99
-
100
  const { text: extractedText } = await res.json();
101
  setText(extractedText);
102
- } catch (e: any) {
103
- setError(e.message || "Could not read file.");
104
- }
105
  setLoading(false);
106
  if (fileInputRef.current) fileInputRef.current.value = "";
107
  }
@@ -119,7 +155,7 @@ export default function AnalyzePage() {
119
 
120
  function handleCopy() {
121
  if (!results) return;
122
- const summary = `ClauseGuard Report\nRisk: ${results.risk_score}/100 (Grade ${results.grade})\n${results.flagged_count} of ${results.total_clauses} clauses flagged\n\n` +
123
  results.results.filter(r => r.categories.length > 0).map((r, i) =>
124
  `${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
125
  ).join("\n");
@@ -130,82 +166,87 @@ export default function AnalyzePage() {
130
  const flagged = results?.results.filter(r => r.categories.length > 0) || [];
131
  const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
132
 
133
- const sevCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
134
  flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  return (
137
  <div className="min-h-screen bg-white">
138
- {/* Upgrade modal */}
139
  {showUpgrade && (
140
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
141
  <div className="bg-white rounded-2xl p-6 max-w-sm mx-4 shadow-xl">
142
  <div className="flex justify-between items-start">
143
- <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center">
144
- <Lock className="w-5 h-5 text-amber-600" />
145
- </div>
146
  <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
147
  </div>
148
- <h3 className="mt-4 text-lg font-semibold">
149
- {userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}
150
- </h3>
151
  <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
152
  {userPlan === "free" && scanCount >= FREE_LIMIT
153
- ? `You have used all ${FREE_LIMIT} free scans this month. Upgrade to Pro for unlimited scans, file uploads, and AI explanations.`
154
  : "File upload is available on the Pro plan. Upgrade to scan contracts and leases directly."}
155
  </p>
156
  <div className="mt-5 flex gap-2">
157
- <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">
158
- View plans
159
- </a>
160
- <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">
161
- Not now
162
- </button>
163
  </div>
164
  </div>
165
  </div>
166
  )}
167
 
168
- <div className="max-w-6xl mx-auto px-5 py-10">
169
  <div className="mb-8 flex items-start justify-between">
170
  <div>
171
  <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
172
  <ScanText className="w-6 h-6 text-zinc-400" />
173
  Scan a document
174
  </h1>
175
- <p className="mt-1 text-sm text-zinc-500">Paste text or upload a file (.pdf, .docx, .txt).</p>
176
  </div>
177
  {userPlan === "free" && (
178
- <span className="text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md">
179
- {scanCount}/{FREE_LIMIT} free scans
180
- </span>
181
  )}
182
  </div>
183
 
184
  <div className="grid lg:grid-cols-5 gap-6">
185
- {/* Input β€” 2 cols */}
186
  <div className="lg:col-span-2">
187
  <textarea value={text} onChange={(e) => setText(e.target.value)}
188
- placeholder="Paste your document text here..."
189
  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" />
190
  <div className="mt-3 flex gap-2">
191
  <button onClick={handleAnalyze} disabled={loading}
192
  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">
193
- {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Scanning...</> : <><ScanText className="w-4 h-4" /> Scan</>}
194
- </button>
195
- <button onClick={() => setText(EXAMPLE)}
196
- className="px-3 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">
197
- Example
198
  </button>
 
199
  <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
200
- <button onClick={() => fileInputRef.current?.click()}
201
- className="px-3 border border-zinc-200 rounded-lg text-zinc-500 hover:bg-zinc-50 transition-colors" title="Upload file">
202
- <Upload className="w-4 h-4" />
203
- </button>
204
  </div>
205
  {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>}
206
  </div>
207
 
208
- {/* Results β€” 3 cols */}
209
  <div className="lg:col-span-3">
210
  {results ? (
211
  <div className="space-y-4">
@@ -228,32 +269,27 @@ export default function AnalyzePage() {
228
  </span>
229
  </div>
230
  <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
231
- <span>{results.total_clauses} clauses</span>
232
- <span className="w-px h-3 bg-zinc-200" />
233
- <span>{results.flagged_count} flagged</span>
234
- <span className="w-px h-3 bg-zinc-200" />
235
- <span>{results.latency_ms}ms</span>
236
- <span className="w-px h-3 bg-zinc-200" />
237
- <span className="flex items-center gap-1">
238
- {results.model === "ml" && <SparklesIcon className="w-3 h-3" />}
239
- {results.model === "ml" ? "Legal-BERT" : "Pattern matching"}
240
- </span>
241
  </div>
242
  </div>
243
 
244
- {/* Actions bar */}
245
  <div className="flex items-center justify-between">
246
  <div className="flex gap-1">
247
  {[
248
  { key: "all", label: "All", count: flagged.length },
 
249
  { key: "HIGH", label: "High", count: sevCounts.HIGH },
250
  { key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
251
  { key: "LOW", label: "Low", count: sevCounts.LOW },
252
  ].map((f) => (
253
  <button key={f.key} onClick={() => setFilter(f.key)}
254
- className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
255
- filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"
256
- }`}>
257
  {f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
258
  </button>
259
  ))}
@@ -262,74 +298,223 @@ export default function AnalyzePage() {
262
  <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
263
  {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
264
  </button>
265
- <button onClick={handleDownloadPDF} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Download PDF">
266
- <FileDown className="w-4 h-4" />
267
- </button>
268
  </div>
269
  </div>
270
 
271
- {/* Clause list */}
272
- <div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
273
- {filtered.length === 0 ? (
274
- <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
275
- <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
276
- <p className="text-sm text-zinc-500">{filter === "all" ? "No unfair clauses found." : "No clauses at this severity."}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  </div>
278
- ) : filtered.map((clause, i) => {
279
- const maxSev = clause.categories.reduce((m, c) => {
280
- const order: Record<string, number> = { HIGH: 3, MEDIUM: 2, LOW: 1 };
281
- return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
282
- }, "LOW");
283
- const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
284
- const isExpanded = expandedIdx === i;
285
- const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || TriangleAlert;
286
-
287
- return (
288
- <div key={i} className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
289
- <button onClick={() => setExpandedIdx(isExpanded ? null : i)}
290
- className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
291
- <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
292
- <CatIcon className={`w-4 h-4 ${conf.text}`} />
 
 
 
 
 
 
 
 
 
 
 
 
293
  </div>
294
- <div className="flex-1 min-w-0">
295
- <div className="flex items-center gap-2 flex-wrap">
296
- {clause.categories.map((cat, j) => {
297
- const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
298
- return (
299
- <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
300
- {cat.name}{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
301
- </span>
302
- );
303
- })}
 
 
 
 
 
 
 
 
 
 
304
  </div>
305
- <p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
306
  </div>
307
- <div className="shrink-0 mt-1">
308
- {isExpanded ? <ChevronUp className="w-4 h-4 text-zinc-400" /> : <ChevronDown className="w-4 h-4 text-zinc-400" />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </div>
310
- </button>
311
- {isExpanded && (
312
- <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
313
- <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">{clause.text}</p>
314
- {clause.categories.map((cat, j) => (
315
- <div key={j} className="mt-3 flex items-start gap-2">
316
- <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
317
- <p className="text-[13px] text-zinc-500 leading-relaxed">
318
- <span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may be unfair under consumer protection law."}
319
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  </div>
323
- )}
324
- </div>
325
- );
326
- })}
327
  </div>
328
  </div>
329
  ) : (
330
  <div className="border border-dashed border-zinc-200 rounded-xl h-[420px] flex flex-col items-center justify-center">
331
  <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
332
- <p className="text-sm text-zinc-300">Paste text and scan to see results</p>
333
  </div>
334
  )}
335
  </div>
 
1
  "use client";
2
 
3
+ import { useState, useRef } 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, Layers, Landmark, Briefcase,
9
+ AlertTriangle, Tag, BookOpen, ClipboardList, FileCompare, DollarSign,
10
+ Calendar, Building, MapPin, Hash
11
  } from "lucide-react";
12
 
13
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
14
  interface Clause { text: string; categories: Cat[]; }
15
+ interface Entity { text: string; type: string; }
16
+ interface Contradiction { type: string; explanation: string; severity: string; }
17
+ interface Obligation { type: string; party: string; description: string; deadline: string; }
18
+ interface ComplianceCheck { requirement: string; description: string; severity: string; status: string; matched_keywords: string[]; }
19
+ interface ComplianceReg { description: string; compliance_rate: number; checks: ComplianceCheck[]; overall_status: string; }
20
+ interface AnalysisResult {
21
+ risk_score: number;
22
+ grade: string;
23
+ total_clauses: number;
24
+ flagged_count: number;
25
+ results: Clause[];
26
+ entities: Entity[];
27
+ contradictions: Contradiction[];
28
+ obligations: Obligation[];
29
+ compliance: Record<string, ComplianceReg>;
30
+ model: string;
31
+ latency_ms: number;
32
+ }
33
 
34
  const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string }> = {
35
+ CRITICAL: { icon: AlertTriangle, label: "Critical", text: "text-red-700", bg: "bg-red-50", border: "border-red-300" },
36
  HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200" },
37
  MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200" },
38
  LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200" },
 
40
 
41
  const GRADE_STYLE: Record<string, string> = {
42
  A: "bg-emerald-50 text-emerald-700 border-emerald-200",
43
+ B: "bg-emerald-50 text-emerald-600 border-emerald-200",
44
  C: "bg-amber-50 text-amber-700 border-amber-200",
45
+ D: "bg-orange-50 text-orange-700 border-orange-200",
46
  F: "bg-red-50 text-red-700 border-red-200",
47
  };
48
 
49
  const CATEGORY_ICONS: Record<string, any> = {
50
  "Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
51
  "Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
52
+ "Choice of law": Gavel, "Contract by using": Stamp, "Uncapped Liability": AlertTriangle,
53
+ "IP Ownership Assignment": Lock, "Non-Compete": Ban, "Governing Law": Gavel,
54
+ "Termination for Convenience": Ban, "Indemnification": ShieldCheck, "Confidentiality": Lock,
55
+ };
56
+
57
+ const ENTITY_COLORS: Record<string, { bg: string; text: string; border: string; icon: any }> = {
58
+ DATE: { bg: "bg-blue-50", text: "text-blue-700", border: "border-blue-200", icon: Calendar },
59
+ DATE_REF: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200", icon: Calendar },
60
+ MONEY: { bg: "bg-emerald-50", text: "text-emerald-700", border: "border-emerald-200", icon: DollarSign },
61
+ PARTY: { bg: "bg-purple-50", text: "text-purple-700", border: "border-purple-200", icon: Building },
62
+ PARTY_ROLE: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200", icon: Briefcase },
63
+ JURISDICTION: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200", icon: MapPin },
64
+ DEFINED_TERM: { bg: "bg-pink-50", text: "text-pink-700", border: "border-pink-200", icon: Hash },
65
+ };
66
+
67
+ const OBLIGATION_COLORS: Record<string, { bg: string; text: string; icon: any }> = {
68
+ monetary: { bg: "bg-emerald-50", text: "text-emerald-700", icon: DollarSign },
69
+ compliance: { bg: "bg-amber-50", text: "text-amber-700", icon: ShieldCheck },
70
+ reporting: { bg: "bg-blue-50", text: "text-blue-700", icon: ClipboardList },
71
+ delivery: { bg: "bg-purple-50", text: "text-purple-700", icon: FileText },
72
+ termination: { bg: "bg-red-50", text: "text-red-700", icon: Ban },
73
+ };
74
+
75
+ const COMPLIANCE_STATUS: Record<string, { bg: string; text: string }> = {
76
+ COMPLIANT: { bg: "bg-emerald-50", text: "text-emerald-700" },
77
+ PARTIAL: { bg: "bg-amber-50", text: "text-amber-700" },
78
+ "NON-COMPLIANT": { bg: "bg-red-50", text: "text-red-700" },
79
  };
80
 
81
  const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
 
94
 
95
  export default function AnalyzePage() {
96
  const [text, setText] = useState("");
97
+ const [results, setResults] = useState<AnalysisResult | null>(null);
98
  const [loading, setLoading] = useState(false);
99
  const [error, setError] = useState("");
100
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
101
  const [filter, setFilter] = useState<string>("all");
102
+ const [activeTab, setActiveTab] = useState<string>("clauses");
103
  const [copied, setCopied] = useState(false);
104
  const [scanCount, setScanCount] = useState(0);
105
  const [userPlan, setUserPlan] = useState("free");
 
111
 
112
  async function handleAnalyze() {
113
  if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
 
114
  if (!canScan) { setShowUpgrade(true); return; }
115
 
116
  setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
 
127
  async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
128
  const file = e.target.files?.[0];
129
  if (!file) return;
 
130
  if (userPlan === "free") { setShowUpgrade(true); return; }
131
 
132
+ setLoading(true); setError("");
 
 
133
  try {
134
  const formData = new FormData();
135
  formData.append("file", file);
 
136
  const res = await fetch("/api/parse-upload", { method: "POST", body: formData });
137
+ if (!res.ok) throw new Error((await res.json()).error || "Failed to parse file");
 
 
 
 
138
  const { text: extractedText } = await res.json();
139
  setText(extractedText);
140
+ } catch (e: any) { setError(e.message || "Could not read file."); }
 
 
141
  setLoading(false);
142
  if (fileInputRef.current) fileInputRef.current.value = "";
143
  }
 
155
 
156
  function handleCopy() {
157
  if (!results) return;
158
+ 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} found\nContradictions: ${results.contradictions.length} detected\n\n` +
159
  results.results.filter(r => r.categories.length > 0).map((r, i) =>
160
  `${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
161
  ).join("\n");
 
166
  const flagged = results?.results.filter(r => r.categories.length > 0) || [];
167
  const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
168
 
169
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
170
  flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
171
 
172
+ // Group entities by type
173
+ const entityGroups: Record<string, string[]> = {};
174
+ results?.entities.forEach(e => {
175
+ if (!entityGroups[e.type]) entityGroups[e.type] = [];
176
+ if (!entityGroups[e.type].includes(e.text)) entityGroups[e.type].push(e.text);
177
+ });
178
+
179
+ // Group obligations by type
180
+ const obligationGroups: Record<string, Obligation[]> = {};
181
+ results?.obligations.forEach(o => {
182
+ if (!obligationGroups[o.type]) obligationGroups[o.type] = [];
183
+ obligationGroups[o.type].push(o);
184
+ });
185
+
186
+ const tabs = [
187
+ { key: "clauses", label: "Clauses", icon: Layers },
188
+ { key: "entities", label: "Entities", icon: Tag },
189
+ { key: "contradictions", label: "Contradictions", icon: AlertTriangle },
190
+ { key: "obligations", label: "Obligations", icon: ClipboardList },
191
+ { key: "compliance", label: "Compliance", icon: ShieldCheck },
192
+ ];
193
+
194
  return (
195
  <div className="min-h-screen bg-white">
 
196
  {showUpgrade && (
197
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
198
  <div className="bg-white rounded-2xl p-6 max-w-sm mx-4 shadow-xl">
199
  <div className="flex justify-between items-start">
200
+ <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center"><Lock className="w-5 h-5 text-amber-600" /></div>
 
 
201
  <button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
202
  </div>
203
+ <h3 className="mt-4 text-lg font-semibold">{userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}</h3>
 
 
204
  <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
205
  {userPlan === "free" && scanCount >= FREE_LIMIT
206
+ ? `You have used all ${FREE_LIMIT} free scans. Upgrade to Pro for unlimited scans, file uploads, and full analysis.`
207
  : "File upload is available on the Pro plan. Upgrade to scan contracts and leases directly."}
208
  </p>
209
  <div className="mt-5 flex gap-2">
210
+ <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">View plans</a>
211
+ <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">Not now</button>
 
 
 
 
212
  </div>
213
  </div>
214
  </div>
215
  )}
216
 
217
+ <div className="max-w-7xl mx-auto px-5 py-10">
218
  <div className="mb-8 flex items-start justify-between">
219
  <div>
220
  <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
221
  <ScanText className="w-6 h-6 text-zinc-400" />
222
  Scan a document
223
  </h1>
224
+ <p className="mt-1 text-sm text-zinc-500">Paste text or upload a file (.pdf, .docx, .txt). Get 41-category clause detection, risk scoring, NER, compliance, and more.</p>
225
  </div>
226
  {userPlan === "free" && (
227
+ <span className="text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md">{scanCount}/{FREE_LIMIT} free scans</span>
 
 
228
  )}
229
  </div>
230
 
231
  <div className="grid lg:grid-cols-5 gap-6">
232
+ {/* Input */}
233
  <div className="lg:col-span-2">
234
  <textarea value={text} onChange={(e) => setText(e.target.value)}
235
+ placeholder="Paste your contract or terms text here..."
236
  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" />
237
  <div className="mt-3 flex gap-2">
238
  <button onClick={handleAnalyze} disabled={loading}
239
  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">
240
+ {loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Analyzing...</> : <><ScanText className="w-4 h-4" /> Analyze</>}
 
 
 
 
241
  </button>
242
+ <button onClick={() => setText(EXAMPLE)} className="px-3 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">Example</button>
243
  <input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.docx" className="hidden" onChange={handleFileUpload} />
244
+ <button onClick={() => fileInputRef.current?.click()} className="px-3 border border-zinc-200 rounded-lg text-zinc-500 hover:bg-zinc-50 transition-colors" title="Upload file"><Upload className="w-4 h-4" /></button>
 
 
 
245
  </div>
246
  {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>}
247
  </div>
248
 
249
+ {/* Results */}
250
  <div className="lg:col-span-3">
251
  {results ? (
252
  <div className="space-y-4">
 
269
  </span>
270
  </div>
271
  <div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
272
+ <span>{results.total_clauses} clauses</span><span className="w-px h-3 bg-zinc-200" />
273
+ <span>{results.flagged_count} flagged</span><span className="w-px h-3 bg-zinc-200" />
274
+ <span>{results.entities.length} entities</span><span className="w-px h-3 bg-zinc-200" />
275
+ <span>{results.contradictions.length} issues</span><span className="w-px h-3 bg-zinc-200" />
276
+ <span>{results.latency_ms}ms</span><span className="w-px h-3 bg-zinc-200" />
277
+ <span className="flex items-center gap-1">{results.model !== "regex" && <SparklesIcon className="w-3 h-3" />}{results.model !== "regex" ? "Legal-BERT v2" : "Pattern fallback"}</span>
 
 
 
 
278
  </div>
279
  </div>
280
 
281
+ {/* Filter + Actions */}
282
  <div className="flex items-center justify-between">
283
  <div className="flex gap-1">
284
  {[
285
  { key: "all", label: "All", count: flagged.length },
286
+ { key: "CRITICAL", label: "Critical", count: sevCounts.CRITICAL },
287
  { key: "HIGH", label: "High", count: sevCounts.HIGH },
288
  { key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
289
  { key: "LOW", label: "Low", count: sevCounts.LOW },
290
  ].map((f) => (
291
  <button key={f.key} onClick={() => setFilter(f.key)}
292
+ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"}`}>
 
 
293
  {f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
294
  </button>
295
  ))}
 
298
  <button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
299
  {copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
300
  </button>
301
+ <button onClick={handleDownloadPDF} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Download PDF"><FileDown className="w-4 h-4" /></button>
 
 
302
  </div>
303
  </div>
304
 
305
+ {/* Tabs */}
306
+ <div className="border-b border-zinc-200">
307
+ <div className="flex gap-1">
308
+ {tabs.map((t) => (
309
+ <button key={t.key} onClick={() => setActiveTab(t.key)}
310
+ className={`flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
311
+ activeTab === t.key ? "border-zinc-900 text-zinc-900" : "border-transparent text-zinc-400 hover:text-zinc-600"
312
+ }`}>
313
+ <t.icon className="w-4 h-4" />{t.label}
314
+ </button>
315
+ ))}
316
+ </div>
317
+ </div>
318
+
319
+ {/* Tab Content */}
320
+ <div className="max-h-[420px] overflow-y-auto pr-1">
321
+ {/* Clauses Tab */}
322
+ {activeTab === "clauses" && (
323
+ <div className="space-y-2">
324
+ {filtered.length === 0 ? (
325
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
326
+ <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
327
+ <p className="text-sm text-zinc-500">{filter === "all" ? "No flagged clauses found." : "No clauses at this severity."}</p>
328
+ </div>
329
+ ) : filtered.map((clause, i) => {
330
+ const maxSev = clause.categories.reduce((m, c) => {
331
+ const order: Record<string, number> = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
332
+ return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
333
+ }, "LOW");
334
+ const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
335
+ const isExpanded = expandedIdx === i;
336
+ const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || Layers;
337
+ return (
338
+ <div key={i} className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
339
+ <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">
340
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
341
+ <CatIcon className={`w-4 h-4 ${conf.text}`} />
342
+ </div>
343
+ <div className="flex-1 min-w-0">
344
+ <div className="flex items-center gap-2 flex-wrap">
345
+ {clause.categories.map((cat, j) => {
346
+ const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
347
+ return (
348
+ <span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
349
+ {cat.name}{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
350
+ </span>
351
+ );
352
+ })}
353
+ </div>
354
+ <p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
355
+ </div>
356
+ <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>
357
+ </button>
358
+ {isExpanded && (
359
+ <div className="px-4 pb-4 pt-0 border-t border-zinc-100">
360
+ <p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">{clause.text}</p>
361
+ {clause.categories.map((cat, j) => (
362
+ <div key={j} className="mt-3 flex items-start gap-2">
363
+ <TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
364
+ <p className="text-[13px] text-zinc-500 leading-relaxed">
365
+ <span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may contain risks. Review carefully."}
366
+ </p>
367
+ </div>
368
+ ))}
369
+ </div>
370
+ )}
371
+ </div>
372
+ );
373
+ })}
374
  </div>
375
+ )}
376
+
377
+ {/* Entities Tab */}
378
+ {activeTab === "entities" && (
379
+ <div className="space-y-4">
380
+ {Object.keys(entityGroups).length === 0 ? (
381
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
382
+ <Tag className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
383
+ <p className="text-sm text-zinc-500">No entities detected.</p>
384
+ </div>
385
+ ) : Object.entries(entityGroups).map(([type, items]) => {
386
+ const cfg = ENTITY_COLORS[type] || { bg: "bg-zinc-50", text: "text-zinc-700", border: "border-zinc-200", icon: Tag };
387
+ const Icon = cfg.icon;
388
+ return (
389
+ <div key={type}>
390
+ <div className="flex items-center gap-2 mb-2">
391
+ <Icon className={`w-4 h-4 ${cfg.text}`} />
392
+ <span className="text-sm font-medium text-zinc-700">{type.replace("_", " ")}</span>
393
+ <span className="text-xs text-zinc-400">({items.length})</span>
394
+ </div>
395
+ <div className="flex flex-wrap gap-2">
396
+ {items.slice(0, 20).map((item, i) => (
397
+ <span key={i} className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${cfg.bg} ${cfg.text} border ${cfg.border}`}>
398
+ {item}
399
+ </span>
400
+ ))}
401
+ </div>
402
  </div>
403
+ );
404
+ })}
405
+ </div>
406
+ )}
407
+
408
+ {/* Contradictions Tab */}
409
+ {activeTab === "contradictions" && (
410
+ <div className="space-y-2">
411
+ {results.contradictions.length === 0 ? (
412
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
413
+ <CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
414
+ <p className="text-sm text-zinc-500">No contradictions or missing clauses detected.</p>
415
+ </div>
416
+ ) : results.contradictions.map((c, i) => {
417
+ const conf = SEV_CONFIG[c.severity] || SEV_CONFIG.MEDIUM;
418
+ return (
419
+ <div key={i} className={`border rounded-xl p-4 ${conf.border} ${conf.bg}`}>
420
+ <div className="flex items-center gap-2 mb-2">
421
+ <conf.icon className={`w-4 h-4 ${conf.text}`} />
422
+ <span className={`text-xs font-semibold uppercase ${conf.text}`}>{c.type}</span>
423
  </div>
424
+ <p className="text-sm text-zinc-700">{c.explanation}</p>
425
  </div>
426
+ );
427
+ })}
428
+ </div>
429
+ )}
430
+
431
+ {/* Obligations Tab */}
432
+ {activeTab === "obligations" && (
433
+ <div className="space-y-4">
434
+ {Object.keys(obligationGroups).length === 0 ? (
435
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
436
+ <ClipboardList className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
437
+ <p className="text-sm text-zinc-500">No obligations detected.</p>
438
+ </div>
439
+ ) : Object.entries(obligationGroups).map(([type, items]) => {
440
+ const cfg = OBLIGATION_COLORS[type] || { bg: "bg-zinc-50", text: "text-zinc-700", icon: ClipboardList };
441
+ const Icon = cfg.icon;
442
+ return (
443
+ <div key={type}>
444
+ <div className="flex items-center gap-2 mb-2">
445
+ <Icon className={`w-4 h-4 ${cfg.text}`} />
446
+ <span className="text-sm font-medium capitalize text-zinc-700">{type} Obligations</span>
447
+ <span className="text-xs text-zinc-400">({items.length})</span>
448
+ </div>
449
+ <div className="space-y-2">
450
+ {items.map((o, i) => (
451
+ <div key={i} className="border border-zinc-200 rounded-lg p-3">
452
+ <div className="flex items-center justify-between mb-1">
453
+ <span className="text-xs font-medium text-zinc-600">{o.party}</span>
454
+ <span className="text-[11px] text-zinc-400 bg-zinc-100 px-2 py-0.5 rounded">{o.deadline}</span>
455
+ </div>
456
+ <p className="text-sm text-zinc-600">{o.description}</p>
457
+ </div>
458
+ ))}
459
+ </div>
460
  </div>
461
+ );
462
+ })}
463
+ </div>
464
+ )}
465
+
466
+ {/* Compliance Tab */}
467
+ {activeTab === "compliance" && (
468
+ <div className="space-y-4">
469
+ {Object.keys(results.compliance).length === 0 ? (
470
+ <div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
471
+ <ShieldCheck className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
472
+ <p className="text-sm text-zinc-500">No compliance data available.</p>
473
+ </div>
474
+ ) : Object.entries(results.compliance).map(([regName, reg]) => {
475
+ const status = COMPLIANCE_STATUS[reg.overall_status] || COMPLIANCE_STATUS.PARTIAL;
476
+ return (
477
+ <div key={regName} className="border border-zinc-200 rounded-xl overflow-hidden">
478
+ <div className="flex items-center justify-between p-4 border-b border-zinc-100 bg-zinc-50/50">
479
+ <div>
480
+ <span className="text-sm font-semibold text-zinc-900">{regName}</span>
481
+ <p className="text-[11px] text-zinc-500 mt-0.5">{reg.description}</p>
482
+ </div>
483
+ <div className="text-right">
484
+ <span className={`text-lg font-bold ${status.text}`}>{reg.compliance_rate}%</span>
485
+ <span className={`text-[11px] font-medium block ${status.text}`}>{reg.overall_status}</span>
486
  </div>
487
+ </div>
488
+ <div className="p-3 space-y-1">
489
+ {reg.checks.map((check, i) => {
490
+ const sev = SEV_CONFIG[check.severity] || SEV_CONFIG.MEDIUM;
491
+ return (
492
+ <div key={i} className="flex items-center justify-between py-2 px-2 hover:bg-zinc-50 rounded-md">
493
+ <div className="flex-1 min-w-0">
494
+ <p className="text-xs text-zinc-600">{check.description}</p>
495
+ {check.matched_keywords.length > 0 && (
496
+ <p className="text-[10px] text-zinc-400 mt-0.5">Matched: {check.matched_keywords.slice(0, 3).join(", ")}</p>
497
+ )}
498
+ </div>
499
+ <div className="flex items-center gap-2 ml-3">
500
+ <span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${sev.bg} ${sev.text}`}>{check.severity}</span>
501
+ <span className="text-sm">{check.status === "PASS" ? "βœ…" : "❌"}</span>
502
+ </div>
503
+ </div>
504
+ );
505
+ })}
506
+ </div>
507
  </div>
508
+ );
509
+ })}
510
+ </div>
511
+ )}
512
  </div>
513
  </div>
514
  ) : (
515
  <div className="border border-dashed border-zinc-200 rounded-xl h-[420px] flex flex-col items-center justify-center">
516
  <ScanText className="w-10 h-10 text-zinc-200 mb-3" />
517
+ <p className="text-sm text-zinc-300">Paste text and analyze to see results</p>
518
  </div>
519
  )}
520
  </div>
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
+ }
web/app/dashboard-pages/dashboard/page.tsx CHANGED
@@ -1,5 +1,9 @@
1
  import { createClient } from "@/lib/supabase/server";
2
  import Link from "next/link";
 
 
 
 
3
 
4
  export default async function DashboardPage() {
5
  const supabase = await createClient();
@@ -22,6 +26,15 @@ export default async function DashboardPage() {
22
  const usedThisMonth = profile?.analyses_this_month || 0;
23
  const limit = plan === "free" ? 10 : "∞";
24
 
 
 
 
 
 
 
 
 
 
25
  return (
26
  <div className="min-h-screen bg-gray-50">
27
  <div className="max-w-6xl mx-auto px-6 py-12">
@@ -42,7 +55,7 @@ export default async function DashboardPage() {
42
  </div>
43
 
44
  {/* Stats */}
45
- <div className="grid md:grid-cols-4 gap-6 mb-10">
46
  <div className="bg-white rounded-xl p-6 border border-gray-200">
47
  <p className="text-sm text-gray-500">Plan</p>
48
  <p className="text-2xl font-bold text-gray-900 capitalize mt-1">{plan}</p>
@@ -57,14 +70,63 @@ export default async function DashboardPage() {
57
  </div>
58
  <div className="bg-white rounded-xl p-6 border border-gray-200">
59
  <p className="text-sm text-gray-500">Avg Risk Score</p>
60
- <p className="text-2xl font-bold text-gray-900 mt-1">
61
- {analyses && analyses.length > 0
62
- ? Math.round(analyses.reduce((s, a) => s + a.risk_score, 0) / analyses.length)
63
- : "β€”"}
64
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
  </div>
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  {/* Recent Scans */}
69
  <div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
70
  <div className="px-6 py-4 border-b border-gray-100">
@@ -78,9 +140,17 @@ export default async function DashboardPage() {
78
  <p className="text-sm font-medium text-gray-900 truncate">
79
  {a.source_url || "Manual scan"}
80
  </p>
81
- <p className="text-xs text-gray-500 mt-1">
82
- {new Date(a.created_at).toLocaleDateString()} Β· {a.total_clauses} clauses Β· {a.flagged_count} flagged
83
- </p>
 
 
 
 
 
 
 
 
84
  </div>
85
  <div className="flex items-center gap-3">
86
  <span className={`text-sm font-bold px-3 py-1 rounded-full ${
@@ -108,7 +178,7 @@ export default async function DashboardPage() {
108
  <div className="mt-8 bg-indigo-50 border border-indigo-200 rounded-xl p-6 flex items-center justify-between">
109
  <div>
110
  <p className="font-semibold text-indigo-900">Upgrade to Pro</p>
111
- <p className="text-sm text-indigo-700 mt-1">Unlimited scans, AI explanations, PDF exports, and more.</p>
112
  </div>
113
  <Link
114
  href="/#pricing"
 
1
  import { createClient } from "@/lib/supabase/server";
2
  import Link from "next/link";
3
+ import {
4
+ ScanText, ShieldCheck, TriangleAlert, Tag, AlertTriangle,
5
+ ClipboardList, FileCompare, TrendingUp, Clock
6
+ } from "lucide-react";
7
 
8
  export default async function DashboardPage() {
9
  const supabase = await createClient();
 
26
  const usedThisMonth = profile?.analyses_this_month || 0;
27
  const limit = plan === "free" ? 10 : "∞";
28
 
29
+ // Calculate stats
30
+ const avgRisk = analyses && analyses.length > 0
31
+ ? Math.round(analyses.reduce((s, a) => s + a.risk_score, 0) / analyses.length)
32
+ : null;
33
+
34
+ const totalEntities = analyses?.reduce((s, a) => s + (a.entities?.length || 0), 0) || 0;
35
+ const totalContradictions = analyses?.reduce((s, a) => s + (a.contradictions?.length || 0), 0) || 0;
36
+ const totalObligations = analyses?.reduce((s, a) => s + (a.obligations?.length || 0), 0) || 0;
37
+
38
  return (
39
  <div className="min-h-screen bg-gray-50">
40
  <div className="max-w-6xl mx-auto px-6 py-12">
 
55
  </div>
56
 
57
  {/* Stats */}
58
+ <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
59
  <div className="bg-white rounded-xl p-6 border border-gray-200">
60
  <p className="text-sm text-gray-500">Plan</p>
61
  <p className="text-2xl font-bold text-gray-900 capitalize mt-1">{plan}</p>
 
70
  </div>
71
  <div className="bg-white rounded-xl p-6 border border-gray-200">
72
  <p className="text-sm text-gray-500">Avg Risk Score</p>
73
+ <p className="text-2xl font-bold text-gray-900 mt-1">{avgRisk !== null ? avgRisk : "β€”"}</p>
74
+ </div>
75
+ </div>
76
+
77
+ {/* Extended Stats v2 */}
78
+ <div className="grid md:grid-cols-3 gap-6 mb-10">
79
+ <div className="bg-white rounded-xl p-6 border border-gray-200 flex items-center gap-4">
80
+ <div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
81
+ <Tag className="w-5 h-5 text-blue-600" />
82
+ </div>
83
+ <div>
84
+ <p className="text-sm text-gray-500">Entities Extracted</p>
85
+ <p className="text-xl font-bold text-gray-900">{totalEntities}</p>
86
+ </div>
87
+ </div>
88
+ <div className="bg-white rounded-xl p-6 border border-gray-200 flex items-center gap-4">
89
+ <div className="w-10 h-10 rounded-lg bg-amber-50 flex items-center justify-center">
90
+ <AlertTriangle className="w-5 h-5 text-amber-600" />
91
+ </div>
92
+ <div>
93
+ <p className="text-sm text-gray-500">Contradictions Found</p>
94
+ <p className="text-xl font-bold text-gray-900">{totalContradictions}</p>
95
+ </div>
96
+ </div>
97
+ <div className="bg-white rounded-xl p-6 border border-gray-200 flex items-center gap-4">
98
+ <div className="w-10 h-10 rounded-lg bg-emerald-50 flex items-center justify-center">
99
+ <ClipboardList className="w-5 h-5 text-emerald-600" />
100
+ </div>
101
+ <div>
102
+ <p className="text-sm text-gray-500">Obligations Tracked</p>
103
+ <p className="text-xl font-bold text-gray-900">{totalObligations}</p>
104
+ </div>
105
  </div>
106
  </div>
107
 
108
+ {/* Quick Actions */}
109
+ <div className="grid md:grid-cols-2 gap-6 mb-10">
110
+ <Link href="/dashboard-pages/analyze" className="bg-white rounded-xl p-6 border border-gray-200 hover:border-indigo-200 hover:shadow-sm transition-all group">
111
+ <div className="flex items-center gap-3 mb-2">
112
+ <div className="w-10 h-10 rounded-lg bg-indigo-50 flex items-center justify-center group-hover:bg-indigo-100 transition-colors">
113
+ <ScanText className="w-5 h-5 text-indigo-600" />
114
+ </div>
115
+ <h3 className="font-semibold text-gray-900">Analyze Contract</h3>
116
+ </div>
117
+ <p className="text-sm text-gray-500">Scan a contract for 41 clause types, risk scoring, NER, and compliance.</p>
118
+ </Link>
119
+ <Link href="/dashboard-pages/compare" className="bg-white rounded-xl p-6 border border-gray-200 hover:border-indigo-200 hover:shadow-sm transition-all group">
120
+ <div className="flex items-center gap-3 mb-2">
121
+ <div className="w-10 h-10 rounded-lg bg-indigo-50 flex items-center justify-center group-hover:bg-indigo-100 transition-colors">
122
+ <FileCompare className="w-5 h-5 text-indigo-600" />
123
+ </div>
124
+ <h3 className="font-semibold text-gray-900">Compare Contracts</h3>
125
+ </div>
126
+ <p className="text-sm text-gray-500">Side-by-side diff with alignment scoring and risk delta analysis.</p>
127
+ </Link>
128
+ </div>
129
+
130
  {/* Recent Scans */}
131
  <div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
132
  <div className="px-6 py-4 border-b border-gray-100">
 
140
  <p className="text-sm font-medium text-gray-900 truncate">
141
  {a.source_url || "Manual scan"}
142
  </p>
143
+ <div className="flex items-center gap-3 mt-1">
144
+ <p className="text-xs text-gray-500">
145
+ {new Date(a.created_at).toLocaleDateString()} Β· {a.total_clauses} clauses Β· {a.flagged_count} flagged
146
+ </p>
147
+ {a.entities && a.entities.length > 0 && (
148
+ <span className="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">{a.entities.length} entities</span>
149
+ )}
150
+ {a.contradictions && a.contradictions.length > 0 && (
151
+ <span className="text-[10px] bg-amber-50 text-amber-600 px-1.5 py-0.5 rounded">{a.contradictions.length} issues</span>
152
+ )}
153
+ </div>
154
  </div>
155
  <div className="flex items-center gap-3">
156
  <span className={`text-sm font-bold px-3 py-1 rounded-full ${
 
178
  <div className="mt-8 bg-indigo-50 border border-indigo-200 rounded-xl p-6 flex items-center justify-between">
179
  <div>
180
  <p className="font-semibold text-indigo-900">Upgrade to Pro</p>
181
+ <p className="text-sm text-indigo-700 mt-1">Unlimited scans, contract comparison, PDF exports, and team features.</p>
182
  </div>
183
  <Link
184
  href="/#pricing"
web/app/page.tsx CHANGED
@@ -2,45 +2,54 @@ import Link from "next/link";
2
  import {
3
  ShieldCheck, ShieldAlert, Scale, Gavel, ScrollText, Handshake,
4
  ScanText, FileCheck, TriangleAlert, ArrowRight, Zap, Eye, Download,
5
- ChevronRight, Sparkles, Lock, Globe, Ban, FileX, Stamp
 
 
6
  } from "lucide-react";
7
 
8
  const CLAUSES = [
9
- { icon: Scale, name: "Arbitration", desc: "Waives your right to sue in court", severity: "high" },
10
- { icon: ShieldAlert, name: "Liability limits", desc: "Company avoids responsibility for damages", severity: "high" },
11
- { icon: Ban, name: "Unilateral termination", desc: "They can close your account without reason", severity: "high" },
12
- { icon: FileX, name: "Unilateral change", desc: "Terms can change without your consent", severity: "medium" },
13
- { icon: Eye, name: "Content removal", desc: "Your content deleted without notice", severity: "medium" },
14
  { icon: Globe, name: "Jurisdiction", desc: "Disputes handled in their preferred court", severity: "medium" },
15
  { icon: Gavel, name: "Choice of law", desc: "Foreign law overrides your local protections", severity: "medium" },
16
- { icon: Stamp, name: "Contract by using", desc: "You agree just by visiting the site", severity: "low" },
 
 
 
 
 
 
17
  ];
18
 
19
  const STEPS = [
20
- { icon: Download, title: "Install", desc: "Add the Chrome extension. Two clicks, no signup required." },
21
- { icon: ScanText, title: "Browse normally", desc: "Visit any terms page. ClauseGuard scans it in the background." },
22
- { icon: TriangleAlert, title: "See the flags", desc: "Unfair clauses get highlighted. Open the sidebar for the full breakdown." },
23
  ];
24
 
25
  const PRICING = [
26
  {
27
  name: "Free", price: "β‚Ή0", period: "", highlight: false, cta: "Get started",
28
- features: ["10 scans per month", "All 8 clause types", "Risk score and grade", "Chrome extension", "Local fallback"],
29
  },
30
  {
31
  name: "Pro", price: "β‚Ή999", period: "/mo", highlight: true, cta: "Start free trial",
32
- features: ["Unlimited scans", "Upload contracts and leases", "AI clause explanations", "Scan history", "PDF report export", "Email scan reports", "Priority support"],
33
  },
34
  {
35
  name: "Team", price: "β‚Ή3,999", period: "/mo", highlight: false, cta: "Talk to us",
36
- features: ["Everything in Pro", "5 team seats", "10,000 API calls", "Shared dashboard", "Slack support", "Custom clause rules"],
37
  },
38
  ];
39
 
40
  const sevColor: Record<string, string> = {
41
- high: "text-red-500 bg-red-50",
42
- medium: "text-amber-500 bg-amber-50",
43
- low: "text-blue-500 bg-blue-50",
 
44
  };
45
 
46
  export default function Home() {
@@ -51,27 +60,26 @@ export default function Home() {
51
  <div className="max-w-2xl">
52
  <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-zinc-200 text-[13px] text-zinc-500 mb-6">
53
  <Sparkles className="w-3.5 h-3.5 text-zinc-400" />
54
- Trained on 9,414 legal clauses
55
  </div>
56
  <h1 className="text-[42px] sm:text-5xl font-semibold tracking-tight leading-[1.1]">
57
  Know what you are<br />agreeing to
58
  </h1>
59
  <p className="mt-5 text-[17px] text-zinc-500 leading-relaxed max-w-lg">
60
- ClauseGuard scans Terms of Service, contracts, and leases for unfair clauses.
61
- Get a clear risk score before you click accept.
62
  </p>
63
  <div className="mt-8 flex flex-wrap gap-3">
64
  <Link href="/dashboard-pages/analyze" 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 transition-colors">
65
- <Download className="w-4 h-4" />
66
- Add to Chrome
67
- </Link>
68
- <Link href="/dashboard-pages/analyze"
69
- className="inline-flex items-center gap-2 border border-zinc-200 px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">
70
  Try the scanner
 
 
 
71
  <ArrowRight className="w-4 h-4" />
72
  </Link>
73
  </div>
74
- <p className="mt-4 text-xs text-zinc-400">No account needed for free tier</p>
75
  </div>
76
  </section>
77
 
@@ -82,15 +90,15 @@ export default function Home() {
82
  <ShieldCheck className="w-4 h-4 text-zinc-400" />
83
  <p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">Detection</p>
84
  </div>
85
- <h2 className="text-2xl font-semibold tracking-tight">Eight types of unfair clauses</h2>
86
  <p className="mt-2 text-zinc-500 text-[15px] max-w-lg">
87
- Based on the CLAUDETTE taxonomy β€” the same framework used by EU consumer protection researchers.
88
  </p>
89
 
90
  <div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
91
  {CLAUSES.map((c) => (
92
  <div key={c.name} className="group border border-zinc-100 rounded-xl p-4 hover:border-zinc-200 hover:shadow-sm transition-all cursor-default">
93
- <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${sevColor[c.severity]}`}>
94
  <c.icon className="w-4 h-4" />
95
  </div>
96
  <p className="mt-3 text-sm font-medium">{c.name}</p>
@@ -108,7 +116,7 @@ export default function Home() {
108
  <Zap className="w-4 h-4 text-zinc-400" />
109
  <p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">How it works</p>
110
  </div>
111
- <h2 className="text-2xl font-semibold tracking-tight">Three steps, under two seconds</h2>
112
 
113
  <div className="mt-10 grid sm:grid-cols-3 gap-8">
114
  {STEPS.map((s, i) => (
@@ -127,6 +135,33 @@ export default function Home() {
127
  </div>
128
  </section>
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  {/* Pricing */}
131
  <section id="pricing" className="border-t border-zinc-100">
132
  <div className="max-w-6xl mx-auto px-5 py-20">
@@ -137,9 +172,7 @@ export default function Home() {
137
  {PRICING.map((plan) => (
138
  <div key={plan.name}
139
  className={`rounded-xl p-6 transition-shadow ${
140
- plan.highlight
141
- ? "border-2 border-zinc-900 shadow-sm"
142
- : "border border-zinc-200"
143
  }`}>
144
  <p className="text-[13px] font-medium text-zinc-400">{plan.name}</p>
145
  <p className="mt-2 flex items-baseline gap-1">
@@ -156,9 +189,7 @@ export default function Home() {
156
  </ul>
157
  <Link href={plan.name === "Free" ? "/auth/signup" : plan.name === "Team" ? "mailto:hello@clauseguardweb.netlify.app" : "/auth/signup"}
158
  className={`mt-6 block w-full py-2.5 rounded-lg text-[13px] font-medium text-center transition-colors ${
159
- plan.highlight
160
- ? "bg-zinc-900 text-white hover:bg-zinc-800"
161
- : "border border-zinc-200 text-zinc-700 hover:bg-zinc-50"
162
  }`}>
163
  {plan.cta}
164
  </Link>
@@ -176,11 +207,15 @@ export default function Home() {
176
  <p className="mt-2 text-[15px] text-zinc-500 max-w-md mx-auto">
177
  Join thousands protecting themselves before clicking accept.
178
  </p>
179
- <div className="mt-6">
180
  <Link href="/auth/signup" className="inline-flex items-center gap-2 bg-zinc-900 text-white px-6 py-3 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
181
- <Download className="w-4 h-4" />
182
  Get started free
183
  </Link>
 
 
 
 
184
  </div>
185
  </div>
186
  </section>
 
2
  import {
3
  ShieldCheck, ShieldAlert, Scale, Gavel, ScrollText, Handshake,
4
  ScanText, FileCheck, TriangleAlert, ArrowRight, Zap, Eye, Download,
5
+ ChevronRight, Sparkles, Lock, Globe, Ban, FileX, Stamp, Layers,
6
+ Tag, AlertTriangle, ClipboardList, Landmark, Building, DollarSign,
7
+ MapPin, Hash, BookOpen, CheckCircle
8
  } from "lucide-react";
9
 
10
  const CLAUSES = [
11
+ { icon: Scale, name: "Arbitration", desc: "Waives your right to sue in court", severity: "critical" },
12
+ { icon: ShieldAlert, name: "Liability limits", desc: "Company avoids responsibility for damages", severity: "critical" },
13
+ { icon: Ban, name: "Unilateral termination", desc: "They can close your account without reason", severity: "critical" },
14
+ { icon: FileX, name: "Unilateral change", desc: "Terms can change without your consent", severity: "high" },
15
+ { icon: Eye, name: "Content removal", desc: "Your content deleted without notice", severity: "high" },
16
  { icon: Globe, name: "Jurisdiction", desc: "Disputes handled in their preferred court", severity: "medium" },
17
  { icon: Gavel, name: "Choice of law", desc: "Foreign law overrides your local protections", severity: "medium" },
18
+ { icon: Lock, name: "IP Ownership", desc: "Intellectual property transferred entirely", severity: "critical" },
19
+ { icon: Layers, name: "41 CUAD Categories", desc: "Full taxonomy: NDA, MSA, SLA, and more", severity: "low" },
20
+ { icon: Tag, name: "Legal NER", desc: "Extract parties, dates, money, jurisdictions", severity: "low" },
21
+ { icon: AlertTriangle, name: "Contradictions", desc: "Detect conflicting clauses automatically", severity: "high" },
22
+ { icon: ClipboardList, name: "Obligations", desc: "Track monetary, compliance, reporting tasks", severity: "medium" },
23
+ { icon: Landmark, name: "Compliance", desc: "GDPR, CCPA, SOX, HIPAA, FINRA checks", severity: "high" },
24
+ { icon: BookOpen, name: "Compare Contracts", desc: "Side-by-side diff with alignment scoring", severity: "low" },
25
  ];
26
 
27
  const STEPS = [
28
+ { icon: Download, title: "Upload or paste", desc: "Drop a PDF, DOCX, or paste contract text directly." },
29
+ { icon: ScanText, title: "AI scans 41 categories", desc: "Legal-BERT + CUAD detects clauses, risks, entities." },
30
+ { icon: TriangleAlert, title: "Get actionable insights", desc: "Risk score, contradictions, obligations, compliance gaps." },
31
  ];
32
 
33
  const PRICING = [
34
  {
35
  name: "Free", price: "β‚Ή0", period: "", highlight: false, cta: "Get started",
36
+ features: ["10 scans per month", "41 clause categories", "Risk scoring", "Legal NER", "Contradiction detection", "Compliance checks"],
37
  },
38
  {
39
  name: "Pro", price: "β‚Ή999", period: "/mo", highlight: true, cta: "Start free trial",
40
+ features: ["Unlimited scans", "Upload PDF/DOCX files", "Contract comparison", "AI clause explanations", "Scan history", "PDF report export", "Obligation tracker", "Priority support"],
41
  },
42
  {
43
  name: "Team", price: "β‚Ή3,999", period: "/mo", highlight: false, cta: "Talk to us",
44
+ features: ["Everything in Pro", "5 team seats", "10,000 API calls", "Shared dashboard", "Slack support", "Custom clause rules", "Enterprise compliance"],
45
  },
46
  ];
47
 
48
  const sevColor: Record<string, string> = {
49
+ critical: "text-red-500 bg-red-50 border-red-200",
50
+ high: "text-amber-500 bg-amber-50 border-amber-200",
51
+ medium: "text-blue-500 bg-blue-50 border-blue-200",
52
+ low: "text-emerald-500 bg-emerald-50 border-emerald-200",
53
  };
54
 
55
  export default function Home() {
 
60
  <div className="max-w-2xl">
61
  <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-zinc-200 text-[13px] text-zinc-500 mb-6">
62
  <Sparkles className="w-3.5 h-3.5 text-zinc-400" />
63
+ Trained on 13,000+ legal clauses across 41 categories
64
  </div>
65
  <h1 className="text-[42px] sm:text-5xl font-semibold tracking-tight leading-[1.1]">
66
  Know what you are<br />agreeing to
67
  </h1>
68
  <p className="mt-5 text-[17px] text-zinc-500 leading-relaxed max-w-lg">
69
+ ClauseGuard scans contracts, terms of service, and leases using AI trained on legal data.
70
+ Get clause detection, risk scoring, entity extraction, contradiction alerts, and compliance checks.
71
  </p>
72
  <div className="mt-8 flex flex-wrap gap-3">
73
  <Link href="/dashboard-pages/analyze" 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 transition-colors">
74
+ <ScanText className="w-4 h-4" />
 
 
 
 
75
  Try the scanner
76
+ </Link>
77
+ <Link href="/dashboard-pages/compare" className="inline-flex items-center gap-2 border border-zinc-200 px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">
78
+ Compare contracts
79
  <ArrowRight className="w-4 h-4" />
80
  </Link>
81
  </div>
82
+ <p className="mt-4 text-xs text-zinc-400">No account needed for free tier Β· 10 scans/month</p>
83
  </div>
84
  </section>
85
 
 
90
  <ShieldCheck className="w-4 h-4 text-zinc-400" />
91
  <p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">Detection</p>
92
  </div>
93
+ <h2 className="text-2xl font-semibold tracking-tight">14 powerful analysis features</h2>
94
  <p className="mt-2 text-zinc-500 text-[15px] max-w-lg">
95
+ Based on the CUAD taxonomy + CLAUDETTE framework β€” the same datasets used by EU consumer protection researchers and Stanford NLP.
96
  </p>
97
 
98
  <div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
99
  {CLAUSES.map((c) => (
100
  <div key={c.name} className="group border border-zinc-100 rounded-xl p-4 hover:border-zinc-200 hover:shadow-sm transition-all cursor-default">
101
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center border ${sevColor[c.severity]}`}>
102
  <c.icon className="w-4 h-4" />
103
  </div>
104
  <p className="mt-3 text-sm font-medium">{c.name}</p>
 
116
  <Zap className="w-4 h-4 text-zinc-400" />
117
  <p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">How it works</p>
118
  </div>
119
+ <h2 className="text-2xl font-semibold tracking-tight">Three steps, under 30 seconds</h2>
120
 
121
  <div className="mt-10 grid sm:grid-cols-3 gap-8">
122
  {STEPS.map((s, i) => (
 
135
  </div>
136
  </section>
137
 
138
+ {/* Models */}
139
+ <section className="border-t border-zinc-100">
140
+ <div className="max-w-6xl mx-auto px-5 py-20">
141
+ <div className="flex items-center gap-2 mb-2">
142
+ <CheckCircle className="w-4 h-4 text-zinc-400" />
143
+ <p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">Technology</p>
144
+ </div>
145
+ <h2 className="text-2xl font-semibold tracking-tight">Built on production-grade models</h2>
146
+ <div className="mt-8 grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
147
+ {[
148
+ { name: "Legal-BERT + CUAD", desc: "41 clause categories fine-tuned on 510 contracts, 13K annotations", source: "Mokshith31/legalbert-contract-clause-classification" },
149
+ { name: "Legal NER Engine", desc: "Regex + pattern-based extraction for parties, dates, money, jurisdictions, defined terms", source: "Custom" },
150
+ { name: "NLI Detection", desc: "Heuristic contradiction detection: liability caps, governing law conflicts, IP ownership", source: "Custom" },
151
+ { name: "Compliance Engine", desc: "GDPR, CCPA, SOX, HIPAA, FINRA keyword matching with severity scoring", source: "Custom" },
152
+ { name: "Obligation Tracker", desc: "Extracts monetary, compliance, reporting, delivery, and termination obligations", source: "Custom" },
153
+ { name: "Comparison Engine", desc: "SequenceMatcher-based clause alignment with risk delta analysis", source: "Custom" },
154
+ ].map((m) => (
155
+ <div key={m.name} className="border border-zinc-100 rounded-xl p-4 hover:border-zinc-200 transition-all">
156
+ <p className="text-sm font-medium text-zinc-900">{m.name}</p>
157
+ <p className="text-[13px] text-zinc-500 mt-1 leading-relaxed">{m.desc}</p>
158
+ <p className="text-[11px] text-zinc-400 mt-2">{m.source}</p>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ </section>
164
+
165
  {/* Pricing */}
166
  <section id="pricing" className="border-t border-zinc-100">
167
  <div className="max-w-6xl mx-auto px-5 py-20">
 
172
  {PRICING.map((plan) => (
173
  <div key={plan.name}
174
  className={`rounded-xl p-6 transition-shadow ${
175
+ plan.highlight ? "border-2 border-zinc-900 shadow-sm" : "border border-zinc-200"
 
 
176
  }`}>
177
  <p className="text-[13px] font-medium text-zinc-400">{plan.name}</p>
178
  <p className="mt-2 flex items-baseline gap-1">
 
189
  </ul>
190
  <Link href={plan.name === "Free" ? "/auth/signup" : plan.name === "Team" ? "mailto:hello@clauseguardweb.netlify.app" : "/auth/signup"}
191
  className={`mt-6 block w-full py-2.5 rounded-lg text-[13px] font-medium text-center transition-colors ${
192
+ plan.highlight ? "bg-zinc-900 text-white hover:bg-zinc-800" : "border border-zinc-200 text-zinc-700 hover:bg-zinc-50"
 
 
193
  }`}>
194
  {plan.cta}
195
  </Link>
 
207
  <p className="mt-2 text-[15px] text-zinc-500 max-w-md mx-auto">
208
  Join thousands protecting themselves before clicking accept.
209
  </p>
210
+ <div className="mt-6 flex gap-3 justify-center">
211
  <Link href="/auth/signup" className="inline-flex items-center gap-2 bg-zinc-900 text-white px-6 py-3 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
212
+ <ScanText className="w-4 h-4" />
213
  Get started free
214
  </Link>
215
+ <Link href="/dashboard-pages/compare" className="inline-flex items-center gap-2 border border-zinc-200 px-6 py-3 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">
216
+ <ArrowRight className="w-4 h-4" />
217
+ Compare contracts
218
+ </Link>
219
  </div>
220
  </div>
221
  </section>
web/components/nav.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import Link from "next/link";
4
  import { usePathname } from "next/navigation";
5
- import { ShieldCheck, Menu, X, Crown } from "lucide-react";
6
  import { useState, useEffect } from "react";
7
  import { createClient } from "@/lib/supabase/client";
8
 
@@ -10,6 +10,7 @@ const links = [
10
  { href: "/#features", label: "Features" },
11
  { href: "/#pricing", label: "Pricing" },
12
  { href: "/dashboard-pages/analyze", label: "Scanner" },
 
13
  ];
14
 
15
  const ADMIN_EMAILS = ["ankygaur9972@gmail.com"];
@@ -34,6 +35,7 @@ export function Nav() {
34
  <Link href="/" className="flex items-center gap-2">
35
  <ShieldCheck className="w-5 h-5 text-zinc-900" strokeWidth={2.2} />
36
  <span className="font-semibold text-[15px] tracking-tight text-zinc-900">ClauseGuard</span>
 
37
  </Link>
38
 
39
  <div className="hidden md:flex items-center gap-1">
 
2
 
3
  import Link from "next/link";
4
  import { usePathname } from "next/navigation";
5
+ import { ShieldCheck, Menu, X, Crown, FileCompare } from "lucide-react";
6
  import { useState, useEffect } from "react";
7
  import { createClient } from "@/lib/supabase/client";
8
 
 
10
  { href: "/#features", label: "Features" },
11
  { href: "/#pricing", label: "Pricing" },
12
  { href: "/dashboard-pages/analyze", label: "Scanner" },
13
+ { href: "/dashboard-pages/compare", label: "Compare", icon: FileCompare },
14
  ];
15
 
16
  const ADMIN_EMAILS = ["ankygaur9972@gmail.com"];
 
35
  <Link href="/" className="flex items-center gap-2">
36
  <ShieldCheck className="w-5 h-5 text-zinc-900" strokeWidth={2.2} />
37
  <span className="font-semibold text-[15px] tracking-tight text-zinc-900">ClauseGuard</span>
38
+ <span className="hidden sm:inline text-[10px] font-medium text-zinc-400 ml-1 border border-zinc-200 px-1.5 py-0.5 rounded">v2.0</span>
39
  </Link>
40
 
41
  <div className="hidden md:flex items-center gap-1">
web/lib/supabase/schema.sql CHANGED
@@ -1,4 +1,4 @@
1
- -- ClauseGuard β€” Full Database Schema
2
  -- Tables ordered by dependency (no forward references)
3
 
4
  -- ─── 1. Teams (no dependencies) ───
@@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS public.team_invites (
42
  UNIQUE(team_id, email)
43
  );
44
 
45
- -- ─── 4. Analyses (depends on profiles, teams) ───
46
  CREATE TABLE IF NOT EXISTS public.analyses (
47
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
48
  user_id UUID REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
@@ -54,6 +54,13 @@ CREATE TABLE IF NOT EXISTS public.analyses (
54
  risk_score INT NOT NULL CHECK (risk_score >= 0 AND risk_score <= 100),
55
  grade CHAR(1) NOT NULL CHECK (grade IN ('A', 'B', 'C', 'D', 'F')),
56
  clauses JSONB NOT NULL DEFAULT '[]',
 
 
 
 
 
 
 
57
  created_at TIMESTAMPTZ DEFAULT NOW()
58
  );
59
 
@@ -178,8 +185,8 @@ CREATE TRIGGER on_auth_user_created
178
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
179
 
180
  -- ─── Set admin ───
181
- -- Run this AFTER your first signup with ankygaur9972@gmail.com:
182
- -- UPDATE public.profiles SET role = 'admin' WHERE email = 'ankygaur9972@gmail.com';
183
 
184
  -- ─── Monthly reset function ───
185
  CREATE OR REPLACE FUNCTION public.reset_monthly_usage()
 
1
+ -- ClauseGuard β€” Full Database Schema v2.0
2
  -- Tables ordered by dependency (no forward references)
3
 
4
  -- ─── 1. Teams (no dependencies) ───
 
42
  UNIQUE(team_id, email)
43
  );
44
 
45
+ -- ─── 4. Analyses (depends on profiles, teams) β€” v2.0 with full analysis data ───
46
  CREATE TABLE IF NOT EXISTS public.analyses (
47
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
48
  user_id UUID REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
 
54
  risk_score INT NOT NULL CHECK (risk_score >= 0 AND risk_score <= 100),
55
  grade CHAR(1) NOT NULL CHECK (grade IN ('A', 'B', 'C', 'D', 'F')),
56
  clauses JSONB NOT NULL DEFAULT '[]',
57
+ entities JSONB DEFAULT '[]',
58
+ contradictions JSONB DEFAULT '[]',
59
+ obligations JSONB DEFAULT '[]',
60
+ compliance JSONB DEFAULT '{}',
61
+ raw_text TEXT,
62
+ model TEXT DEFAULT 'regex',
63
+ latency_ms INT DEFAULT 0,
64
  created_at TIMESTAMPTZ DEFAULT NOW()
65
  );
66
 
 
185
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
186
 
187
  -- ─── Set admin ───
188
+ -- Run this AFTER your first signup with your email:
189
+ -- UPDATE public.profiles SET role = 'admin' WHERE email = 'your-email@example.com';
190
 
191
  -- ─── Monthly reset function ───
192
  CREATE OR REPLACE FUNCTION public.reset_monthly_usage()