Spaces:
Running
Running
v2: Fix all versions + minimal UI redesign (no gradients, clean layout)
Browse files- README.md +2 -54
- api/requirements.txt +4 -4
- app.py +118 -202
- ml/requirements.txt +1 -2
- web/app/auth/login/page.tsx +38 -82
- web/app/auth/signup/page.tsx +39 -104
- web/app/dashboard-pages/analyze/page.tsx +68 -122
- web/app/page.tsx +91 -223
- web/lib/stripe.ts +3 -3
- web/package.json +16 -18
README.md
CHANGED
|
@@ -1,63 +1,11 @@
|
|
| 1 |
---
|
| 2 |
title: ClauseGuard
|
| 3 |
emoji: 🛡️
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 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
|
| 2 |
-
uvicorn[standard]
|
| 3 |
-
pydantic
|
| 4 |
-
transformers
|
| 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
|
| 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 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
| 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 |
-
|
| 56 |
-
return [c.strip() for c in
|
| 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 |
-
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
|
|
|
|
| 168 |
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
|
|
|
| 174 |
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
-
|
| 180 |
|
| 181 |
-
|
| 182 |
|
| 183 |
-
|
| 184 |
|
| 185 |
-
|
| 186 |
|
| 187 |
-
|
| 188 |
|
| 189 |
-
|
| 190 |
|
| 191 |
-
|
| 192 |
|
| 193 |
-
The
|
| 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.
|
| 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
|
| 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
|
| 206 |
|
| 207 |
with demo:
|
| 208 |
-
gr.HTML("""
|
| 209 |
-
|
| 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="
|
| 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("
|
| 224 |
-
clear_btn = gr.Button("
|
| 225 |
-
gr.Examples(examples=[[
|
| 226 |
-
|
| 227 |
with gr.Column(scale=1):
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 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
|
| 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 |
-
|
| 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-
|
| 39 |
-
<div className="w-full max-w-
|
| 40 |
-
<div className="
|
| 41 |
-
<Link href="/" className="
|
| 42 |
-
|
| 43 |
-
</Link>
|
| 44 |
-
<p className="mt-2 text-gray-600">Welcome back</p>
|
| 45 |
</div>
|
| 46 |
|
| 47 |
-
<div className="
|
| 48 |
-
{
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
>
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 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 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
|
| 72 |
-
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 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 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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 [
|
| 14 |
-
|
| 15 |
const supabase = createClient();
|
| 16 |
|
| 17 |
async function handleSignup(e: React.FormEvent) {
|
| 18 |
-
e.preventDefault();
|
| 19 |
-
|
| 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 (
|
| 44 |
return (
|
| 45 |
-
<div className="min-h-screen flex items-center justify-center bg-
|
| 46 |
-
<div className="text-center max-w-
|
| 47 |
-
<
|
| 48 |
-
<
|
| 49 |
-
<
|
| 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-
|
| 60 |
-
<div className="w-full max-w-
|
| 61 |
-
<div className="
|
| 62 |
-
<Link href="/" className="
|
| 63 |
-
|
| 64 |
-
</Link>
|
| 65 |
-
<p className="mt-2 text-gray-600">Create your free account</p>
|
| 66 |
</div>
|
| 67 |
|
| 68 |
-
<div className="
|
| 69 |
-
<
|
| 70 |
-
<button
|
| 71 |
-
|
| 72 |
-
|
| 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 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
<div className="flex-1 h-px bg-gray-200" />
|
| 88 |
-
</div>
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 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 |
-
|
| 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 |
-
|
| 21 |
-
total_clauses: number;
|
| 22 |
-
flagged_count: number;
|
| 23 |
-
results: ClauseResult[];
|
| 24 |
-
model: string;
|
| 25 |
-
latency_ms: number;
|
| 26 |
}
|
| 27 |
|
| 28 |
-
const
|
| 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
|
| 33 |
|
| 34 |
-
Spotify reserves the right to remove or disable access to any User Content for any reason,
|
| 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,
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 72 |
-
} finally {
|
| 73 |
-
setLoading(false);
|
| 74 |
-
}
|
| 75 |
}
|
| 76 |
|
| 77 |
-
const
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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-
|
| 93 |
-
<div className="max-w-
|
| 94 |
-
<div className="
|
| 95 |
-
<h1 className="text-
|
| 96 |
-
<p className="mt-
|
| 97 |
</div>
|
| 98 |
|
| 99 |
<div className="grid lg:grid-cols-2 gap-8">
|
| 100 |
-
{/* Input */}
|
| 101 |
<div>
|
| 102 |
<textarea
|
| 103 |
-
value={text}
|
| 104 |
-
|
| 105 |
-
|
| 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-
|
| 109 |
-
<button
|
| 110 |
-
|
| 111 |
-
|
| 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 |
-
|
| 118 |
-
|
| 119 |
-
>
|
| 120 |
-
Try Example
|
| 121 |
</button>
|
| 122 |
</div>
|
| 123 |
-
{error && <p className="mt-
|
| 124 |
</div>
|
| 125 |
|
| 126 |
-
{/* Results */}
|
| 127 |
<div>
|
| 128 |
{results ? (
|
| 129 |
-
<div
|
| 130 |
-
{/*
|
| 131 |
-
<div className="
|
| 132 |
-
<div className="flex
|
| 133 |
<div>
|
| 134 |
-
<
|
| 135 |
-
<
|
| 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 |
-
<
|
| 145 |
-
|
| 146 |
-
</
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
{/*
|
| 150 |
-
<div className="space-y-
|
| 151 |
-
{results.results
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
<div
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
{cat.name}
|
| 161 |
-
</span>
|
| 162 |
-
))}
|
| 163 |
-
</div>
|
| 164 |
</div>
|
| 165 |
-
|
|
|
|
| 166 |
{results.flagged_count === 0 && (
|
| 167 |
-
<div className="
|
| 168 |
-
<
|
| 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="
|
| 177 |
-
<
|
| 178 |
-
<div className="text-5xl mb-4">🔍</div>
|
| 179 |
-
<p>Paste text and click "Scan" 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
|
| 9 |
-
{
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
},
|
| 14 |
-
{
|
| 15 |
-
|
| 16 |
-
|
| 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 |
-
|
| 51 |
-
|
| 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 |
-
|
| 59 |
-
|
| 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 |
-
|
| 75 |
-
|
| 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
|
| 90 |
return (
|
| 91 |
-
<main className="min-h-screen bg-white">
|
| 92 |
{/* Nav */}
|
| 93 |
-
<nav className="
|
| 94 |
-
<div className="max-w-
|
| 95 |
-
<
|
| 96 |
-
|
| 97 |
-
<
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
<
|
| 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="
|
| 116 |
-
<div className="
|
| 117 |
-
|
| 118 |
-
<
|
| 119 |
-
|
| 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-
|
| 127 |
-
ClauseGuard
|
| 128 |
-
|
| 129 |
</p>
|
| 130 |
-
<div className="mt-
|
| 131 |
-
<a
|
| 132 |
-
|
| 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 |
-
|
| 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 |
-
{/*
|
| 151 |
-
<section id="features" className="
|
| 152 |
-
<div className="max-w-
|
| 153 |
-
<
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 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
|
| 174 |
-
<section
|
| 175 |
-
<div className="max-w-
|
| 176 |
-
<
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
</
|
| 186 |
-
<
|
| 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="
|
| 196 |
-
<div className="max-w-
|
| 197 |
-
<
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 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-
|
| 265 |
-
<div className="max-w-
|
| 266 |
-
<
|
| 267 |
-
|
| 268 |
-
<
|
| 269 |
-
|
| 270 |
-
|
| 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: "
|
| 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
|
| 14 |
},
|
| 15 |
pro: {
|
| 16 |
name: "Pro",
|
| 17 |
scans: Infinity,
|
| 18 |
price_id: process.env.STRIPE_PRO_PRICE_ID!,
|
| 19 |
-
features: ["Unlimited scans", "Contract uploads", "
|
| 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": "
|
| 13 |
-
"react": "
|
| 14 |
-
"react-dom": "
|
| 15 |
-
"@supabase/supabase-js": "
|
| 16 |
-
"@supabase/ssr": "
|
| 17 |
-
"stripe": "
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"clsx": "^2.1.1",
|
| 22 |
-
"tailwind-merge": "^3.0.0"
|
| 23 |
},
|
| 24 |
"devDependencies": {
|
| 25 |
-
"typescript": "
|
| 26 |
-
"@types/node": "
|
| 27 |
-
"@types/react": "
|
| 28 |
-
"@types/react-dom": "
|
| 29 |
-
"@tailwindcss/postcss": "
|
| 30 |
-
"tailwindcss": "
|
| 31 |
-
"postcss": "
|
| 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 |
}
|