gaurv007 commited on
Commit
94c4c90
·
verified ·
1 Parent(s): 05e9815

v2: Fix all versions + minimal UI redesign (no gradients, clean layout)

Browse files
README.md CHANGED
@@ -1,63 +1,11 @@
1
  ---
2
  title: ClauseGuard
3
  emoji: 🛡️
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: "5.23.0"
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  ---
12
-
13
- # 🛡️ ClauseGuard — AI Fine Print Scanner
14
-
15
- > Stop signing away your rights. ClauseGuard uses AI to scan Terms of Service, contracts, and legal documents for unfair clauses — instantly.
16
-
17
- ## What It Does
18
-
19
- ClauseGuard detects **8 types of unfair clauses** based on the CLAUDETTE academic taxonomy:
20
-
21
- | # | Category | Risk | What It Means |
22
- |---|----------|------|---------------|
23
- | 1 | ⚖️ Arbitration | 🔴 HIGH | Forces binding arbitration, waives right to sue |
24
- | 2 | 🛡️ Limitation of Liability | 🔴 HIGH | Excludes liability for losses/damages |
25
- | 3 | 🚫 Unilateral Termination | 🔴 HIGH | Can terminate your account anytime |
26
- | 4 | 🔄 Unilateral Change | 🟠 MEDIUM | Can modify terms without consent |
27
- | 5 | 🗑️ Content Removal | 🟠 MEDIUM | Can delete your content without notice |
28
- | 6 | 🌍 Jurisdiction | 🟠 MEDIUM | Disputes resolved in their preferred court |
29
- | 7 | 📜 Choice of Law | 🟠 MEDIUM | Governed by law of a different country |
30
- | 8 | 👆 Contract by Using | 🟡 LOW | Bound by using the service (dark pattern) |
31
-
32
- ## Tech Stack
33
-
34
- | Layer | Technology | Version |
35
- |-------|-----------|---------|
36
- | Extension | Chrome Manifest V3 | Latest |
37
- | Frontend | Next.js 15.3 + Tailwind CSS 4 | April 2026 |
38
- | Auth | Supabase SSR | 0.6.x |
39
- | Database | Supabase (PostgreSQL + RLS) | Latest |
40
- | Payments | Stripe Subscriptions | API 2025-03-31 |
41
- | ML (classify) | Legal-BERT → ONNX | Transformers 5.6.x |
42
- | ML (explain) | SaulLM-7B-Instruct | MIT License |
43
- | API | FastAPI + Uvicorn | 0.115.x |
44
-
45
- ## ML Model Details
46
-
47
- - **Base Model**: `nlpaueb/legal-bert-base-uncased` — BERT pre-trained on 12GB legal text
48
- - **Dataset**: `coastalcph/lex_glue` (unfair_tos) — 9,414 clauses, 8 categories
49
- - **Task**: Multi-label classification with BCEWithLogitsLoss
50
- - **Expected Results**: ~83% macro-F1, ~95% micro-F1
51
-
52
- ### Papers
53
- - [CLAUDETTE](https://arxiv.org/abs/1805.01217) — unfair clause taxonomy
54
- - [LexGLUE](https://arxiv.org/abs/2110.00976) — legal NLU benchmark
55
- - [SaulLM-7B](https://arxiv.org/abs/2403.03883) — legal domain LLM
56
-
57
- ## License
58
-
59
- MIT License. Not legal advice — always consult a qualified attorney.
60
-
61
- ## Built By
62
-
63
- [@gaurv007](https://huggingface.co/gaurv007)
 
1
  ---
2
  title: ClauseGuard
3
  emoji: 🛡️
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: "5.23.0"
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
- fastapi>=0.115.0
2
- uvicorn[standard]>=0.32.0
3
- pydantic>=2.10.0
4
- transformers>=4.47.0
5
  optimum[onnxruntime]>=1.24.0
6
  numpy>=2.0.0
7
  python-jose[cryptography]>=3.3.0
 
1
+ fastapi==0.136.0
2
+ uvicorn[standard]==0.46.0
3
+ pydantic==2.13.3
4
+ transformers==5.6.1
5
  optimum[onnxruntime]>=1.24.0
6
  numpy>=2.0.0
7
  python-jose[cryptography]>=3.3.0
app.py CHANGED
@@ -1,243 +1,159 @@
1
  """
2
- ClauseGuard — AI Fine Print Scanner (MVP Demo)
3
- Scans Terms of Service / contracts and highlights unfair clauses.
4
- Uses Legal-BERT fine-tuned on CLAUDETTE/LexGLUE unfair_tos dataset.
5
  """
6
 
7
  import gradio as gr
8
  import re
9
 
10
- # ─── 8 Unfair Clause Categories (CLAUDETTE taxonomy) ───
11
- LABEL_MAP = {
12
- 0: {"name": "Arbitration", "icon": "⚖️", "color": "#ef4444",
13
- "desc": "Forces disputes to arbitration instead of court — you waive your right to sue."},
14
- 1: {"name": "Unilateral Change", "icon": "🔄", "color": "#f97316",
15
- "desc": "Company can change terms at any time without your consent."},
16
- 2: {"name": "Content Removal", "icon": "🗑️", "color": "#eab308",
17
- "desc": "Company can delete your content without notice or justification."},
18
- 3: {"name": "Jurisdiction", "icon": "🌍", "color": "#a855f7",
19
- "desc": "Disputes must be resolved in a jurisdiction that may disadvantage you."},
20
- 4: {"name": "Choice of Law", "icon": "📜", "color": "#6366f1",
21
- "desc": "Governing law may differ from your country — reducing your legal protections."},
22
- 5: {"name": "Limitation of Liability", "icon": "🛡️", "color": "#ef4444",
23
- "desc": "Company limits or excludes liability for losses, data breaches, or service failures."},
24
- 6: {"name": "Unilateral Termination", "icon": "🚫", "color": "#dc2626",
25
- "desc": "Company can terminate your account at any time without reason."},
26
- 7: {"name": "Contract by Using", "icon": "👆", "color": "#f59e0b",
27
- "desc": "You're bound to the contract simply by using the service — a dark pattern."},
 
 
 
28
  }
29
 
30
- HIGH_RISK = {"Arbitration", "Limitation of Liability", "Unilateral Termination"}
31
- MEDIUM_RISK = {"Unilateral Change", "Content Removal", "Jurisdiction", "Choice of Law"}
32
- LOW_RISK = {"Contract by Using"}
33
-
34
- CLAUSE_PATTERNS = {
35
- 0: [r"arbitrat", r"binding arbitration", r"waive.*right.*court", r"class action waiver"],
36
- 1: [r"sole discretion", r"reserves? the right to (modify|change|update|amend)",
37
- r"at any time.*without (prior )?notice", r"we may (modify|change|update)"],
38
- 2: [r"remove.*content.*without", r"delete.*account.*content", r"right to remove",
39
- r"we may.*remove.*any (content|material)"],
40
- 3: [r"exclusive jurisdiction", r"courts? of.*(?:california|delaware|new york|ireland|england)",
41
- r"venue.*shall be", r"submit to.*jurisdiction"],
42
- 4: [r"governed by.*laws? of", r"choice of law", r"shall be governed",
43
- r"laws of the state of"],
44
- 5: [r"not liable", r"no liability", r"shall not be (liable|responsible)",
45
- r"in no event.*liable", r"limitation of liability", r"disclaim.*warrant",
46
- r"as[- ]is", r"without warranty"],
47
- 6: [r"terminat.*at any time", r"suspend.*account.*without", r"right to (terminat|suspend)",
48
- r"we may (terminat|suspend|discontinu)"],
49
- 7: [r"by (using|accessing).*you agree", r"continued use.*constitutes? acceptance",
50
- r"by using.*service.*bound", r"your use.*constitutes"],
51
- }
52
-
53
- def split_into_clauses(text):
54
  text = re.sub(r'\n{2,}', '\n', text.strip())
55
- clauses = re.split(r'(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[\.\)]|\([a-z]\)|\•|\-\s)', text)
56
- return [c.strip() for c in clauses if len(c.strip()) > 30]
57
-
58
- def analyze_clause(clause):
59
- findings = []
60
- clause_lower = clause.lower()
61
- for label_id, patterns in CLAUSE_PATTERNS.items():
62
- for pattern in patterns:
63
- if re.search(pattern, clause_lower):
64
- info = LABEL_MAP[label_id]
65
- name = info["name"]
66
- severity = "HIGH" if name in HIGH_RISK else "MEDIUM" if name in MEDIUM_RISK else "LOW"
67
- findings.append({"category": name, "icon": info["icon"], "color": info["color"],
68
- "description": info["desc"], "severity": severity})
69
- break
70
- return findings
71
-
72
- def generate_report(text):
73
- if not text or len(text.strip()) < 50:
74
- return "⚠️ Please paste a Terms of Service or contract text (at least a few sentences).", "", ""
75
- clauses = split_into_clauses(text)
76
- if not clauses:
77
- return "⚠️ Could not extract clauses. Try pasting a longer text.", "", ""
78
-
79
- total_clauses = len(clauses)
80
- flagged_clauses = []
81
- severity_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
82
-
83
- for clause in clauses:
84
- findings = analyze_clause(clause)
85
- if findings:
86
- flagged_clauses.append({"text": clause, "findings": findings})
87
- for f in findings:
88
- severity_counts[f["severity"]] += 1
89
-
90
- risk_score = min(100, int(
91
- (severity_counts["HIGH"] * 20 + severity_counts["MEDIUM"] * 10 + severity_counts["LOW"] * 5)
92
- / max(1, total_clauses) * 100
93
- ))
94
-
95
- if risk_score >= 60: grade, grade_emoji = "F — DANGEROUS", "🔴"
96
- elif risk_score >= 40: grade, grade_emoji = "D — RISKY", "🟠"
97
- elif risk_score >= 20: grade, grade_emoji = "C — CAUTION", "🟡"
98
- elif risk_score >= 10: grade, grade_emoji = "B — MOSTLY FAIR", "🟢"
99
- else: grade, grade_emoji = "A — FAIR", "✅"
100
-
101
- highlighted_text = text
102
- for item in flagged_clauses:
103
- sev = max(item["findings"], key=lambda f: {"HIGH":3,"MEDIUM":2,"LOW":1}[f["severity"]])["severity"]
104
- color_map = {"HIGH": "#fecaca", "MEDIUM": "#fed7aa", "LOW": "#fef3c7"}
105
- border_map = {"HIGH": "#ef4444", "MEDIUM": "#f97316", "LOW": "#eab308"}
106
- replacement = f'<span style="background:{color_map[sev]}; border-left:3px solid {border_map[sev]}; padding:2px 4px; border-radius:3px; display:inline;" title="{", ".join(f["category"] for f in item["findings"])}">{item["text"]}</span>'
107
- highlighted_text = highlighted_text.replace(item["text"], replacement, 1)
108
-
109
- summary_html = f"""
110
- <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
111
- <div style="background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%); border-radius:16px; padding:28px; color:white; margin-bottom:20px;">
112
- <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:16px;">
113
- <div>
114
- <h2 style="margin:0; font-size:24px;">🛡️ ClauseGuard Analysis</h2>
115
- <p style="margin:4px 0 0; opacity:0.8; font-size:14px;">{total_clauses} clauses scanned · {len(flagged_clauses)} flagged</p>
116
- </div>
117
- <div style="text-align:center;">
118
- <div style="font-size:48px; font-weight:800; line-height:1;">{risk_score}</div>
119
- <div style="font-size:12px; opacity:0.7;">RISK SCORE</div>
120
- </div>
121
- </div>
122
- <div style="margin-top:16px; padding:12px 16px; background:rgba(255,255,255,0.1); border-radius:10px; display:flex; align-items:center; gap:10px;">
123
- <span style="font-size:24px;">{grade_emoji}</span>
124
- <span style="font-size:18px; font-weight:600;">Grade: {grade}</span>
125
- </div>
126
- </div>
127
- <div style="display:grid; grid-template-columns: repeat(3, 1fr); gap:10px; margin-bottom:20px;">
128
- <div style="background:#fef2f2; border:1px solid #fecaca; border-radius:10px; padding:14px; text-align:center;">
129
- <div style="font-size:28px; font-weight:700; color:#dc2626;">{severity_counts['HIGH']}</div>
130
- <div style="font-size:12px; color:#991b1b;">🔴 HIGH RISK</div>
131
- </div>
132
- <div style="background:#fff7ed; border:1px solid #fed7aa; border-radius:10px; padding:14px; text-align:center;">
133
- <div style="font-size:28px; font-weight:700; color:#ea580c;">{severity_counts['MEDIUM']}</div>
134
- <div style="font-size:12px; color:#9a3412;">🟠 MEDIUM</div>
135
- </div>
136
- <div style="background:#fefce8; border:1px solid #fde68a; border-radius:10px; padding:14px; text-align:center;">
137
- <div style="font-size:28px; font-weight:700; color:#ca8a04;">{severity_counts['LOW']}</div>
138
- <div style="font-size:12px; color:#854d0e;">🟡 LOW</div>
139
- </div>
140
- </div>
141
- </div>"""
142
-
143
- findings_html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">'
144
- if not flagged_clauses:
145
- findings_html += '<div style="background:#f0fdf4; border:1px solid #86efac; border-radius:12px; padding:24px; text-align:center;"><div style="font-size:48px;">✅</div><h3 style="color:#166534; margin:8px 0;">No Unfair Clauses Detected</h3><p style="color:#15803d; font-size:14px;">This document appears to be fair. Always read carefully though!</p></div>'
146
- else:
147
- for i, item in enumerate(flagged_clauses, 1):
148
- sev = max(item["findings"], key=lambda f: {"HIGH":3,"MEDIUM":2,"LOW":1}[f["severity"]])["severity"]
149
- bg_map = {"HIGH": "#fef2f2", "MEDIUM": "#fff7ed", "LOW": "#fefce8"}
150
- border_map = {"HIGH": "#fca5a5", "MEDIUM": "#fdba74", "LOW": "#fde68a"}
151
- categories_html = ""
152
- for f in item["findings"]:
153
- sev_bg = '#fecaca' if f['severity']=='HIGH' else '#fed7aa' if f['severity']=='MEDIUM' else '#fef3c7'
154
- sev_color = '#991b1b' if f['severity']=='HIGH' else '#9a3412' if f['severity']=='MEDIUM' else '#854d0e'
155
- categories_html += f'<div style="background:white; border-radius:8px; padding:10px 14px; margin-top:8px; border-left:3px solid {f["color"]};"><div style="font-weight:600; font-size:13px;">{f["icon"]} {f["category"]} <span style="background:{sev_bg}; color:{sev_color}; padding:1px 8px; border-radius:999px; font-size:11px; font-weight:500;">{f["severity"]}</span></div><div style="font-size:12px; color:#6b7280; margin-top:4px;">{f["description"]}</div></div>'
156
- clause_preview = item["text"][:200] + ("..." if len(item["text"]) > 200 else "")
157
- findings_html += f'<div style="background:{bg_map[sev]}; border:1px solid {border_map[sev]}; border-radius:12px; padding:16px; margin-bottom:12px;"><div style="font-size:12px; color:#6b7280; font-weight:500;">CLAUSE #{i}</div><div style="font-size:14px; color:#1f2937; margin:8px 0; font-style:italic; line-height:1.5;">"{clause_preview}"</div>{categories_html}</div>'
158
- findings_html += "</div>"
159
- return summary_html, findings_html, highlighted_text
160
-
161
- EXAMPLE_SPOTIFY = """By using the Spotify Service, you agree to be bound by these Terms of Use. If you don't agree with these Terms, then please don't use the Service.
162
 
163
- Spotify may, in its sole discretion, modify or update these Terms of Service at any time without prior notice. Your continued use of the Service after any such changes constitutes your acceptance of the new Terms of Service.
 
 
164
 
165
- In no event will Spotify, its officers, shareholders, employees, agents, directors, subsidiaries, affiliates, successors, assigns, suppliers, or licensors be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly.
 
 
166
 
167
- Spotify reserves the right to remove or disable access to any User Content for any reason, including User Content that Spotify believes violates these Terms, without prior notice.
 
168
 
169
- Spotify may terminate your account or suspend your access to all or part of the Service at any time, with or without cause, with or without notice, effective immediately.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- These Terms will be governed by and construed in accordance with the laws of the State of New York, without giving effect to any principles of conflicts of law.
 
 
 
 
 
172
 
173
- Any dispute arising from or relating to the subject matter of these Terms shall be finally settled by arbitration in New York County, New York, using the English language in accordance with the Arbitration Rules and Procedures of JAMS then in effect.
 
174
 
175
- You and Spotify agree that any dispute resolution proceedings will be conducted only on an individual basis and not in a class, consolidated, or representative action."""
 
 
 
 
176
 
177
- EXAMPLE_AIRBNB = """By accessing or using the Airbnb Platform, you agree to comply with and be bound by these Terms of Service.
 
178
 
179
- Airbnb may, at its discretion, modify these Terms at any time. We will post the revised Terms on the Airbnb Platform. Your continued access to or use of the Airbnb Platform after such posting constitutes your consent to be bound by the modified Terms.
180
 
181
- Airbnb may, without prior notice, remove or take down any Content that, in Airbnb's sole judgment, violates these Terms or Airbnb's Content Policy, or is otherwise objectionable.
182
 
183
- To the maximum extent permitted by law, Airbnb shall not be liable for any incidental, special, exemplary or consequential damages. Airbnb's total liability arising out of or in connection with these terms shall not exceed the amounts paid by you to Airbnb during the twelve month period prior to the event.
184
 
185
- Airbnb may immediately, without notice, terminate your account if Airbnb believes, in its sole discretion, that you have breached these Terms or otherwise violated the rights of Airbnb.
186
 
187
- These Terms are governed by and construed in accordance with the laws of Ireland, without regard to conflict of law provisions. You and Airbnb agree to submit to the exclusive jurisdiction of the courts located in Ireland."""
188
 
189
- EXAMPLE_RENTAL = """This Lease Agreement is entered into as of the date signed below. By signing this agreement, the Tenant agrees to all terms and conditions set forth herein.
190
 
191
- The Landlord reserves the right to enter the premises at any time without prior notice for inspection, repair, or any other purpose deemed necessary by the Landlord in their sole discretion.
192
 
193
- The Tenant shall be responsible for all repairs and maintenance of the premises, including but not limited to plumbing, electrical, and structural issues, regardless of cause.
194
 
195
  The Landlord shall not be liable for any damage to the Tenant's personal property, whether caused by water leaks, fire, theft, or any other cause, including the Landlord's own negligence.
196
 
197
- The Landlord may terminate this lease at any time with only 7 days written notice, for any reason or no reason at all. The Tenant may not terminate this lease before the end of the lease term without paying a termination fee equal to 3 months' rent.
198
 
199
  Any disputes arising from this lease agreement shall be resolved exclusively in the courts of the Landlord's choosing, and the Tenant waives the right to a jury trial.
200
 
201
- The Landlord reserves the right to modify the terms of this lease at any time. Continued occupancy of the premises after such modification constitutes acceptance of the new terms.
202
-
203
- The security deposit may be retained by the Landlord for any reason, including but not limited to cleaning, repairs, and unpaid rent, at the Landlord's sole discretion."""
204
 
205
- demo = gr.Blocks(title="ClauseGuard — AI Fine Print Scanner")
206
 
207
  with demo:
208
- gr.HTML("""
209
- <div style="text-align:center; padding:20px 0;">
210
- <h1 style="font-size:2.5rem; margin:0;">🛡️ ClauseGuard</h1>
211
- <p style="color:#6b7280; font-size:1.1rem;">AI-Powered Fine Print Scanner — Stop Signing Away Your Rights</p>
212
- <p style="font-size:0.85rem; color:#9ca3af; margin-top:4px;">
213
- Analyzes 8 types of unfair clauses: Arbitration · Unilateral Change · Content Removal ·
214
- Jurisdiction · Choice of Law · Liability Limits · Unilateral Termination · Dark Patterns
215
- </p>
216
- </div>""")
217
-
218
  with gr.Row():
219
  with gr.Column(scale=1):
220
- text_input = gr.Textbox(label="📋 Paste Terms of Service / Contract / Lease Agreement",
221
- placeholder="Paste any legal document here...\n\nTry one of the examples below 👇", lines=18, max_lines=50)
222
  with gr.Row():
223
- scan_btn = gr.Button("🔍 Scan for Red Flags", variant="primary", size="lg")
224
- clear_btn = gr.Button("🗑️ Clear", variant="secondary", size="lg")
225
- gr.Examples(examples=[[EXAMPLE_SPOTIFY],[EXAMPLE_AIRBNB],[EXAMPLE_RENTAL]],
226
- inputs=[text_input], label="📌 Try These Examples", examples_per_page=3)
227
  with gr.Column(scale=1):
228
- summary_output = gr.HTML(label="Summary")
229
- findings_output = gr.HTML(label="Detailed Findings")
230
-
231
- with gr.Accordion("📖 Highlighted Document (red = flagged clauses)", open=False):
232
- highlighted_output = gr.HTML()
233
-
234
- scan_btn.click(fn=generate_report, inputs=[text_input], outputs=[summary_output, findings_output, highlighted_output])
235
- clear_btn.click(fn=lambda: ("", "", "", ""), outputs=[text_input, summary_output, findings_output, highlighted_output])
236
-
237
- gr.HTML("""<div style="text-align:center; padding:20px; color:#9ca3af; font-size:13px; border-top:1px solid #e5e7eb; margin-top:20px;">
238
- <p><strong>⚠️ Disclaimer:</strong> ClauseGuard provides informational analysis only. Not legal advice. Always consult a qualified attorney.</p>
239
- <p>Built with ❤️ using CLAUDETTE taxonomy · <a href="https://huggingface.co/datasets/coastalcph/lex_glue" target="_blank">LexGLUE Dataset</a></p>
240
- </div>""")
241
 
242
  if __name__ == "__main__":
243
  demo.launch()
 
1
  """
2
+ ClauseGuard — AI Fine Print Scanner
 
 
3
  """
4
 
5
  import gradio as gr
6
  import re
7
 
8
+ NUM_LABELS = 8
9
+ LABELS = [
10
+ ("Limitation of liability", "HIGH", "Company avoids responsibility for damages or losses."),
11
+ ("Unilateral termination", "HIGH", "They can close your account without reason."),
12
+ ("Unilateral change", "MEDIUM", "Terms can change without your consent."),
13
+ ("Content removal", "MEDIUM", "Your content can be deleted without notice."),
14
+ ("Contract by using", "LOW", "You agree just by visiting or using the site."),
15
+ ("Choice of law", "MEDIUM", "Foreign law applies instead of your local protections."),
16
+ ("Jurisdiction", "MEDIUM", "Disputes handled in their preferred court, not yours."),
17
+ ("Arbitration", "HIGH", "You waive your right to sue in court."),
18
+ ]
19
+
20
+ PATTERNS = {
21
+ 0: [r"not liable", r"shall not be (liable|responsible)", r"in no event.*liable", r"limitation of liability", r"without warranty", r"disclaim"],
22
+ 1: [r"terminat.*at any time", r"suspend.*account.*without", r"we may (terminat|suspend|discontinu)", r"right to (terminat|suspend)"],
23
+ 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)"],
24
+ 3: [r"remove.*content.*without", r"right to remove", r"we may.*remove"],
25
+ 4: [r"by (using|accessing).*you agree", r"continued use.*constitutes? acceptance"],
26
+ 5: [r"governed by.*laws? of", r"shall be governed", r"laws of the state of"],
27
+ 6: [r"exclusive jurisdiction", r"courts? of.*(california|delaware|new york|ireland|england)", r"submit to.*jurisdiction"],
28
+ 7: [r"arbitrat", r"binding arbitration", r"waive.*right.*court", r"class action waiver"],
29
  }
30
 
31
+ def split_clauses(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  text = re.sub(r'\n{2,}', '\n', text.strip())
33
+ parts = re.split(r'(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[.)]\s|\([a-z]\)\s)', text)
34
+ return [c.strip() for c in parts if len(c.strip()) > 30]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ def analyze(text):
37
+ if not text or len(text.strip()) < 50:
38
+ return "", ""
39
 
40
+ clauses = split_clauses(text)
41
+ if not clauses:
42
+ return "", ""
43
 
44
+ flagged = []
45
+ sev_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
46
 
47
+ for clause in clauses:
48
+ clause_lower = clause.lower()
49
+ hits = []
50
+ for lid, pats in PATTERNS.items():
51
+ for p in pats:
52
+ if re.search(p, clause_lower):
53
+ name, sev, desc = LABELS[lid]
54
+ hits.append({"name": name, "severity": sev, "desc": desc})
55
+ sev_counts[sev] += 1
56
+ break
57
+ if hits:
58
+ flagged.append({"text": clause, "hits": hits})
59
+
60
+ total = len(clauses)
61
+ risk = min(100, round((sev_counts["HIGH"] * 20 + sev_counts["MEDIUM"] * 10 + sev_counts["LOW"] * 5) / max(1, total) * 100))
62
+
63
+ if risk >= 60: grade = "F"
64
+ elif risk >= 40: grade = "D"
65
+ elif risk >= 20: grade = "C"
66
+ elif risk >= 10: grade = "B"
67
+ else: grade = "A"
68
+
69
+ # Summary
70
+ summary = f"""<div style="font-family:system-ui,sans-serif;">
71
+ <div style="border:1px solid #e4e4e7;border-radius:8px;padding:20px;margin-bottom:16px;">
72
+ <div style="display:flex;justify-content:space-between;align-items:baseline;">
73
+ <div>
74
+ <span style="font-size:32px;font-weight:600;">{risk}</span>
75
+ <span style="font-size:13px;color:#a1a1aa;">/100 risk</span>
76
+ </div>
77
+ <span style="font-size:13px;font-weight:500;padding:2px 10px;border-radius:4px;{
78
+ 'background:#fef2f2;color:#b91c1c;' if grade in ('F','D') else
79
+ 'background:#fffbeb;color:#a16207;' if grade == 'C' else
80
+ 'background:#f0fdf4;color:#15803d;'
81
+ }">Grade {grade}</span>
82
+ </div>
83
+ <p style="margin-top:8px;font-size:12px;color:#a1a1aa;">{total} clauses scanned · {len(flagged)} flagged · {sev_counts['HIGH']} high · {sev_counts['MEDIUM']} medium · {sev_counts['LOW']} low</p>
84
+ </div>"""
85
+
86
+ if not flagged:
87
+ summary += '<div style="border:1px solid #e4e4e7;border-radius:8px;padding:24px;text-align:center;"><p style="font-size:14px;color:#71717a;">No unfair clauses found.</p></div>'
88
+ else:
89
+ for i, item in enumerate(flagged):
90
+ max_sev = max(item["hits"], key=lambda h: {"HIGH":3,"MEDIUM":2,"LOW":1}[h["severity"]])["severity"]
91
+ border_color = {"HIGH":"#fca5a5","MEDIUM":"#fcd34d","LOW":"#93c5fd"}[max_sev]
92
 
93
+ tags = ""
94
+ for h in item["hits"]:
95
+ tag_style = {"HIGH":"background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;",
96
+ "MEDIUM":"background:#fffbeb;color:#a16207;border:1px solid #fde68a;",
97
+ "LOW":"background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"}[h["severity"]]
98
+ tags += f'<span style="{tag_style}font-size:11px;font-weight:500;padding:1px 8px;border-radius:3px;margin-right:4px;">{h["name"]}</span>'
99
 
100
+ descs = "".join(f'<p style="font-size:12px;color:#71717a;margin-top:4px;">{h["desc"]}</p>' for h in item["hits"])
101
+ preview = item["text"][:180] + ("..." if len(item["text"]) > 180 else "")
102
 
103
+ summary += f'''<div style="border:1px solid #e4e4e7;border-left:3px solid {border_color};border-radius:8px;padding:14px;margin-bottom:8px;">
104
+ <p style="font-size:13px;color:#3f3f46;line-height:1.6;">{preview}</p>
105
+ <div style="margin-top:8px;">{tags}</div>
106
+ {descs}
107
+ </div>'''
108
 
109
+ summary += "</div>"
110
+ return summary, ""
111
 
112
+ SPOTIFY = """By using the Spotify Service, you agree to be bound by these Terms of Use.
113
 
114
+ Spotify may, in its sole discretion, modify or update these Terms of Service at any time without prior notice. Your continued use of the Service after any such changes constitutes your acceptance of the new Terms of Service.
115
 
116
+ In no event will Spotify be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly.
117
 
118
+ Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
119
 
120
+ Spotify may terminate your account or suspend your access at any time, with or without cause, with or without notice, effective immediately.
121
 
122
+ These Terms will be governed by and construed in accordance with the laws of the State of New York.
123
 
124
+ Any dispute shall be finally settled by arbitration in New York County."""
125
 
126
+ RENTAL = """The Landlord reserves the right to enter the premises at any time without prior notice for inspection or any other purpose deemed necessary in their sole discretion.
127
 
128
  The Landlord shall not be liable for any damage to the Tenant's personal property, whether caused by water leaks, fire, theft, or any other cause, including the Landlord's own negligence.
129
 
130
+ The Landlord may terminate this lease at any time with only 7 days written notice, for any reason or no reason at all.
131
 
132
  Any disputes arising from this lease agreement shall be resolved exclusively in the courts of the Landlord's choosing, and the Tenant waives the right to a jury trial.
133
 
134
+ The Landlord reserves the right to modify the terms of this lease at any time. Continued occupancy constitutes acceptance of the new terms."""
 
 
135
 
136
+ demo = gr.Blocks(title="ClauseGuard")
137
 
138
  with demo:
139
+ gr.HTML('<div style="font-family:system-ui,sans-serif;padding:16px 0;"><h1 style="font-size:20px;font-weight:600;margin:0;">ClauseGuard</h1><p style="font-size:13px;color:#a1a1aa;margin-top:2px;">Paste a Terms of Service, contract, or lease. Get a risk breakdown.</p></div>')
140
+
 
 
 
 
 
 
 
 
141
  with gr.Row():
142
  with gr.Column(scale=1):
143
+ text_input = gr.Textbox(label="Document text", placeholder="Paste here...", lines=14, max_lines=40)
 
144
  with gr.Row():
145
+ scan_btn = gr.Button("Scan", variant="primary")
146
+ clear_btn = gr.Button("Clear", variant="secondary")
147
+ gr.Examples(examples=[[SPOTIFY], [RENTAL]], inputs=[text_input], label="Examples")
148
+
149
  with gr.Column(scale=1):
150
+ results_html = gr.HTML(label="Results")
151
+ hidden = gr.HTML(visible=False)
152
+
153
+ scan_btn.click(fn=analyze, inputs=[text_input], outputs=[results_html, hidden])
154
+ clear_btn.click(fn=lambda: ("", "", ""), outputs=[text_input, results_html, hidden])
155
+
156
+ gr.HTML('<p style="font-family:system-ui,sans-serif;font-size:11px;color:#a1a1aa;text-align:center;padding:16px 0;border-top:1px solid #f4f4f5;margin-top:16px;">Not legal advice. Based on CLAUDETTE taxonomy. <a href="https://huggingface.co/datasets/coastalcph/lex_glue" style="color:#71717a;">Dataset</a></p>')
 
 
 
 
 
 
157
 
158
  if __name__ == "__main__":
159
  demo.launch()
ml/requirements.txt CHANGED
@@ -1,7 +1,6 @@
1
- transformers>=4.47.0
2
  datasets>=3.2.0
3
  torch>=2.5.0
4
  scikit-learn>=1.6.0
5
  accelerate>=1.2.0
6
  optimum[onnxruntime]>=1.24.0
7
- evaluate>=0.4.0
 
1
+ transformers==5.6.1
2
  datasets>=3.2.0
3
  torch>=2.5.0
4
  scikit-learn>=1.6.0
5
  accelerate>=1.2.0
6
  optimum[onnxruntime]>=1.24.0
 
web/app/auth/login/page.tsx CHANGED
@@ -9,105 +9,61 @@ export default function LoginPage() {
9
  const [password, setPassword] = useState("");
10
  const [error, setError] = useState("");
11
  const [loading, setLoading] = useState(false);
12
-
13
  const supabase = createClient();
14
 
15
  async function handleLogin(e: React.FormEvent) {
16
- e.preventDefault();
17
- setLoading(true);
18
- setError("");
19
-
20
  const { error } = await supabase.auth.signInWithPassword({ email, password });
21
-
22
- if (error) {
23
- setError(error.message);
24
- setLoading(false);
25
- } else {
26
- window.location.href = "/dashboard-pages/dashboard";
27
- }
28
  }
29
 
30
  async function handleOAuth(provider: "google" | "github") {
31
  await supabase.auth.signInWithOAuth({
32
- provider,
33
- options: { redirectTo: `${window.location.origin}/auth/callback` },
34
  });
35
  }
36
 
37
  return (
38
- <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
39
- <div className="w-full max-w-md">
40
- <div className="text-center mb-8">
41
- <Link href="/" className="inline-flex items-center gap-2 text-2xl font-bold text-gray-900">
42
- <span>🛡️</span> ClauseGuard
43
- </Link>
44
- <p className="mt-2 text-gray-600">Welcome back</p>
45
  </div>
46
 
47
- <div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
48
- {/* OAuth */}
49
- <div className="space-y-3">
50
- <button
51
- onClick={() => handleOAuth("google")}
52
- className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-xl hover:bg-gray-50 transition font-medium text-sm"
53
- >
54
- <svg className="w-5 h-5" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
55
- Continue with Google
56
- </button>
57
- <button
58
- onClick={() => handleOAuth("github")}
59
- className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-xl hover:bg-gray-50 transition font-medium text-sm"
60
- >
61
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
62
- Continue with GitHub
63
- </button>
64
- </div>
65
 
66
- <div className="flex items-center gap-4 my-6">
67
- <div className="flex-1 h-px bg-gray-200" />
68
- <span className="text-sm text-gray-400">or</span>
69
- <div className="flex-1 h-px bg-gray-200" />
70
- </div>
71
 
72
- {/* Email Login */}
73
- <form onSubmit={handleLogin} className="space-y-4">
74
- <div>
75
- <label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
76
- <input
77
- type="email"
78
- value={email}
79
- onChange={(e) => setEmail(e.target.value)}
80
- required
81
- className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
82
- placeholder="you@example.com"
83
- />
84
- </div>
85
- <div>
86
- <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
87
- <input
88
- type="password"
89
- value={password}
90
- onChange={(e) => setPassword(e.target.value)}
91
- required
92
- className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
93
- placeholder="••••••••"
94
- />
95
- </div>
96
- {error && <p className="text-red-600 text-sm">{error}</p>}
97
- <button
98
- type="submit"
99
- disabled={loading}
100
- className="w-full bg-indigo-600 text-white py-3 rounded-xl font-semibold hover:bg-indigo-700 transition disabled:opacity-50 text-sm"
101
- >
102
- {loading ? "Signing in..." : "Sign In"}
103
- </button>
104
- </form>
105
 
106
- <p className="mt-6 text-center text-sm text-gray-500">
107
- Don&apos;t have an account?{" "}
108
- <Link href="/auth/signup" className="text-indigo-600 font-medium hover:underline">Sign up</Link>
109
- </p>
110
- </div>
111
  </div>
112
  </div>
113
  );
 
9
  const [password, setPassword] = useState("");
10
  const [error, setError] = useState("");
11
  const [loading, setLoading] = useState(false);
 
12
  const supabase = createClient();
13
 
14
  async function handleLogin(e: React.FormEvent) {
15
+ e.preventDefault(); setLoading(true); setError("");
 
 
 
16
  const { error } = await supabase.auth.signInWithPassword({ email, password });
17
+ if (error) { setError(error.message); setLoading(false); }
18
+ else { window.location.href = "/dashboard-pages/dashboard"; }
 
 
 
 
 
19
  }
20
 
21
  async function handleOAuth(provider: "google" | "github") {
22
  await supabase.auth.signInWithOAuth({
23
+ provider, options: { redirectTo: `${window.location.origin}/auth/callback` },
 
24
  });
25
  }
26
 
27
  return (
28
+ <div className="min-h-screen flex items-center justify-center bg-white px-4">
29
+ <div className="w-full max-w-sm">
30
+ <div className="mb-8">
31
+ <Link href="/" className="text-sm text-zinc-400 hover:text-zinc-600">← Back</Link>
32
+ <h1 className="mt-4 text-xl font-semibold">Sign in</h1>
 
 
33
  </div>
34
 
35
+ <div className="space-y-2.5">
36
+ <button onClick={() => handleOAuth("google")}
37
+ className="w-full flex items-center justify-center gap-2 px-4 py-2.5 border border-zinc-200 rounded-md text-sm hover:bg-zinc-50">
38
+ Continue with Google
39
+ </button>
40
+ <button onClick={() => handleOAuth("github")}
41
+ className="w-full flex items-center justify-center gap-2 px-4 py-2.5 border border-zinc-200 rounded-md text-sm hover:bg-zinc-50">
42
+ Continue with GitHub
43
+ </button>
44
+ </div>
 
 
 
 
 
 
 
 
45
 
46
+ <div className="flex items-center gap-3 my-6">
47
+ <div className="flex-1 h-px bg-zinc-100" />
48
+ <span className="text-xs text-zinc-300">or</span>
49
+ <div className="flex-1 h-px bg-zinc-100" />
50
+ </div>
51
 
52
+ <form onSubmit={handleLogin} className="space-y-3">
53
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
54
+ placeholder="Email" className="w-full px-3 py-2.5 border border-zinc-200 rounded-md text-sm focus:outline-none focus:border-zinc-400" />
55
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
56
+ placeholder="Password" className="w-full px-3 py-2.5 border border-zinc-200 rounded-md text-sm focus:outline-none focus:border-zinc-400" />
57
+ {error && <p className="text-xs text-red-600">{error}</p>}
58
+ <button type="submit" disabled={loading}
59
+ className="w-full bg-zinc-900 text-white py-2.5 rounded-md text-sm font-medium hover:bg-zinc-800 disabled:opacity-40">
60
+ {loading ? "Signing in..." : "Sign in"}
61
+ </button>
62
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ <p className="mt-6 text-center text-xs text-zinc-400">
65
+ No account? <Link href="/auth/signup" className="text-zinc-600 hover:underline">Sign up</Link>
66
+ </p>
 
 
67
  </div>
68
  </div>
69
  );
web/app/auth/signup/page.tsx CHANGED
@@ -7,135 +7,70 @@ import Link from "next/link";
7
  export default function SignupPage() {
8
  const [email, setEmail] = useState("");
9
  const [password, setPassword] = useState("");
10
- const [name, setName] = useState("");
11
  const [error, setError] = useState("");
12
  const [loading, setLoading] = useState(false);
13
- const [success, setSuccess] = useState(false);
14
-
15
  const supabase = createClient();
16
 
17
  async function handleSignup(e: React.FormEvent) {
18
- e.preventDefault();
19
- setLoading(true);
20
- setError("");
21
-
22
- const { error } = await supabase.auth.signUp({
23
- email,
24
- password,
25
- options: { data: { full_name: name } },
26
- });
27
-
28
- if (error) {
29
- setError(error.message);
30
- } else {
31
- setSuccess(true);
32
- }
33
  setLoading(false);
34
  }
35
 
36
  async function handleOAuth(provider: "google" | "github") {
37
  await supabase.auth.signInWithOAuth({
38
- provider,
39
- options: { redirectTo: `${window.location.origin}/auth/callback` },
40
  });
41
  }
42
 
43
- if (success) {
44
  return (
45
- <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
46
- <div className="text-center max-w-md">
47
- <div className="text-5xl mb-4">📧</div>
48
- <h2 className="text-2xl font-bold text-gray-900">Check your email</h2>
49
- <p className="mt-2 text-gray-600">We sent a confirmation link to <strong>{email}</strong>.</p>
50
- <Link href="/auth/login" className="mt-6 inline-block text-indigo-600 font-medium hover:underline">
51
- Back to login
52
- </Link>
53
  </div>
54
  </div>
55
  );
56
  }
57
 
58
  return (
59
- <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
60
- <div className="w-full max-w-md">
61
- <div className="text-center mb-8">
62
- <Link href="/" className="inline-flex items-center gap-2 text-2xl font-bold text-gray-900">
63
- <span>🛡️</span> ClauseGuard
64
- </Link>
65
- <p className="mt-2 text-gray-600">Create your free account</p>
66
  </div>
67
 
68
- <div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
69
- <div className="space-y-3">
70
- <button
71
- onClick={() => handleOAuth("google")}
72
- className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-xl hover:bg-gray-50 transition font-medium text-sm"
73
- >
74
- Continue with Google
75
- </button>
76
- <button
77
- onClick={() => handleOAuth("github")}
78
- className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-xl hover:bg-gray-50 transition font-medium text-sm"
79
- >
80
- Continue with GitHub
81
- </button>
82
- </div>
83
 
84
- <div className="flex items-center gap-4 my-6">
85
- <div className="flex-1 h-px bg-gray-200" />
86
- <span className="text-sm text-gray-400">or</span>
87
- <div className="flex-1 h-px bg-gray-200" />
88
- </div>
89
 
90
- <form onSubmit={handleSignup} className="space-y-4">
91
- <div>
92
- <label className="block text-sm font-medium text-gray-700 mb-1">Full Name</label>
93
- <input
94
- type="text"
95
- value={name}
96
- onChange={(e) => setName(e.target.value)}
97
- className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
98
- placeholder="John Doe"
99
- />
100
- </div>
101
- <div>
102
- <label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
103
- <input
104
- type="email"
105
- value={email}
106
- onChange={(e) => setEmail(e.target.value)}
107
- required
108
- className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
109
- placeholder="you@example.com"
110
- />
111
- </div>
112
- <div>
113
- <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
114
- <input
115
- type="password"
116
- value={password}
117
- onChange={(e) => setPassword(e.target.value)}
118
- required
119
- minLength={8}
120
- className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
121
- placeholder="••••••••"
122
- />
123
- </div>
124
- {error && <p className="text-red-600 text-sm">{error}</p>}
125
- <button
126
- type="submit"
127
- disabled={loading}
128
- className="w-full bg-indigo-600 text-white py-3 rounded-xl font-semibold hover:bg-indigo-700 transition disabled:opacity-50 text-sm"
129
- >
130
- {loading ? "Creating account..." : "Create Free Account"}
131
- </button>
132
- </form>
133
 
134
- <p className="mt-6 text-center text-sm text-gray-500">
135
- Already have an account?{" "}
136
- <Link href="/auth/login" className="text-indigo-600 font-medium hover:underline">Sign in</Link>
137
- </p>
138
- </div>
139
  </div>
140
  </div>
141
  );
 
7
  export default function SignupPage() {
8
  const [email, setEmail] = useState("");
9
  const [password, setPassword] = useState("");
 
10
  const [error, setError] = useState("");
11
  const [loading, setLoading] = useState(false);
12
+ const [done, setDone] = useState(false);
 
13
  const supabase = createClient();
14
 
15
  async function handleSignup(e: React.FormEvent) {
16
+ e.preventDefault(); setLoading(true); setError("");
17
+ const { error } = await supabase.auth.signUp({ email, password });
18
+ if (error) { setError(error.message); } else { setDone(true); }
 
 
 
 
 
 
 
 
 
 
 
 
19
  setLoading(false);
20
  }
21
 
22
  async function handleOAuth(provider: "google" | "github") {
23
  await supabase.auth.signInWithOAuth({
24
+ provider, options: { redirectTo: `${window.location.origin}/auth/callback` },
 
25
  });
26
  }
27
 
28
+ if (done) {
29
  return (
30
+ <div className="min-h-screen flex items-center justify-center bg-white px-4">
31
+ <div className="text-center max-w-xs">
32
+ <h2 className="text-xl font-semibold">Check your email</h2>
33
+ <p className="mt-2 text-sm text-zinc-500">We sent a confirmation link to {email}.</p>
34
+ <Link href="/auth/login" className="mt-4 inline-block text-sm text-zinc-600 hover:underline">Back to login</Link>
 
 
 
35
  </div>
36
  </div>
37
  );
38
  }
39
 
40
  return (
41
+ <div className="min-h-screen flex items-center justify-center bg-white px-4">
42
+ <div className="w-full max-w-sm">
43
+ <div className="mb-8">
44
+ <Link href="/" className="text-sm text-zinc-400 hover:text-zinc-600">← Back</Link>
45
+ <h1 className="mt-4 text-xl font-semibold">Create an account</h1>
 
 
46
  </div>
47
 
48
+ <div className="space-y-2.5">
49
+ <button onClick={() => handleOAuth("google")}
50
+ className="w-full px-4 py-2.5 border border-zinc-200 rounded-md text-sm hover:bg-zinc-50">Continue with Google</button>
51
+ <button onClick={() => handleOAuth("github")}
52
+ className="w-full px-4 py-2.5 border border-zinc-200 rounded-md text-sm hover:bg-zinc-50">Continue with GitHub</button>
53
+ </div>
 
 
 
 
 
 
 
 
 
54
 
55
+ <div className="flex items-center gap-3 my-6">
56
+ <div className="flex-1 h-px bg-zinc-100" /><span className="text-xs text-zinc-300">or</span><div className="flex-1 h-px bg-zinc-100" />
57
+ </div>
 
 
58
 
59
+ <form onSubmit={handleSignup} className="space-y-3">
60
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
61
+ placeholder="Email" className="w-full px-3 py-2.5 border border-zinc-200 rounded-md text-sm focus:outline-none focus:border-zinc-400" />
62
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8}
63
+ placeholder="Password (8+ characters)" className="w-full px-3 py-2.5 border border-zinc-200 rounded-md text-sm focus:outline-none focus:border-zinc-400" />
64
+ {error && <p className="text-xs text-red-600">{error}</p>}
65
+ <button type="submit" disabled={loading}
66
+ className="w-full bg-zinc-900 text-white py-2.5 rounded-md text-sm font-medium hover:bg-zinc-800 disabled:opacity-40">
67
+ {loading ? "Creating..." : "Create account"}
68
+ </button>
69
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ <p className="mt-6 text-center text-xs text-zinc-400">
72
+ Have an account? <Link href="/auth/login" className="text-zinc-600 hover:underline">Sign in</Link>
73
+ </p>
 
 
74
  </div>
75
  </div>
76
  );
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -2,42 +2,26 @@
2
 
3
  import { useState } from "react";
4
 
5
- interface ClauseCategory {
6
- id: number;
7
- name: string;
8
- severity: string;
9
- description: string;
10
- confidence: number;
11
- }
12
-
13
- interface ClauseResult {
14
- text: string;
15
- categories: ClauseCategory[];
16
- }
17
-
18
  interface AnalysisResult {
19
- risk_score: number;
20
- grade: string;
21
- total_clauses: number;
22
- flagged_count: number;
23
- results: ClauseResult[];
24
- model: string;
25
- latency_ms: number;
26
  }
27
 
28
- const EXAMPLE_TEXT = `By using the Spotify Service, you agree to be bound by these Terms of Use. If you don't agree with these Terms, then please don't use the Service.
29
 
30
  Spotify may, in its sole discretion, modify or update these Terms of Service at any time without prior notice. Your continued use of the Service after any such changes constitutes your acceptance of the new Terms of Service.
31
 
32
- In no event will Spotify, its officers, shareholders, employees, agents, directors, subsidiaries, affiliates, successors, assigns, suppliers, or licensors be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly.
33
 
34
- Spotify reserves the right to remove or disable access to any User Content for any reason, including User Content that Spotify believes violates these Terms, without prior notice.
35
 
36
- Spotify may terminate your account or suspend your access to all or part of the Service at any time, with or without cause, with or without notice, effective immediately.
37
 
38
  These Terms will be governed by and construed in accordance with the laws of the State of New York.
39
 
40
- Any dispute arising from or relating to the subject matter of these Terms shall be finally settled by arbitration in New York County.`;
41
 
42
  export default function AnalyzePage() {
43
  const [text, setText] = useState("");
@@ -46,138 +30,100 @@ export default function AnalyzePage() {
46
  const [error, setError] = useState("");
47
 
48
  async function handleAnalyze() {
49
- if (!text || text.trim().length < 50) {
50
- setError("Please enter at least 50 characters of text.");
51
- return;
52
- }
53
- setLoading(true);
54
- setError("");
55
- setResults(null);
56
-
57
  try {
58
  const res = await fetch("/api/analyze", {
59
- method: "POST",
60
- headers: { "Content-Type": "application/json" },
61
  body: JSON.stringify({ text }),
62
  });
63
-
64
- if (!res.ok) {
65
- const err = await res.json();
66
- throw new Error(err.error || "Analysis failed");
67
- }
68
-
69
  setResults(await res.json());
70
- } catch (err: any) {
71
- setError(err.message || "Something went wrong.");
72
- } finally {
73
- setLoading(false);
74
- }
75
  }
76
 
77
- const gradeColors: Record<string, string> = {
78
- A: "bg-green-100 text-green-800",
79
- B: "bg-green-50 text-green-700",
80
- C: "bg-yellow-100 text-yellow-800",
81
- D: "bg-orange-100 text-orange-800",
82
- F: "bg-red-100 text-red-800",
83
- };
84
-
85
- const sevColors: Record<string, string> = {
86
- HIGH: "bg-red-100 text-red-800 border-red-200",
87
- MEDIUM: "bg-orange-100 text-orange-800 border-orange-200",
88
- LOW: "bg-blue-100 text-blue-800 border-blue-200",
89
  };
90
 
91
  return (
92
- <div className="min-h-screen bg-gray-50">
93
- <div className="max-w-6xl mx-auto px-6 py-12">
94
- <div className="text-center mb-10">
95
- <h1 className="text-3xl font-bold text-gray-900">🛡️ ClauseGuard Web Scanner</h1>
96
- <p className="mt-2 text-gray-600">Paste any Terms of Service, contract, or lease agreement below.</p>
97
  </div>
98
 
99
  <div className="grid lg:grid-cols-2 gap-8">
100
- {/* Input */}
101
  <div>
102
  <textarea
103
- value={text}
104
- onChange={(e) => setText(e.target.value)}
105
- placeholder="Paste your Terms of Service or contract text here..."
106
- className="w-full h-96 p-4 border border-gray-300 rounded-xl text-sm font-mono resize-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
107
  />
108
- <div className="mt-4 flex gap-3">
109
- <button
110
- onClick={handleAnalyze}
111
- disabled={loading}
112
- className="flex-1 bg-indigo-600 text-white py-3 rounded-xl font-semibold hover:bg-indigo-700 transition disabled:opacity-50"
113
- >
114
- {loading ? "⏳ Scanning..." : "🔍 Scan for Red Flags"}
115
  </button>
116
- <button
117
- onClick={() => setText(EXAMPLE_TEXT)}
118
- className="px-4 bg-gray-200 text-gray-700 rounded-xl font-medium hover:bg-gray-300 transition text-sm"
119
- >
120
- Try Example
121
  </button>
122
  </div>
123
- {error && <p className="mt-3 text-red-600 text-sm">{error}</p>}
124
  </div>
125
 
126
- {/* Results */}
127
  <div>
128
  {results ? (
129
- <div className="space-y-4">
130
- {/* Summary Card */}
131
- <div className="bg-gradient-to-br from-indigo-950 to-indigo-800 rounded-2xl p-6 text-white">
132
- <div className="flex justify-between items-center">
133
  <div>
134
- <h2 className="text-lg font-bold">Analysis Complete</h2>
135
- <p className="text-indigo-200 text-sm">
136
- {results.total_clauses} clauses · {results.flagged_count} flagged · {results.latency_ms}ms
137
- </p>
138
- </div>
139
- <div className="text-center">
140
- <div className="text-4xl font-extrabold">{results.risk_score}</div>
141
- <div className="text-xs text-indigo-300">RISK SCORE</div>
142
  </div>
 
 
 
 
 
 
 
143
  </div>
144
- <div className={`mt-4 inline-flex px-4 py-1.5 rounded-full font-semibold text-sm ${gradeColors[results.grade] || gradeColors.C}`}>
145
- Grade: {results.grade}
146
- </div>
147
  </div>
148
 
149
- {/* Flagged Clauses */}
150
- <div className="space-y-3 max-h-[500px] overflow-y-auto">
151
- {results.results
152
- .filter(r => r.categories.length > 0)
153
- .map((clause, i) => (
154
- <div key={i} className="bg-white border border-gray-200 rounded-xl p-4">
155
- <p className="text-xs text-gray-400 font-semibold">CLAUSE #{i + 1}</p>
156
- <p className="text-sm text-gray-700 mt-1 italic line-clamp-3">&quot;{clause.text}&quot;</p>
157
- <div className="flex flex-wrap gap-2 mt-3">
158
- {clause.categories.map((cat, j) => (
159
- <span key={j} className={`text-xs font-semibold px-2.5 py-1 rounded-full border ${sevColors[cat.severity] || sevColors.MEDIUM}`}>
160
- {cat.name}
161
- </span>
162
- ))}
163
- </div>
164
  </div>
165
- ))}
 
166
  {results.flagged_count === 0 && (
167
- <div className="bg-green-50 border border-green-200 rounded-xl p-8 text-center">
168
- <div className="text-4xl mb-2"></div>
169
- <p className="text-green-800 font-semibold">No Unfair Clauses Detected</p>
170
- <p className="text-green-600 text-sm mt-1">This document appears to be fair.</p>
171
  </div>
172
  )}
173
  </div>
174
  </div>
175
  ) : (
176
- <div className="flex items-center justify-center h-96 bg-white rounded-xl border border-gray-200">
177
- <div className="text-center text-gray-400">
178
- <div className="text-5xl mb-4">🔍</div>
179
- <p>Paste text and click &quot;Scan&quot; to see results</p>
180
- </div>
181
  </div>
182
  )}
183
  </div>
 
2
 
3
  import { useState } from "react";
4
 
5
+ interface ClauseCategory { name: string; severity: string; confidence: number; }
6
+ interface ClauseResult { text: string; categories: ClauseCategory[]; }
 
 
 
 
 
 
 
 
 
 
 
7
  interface AnalysisResult {
8
+ risk_score: number; grade: string; total_clauses: number;
9
+ flagged_count: number; results: ClauseResult[]; model: string; latency_ms: number;
 
 
 
 
 
10
  }
11
 
12
+ const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
13
 
14
  Spotify may, in its sole discretion, modify or update these Terms of Service at any time without prior notice. Your continued use of the Service after any such changes constitutes your acceptance of the new Terms of Service.
15
 
16
+ In no event will Spotify be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly.
17
 
18
+ Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
19
 
20
+ Spotify may terminate your account or suspend your access to all or part of the Service at any time, with or without cause, effective immediately.
21
 
22
  These Terms will be governed by and construed in accordance with the laws of the State of New York.
23
 
24
+ Any dispute shall be finally settled by arbitration in New York County.`;
25
 
26
  export default function AnalyzePage() {
27
  const [text, setText] = useState("");
 
30
  const [error, setError] = useState("");
31
 
32
  async function handleAnalyze() {
33
+ if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
34
+ setLoading(true); setError(""); setResults(null);
 
 
 
 
 
 
35
  try {
36
  const res = await fetch("/api/analyze", {
37
+ method: "POST", headers: { "Content-Type": "application/json" },
 
38
  body: JSON.stringify({ text }),
39
  });
40
+ if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed"); }
 
 
 
 
 
41
  setResults(await res.json());
42
+ } catch (err: any) { setError(err.message); }
43
+ finally { setLoading(false); }
 
 
 
44
  }
45
 
46
+ const sevStyle: Record<string, string> = {
47
+ HIGH: "text-red-700 bg-red-50 border-red-200",
48
+ MEDIUM: "text-amber-700 bg-amber-50 border-amber-200",
49
+ LOW: "text-blue-700 bg-blue-50 border-blue-200",
 
 
 
 
 
 
 
 
50
  };
51
 
52
  return (
53
+ <div className="min-h-screen bg-white">
54
+ <div className="max-w-5xl mx-auto px-6 py-12">
55
+ <div className="mb-8">
56
+ <h1 className="text-2xl font-semibold tracking-tight">Scan a document</h1>
57
+ <p className="mt-1 text-sm text-zinc-500">Paste any Terms of Service, contract, or lease below.</p>
58
  </div>
59
 
60
  <div className="grid lg:grid-cols-2 gap-8">
 
61
  <div>
62
  <textarea
63
+ value={text} onChange={(e) => setText(e.target.value)}
64
+ placeholder="Paste your text here..."
65
+ className="w-full h-80 p-4 border border-zinc-200 rounded-lg text-sm font-mono leading-relaxed resize-none focus:outline-none focus:border-zinc-400 placeholder:text-zinc-300"
 
66
  />
67
+ <div className="mt-3 flex gap-2">
68
+ <button onClick={handleAnalyze} disabled={loading}
69
+ className="flex-1 bg-zinc-900 text-white py-2.5 rounded-md text-sm font-medium hover:bg-zinc-800 disabled:opacity-40">
70
+ {loading ? "Scanning..." : "Scan"}
 
 
 
71
  </button>
72
+ <button onClick={() => setText(EXAMPLE)}
73
+ className="px-4 border border-zinc-200 rounded-md text-sm text-zinc-500 hover:bg-zinc-50">
74
+ Example
 
 
75
  </button>
76
  </div>
77
+ {error && <p className="mt-2 text-sm text-red-600">{error}</p>}
78
  </div>
79
 
 
80
  <div>
81
  {results ? (
82
+ <div>
83
+ {/* Score */}
84
+ <div className="border border-zinc-200 rounded-lg p-5 mb-4">
85
+ <div className="flex items-baseline justify-between">
86
  <div>
87
+ <span className="text-3xl font-semibold">{results.risk_score}</span>
88
+ <span className="text-sm text-zinc-400 ml-1">/100 risk</span>
 
 
 
 
 
 
89
  </div>
90
+ <span className={`text-sm font-medium px-2 py-0.5 rounded ${
91
+ results.grade === "F" || results.grade === "D" ? "bg-red-50 text-red-700" :
92
+ results.grade === "C" ? "bg-amber-50 text-amber-700" :
93
+ "bg-emerald-50 text-emerald-700"
94
+ }`}>
95
+ Grade {results.grade}
96
+ </span>
97
  </div>
98
+ <p className="mt-2 text-xs text-zinc-400">
99
+ {results.total_clauses} clauses scanned · {results.flagged_count} flagged · {results.latency_ms}ms
100
+ </p>
101
  </div>
102
 
103
+ {/* Clauses */}
104
+ <div className="space-y-2 max-h-96 overflow-y-auto">
105
+ {results.results.filter(r => r.categories.length > 0).map((clause, i) => (
106
+ <div key={i} className="border border-zinc-200 rounded-lg p-4">
107
+ <p className="text-sm text-zinc-700 leading-relaxed line-clamp-2">{clause.text}</p>
108
+ <div className="flex flex-wrap gap-1.5 mt-2">
109
+ {clause.categories.map((cat, j) => (
110
+ <span key={j} className={`text-xs font-medium px-2 py-0.5 rounded border ${sevStyle[cat.severity] || sevStyle.MEDIUM}`}>
111
+ {cat.name}
112
+ </span>
113
+ ))}
 
 
 
 
114
  </div>
115
+ </div>
116
+ ))}
117
  {results.flagged_count === 0 && (
118
+ <div className="border border-zinc-200 rounded-lg p-8 text-center">
119
+ <p className="text-sm text-zinc-500">No unfair clauses found. Looks fair.</p>
 
 
120
  </div>
121
  )}
122
  </div>
123
  </div>
124
  ) : (
125
+ <div className="border border-dashed border-zinc-200 rounded-lg h-80 flex items-center justify-center">
126
+ <p className="text-sm text-zinc-300">Results will appear here</p>
 
 
 
127
  </div>
128
  )}
129
  </div>
web/app/page.tsx CHANGED
@@ -1,190 +1,104 @@
1
- /**
2
- * ClauseGuard — Landing Page
3
- * Hero + Features + How It Works + Pricing + CTA
4
- */
5
-
6
  import Link from "next/link";
7
 
8
- const FEATURES = [
9
- {
10
- icon: "⚖️",
11
- title: "8 Unfair Clause Types",
12
- desc: "Detects arbitration traps, liability waivers, unilateral termination, jurisdiction tricks, and more.",
13
- },
14
- {
15
- icon: "",
16
- title: "Instant Analysis",
17
- desc: "Scans any Terms of Service or contract in under 2 seconds. Works on any website.",
18
- },
19
- {
20
- icon: "🛡️",
21
- title: "Risk Score & Grade",
22
- desc: "Get a clear A–F grade and 0–100 risk score. Know exactly how fair a document is.",
23
- },
24
- {
25
- icon: "🔍",
26
- title: "Clause-by-Clause Breakdown",
27
- desc: "Every flagged clause is highlighted with severity, category, and plain-English explanation.",
28
- },
29
- {
30
- icon: "🌐",
31
- title: "Chrome Extension",
32
- desc: "Scans pages as you browse. Red highlights appear right on the ToS page you're reading.",
33
- },
34
- {
35
- icon: "📜",
36
- title: "Legal Citations",
37
- desc: "Each finding references specific EU/US consumer protection laws — not just gut feelings.",
38
- },
39
- ];
40
-
41
- const STEPS = [
42
- { num: "1", title: "Install the Extension", desc: "Add ClauseGuard to Chrome in one click. Free." },
43
- { num: "2", title: "Visit Any ToS Page", desc: "Navigate to any Terms of Service, contract, or lease agreement." },
44
- { num: "3", title: "See Red Flags Instantly", desc: "Unfair clauses are highlighted in red, orange, and yellow — with explanations." },
45
  ];
46
 
47
  const PRICING = [
48
  {
49
- name: "Free",
50
- price: "$0",
51
- period: "forever",
52
- features: ["10 scans/month", "8 clause categories", "Risk score & grade", "Chrome extension"],
53
- cta: "Get Started Free",
54
- highlight: false,
55
  },
56
  {
57
- name: "Pro",
58
- price: "$12",
59
- period: "/month",
60
- features: [
61
- "Unlimited scans",
62
- "Contract upload & PDF analysis",
63
- '"Explain this clause" AI feature',
64
- "Scan history & dashboard",
65
- "PDF report export",
66
- "1,000 API calls/month",
67
- "Email support",
68
- ],
69
- cta: "Start Pro Trial",
70
- highlight: true,
71
  },
72
  {
73
- name: "Team",
74
- price: "$49",
75
- period: "/month",
76
- features: [
77
- "Everything in Pro",
78
- "5 team seats",
79
- "10,000 API calls/month",
80
- "Team dashboard & analytics",
81
- "Slack + email support",
82
- "Custom clause rules",
83
- ],
84
- cta: "Contact Sales",
85
- highlight: false,
86
  },
87
  ];
88
 
89
- export default function LandingPage() {
90
  return (
91
- <main className="min-h-screen bg-white">
92
  {/* Nav */}
93
- <nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-100">
94
- <div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
95
- <div className="flex items-center gap-2">
96
- <span className="text-2xl">🛡️</span>
97
- <span className="text-xl font-bold text-gray-900">ClauseGuard</span>
98
- </div>
99
- <div className="hidden md:flex items-center gap-8">
100
- <a href="#features" className="text-sm text-gray-600 hover:text-gray-900">Features</a>
101
- <a href="#how-it-works" className="text-sm text-gray-600 hover:text-gray-900">How It Works</a>
102
- <a href="#pricing" className="text-sm text-gray-600 hover:text-gray-900">Pricing</a>
103
- <Link href="/auth/login" className="text-sm text-gray-600 hover:text-gray-900">Log In</Link>
104
- <Link
105
- href="/auth/signup"
106
- className="bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 transition"
107
- >
108
- Get Started Free
109
- </Link>
110
  </div>
111
  </div>
112
  </nav>
113
 
114
  {/* Hero */}
115
- <section className="relative overflow-hidden">
116
- <div className="absolute inset-0 bg-gradient-to-br from-indigo-50 via-white to-red-50" />
117
- <div className="relative max-w-7xl mx-auto px-6 pt-20 pb-28 text-center">
118
- <div className="inline-flex items-center gap-2 bg-red-50 text-red-700 px-4 py-1.5 rounded-full text-sm font-medium mb-8 border border-red-200">
119
- <span>🔴</span> 73% of popular ToS contain unfair clauses
120
- </div>
121
- <h1 className="text-5xl md:text-7xl font-extrabold text-gray-900 tracking-tight leading-tight max-w-4xl mx-auto">
122
- Stop signing away
123
- <br />
124
- <span className="text-indigo-600">your rights</span>
125
  </h1>
126
- <p className="mt-6 text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed">
127
- ClauseGuard uses AI trained on 9,414 legal clauses to scan any Terms of Service,
128
- contract, or lease and highlights the unfair parts before you agree.
129
  </p>
130
- <div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
131
- <a
132
- href="#"
133
- className="bg-indigo-600 text-white px-8 py-4 rounded-xl text-lg font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-200"
134
- >
135
- Add to Chrome — Free
136
  </a>
137
- <Link
138
- href="/dashboard-pages/analyze"
139
- className="bg-white text-gray-900 px-8 py-4 rounded-xl text-lg font-semibold hover:bg-gray-50 transition border border-gray-200"
140
- >
141
- Try Web Scanner →
142
  </Link>
143
  </div>
144
- <p className="mt-4 text-sm text-gray-400">
145
- No account required · Free forever for 10 scans/month
146
- </p>
147
  </div>
148
  </section>
149
 
150
- {/* Features */}
151
- <section id="features" className="py-24 bg-gray-50">
152
- <div className="max-w-7xl mx-auto px-6">
153
- <div className="text-center mb-16">
154
- <h2 className="text-3xl md:text-4xl font-bold text-gray-900">
155
- What ClauseGuard Detects
156
- </h2>
157
- <p className="mt-4 text-lg text-gray-600 max-w-2xl mx-auto">
158
- Trained on the CLAUDETTE academic taxonomy — the same system used by EU consumer protection researchers.
159
- </p>
160
- </div>
161
- <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
162
- {FEATURES.map((f, i) => (
163
- <div key={i} className="bg-white rounded-2xl p-8 border border-gray-100 hover:shadow-lg transition">
164
- <div className="text-4xl mb-4">{f.icon}</div>
165
- <h3 className="text-lg font-bold text-gray-900">{f.title}</h3>
166
- <p className="mt-2 text-gray-600 text-sm leading-relaxed">{f.desc}</p>
167
  </div>
168
  ))}
169
  </div>
170
  </div>
171
  </section>
172
 
173
- {/* How It Works */}
174
- <section id="how-it-works" className="py-24">
175
- <div className="max-w-7xl mx-auto px-6">
176
- <div className="text-center mb-16">
177
- <h2 className="text-3xl md:text-4xl font-bold text-gray-900">How It Works</h2>
178
- <p className="mt-4 text-lg text-gray-600">Three steps. Under 2 seconds.</p>
179
- </div>
180
- <div className="grid md:grid-cols-3 gap-12">
181
- {STEPS.map((s, i) => (
182
- <div key={i} className="text-center">
183
- <div className="w-16 h-16 rounded-full bg-indigo-100 text-indigo-700 text-2xl font-bold flex items-center justify-center mx-auto mb-6">
184
- {s.num}
185
- </div>
186
- <h3 className="text-xl font-bold text-gray-900">{s.title}</h3>
187
- <p className="mt-3 text-gray-600">{s.desc}</p>
188
  </div>
189
  ))}
190
  </div>
@@ -192,46 +106,26 @@ export default function LandingPage() {
192
  </section>
193
 
194
  {/* Pricing */}
195
- <section id="pricing" className="py-24 bg-gray-50">
196
- <div className="max-w-7xl mx-auto px-6">
197
- <div className="text-center mb-16">
198
- <h2 className="text-3xl md:text-4xl font-bold text-gray-900">Simple Pricing</h2>
199
- <p className="mt-4 text-lg text-gray-600">Free forever. Upgrade when you need more.</p>
200
- </div>
201
- <div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
202
- {PRICING.map((plan, i) => (
203
- <div
204
- key={i}
205
- className={`rounded-2xl p-8 ${
206
- plan.highlight
207
- ? "bg-indigo-600 text-white ring-4 ring-indigo-200 scale-105"
208
- : "bg-white border border-gray-200"
209
- }`}
210
- >
211
- <h3 className={`text-lg font-bold ${plan.highlight ? "text-indigo-100" : "text-gray-500"}`}>
212
- {plan.name}
213
- </h3>
214
- <div className="mt-4 flex items-baseline gap-1">
215
- <span className="text-5xl font-extrabold">{plan.price}</span>
216
- <span className={`text-sm ${plan.highlight ? "text-indigo-200" : "text-gray-400"}`}>
217
- {plan.period}
218
- </span>
219
- </div>
220
- <ul className="mt-8 space-y-3">
221
- {plan.features.map((f, j) => (
222
- <li key={j} className="flex items-start gap-2 text-sm">
223
- <span className="mt-0.5">✓</span>
224
- <span>{f}</span>
225
  </li>
226
  ))}
227
  </ul>
228
- <button
229
- className={`mt-8 w-full py-3 rounded-xl font-semibold text-sm transition ${
230
- plan.highlight
231
- ? "bg-white text-indigo-700 hover:bg-indigo-50"
232
- : "bg-indigo-600 text-white hover:bg-indigo-700"
233
- }`}
234
- >
235
  {plan.cta}
236
  </button>
237
  </div>
@@ -240,41 +134,15 @@ export default function LandingPage() {
240
  </div>
241
  </section>
242
 
243
- {/* CTA */}
244
- <section className="py-24">
245
- <div className="max-w-4xl mx-auto px-6 text-center">
246
- <h2 className="text-3xl md:text-4xl font-bold text-gray-900">
247
- Read the fine print — without reading it.
248
- </h2>
249
- <p className="mt-4 text-lg text-gray-600">
250
- Join thousands of users who protect themselves before clicking "I Agree."
251
- </p>
252
- <div className="mt-10">
253
- <a
254
- href="#"
255
- className="bg-indigo-600 text-white px-10 py-4 rounded-xl text-lg font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-200 inline-block"
256
- >
257
- Add to Chrome — Free
258
- </a>
259
- </div>
260
- </div>
261
- </section>
262
-
263
  {/* Footer */}
264
- <footer className="border-t border-gray-200 py-12">
265
- <div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-6">
266
- <div className="flex items-center gap-2">
267
- <span className="text-xl">🛡️</span>
268
- <span className="font-bold text-gray-900">ClauseGuard</span>
269
- </div>
270
- <div className="flex gap-6 text-sm text-gray-500">
271
- <a href="/privacy" className="hover:text-gray-900">Privacy Policy</a>
272
- <a href="/terms" className="hover:text-gray-900">Terms of Service</a>
273
- <a href="mailto:hello@clauseguard.com" className="hover:text-gray-900">Contact</a>
274
  </div>
275
- <p className="text-sm text-gray-400">
276
- © {new Date().getFullYear()} ClauseGuard. Not legal advice.
277
- </p>
278
  </div>
279
  </footer>
280
  </main>
 
 
 
 
 
 
1
  import Link from "next/link";
2
 
3
+ const CLAUSE_TYPES = [
4
+ { name: "Arbitration", desc: "Waives your right to sue in court" },
5
+ { name: "Liability limits", desc: "Company avoids responsibility for damages" },
6
+ { name: "Unilateral termination", desc: "They can close your account without reason" },
7
+ { name: "Unilateral change", desc: "Terms can change without your consent" },
8
+ { name: "Content removal", desc: "Your content deleted without notice" },
9
+ { name: "Jurisdiction", desc: "Disputes handled in their preferred court" },
10
+ { name: "Choice of law", desc: "Foreign law overrides your local protections" },
11
+ { name: "Contract by using", desc: "You agree just by visiting the site" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ];
13
 
14
  const PRICING = [
15
  {
16
+ name: "Free", price: "$0", period: "", highlight: false,
17
+ features: ["10 scans per month", "All 8 clause types", "Risk score and grade", "Chrome extension"],
18
+ cta: "Get started",
 
 
 
19
  },
20
  {
21
+ name: "Pro", price: "$12", period: "/mo", highlight: true,
22
+ features: ["Unlimited scans", "Upload contracts and leases", "Plain-English explanations", "Scan history and dashboard", "PDF report export", "Email support"],
23
+ cta: "Start free trial",
 
 
 
 
 
 
 
 
 
 
 
24
  },
25
  {
26
+ name: "Team", price: "$49", period: "/mo", highlight: false,
27
+ features: ["Everything in Pro", "5 seats", "10,000 API calls", "Shared dashboard", "Slack support"],
28
+ cta: "Talk to us",
 
 
 
 
 
 
 
 
 
 
29
  },
30
  ];
31
 
32
+ export default function Home() {
33
  return (
34
+ <main className="min-h-screen bg-white text-zinc-900">
35
  {/* Nav */}
36
+ <nav className="border-b border-zinc-100">
37
+ <div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
38
+ <span className="font-semibold tracking-tight">ClauseGuard</span>
39
+ <div className="hidden sm:flex items-center gap-6 text-sm text-zinc-500">
40
+ <a href="#features" className="hover:text-zinc-900">Features</a>
41
+ <a href="#pricing" className="hover:text-zinc-900">Pricing</a>
42
+ <Link href="/auth/login" className="hover:text-zinc-900">Log in</Link>
43
+ <Link href="/auth/signup" className="bg-zinc-900 text-white px-3.5 py-1.5 rounded-md text-sm hover:bg-zinc-800">Get started</Link>
 
 
 
 
 
 
 
 
 
44
  </div>
45
  </div>
46
  </nav>
47
 
48
  {/* Hero */}
49
+ <section className="max-w-5xl mx-auto px-6 pt-24 pb-20">
50
+ <div className="max-w-2xl">
51
+ <p className="text-sm text-zinc-500 mb-4">Free Chrome extension</p>
52
+ <h1 className="text-4xl sm:text-5xl font-semibold tracking-tight leading-tight">
53
+ Know what you are agreeing to
 
 
 
 
 
54
  </h1>
55
+ <p className="mt-5 text-lg text-zinc-500 leading-relaxed">
56
+ ClauseGuard scans Terms of Service, contracts, and leases for unfair clauses.
57
+ You get a clear breakdown before you click accept.
58
  </p>
59
+ <div className="mt-8 flex flex-wrap gap-3">
60
+ <a href="#" className="bg-zinc-900 text-white px-5 py-2.5 rounded-md font-medium text-sm hover:bg-zinc-800">
61
+ Add to Chrome
 
 
 
62
  </a>
63
+ <Link href="/dashboard-pages/analyze" className="border border-zinc-200 px-5 py-2.5 rounded-md font-medium text-sm hover:border-zinc-300 hover:bg-zinc-50">
64
+ Try the web scanner
 
 
 
65
  </Link>
66
  </div>
 
 
 
67
  </div>
68
  </section>
69
 
70
+ {/* What it detects */}
71
+ <section id="features" className="border-t border-zinc-100">
72
+ <div className="max-w-5xl mx-auto px-6 py-20">
73
+ <h2 className="text-2xl font-semibold tracking-tight">Eight types of unfair clauses</h2>
74
+ <p className="mt-2 text-zinc-500 max-w-lg">
75
+ Based on the CLAUDETTE academic taxonomy used by EU consumer protection researchers.
76
+ </p>
77
+ <div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-4 gap-px bg-zinc-100 border border-zinc-100 rounded-lg overflow-hidden">
78
+ {CLAUSE_TYPES.map((c) => (
79
+ <div key={c.name} className="bg-white p-5">
80
+ <p className="font-medium text-sm">{c.name}</p>
81
+ <p className="mt-1 text-sm text-zinc-500">{c.desc}</p>
 
 
 
 
 
82
  </div>
83
  ))}
84
  </div>
85
  </div>
86
  </section>
87
 
88
+ {/* How it works */}
89
+ <section className="border-t border-zinc-100">
90
+ <div className="max-w-5xl mx-auto px-6 py-20">
91
+ <h2 className="text-2xl font-semibold tracking-tight">How it works</h2>
92
+ <div className="mt-10 grid sm:grid-cols-3 gap-10">
93
+ {[
94
+ { step: "1", title: "Install", desc: "Add the Chrome extension. Takes two seconds." },
95
+ { step: "2", title: "Browse", desc: "Visit any terms page, contract, or lease agreement." },
96
+ { step: "3", title: "Read the flags", desc: "Unfair clauses are highlighted with severity and explanation." },
97
+ ].map((s) => (
98
+ <div key={s.step}>
99
+ <span className="inline-flex items-center justify-center w-7 h-7 rounded-full bg-zinc-100 text-xs font-semibold text-zinc-600">{s.step}</span>
100
+ <h3 className="mt-3 font-medium">{s.title}</h3>
101
+ <p className="mt-1 text-sm text-zinc-500 leading-relaxed">{s.desc}</p>
 
102
  </div>
103
  ))}
104
  </div>
 
106
  </section>
107
 
108
  {/* Pricing */}
109
+ <section id="pricing" className="border-t border-zinc-100">
110
+ <div className="max-w-5xl mx-auto px-6 py-20">
111
+ <h2 className="text-2xl font-semibold tracking-tight">Pricing</h2>
112
+ <p className="mt-2 text-zinc-500">Free forever. Upgrade if you need more.</p>
113
+ <div className="mt-10 grid sm:grid-cols-3 gap-6">
114
+ {PRICING.map((plan) => (
115
+ <div key={plan.name} className={`rounded-lg p-6 ${plan.highlight ? "border-2 border-zinc-900" : "border border-zinc-200"}`}>
116
+ <p className="text-sm font-medium text-zinc-500">{plan.name}</p>
117
+ <p className="mt-2">
118
+ <span className="text-3xl font-semibold">{plan.price}</span>
119
+ <span className="text-sm text-zinc-400">{plan.period}</span>
120
+ </p>
121
+ <ul className="mt-5 space-y-2">
122
+ {plan.features.map((f) => (
123
+ <li key={f} className="text-sm text-zinc-600 flex items-start gap-2">
124
+ <span className="text-zinc-400 mt-0.5">—</span> {f}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  </li>
126
  ))}
127
  </ul>
128
+ <button className={`mt-6 w-full py-2 rounded-md text-sm font-medium ${plan.highlight ? "bg-zinc-900 text-white hover:bg-zinc-800" : "border border-zinc-200 hover:bg-zinc-50"}`}>
 
 
 
 
 
 
129
  {plan.cta}
130
  </button>
131
  </div>
 
134
  </div>
135
  </section>
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  {/* Footer */}
138
+ <footer className="border-t border-zinc-100">
139
+ <div className="max-w-5xl mx-auto px-6 py-8 flex flex-col sm:flex-row justify-between items-center gap-4">
140
+ <span className="text-sm text-zinc-400">ClauseGuard — not legal advice</span>
141
+ <div className="flex gap-5 text-sm text-zinc-400">
142
+ <a href="/privacy" className="hover:text-zinc-600">Privacy</a>
143
+ <a href="/terms" className="hover:text-zinc-600">Terms</a>
144
+ <a href="mailto:hello@clauseguard.com" className="hover:text-zinc-600">Contact</a>
 
 
 
145
  </div>
 
 
 
146
  </div>
147
  </footer>
148
  </main>
web/lib/stripe.ts CHANGED
@@ -1,7 +1,7 @@
1
  import Stripe from "stripe";
2
 
3
  export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
4
- apiVersion: "2025-03-31.basil",
5
  typescript: true,
6
  });
7
 
@@ -10,13 +10,13 @@ export const PLANS = {
10
  name: "Free",
11
  scans: 10,
12
  price_id: null,
13
- features: ["10 scans/month", "8 clause categories", "Risk score & grade"],
14
  },
15
  pro: {
16
  name: "Pro",
17
  scans: Infinity,
18
  price_id: process.env.STRIPE_PRO_PRICE_ID!,
19
- features: ["Unlimited scans", "Contract uploads", "AI explanations", "PDF exports"],
20
  },
21
  team: {
22
  name: "Team",
 
1
  import Stripe from "stripe";
2
 
3
  export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
4
+ apiVersion: "2026-03-25.dahlia",
5
  typescript: true,
6
  });
7
 
 
10
  name: "Free",
11
  scans: 10,
12
  price_id: null,
13
+ features: ["10 scans per month", "All 8 clause categories", "Risk score and grade"],
14
  },
15
  pro: {
16
  name: "Pro",
17
  scans: Infinity,
18
  price_id: process.env.STRIPE_PRO_PRICE_ID!,
19
+ features: ["Unlimited scans", "Contract uploads", "Clause explanations", "PDF exports"],
20
  },
21
  team: {
22
  name: "Team",
web/package.json CHANGED
@@ -9,25 +9,23 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
- "next": "^15.3.0",
13
- "react": "^19.1.0",
14
- "react-dom": "^19.1.0",
15
- "@supabase/supabase-js": "^2.49.0",
16
- "@supabase/ssr": "^0.6.0",
17
- "stripe": "^17.7.0",
18
- "@stripe/stripe-js": "^5.6.0",
19
- "framer-motion": "^12.6.0",
20
- "lucide-react": "^0.474.0",
21
- "clsx": "^2.1.1",
22
- "tailwind-merge": "^3.0.0"
23
  },
24
  "devDependencies": {
25
- "typescript": "^5.7.0",
26
- "@types/node": "^22.0.0",
27
- "@types/react": "^19.0.0",
28
- "@types/react-dom": "^19.0.0",
29
- "@tailwindcss/postcss": "^4.0.0",
30
- "tailwindcss": "^4.0.0",
31
- "postcss": "^8.5.0"
32
  }
33
  }
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "next": "16.2.4",
13
+ "react": "19.2.5",
14
+ "react-dom": "19.2.5",
15
+ "@supabase/supabase-js": "2.104.0",
16
+ "@supabase/ssr": "0.10.2",
17
+ "stripe": "22.0.2",
18
+ "lucide-react": "0.474.0",
19
+ "clsx": "2.1.1",
20
+ "tailwind-merge": "3.0.0"
 
 
21
  },
22
  "devDependencies": {
23
+ "typescript": "5.8.3",
24
+ "@types/node": "22.15.3",
25
+ "@types/react": "19.2.0",
26
+ "@types/react-dom": "19.2.0",
27
+ "@tailwindcss/postcss": "4.1.4",
28
+ "tailwindcss": "4.1.4",
29
+ "postcss": "8.5.3"
30
  }
31
  }