Spaces:
Sleeping
Sleeping
File size: 31,440 Bytes
8522824 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | """
ClauseGuard β Clause Redlining Engine v1.0
βββββββββββββββββββββββββββββββββββββββββββ
3-Tier Hybrid Architecture:
Tier 1 β Template lookup (instant, zero hallucination risk)
Tier 2 β RAG retrieval from clause corpus (find fairer precedents)
Tier 3 β LLM refinement (adapt template using retrieved precedents)
Anti-hallucination guardrails:
β’ Template anchor: LLM can only refine, not generate from scratch
β’ RAG grounding: Retrieved precedents constrain the output space
β’ Disclaimer: "Not legal advice. Consult an attorney before executing."
β’ Legal citation: Prompt requires LLM to cite the consumer protection standard applied
"""
import os
import re
from collections import defaultdict
# ββ HF Inference Client (soft-fail) βββββββββββββββββββββββββββββββββ
_HAS_INFERENCE = False
try:
from huggingface_hub import InferenceClient
_HAS_INFERENCE = True
except ImportError:
pass
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# TIER 1: TEMPLATE LIBRARY (18+ clause types)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Based on FTC guidelines, EU Directive 93/13, and CFPB guidance.
SAFE_ALTERNATIVES = {
# ββ CRITICAL Risk Clauses ββββββββββββββββββββββββββββββββββββββ
"Uncapped Liability": {
"risky_pattern": "Total liability shall not exceed $1 / unlimited liability exposure",
"safe_alternative": (
"Provider's aggregate liability under this Agreement shall not exceed the total "
"fees paid by the Customer in the twelve (12) months preceding the claim. "
"This limitation shall not apply to: (a) gross negligence or willful misconduct, "
"(b) breach of confidentiality obligations, (c) intellectual property indemnification "
"obligations, or (d) violations of applicable law."
),
"legal_basis": "UCC Β§ 2-719; Restatement (Second) of Contracts Β§ 356",
"consumer_standard": "FTC guidelines on unconscionable contract terms",
"risk_level": "CRITICAL",
},
"Arbitration": {
"risky_pattern": "All disputes via binding arbitration / class action waiver",
"safe_alternative": (
"Disputes involving claims under [Dollar Amount] shall be resolved in small claims "
"court in the consumer's jurisdiction of residence. For other disputes, either party "
"may elect binding arbitration under [AAA/JAMS] rules. The consumer may opt out of "
"arbitration by providing written notice within thirty (30) days of accepting these "
"terms. Each party bears its own arbitration costs; the prevailing party may recover "
"reasonable attorney's fees."
),
"legal_basis": "Federal Arbitration Act Β§ 2; AT&T Mobility v. Concepcion, 563 U.S. 333 (2011)",
"consumer_standard": "CFPB Arbitration Rule guidance; EU Directive 93/13/EEC Art. 3",
"risk_level": "CRITICAL",
},
"IP Ownership Assignment": {
"risky_pattern": "All IP rights assigned to company / work-for-hire everything",
"safe_alternative": (
"Intellectual property created by the Receiving Party specifically in performance of "
"this Agreement ('Work Product IP') shall be assigned to the Disclosing Party. "
"Pre-existing IP and general knowledge, skills, and experience of the Receiving Party "
"remain the Receiving Party's property. The Disclosing Party grants the Receiving Party "
"a non-exclusive, perpetual license to use Work Product IP for internal portfolio and "
"reference purposes."
),
"legal_basis": "17 U.S.C. Β§ 101 (work for hire); Copyright Act Β§ 201(b)",
"consumer_standard": "Standard IP assignment with carve-outs for pre-existing IP",
"risk_level": "CRITICAL",
},
"Termination for Convenience": {
"risky_pattern": "Terminate at any time without notice",
"safe_alternative": (
"Either party may terminate this Agreement for convenience upon thirty (30) days' "
"prior written notice. Immediate termination is permitted only for material breach "
"that remains uncured after a ten (10) day cure period following written notice "
"specifying the breach. Upon termination: (a) all outstanding fees become due, "
"(b) each party shall return or destroy confidential information within fifteen (15) "
"business days, and (c) licenses granted hereunder shall terminate except as "
"expressly stated to survive."
),
"legal_basis": "Restatement (Second) of Contracts Β§ 237; UCC Β§ 2-309",
"consumer_standard": "FTC: adequate notice period required for service termination",
"risk_level": "CRITICAL",
},
"Limitation of liability": {
"risky_pattern": "Company not liable for any damages / complete disclaimer",
"safe_alternative": (
"Neither party shall be liable for indirect, incidental, special, or consequential "
"damages, EXCEPT in cases of: (a) gross negligence or willful misconduct, "
"(b) breach of confidentiality, (c) data breach involving personal information, or "
"(d) intellectual property infringement. Direct damages are limited to fees paid "
"in the prior twelve (12) months. Nothing in this Agreement limits liability for "
"death or personal injury caused by negligence."
),
"legal_basis": "UCC Β§ 2-719(3); EU Directive 93/13/EEC Annex (a)",
"consumer_standard": "Cannot exclude liability for death/personal injury (EU/UK law)",
"risk_level": "CRITICAL",
},
"Unilateral termination": {
"risky_pattern": "Company can terminate account at any time without reason",
"safe_alternative": (
"The Provider may suspend or terminate the User's account for: (a) material breach "
"of these Terms, (b) non-payment after ten (10) days' notice, (c) illegal activity, "
"or (d) extended inactivity exceeding twelve (12) months. The Provider shall provide "
"at least thirty (30) days' written notice before termination, except in cases of "
"illegal activity. Upon termination, the User shall have thirty (30) days to export "
"their data."
),
"legal_basis": "EU Directive 2019/770 (Digital Content); CFPB guidance",
"consumer_standard": "Right to export data upon termination; adequate notice period",
"risk_level": "CRITICAL",
},
"Liquidated Damages": {
"risky_pattern": "Pre-determined damages far exceeding actual harm",
"safe_alternative": (
"In the event of breach, the non-breaching party shall be entitled to liquidated "
"damages in the amount of [specific reasonable amount], which the parties agree "
"represents a reasonable estimate of anticipated harm. This liquidated damages "
"provision shall not apply if actual damages are readily ascertainable, in which "
"case the non-breaching party may recover actual damages proven."
),
"legal_basis": "Restatement (Second) of Contracts Β§ 356; UCC Β§ 2-718",
"consumer_standard": "Liquidated damages must be reasonable estimate, not penalty",
"risk_level": "CRITICAL",
},
# ββ HIGH Risk Clauses ββββββββββββββββββββββββββββββββββββββββββ
"Unilateral change": {
"risky_pattern": "We may modify terms at any time without notice",
"safe_alternative": (
"Material changes to these Terms require thirty (30) days' advance written notice "
"to the User via email and in-app notification. The User has the right to terminate "
"without penalty within the notice period if they do not accept the changes. "
"Non-material changes (e.g., formatting, clarifications) may be made without notice."
),
"legal_basis": "EU Directive 93/13/EEC Art. 3; Restatement (Second) Β§ 89",
"consumer_standard": "FTC: material changes require notice and right to reject",
"risk_level": "HIGH",
},
"Content removal": {
"risky_pattern": "Company can delete content at sole discretion without notice",
"safe_alternative": (
"Content may be removed only for violation of these Terms of Service, applicable law, "
"or valid legal process. The Provider shall provide prior notice specifying the reason "
"for removal (except where legally prohibited). The User has the right to appeal "
"within fourteen (14) days. Removed content shall be preserved for thirty (30) days "
"to allow for appeal resolution."
),
"legal_basis": "EU Digital Services Act Art. 17; First Amendment considerations",
"consumer_standard": "Due process: notice, reason, and right to appeal",
"risk_level": "HIGH",
},
"Non-Compete": {
"risky_pattern": "Broad non-compete with no time/geography limits",
"safe_alternative": (
"During the term of this Agreement and for a period of [6-12] months thereafter, "
"the Receiving Party shall not directly compete with the Disclosing Party in "
"[specific market/geography]. This restriction applies only to [specific business "
"activities] and does not prevent general employment in the industry. The Disclosing "
"Party shall provide [garden leave pay / consideration] during the restricted period."
),
"legal_basis": "Restatement (Second) of Contracts Β§ 188; FTC Non-Compete Rule (2024)",
"consumer_standard": "Reasonable scope, duration, geography; adequate consideration",
"risk_level": "HIGH",
},
"Exclusivity": {
"risky_pattern": "Exclusive dealing with no time limit or exit clause",
"safe_alternative": (
"The exclusivity arrangement shall apply for an initial term of [12-24] months, "
"after which either party may convert to non-exclusive upon sixty (60) days' notice. "
"Exclusivity is limited to [specific product/service category] and [specific "
"geographic area]. Performance benchmarks shall be reviewed quarterly; failure to "
"meet agreed minimums allows termination of exclusivity."
),
"legal_basis": "Sherman Act Β§ 1; EU Competition Law Art. 101 TFEU",
"consumer_standard": "Time-limited, scope-limited, with performance exit clause",
"risk_level": "HIGH",
},
"Anti-Assignment": {
"risky_pattern": "Complete prohibition on assignment without consent",
"safe_alternative": (
"Neither party may assign this Agreement without the prior written consent of the "
"other party, which shall not be unreasonably withheld, conditioned, or delayed. "
"Notwithstanding the foregoing, either party may assign this Agreement without "
"consent in connection with a merger, acquisition, or sale of substantially all "
"of its assets, provided the assignee assumes all obligations hereunder."
),
"legal_basis": "UCC Β§ 2-210; Restatement (Second) of Contracts Β§ 317",
"consumer_standard": "Consent not to be unreasonably withheld; M&A carve-out",
"risk_level": "HIGH",
},
# ββ MEDIUM Risk Clauses ββββββββββββββββββββββββββββββββββββββββ
"Jurisdiction": {
"risky_pattern": "Exclusive jurisdiction in distant/foreign state",
"safe_alternative": (
"The Consumer may bring claims in their jurisdiction of residence or the Provider's "
"principal place of business. Small claims actions may be brought in any court of "
"competent jurisdiction. For commercial contracts: disputes shall be resolved in "
"[mutually agreed location] or the defendant's principal place of business."
),
"legal_basis": "EU Regulation 1215/2012 (Brussels I); CJEU C-585/08",
"consumer_standard": "Consumer may sue in home jurisdiction (EU Directive 93/13)",
"risk_level": "MEDIUM",
},
"Choice of law": {
"risky_pattern": "Governed by laws of a jurisdiction that disadvantages consumer",
"safe_alternative": (
"This Agreement shall be governed by the laws of [State/Country]. Notwithstanding "
"the foregoing, nothing in this choice of law provision shall deprive the Consumer "
"of the protection afforded by mandatory provisions of the law of the Consumer's "
"habitual residence."
),
"legal_basis": "EU Regulation 593/2008 (Rome I) Art. 6; UCC Β§ 1-301",
"consumer_standard": "Cannot override mandatory consumer protection of home jurisdiction",
"risk_level": "MEDIUM",
},
"Contract by using": {
"risky_pattern": "Bound to contract by merely using the service (browsewrap)",
"safe_alternative": (
"By creating an account, the User acknowledges they have read, understood, and agree "
"to be bound by these Terms. The User must affirmatively accept these Terms via "
"checkbox or click-through before account creation. Continued use after material "
"changes requires re-acceptance."
),
"legal_basis": "Specht v. Netscape, 306 F.3d 17 (2d Cir. 2002)",
"consumer_standard": "Clickwrap > browsewrap; affirmative acceptance required",
"risk_level": "MEDIUM",
},
# ββ Additional Common Clauses ββββββββββββββββββββββββββββββββββ
"Auto-Renewal": {
"risky_pattern": "Auto-renews silently without notice",
"safe_alternative": (
"This Agreement shall automatically renew for successive [term] periods unless "
"either party provides written notice of non-renewal at least thirty (30) days "
"before the end of the then-current term. The Provider shall send a reminder "
"notice thirty (30) to sixty (60) days before renewal. The Consumer may cancel "
"within fifteen (15) days of renewal for a pro-rated refund."
),
"legal_basis": "California Auto-Renewal Law (ARL) Bus. & Prof. Code Β§ 17600; FTC Negative Option Rule",
"consumer_standard": "Reminder notice required; easy cancellation; pro-rated refund",
"risk_level": "HIGH",
},
"Indemnification": {
"risky_pattern": "User indemnifies company for all claims without limit",
"safe_alternative": (
"Each party shall indemnify, defend, and hold harmless the other party from "
"third-party claims arising from: (a) the indemnifying party's breach of this "
"Agreement, (b) the indemnifying party's negligence or willful misconduct, or "
"(c) the indemnifying party's violation of applicable law. The User's indemnification "
"obligation is limited to claims arising from the User's own negligence or "
"intentional acts. The maximum indemnification obligation shall not exceed [amount]."
),
"legal_basis": "Restatement (Second) of Contracts Β§ 345; UCC Β§ 2-607",
"consumer_standard": "Mutual indemnification; limited to own acts; capped",
"risk_level": "HIGH",
},
"Confidentiality": {
"risky_pattern": "Overly broad confidentiality with no exceptions or time limit",
"safe_alternative": (
"Each party agrees to maintain the confidentiality of the other's Confidential "
"Information for a period of [3-5] years from disclosure. Confidential Information "
"excludes: (a) publicly available information, (b) independently developed "
"information, (c) information received from a third party without restriction, "
"(d) information required to be disclosed by law or court order (with prompt notice "
"to the disclosing party)."
),
"legal_basis": "Restatement (Third) of Unfair Competition Β§ 39-45",
"consumer_standard": "Time-limited; standard exceptions; required disclosure carve-out",
"risk_level": "MEDIUM",
},
}
# Mapping from CUAD/unfair labels to our template keys
_LABEL_TO_TEMPLATE = {
"Uncapped Liability": "Uncapped Liability",
"Arbitration": "Arbitration",
"IP Ownership Assignment": "IP Ownership Assignment",
"Termination for Convenience": "Termination for Convenience",
"Limitation of liability": "Limitation of liability",
"Unilateral termination": "Unilateral termination",
"Liquidated Damages": "Liquidated Damages",
"Unilateral change": "Unilateral change",
"Content removal": "Content removal",
"Non-Compete": "Non-Compete",
"Exclusivity": "Exclusivity",
"Anti-Assignment": "Anti-Assignment",
"Jurisdiction": "Jurisdiction",
"Choice of law": "Choice of law",
"Contract by using": "Contract by using",
"Cap on Liability": "Limitation of liability", # Similar enough
"No-Solicit of Customers": "Non-Compete", # Use non-compete template
"No-Solicit of Employees": "Non-Compete",
"Non-Disparagement": "Confidentiality", # Similar restrictive clause
}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# TIER 2: RAG RETRIEVAL (find fairer precedent clauses)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _find_similar_templates(clause_label, clause_text):
"""
Find the most relevant safe alternative template(s) for a given clause.
Returns list of matching templates.
"""
matches = []
# Direct label match
template_key = _LABEL_TO_TEMPLATE.get(clause_label)
if template_key and template_key in SAFE_ALTERNATIVES:
matches.append((template_key, SAFE_ALTERNATIVES[template_key], 1.0))
# Also do keyword matching for clauses that might not have exact label matches
clause_lower = clause_text.lower()
keyword_map = {
"Uncapped Liability": ["unlimited liability", "uncapped", "no limit on liability"],
"Arbitration": ["arbitration", "arbitrate", "waive right to court", "class action waiver"],
"Termination for Convenience": ["terminate at any time", "terminate without cause", "terminate without notice"],
"Limitation of liability": ["not liable", "limitation of liability", "in no event", "disclaim"],
"Unilateral change": ["modify at any time", "sole discretion", "change terms", "without notice"],
"Content removal": ["remove content", "delete content", "remove at sole discretion"],
"Auto-Renewal": ["auto-renew", "automatically renew", "automatic renewal"],
"Indemnification": ["indemnif", "hold harmless"],
}
for key, keywords in keyword_map.items():
if key in SAFE_ALTERNATIVES:
for kw in keywords:
if kw in clause_lower:
# Avoid duplicates
if not any(m[0] == key for m in matches):
matches.append((key, SAFE_ALTERNATIVES[key], 0.7))
break
return matches
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# TIER 3: LLM REFINEMENT
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_LLM_MODEL = "Qwen/Qwen2.5-7B-Instruct"
def _refine_with_llm(original_clause, template, clause_label):
"""
Use LLM to adapt the template to the specific clause context.
The LLM refines β it does NOT generate from scratch (anti-hallucination).
"""
if not _HAS_INFERENCE:
return None
try:
token = os.environ.get("HF_TOKEN", "")
client = InferenceClient(
provider="hf-inference",
api_key=token if token else None,
)
prompt = f"""You are a legal contract redlining assistant. Your task is to adapt a safe clause template to fit the specific context of an original risky clause.
RULES:
1. You MUST use the provided template as your base β do NOT generate clauses from scratch.
2. Preserve the legal protections in the template.
3. Adapt specific details (parties, amounts, timeframes) from the original clause.
4. Keep the same legal standard cited in the template.
5. Output ONLY the refined clause text, nothing else.
6. The refined clause should be immediately usable in a contract.
ORIGINAL RISKY CLAUSE:
{original_clause[:500]}
CLAUSE TYPE: {clause_label}
SAFE TEMPLATE:
{template['safe_alternative']}
LEGAL BASIS: {template['legal_basis']}
Write the refined safer clause (adapt the template to this specific contract's context):"""
response = client.chat_completion(
model=_LLM_MODEL,
messages=[
{"role": "system", "content": "You are a legal contract redlining expert. Output ONLY the refined clause text."},
{"role": "user", "content": prompt},
],
max_tokens=512,
temperature=0.2,
)
refined = response.choices[0].message.content.strip()
# Sanity check: refined should be substantial
if len(refined) < 50:
return None
return refined
except Exception as e:
print(f"[ClauseGuard Redline] LLM refinement error: {e}")
return None
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PUBLIC API
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def generate_redlines(analysis_result, use_llm=True):
"""
Generate redline suggestions for all flagged clauses in the analysis.
Returns list of redline suggestions:
[{
"original_text": str,
"clause_label": str,
"risk_level": str,
"safe_alternative": str,
"legal_basis": str,
"consumer_standard": str,
"tier": "template" | "llm_refined",
"confidence": str,
}]
"""
if analysis_result is None:
return []
clauses = analysis_result.get("clauses", [])
if not clauses:
return []
redlines = []
seen_labels = set() # Deduplicate by label
# Sort by risk level: CRITICAL first
risk_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
sorted_clauses = sorted(clauses, key=lambda c: risk_order.get(c.get("risk", "LOW"), 3))
for clause in sorted_clauses:
label = clause.get("label", "")
risk = clause.get("risk", "LOW")
text = clause.get("text", "")
# Skip LOW risk and already-seen labels
if risk == "LOW" or label in seen_labels:
continue
seen_labels.add(label)
# Find matching templates (Tier 1 + Tier 2)
matches = _find_similar_templates(label, text)
if not matches:
continue
best_key, best_template, score = matches[0]
# Tier 3: Try LLM refinement if enabled
refined_text = None
tier = "template"
if use_llm and risk in ("CRITICAL", "HIGH"):
refined_text = _refine_with_llm(text, best_template, label)
if refined_text:
tier = "llm_refined"
redlines.append({
"original_text": text[:500],
"clause_label": label,
"risk_level": risk,
"safe_alternative": refined_text or best_template["safe_alternative"],
"template_alternative": best_template["safe_alternative"],
"legal_basis": best_template["legal_basis"],
"consumer_standard": best_template["consumer_standard"],
"tier": tier,
})
return redlines
def render_redlines_html(redlines):
"""Render redline suggestions as HTML for Gradio."""
if not redlines:
return '''<div style="padding:24px;text-align:center;color:#6b7280;font-family:system-ui,sans-serif;">
<p style="font-size:16px;">π No redline suggestions available.</p>
<p style="font-size:13px;">Analyze a contract first β redlining suggestions will appear for risky clauses.</p>
</div>'''
risk_styles = {
"CRITICAL": ("#dc2626", "#fef2f2", "β οΈ"),
"HIGH": ("#ea580c", "#fff7ed", "β‘"),
"MEDIUM": ("#ca8a04", "#fefce8", "π"),
"LOW": ("#16a34a", "#f0fdf4", "β"),
}
html = '<div style="font-family:system-ui,sans-serif;">'
# Summary header
crit = sum(1 for r in redlines if r["risk_level"] == "CRITICAL")
high = sum(1 for r in redlines if r["risk_level"] == "HIGH")
med = sum(1 for r in redlines if r["risk_level"] == "MEDIUM")
llm_count = sum(1 for r in redlines if r["tier"] == "llm_refined")
html += f'''
<div style="padding:16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:12px;margin-bottom:16px;border:1px solid #e5e7eb;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="font-size:24px;">βοΈ</span>
<h2 style="margin:0;font-size:18px;color:#1f2937;">Clause Redlining Suggestions</h2>
</div>
<p style="font-size:13px;color:#6b7280;margin:0;">
{len(redlines)} suggestions: {crit} Critical Β· {high} High Β· {med} Medium
{f" Β· {llm_count} LLM-refined" if llm_count else ""}
</p>
</div>
'''
for i, redline in enumerate(redlines):
border_color, bg_color, icon = risk_styles.get(
redline["risk_level"], ("#6b7280", "#f9fafb", "β’")
)
tier_badge = (
'<span style="font-size:10px;background:#eff6ff;color:#3b82f6;padding:2px 8px;border-radius:4px;">π€ LLM Refined</span>'
if redline["tier"] == "llm_refined"
else '<span style="font-size:10px;background:#f0fdf4;color:#16a34a;padding:2px 8px;border-radius:4px;">π Template</span>'
)
original_preview = redline["original_text"][:200].replace("<", "<").replace(">", ">")
safe_text = redline["safe_alternative"].replace("<", "<").replace(">", ">")
html += f'''
<div style="border:1px solid #e5e7eb;border-left:4px solid {border_color};border-radius:8px;margin-bottom:12px;overflow:hidden;">
<!-- Header -->
<div style="padding:12px 16px;background:{bg_color};border-bottom:1px solid #e5e7eb;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:16px;">{icon}</span>
<span style="font-size:14px;font-weight:600;color:{border_color};">{redline["clause_label"]}</span>
<span style="font-size:11px;color:{border_color};text-transform:uppercase;font-weight:600;">{redline["risk_level"]}</span>
</div>
{tier_badge}
</div>
</div>
<!-- Body -->
<div style="padding:16px;">
<!-- Original (risky) -->
<div style="margin-bottom:12px;">
<div style="font-size:11px;font-weight:600;color:#991b1b;text-transform:uppercase;margin-bottom:4px;">β Original (Risky)</div>
<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:10px;font-size:12px;color:#991b1b;line-height:1.6;">
<del>{original_preview}{"..." if len(redline["original_text"]) > 200 else ""}</del>
</div>
</div>
<!-- Suggested (safe) -->
<div style="margin-bottom:12px;">
<div style="font-size:11px;font-weight:600;color:#166534;text-transform:uppercase;margin-bottom:4px;">β
Suggested Alternative</div>
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:10px;font-size:12px;color:#166534;line-height:1.6;">
{safe_text}
</div>
</div>
<!-- Legal basis -->
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">π Legal Basis</div>
<div style="font-size:11px;color:#4b5563;">{redline["legal_basis"]}</div>
</div>
<div style="flex:1;min-width:200px;">
<div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">π‘οΈ Consumer Standard</div>
<div style="font-size:11px;color:#4b5563;">{redline["consumer_standard"]}</div>
</div>
</div>
</div>
</div>
'''
# Disclaimer
html += '''
<div style="margin-top:16px;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;">
<p style="font-size:11px;color:#92400e;margin:0;line-height:1.5;">
<strong>β οΈ Disclaimer:</strong> These are AI-generated suggestions based on legal templates and consumer protection standards.
They are NOT legal advice. The suggested alternatives are starting points that should be reviewed and customized by a
qualified attorney before use in any contract. Legal requirements vary by jurisdiction.
</p>
</div>
'''
html += '</div>'
return html
|