muhammadbinmurtza commited on
Commit Β·
e59dc17
1
Parent(s): 81b0dd6
Multi-page Streamlit: shared ui.py, pages for Clauses/Negotiation/Chat/Downloads
Browse files- clauseguard/app.py +9 -1575
- clauseguard/pages/1_π_Clauses.py +15 -0
- clauseguard/pages/2_π¬_Negotiation.py +14 -0
- clauseguard/pages/3_π€_Chat.py +14 -0
- clauseguard/pages/4_π₯_Download.py +56 -0
- clauseguard/ui.py +1513 -0
clauseguard/app.py
CHANGED
|
@@ -1,1521 +1,19 @@
|
|
| 1 |
"""ClauseGuard Streamlit UI β redesigned modern SaaS edition."""
|
| 2 |
-
import asyncio
|
| 3 |
-
import logging
|
| 4 |
import sys
|
| 5 |
-
import time
|
| 6 |
-
from datetime import datetime
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 10 |
|
| 11 |
import streamlit as st
|
| 12 |
-
import pandas as pd
|
| 13 |
|
| 14 |
-
from clauseguard.
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
logger = logging.getLogger(__name__)
|
| 23 |
-
|
| 24 |
-
# ββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
-
|
| 26 |
-
MAX_FILE_SIZE_MB = 10
|
| 27 |
-
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
|
| 28 |
-
ALLOWED_EXTENSIONS = ["pdf", "txt", "docx"]
|
| 29 |
-
|
| 30 |
-
TAB_NAMES = ["π Overview", "π Clauses", "π¬ Negotiation", "π€ Chat Assistant"]
|
| 31 |
-
TAB_SESSION_KEY = "tab_selector_radio"
|
| 32 |
-
|
| 33 |
-
AGENT_NAMES = ["Extractor", "Classifier", "Risk Scorer", "Translator", "Reporter"]
|
| 34 |
-
AGENT_ICONS = {"running": "βοΈ", "completed": "β
", "failed": "β", "pending": "β³"}
|
| 35 |
-
AGENT_STEP_NUMBERS = {"Extractor": "β ", "Classifier": "β‘", "Risk Scorer": "β’",
|
| 36 |
-
"Translator": "β£", "Reporter": "β€"}
|
| 37 |
-
|
| 38 |
-
SEVERITY_STYLE = {
|
| 39 |
-
Severity.CRITICAL: {"badge": "π΄ CRITICAL", "border": "#ff4444", "bg": "rgba(255,68,68,0.12)", "color": "#ff6666", "tag_bg": "rgba(255,68,68,0.18)"},
|
| 40 |
-
Severity.HIGH: {"badge": "π HIGH", "border": "#ff8c00", "bg": "rgba(255,140,0,0.12)", "color": "#ffaa44", "tag_bg": "rgba(255,140,0,0.15)"},
|
| 41 |
-
Severity.MEDIUM: {"badge": "π‘ MEDIUM", "border": "#ffd700", "bg": "rgba(255,215,0,0.12)", "color": "#ffdd55", "tag_bg": "rgba(255,215,0,0.12)"},
|
| 42 |
-
Severity.LOW: {"badge": "π’ LOW", "border": "#32cd32", "bg": "rgba(50,205,50,0.12)", "color": "#55dd55", "tag_bg": "rgba(50,205,50,0.10)"},
|
| 43 |
-
Severity.INFO: {"badge": "βΉοΈ INFO", "border": "#1e90ff", "bg": "rgba(30,144,255,0.08)", "color": "#55aaff", "tag_bg": "rgba(30,144,255,0.08)"},
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _check_model_connectivity() -> tuple[bool, str]:
|
| 48 |
-
"""Quick connectivity check against the configured model endpoint.
|
| 49 |
-
|
| 50 |
-
Returns:
|
| 51 |
-
(ok, error_message) β ok is True if the endpoint is reachable.
|
| 52 |
-
"""
|
| 53 |
-
import asyncio
|
| 54 |
-
from clauseguard.services.model_service import get_client
|
| 55 |
-
from clauseguard.config.settings import MODEL_NAME
|
| 56 |
-
|
| 57 |
-
try:
|
| 58 |
-
loop = asyncio.new_event_loop()
|
| 59 |
-
asyncio.set_event_loop(loop)
|
| 60 |
-
try:
|
| 61 |
-
client = get_client()
|
| 62 |
-
loop.run_until_complete(
|
| 63 |
-
asyncio.wait_for(
|
| 64 |
-
client.models.list(),
|
| 65 |
-
timeout=10,
|
| 66 |
-
)
|
| 67 |
-
)
|
| 68 |
-
return True, ""
|
| 69 |
-
except asyncio.TimeoutError:
|
| 70 |
-
return False, "Model endpoint timed out β the vLLM server may be offline or unreachable"
|
| 71 |
-
except Exception as e:
|
| 72 |
-
err = str(e)
|
| 73 |
-
if "ConnectionRefusedError" in err or "Connection refused" in err or "ConnectError" in err:
|
| 74 |
-
return False, f"Connection refused β vLLM server is not running at the configured BASE_URL"
|
| 75 |
-
if "Name or service not known" in err or "getaddrinfo" in err.lower():
|
| 76 |
-
return False, f"Cannot resolve host β check that the BASE_URL is correct"
|
| 77 |
-
return False, f"Model endpoint error: {err[:120]}"
|
| 78 |
-
finally:
|
| 79 |
-
loop.close()
|
| 80 |
-
except Exception as e:
|
| 81 |
-
return False, f"Connectivity check failed: {str(e)[:120]}"
|
| 82 |
-
|
| 83 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
-
# CUSTOM CSS
|
| 85 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
-
|
| 87 |
-
CUSTOM_CSS = """
|
| 88 |
-
<style>
|
| 89 |
-
.main .block-container { padding-top: 1.5rem; padding-bottom: 2rem; }
|
| 90 |
-
|
| 91 |
-
.stButton > button {
|
| 92 |
-
border-radius: 10px;
|
| 93 |
-
font-weight: 600;
|
| 94 |
-
font-size: 0.95rem;
|
| 95 |
-
padding: 0.65rem 1.5rem;
|
| 96 |
-
transition: all 0.2s ease;
|
| 97 |
-
border: 1px solid rgba(255,255,255,0.08);
|
| 98 |
-
}
|
| 99 |
-
.stButton > button:hover {
|
| 100 |
-
transform: translateY(-1px);
|
| 101 |
-
box-shadow: 0 6px 20px rgba(102,126,234,0.35);
|
| 102 |
-
}
|
| 103 |
-
.stButton > button[kind="primary"] {
|
| 104 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 105 |
-
color: #fff !important;
|
| 106 |
-
border: none !important;
|
| 107 |
-
}
|
| 108 |
-
.stButton > button[kind="secondary"] {
|
| 109 |
-
background: rgba(255,255,255,0.06) !important;
|
| 110 |
-
color: #e0e0e0 !important;
|
| 111 |
-
border: 1px solid rgba(255,255,255,0.12) !important;
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
.stFileUploader section {
|
| 115 |
-
border: 2px dashed #667eea !important;
|
| 116 |
-
border-radius: 14px !important;
|
| 117 |
-
padding: 1.5rem !important;
|
| 118 |
-
background: rgba(102,126,234,0.03) !important;
|
| 119 |
-
transition: all 0.25s ease;
|
| 120 |
-
}
|
| 121 |
-
.stFileUploader section:hover {
|
| 122 |
-
border-color: #8ab4f8 !important;
|
| 123 |
-
background: rgba(102,126,234,0.08) !important;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
div[role="radiogroup"] {
|
| 127 |
-
display: flex; gap: 4px;
|
| 128 |
-
background: #0e1117; padding: 4px;
|
| 129 |
-
border-radius: 14px; border: 1px solid rgba(255,255,255,0.06);
|
| 130 |
-
margin-bottom: 1rem;
|
| 131 |
-
}
|
| 132 |
-
div[role="radiogroup"] label {
|
| 133 |
-
flex: 1; text-align: center;
|
| 134 |
-
padding: 10px 16px !important;
|
| 135 |
-
border-radius: 10px;
|
| 136 |
-
font-weight: 600; font-size: 0.92rem;
|
| 137 |
-
color: #aaa; cursor: pointer;
|
| 138 |
-
transition: all 0.2s ease;
|
| 139 |
-
}
|
| 140 |
-
div[role="radiogroup"] label:hover { background: rgba(255,255,255,0.04); }
|
| 141 |
-
div[role="radiogroup"] label:has(input:checked) {
|
| 142 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 143 |
-
color: #ffffff !important;
|
| 144 |
-
}
|
| 145 |
-
div[role="radiogroup"] input[type="radio"] {
|
| 146 |
-
position: absolute; opacity: 0; width: 0; height: 0;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
@media (max-width: 768px) {
|
| 150 |
-
div[role="radiogroup"] label { padding: 8px 10px; font-size: 0.78rem; }
|
| 151 |
-
}
|
| 152 |
-
.stTabs [data-baseweb="tab"] {
|
| 153 |
-
font-weight: 600;
|
| 154 |
-
font-size: 0.95rem;
|
| 155 |
-
padding: 10px 20px;
|
| 156 |
-
border-radius: 10px;
|
| 157 |
-
color: #aaa;
|
| 158 |
-
}
|
| 159 |
-
.stTabs [aria-selected="true"] {
|
| 160 |
-
color: #ffffff !important;
|
| 161 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 162 |
-
border-radius: 10px !important;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
.stExpander {
|
| 166 |
-
border: 1px solid rgba(255,255,255,0.06) !important;
|
| 167 |
-
border-radius: 10px !important;
|
| 168 |
-
margin-bottom: 0.3rem !important;
|
| 169 |
-
overflow: hidden;
|
| 170 |
-
transition: all 0.15s ease;
|
| 171 |
-
}
|
| 172 |
-
.stExpander:hover {
|
| 173 |
-
border-color: rgba(255,255,255,0.1) !important;
|
| 174 |
-
}
|
| 175 |
-
.stExpander > div:first-child {
|
| 176 |
-
border-radius: 10px !important;
|
| 177 |
-
background: rgba(255,255,255,0.015);
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
.stChatMessage { border-radius: 12px !important; }
|
| 181 |
-
|
| 182 |
-
.stProgress > div > div > div > div {
|
| 183 |
-
background: linear-gradient(90deg, #667eea, #764ba2) !important;
|
| 184 |
-
border-radius: 4px;
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
[data-testid="stMetric"] {
|
| 188 |
-
background: rgba(255,255,255,0.03);
|
| 189 |
-
border: 1px solid rgba(255,255,255,0.06);
|
| 190 |
-
border-radius: 12px;
|
| 191 |
-
padding: 0.75rem 1rem;
|
| 192 |
-
}
|
| 193 |
-
[data-testid="stMetric"] label { font-weight: 500 !important; }
|
| 194 |
-
|
| 195 |
-
.stCodeBlock { border-radius: 10px !important; }
|
| 196 |
-
|
| 197 |
-
.cg-card {
|
| 198 |
-
background: linear-gradient(145deg, #12121f 0%, #0e1117 100%);
|
| 199 |
-
border: 1px solid rgba(255,255,255,0.07);
|
| 200 |
-
border-radius: 14px;
|
| 201 |
-
padding: 1.25rem;
|
| 202 |
-
margin-bottom: 1rem;
|
| 203 |
-
transition: border-color 0.2s ease;
|
| 204 |
-
}
|
| 205 |
-
.cg-card:hover { border-color: rgba(255,255,255,0.12); }
|
| 206 |
-
|
| 207 |
-
.cg-badge {
|
| 208 |
-
display: inline-block;
|
| 209 |
-
padding: 0.25rem 0.7rem;
|
| 210 |
-
border-radius: 20px;
|
| 211 |
-
font-size: 0.75rem;
|
| 212 |
-
font-weight: 700;
|
| 213 |
-
letter-spacing: 0.4px;
|
| 214 |
-
text-transform: uppercase;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
@keyframes pulse-glow {
|
| 218 |
-
0%, 100% { opacity: 1; }
|
| 219 |
-
50% { opacity: 0.6; }
|
| 220 |
-
}
|
| 221 |
-
.agent-running {
|
| 222 |
-
animation: pulse-glow 1.4s ease-in-out infinite;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
.cg-chip {
|
| 226 |
-
display: inline-block;
|
| 227 |
-
padding: 0.35rem 0.9rem;
|
| 228 |
-
border-radius: 20px;
|
| 229 |
-
font-size: 0.78rem;
|
| 230 |
-
font-weight: 500;
|
| 231 |
-
background: rgba(102,126,234,0.12);
|
| 232 |
-
color: #8ab4f8;
|
| 233 |
-
border: 1px solid rgba(102,126,234,0.2);
|
| 234 |
-
cursor: pointer;
|
| 235 |
-
margin: 0.2rem;
|
| 236 |
-
transition: all 0.15s ease;
|
| 237 |
-
}
|
| 238 |
-
.cg-chip:hover {
|
| 239 |
-
background: rgba(102,126,234,0.25);
|
| 240 |
-
border-color: rgba(102,126,234,0.4);
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
@media (max-width: 768px) {
|
| 244 |
-
.stTabs [data-baseweb="tab"] { padding: 8px 12px; font-size: 0.8rem; }
|
| 245 |
-
}
|
| 246 |
-
</style>
|
| 247 |
-
"""
|
| 248 |
-
|
| 249 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 250 |
-
# DEMO REPORT BUILDER
|
| 251 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 252 |
-
|
| 253 |
-
def _build_demo_report() -> FinalReport:
|
| 254 |
-
"""Build a pre-cached demo report showcasing all features with realistic contract data."""
|
| 255 |
-
from clauseguard.models.clause import Clause, ClauseType
|
| 256 |
-
|
| 257 |
-
demo_clauses: list[dict] = [
|
| 258 |
-
{
|
| 259 |
-
"text": "Employee hereby irrevocably assigns to Company all inventions, discoveries, works of authorship, and intellectual property created or conceived by Employee, whether during working hours or on Employee's own time, using Company equipment or Employee's personal equipment, and whether or not related to Company's business. This obligation survives termination of employment for a period of two (2) years.",
|
| 260 |
-
"ctype": "IP_ASSIGNMENT", "sev": "CRITICAL",
|
| 261 |
-
"title": "Overbroad IP Assignment Covering Personal Work",
|
| 262 |
-
"reason": "This clause claims ownership of ALL employee creations made at any time on any equipment, including unrelated personal side projects, and extends the obligation for 2 years after leaving the company β well beyond industry standard.",
|
| 263 |
-
"plain": "You give the company full ownership of everything you ever create β including personal hobbies, side projects, and weekend work done on your own computer β even for two years after you quit or are fired.",
|
| 264 |
-
"action": "Negotiate a strict carve-out limiting IP assignment to work directly related to company business, created during work hours using company resources only.",
|
| 265 |
-
"safer": "Employee assigns to Company all inventions that (a) relate directly to Company's current or planned business, (b) are created during working hours, and (c) use Company resources. Inventions created on Employee's own time using personal equipment and unrelated to Company's business remain Employee's sole property. This obligation ends upon termination of employment.",
|
| 266 |
-
"negotiation": "Hi [Name],\n\nI've reviewed the intellectual property clause and have a concern about its extremely broad scope. As written, it covers personal side projects, hobbies, and work done on my own time with my own equipment β even extending 2 years after I leave. That's well beyond what's standard.\n\nI've drafted an alternative below that protects the company's legitimate interests while respecting my personal creative freedom. The key change: it limits the scope to work actually related to the company's business, done during working hours, using company resources.\n\nWould you be open to this revision?\n\nThanks,\n[Your Name]",
|
| 267 |
-
"impacts": ["You could lose ownership of a personal startup, app, or creative project you build on weekends", "The company could claim royalties or ownership of open-source contributions you make", "Even after quitting, anything you invent for 2 years could be claimed by your former employer"],
|
| 268 |
-
},
|
| 269 |
-
{
|
| 270 |
-
"text": "Any and all disputes, claims, or controversies arising out of or relating to this Agreement shall be resolved exclusively through final and binding arbitration administered by the American Arbitration Association. The Parties hereby expressly waive any right to a trial by jury and waive any right to participate in or bring a class, collective, or representative action. The arbitrator shall have no authority to consolidate claims or conduct class-wide proceedings.",
|
| 271 |
-
"ctype": "ARBITRATION", "sev": "CRITICAL",
|
| 272 |
-
"title": "Mandatory Arbitration Forcing Waiver of All Court Rights",
|
| 273 |
-
"reason": "This clause strips away your right to sue in court, forces mandatory private arbitration, waives your constitutional right to a jury trial, and blocks you from joining any class action β with zero opt-out provision.",
|
| 274 |
-
"plain": "You cannot ever take the company to court. Any dispute β no matter how serious β goes through private arbitration that the company pays for and controls. You also give up your right to join a class-action lawsuit with others who may have been wronged the same way.",
|
| 275 |
-
"action": "Demand an opt-out provision allowing either party to choose court over arbitration. Alternatively, remove the class action waiver entirely.",
|
| 276 |
-
"safer": "Either party may elect to opt out of binding arbitration by providing written notice within thirty (30) days of signing this Agreement. Nothing in this section shall prevent any party from participating in a class, collective, or representative action where permitted by applicable law. Both parties retain the right to seek injunctive or equitable relief in a court of competent jurisdiction.",
|
| 277 |
-
"negotiation": "Hi [Name],\n\nI've reviewed the dispute resolution section and have significant concerns about the mandatory arbitration clause. It completely removes my ability to go to court β even for serious disputes β and blocks class actions entirely.\n\nI'm not opposed to arbitration as an option, but forcing it as the ONLY option with no way out is too one-sided. I've drafted a revised version that adds a 30-day opt-out window (so both parties can choose) and preserves the right to join class actions where the law allows.\n\nThis creates a fair balance. Would you be open to this?\n\nBest,\n[Your Name]",
|
| 278 |
-
"impacts": ["If the company steals your work or fails to pay you, you cannot sue in a public court β you must go through a private arbitrator they help select", "You face the company alone β you cannot pool resources with other employees who were treated the same way", "Arbitration decisions are nearly impossible to appeal, giving you no safety net if the arbitrator makes a mistake"],
|
| 279 |
-
},
|
| 280 |
-
{
|
| 281 |
-
"text": "Employee expressly agrees that during the term of employment and for a period of eighteen (18) months following the termination of employment for any reason, Employee shall not, directly or indirectly, own, manage, operate, control, be employed by, consult for, or render services to any business that is competitive with Company, as determined by Company in its sole discretion, anywhere in the world.",
|
| 282 |
-
"ctype": "NON_COMPETE", "sev": "HIGH",
|
| 283 |
-
"title": "Worldwide Non-Compete with Unlimited Company Discretion",
|
| 284 |
-
"reason": "This 18-month non-compete bans you from working for ANY business the company unilaterally deems 'competitive' β worldwide, with no geographic limit, and the company alone decides who counts as a competitor.",
|
| 285 |
-
"plain": "You cannot work for any company anywhere in the world for 18 months after leaving β and your employer alone gets to decide which companies count as 'competitors.' Even a completely unrelated job could be blocked if the company says so.",
|
| 286 |
-
"action": "Reduce duration to 12 months maximum, limit geographic scope to regions where the company actually operates, and define competitors objectively (not at the company's sole discretion).",
|
| 287 |
-
"safer": "For a period of twelve (12) months following termination, Employee shall not provide services to entities that are direct competitors of Company, limited to the specific metropolitan areas and regions where Company has active and material business operations, and limited to services substantially similar to those Employee performed for Company.",
|
| 288 |
-
"negotiation": "Hi [Name],\n\nThe non-compete clause is exceptionally broad β 18 months, worldwide, covering any business the company chooses to label as a competitor. This would make it nearly impossible for me to find work in my field after leaving.\n\nI've proposed a revised version that's far more reasonable: 12 months, limited to direct competitors (objectively defined), and restricted to regions where the company actually operates. This still protects your legitimate business interests without unfairly restricting my career.\n\nWould this work for you?\n\nThanks,\n[Your Name]",
|
| 289 |
-
"impacts": ["You may be unable to work in your entire industry for a year and a half after leaving β regardless of where you live", "A company in a different country doing vaguely related work could trigger the restriction", "The 'sole discretion' language means the company can retroactively decide you violated it"],
|
| 290 |
-
},
|
| 291 |
-
{
|
| 292 |
-
"text": "Company shall have no liability to Employee for any indirect, incidental, special, consequential, or punitive damages arising out of this Agreement, regardless of the theory of liability, even if Company has been advised of the possibility of such damages. In no event shall Company's total aggregate liability exceed the lesser of (a) $1,000 or (b) one month of Employee's base salary.",
|
| 293 |
-
"ctype": "LIABILITY_CAP", "sev": "HIGH",
|
| 294 |
-
"title": "Extremely Low Liability Cap with Unlimited Damage Waiver",
|
| 295 |
-
"reason": "The company caps its liability at just $1,000 or one month's salary (whichever is lower) and completely waives all indirect, consequential, and punitive damages β even if they knowingly caused harm.",
|
| 296 |
-
"plain": "No matter what the company does to you β even if they intentionally harm you β the most you can ever recover is $1,000 or one month's pay. You cannot claim any additional damages for lost opportunities, emotional distress, or other consequences.",
|
| 297 |
-
"action": "Negotiate liability cap to at least 12 months' salary or the full value of the contract. Remove the blanket waiver of consequential damages for cases of willful misconduct.",
|
| 298 |
-
"safer": "Company's total aggregate liability under this Agreement shall not exceed the greater of (a) twelve (12) months of Employee's base salary or (b) $50,000. This limitation shall not apply to damages arising from Company's willful misconduct, gross negligence, fraud, or violation of applicable law.",
|
| 299 |
-
"negotiation": "Hi [Name],\n\nThe liability cap at $1,000 or one month's salary is extremely low β it basically means the company faces no meaningful consequences even for serious violations of the agreement. I'd like to propose a more balanced cap at 12 months' salary or $50,000, with an exception for willful misconduct and fraud.\n\nThis is standard for contracts like this and ensures both parties have real skin in the game. Let me know your thoughts.\n\nBest,\n[Your Name]",
|
| 300 |
-
"impacts": ["If the company breaches the contract and costs you your career, the most you get is $1,000", "You cannot recover for lost job opportunities, relocation costs, or emotional distress caused by the company's actions"],
|
| 301 |
-
},
|
| 302 |
-
{
|
| 303 |
-
"text": "Company may terminate this Agreement and Employee's engagement at any time, with or without cause, upon providing one (1) day written notice. Upon termination, Employee shall receive no severance, continuation of benefits, or compensation of any kind other than base salary earned through the date of termination.",
|
| 304 |
-
"ctype": "TERMINATION", "sev": "HIGH",
|
| 305 |
-
"title": "No-Notice At-Will Termination with Zero Severance",
|
| 306 |
-
"reason": "The company can fire you with just 1 day notice for any reason β or no reason at all β and you walk away with absolutely nothing: no severance, no benefits continuation, no compensation of any kind.",
|
| 307 |
-
"plain": "The company can fire you tomorrow with one day's notice and pay you nothing beyond what you already earned. No severance, no health insurance continuation, no transition support β you're on your own immediately.",
|
| 308 |
-
"action": "Negotiate a minimum 30-day notice period (or pay in lieu) and at least 2-4 weeks of severance, especially for termination without cause.",
|
| 309 |
-
"safer": "Either party may terminate this Agreement upon thirty (30) days written notice. In the event of termination by Company without cause, Employee shall receive severance equal to four (4) weeks of base salary and continuation of health benefits for thirty (30) days. Termination with cause requires written documentation of the specific cause.",
|
| 310 |
-
"negotiation": "Hi [Name],\n\nI noticed the termination clause allows the company to end the relationship with essentially zero notice and provides no severance or benefits continuation whatsoever. This creates significant financial risk for me.\n\nI'd suggest a more balanced approach: 30 days' notice on both sides, plus a modest severance of 4 weeks' salary if terminated without cause. This is standard practice and ensures stability for both parties.\n\nWould you be open to discussing this?\n\nThanks,\n[Your Name]",
|
| 311 |
-
"impacts": ["You could lose your job with no warning and no financial cushion whatsoever", "You immediately lose health insurance with no COBRA or continuation option provided", "The company can fire you for an arbitrary reason with no documentation required"],
|
| 312 |
-
},
|
| 313 |
-
{
|
| 314 |
-
"text": "This Agreement shall automatically renew for successive one-year terms unless either party provides written notice of non-renewal at least ninety (90) days prior to the end of the then-current term.",
|
| 315 |
-
"ctype": "AUTO_RENEWAL", "sev": "MEDIUM",
|
| 316 |
-
"title": "Auto-Renewal with Long 90-Day Notice Window",
|
| 317 |
-
"reason": "Contract auto-renews annually and requires 90-day notice to cancel β much longer than the standard 30-day notice. Easy to miss the window.",
|
| 318 |
-
"plain": "This agreement renews automatically every year. You must give 90 days' written notice (3 months!) to cancel β miss that window and you're locked in for another full year.",
|
| 319 |
-
"action": "Reduce notice period to 30 days and request automatic email reminders 45 days before each renewal.",
|
| 320 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 321 |
-
},
|
| 322 |
-
{
|
| 323 |
-
"text": "The Recipient shall not disclose any Confidential Information to any third party without prior written consent of the Disclosing Party. 'Confidential Information' means all information disclosed by the Disclosing Party, whether oral, written, or in any other form, regardless of whether it is marked 'confidential.' The obligation of confidentiality shall survive termination of this Agreement indefinitely.",
|
| 324 |
-
"ctype": "NDA", "sev": "MEDIUM",
|
| 325 |
-
"title": "Overly Broad and Perpetual Confidentiality Obligation",
|
| 326 |
-
"reason": "Defines confidential information to include ALL information shared β even oral conversations and unmarked documents β and the obligation lasts forever with no expiration.",
|
| 327 |
-
"plain": "Anything the company tells you β even casual conversations or unmarked documents β counts as confidential, and you must keep it secret forever. There's no time limit and no exception for information that becomes public.",
|
| 328 |
-
"action": "Request that only written information marked 'confidential' be covered, and add a reasonable time limit (e.g., 3-5 years) or a public-domain exception.",
|
| 329 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 330 |
-
},
|
| 331 |
-
{
|
| 332 |
-
"text": "Employee agrees to indemnify, defend, and hold harmless Company and its officers, directors, employees, and agents from and against any and all claims, damages, losses, liabilities, costs, and expenses (including reasonable attorneys' fees) arising out of or related to Employee's performance under this Agreement, regardless of whether Company was negligent or at fault.",
|
| 333 |
-
"ctype": "INDEMNIFICATION", "sev": "HIGH",
|
| 334 |
-
"title": "One-Sided Indemnification Covering Company's Own Negligence",
|
| 335 |
-
"reason": "You must pay for ALL legal costs and damages β even those caused by the company's own negligence β with no reciprocal obligation from the company. This exposes you to unlimited financial liability.",
|
| 336 |
-
"plain": "If the company does something negligent and gets sued, you have to pay all their legal bills and any damages they owe β even though it was their fault. Meanwhile, the company has no obligation to cover you for anything.",
|
| 337 |
-
"action": "Make indemnification mutual (both parties cover each other) and exclude claims arising from the other party's own negligence or misconduct.",
|
| 338 |
-
"safer": "Each party shall indemnify and hold harmless the other party from claims arising from the indemnifying party's own negligence or willful misconduct. Neither party shall be required to indemnify the other for claims arising from the other party's own fault. This obligation is mutual and reciprocal.",
|
| 339 |
-
"negotiation": "Hi [Name],\n\nThe indemnification clause is entirely one-sided β I'm responsible for covering the company's legal costs even when the company is at fault, but the company covers nothing for me. This creates potentially unlimited financial exposure.\n\nI've proposed a mutual version where each party covers claims arising from their own actions, not the other's. This is standard and fair. Can we discuss?\n\nThanks,\n[Your Name]",
|
| 340 |
-
"impacts": ["You could be forced to pay hundreds of thousands in legal fees for a lawsuit caused by the company's own negligence", "Your personal assets (savings, home) could be at risk with no cap on liability"],
|
| 341 |
-
},
|
| 342 |
-
{
|
| 343 |
-
"text": "The Recipient shall not collect, store, process, or transmit any personal data of third parties without obtaining prior express written consent and implementing reasonable security measures. Any data shared with third-party service providers must be governed by a written data processing agreement.",
|
| 344 |
-
"ctype": "DATA_SHARING", "sev": "LOW",
|
| 345 |
-
"title": "Standard Data Protection Clause",
|
| 346 |
-
"reason": "Standard data protection language requiring consent and security measures before handling personal data β no unusual or risky provisions.",
|
| 347 |
-
"plain": "You must get written permission and use proper security before handling anyone's personal data. This is standard practice and protects everyone involved.",
|
| 348 |
-
"action": "No action needed β this is standard and reasonable.",
|
| 349 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 350 |
-
},
|
| 351 |
-
{
|
| 352 |
-
"text": "Invoices shall be submitted monthly and payment shall be made within sixty (60) days of receipt of a properly submitted invoice. Late payments shall accrue interest at a rate of 1.5% per month.",
|
| 353 |
-
"ctype": "PAYMENT", "sev": "MEDIUM",
|
| 354 |
-
"title": "Net-60 Payment Terms with High Late Interest",
|
| 355 |
-
"reason": "Net-60 payment terms (2 months to get paid) are significantly longer than standard Net-30, and the 1.5% monthly late fee compounds to over 19% annually.",
|
| 356 |
-
"plain": "You submit invoices monthly but the company has 60 days (2 months) to pay you. If they're late, they add 1.5% monthly interest β which sounds good but means you wait a long time for your money.",
|
| 357 |
-
"action": "Negotiate Net-30 payment terms and reduce late interest to a standard rate (e.g., 8-10% annually).",
|
| 358 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 359 |
-
},
|
| 360 |
-
{
|
| 361 |
-
"text": "This Agreement shall be governed by and construed in accordance with the laws of the State of New York, without regard to its conflict of laws principles. Any legal action shall be brought exclusively in the state or federal courts located in New York County, New York.",
|
| 362 |
-
"ctype": "GOVERNING_LAW", "sev": "LOW",
|
| 363 |
-
"title": "Standard New York Governing Law and Venue",
|
| 364 |
-
"reason": "Standard choice-of-law and venue clause selecting New York, a common jurisdiction for commercial contracts.",
|
| 365 |
-
"plain": "This agreement is governed by New York law, and any court cases must be handled in New York courts β standard practice for many U.S. contracts.",
|
| 366 |
-
"action": "No action needed unless you're located far from New York and would prefer a more convenient venue.",
|
| 367 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 368 |
-
},
|
| 369 |
-
{
|
| 370 |
-
"text": "If any provision of this Agreement is found to be invalid or unenforceable, the remaining provisions shall continue in full force and effect. This Agreement constitutes the entire agreement between the parties and supersedes all prior agreements, whether written or oral. No modification shall be effective unless in writing and signed by both parties.",
|
| 371 |
-
"ctype": "OTHER", "sev": "INFO",
|
| 372 |
-
"title": "Standard Severability and Entire Agreement",
|
| 373 |
-
"reason": "Standard boilerplate provisions covering severability (invalid parts don't void the whole agreement), entire agreement (this document is the complete deal), and written modification requirement.",
|
| 374 |
-
"plain": "Standard legal wrap-up: if one part of this contract is found invalid, the rest still stands. This document is the complete agreement, and any changes must be in writing and signed.",
|
| 375 |
-
"action": "No action needed β these are standard boilerplate provisions found in virtually every contract.",
|
| 376 |
-
"safer": "", "negotiation": "", "impacts": [],
|
| 377 |
-
},
|
| 378 |
-
]
|
| 379 |
-
|
| 380 |
-
scored = []
|
| 381 |
-
for i, d in enumerate(demo_clauses, 1):
|
| 382 |
-
clause = Clause(
|
| 383 |
-
id=i,
|
| 384 |
-
raw_text=d["text"],
|
| 385 |
-
plain_english=d["plain"],
|
| 386 |
-
clause_type=ClauseType(d["ctype"]),
|
| 387 |
-
section_heading=d["ctype"].replace("_", " "),
|
| 388 |
-
position=i,
|
| 389 |
-
confidence_score=0.95,
|
| 390 |
-
)
|
| 391 |
-
finding = RiskFinding(
|
| 392 |
-
clause_id=i,
|
| 393 |
-
severity=Severity(d["sev"]),
|
| 394 |
-
risk_title=d["title"],
|
| 395 |
-
risk_reason=d["reason"],
|
| 396 |
-
recommended_action=d["action"],
|
| 397 |
-
safer_clause_version=d["safer"],
|
| 398 |
-
negotiation_message=d["negotiation"],
|
| 399 |
-
impact_scenarios=d["impacts"],
|
| 400 |
-
)
|
| 401 |
-
scored.append(ScoredClause(clause=clause, finding=finding))
|
| 402 |
-
|
| 403 |
-
crit = sum(1 for s in scored if s.finding.severity == Severity.CRITICAL)
|
| 404 |
-
high = sum(1 for s in scored if s.finding.severity == Severity.HIGH)
|
| 405 |
-
med = sum(1 for s in scored if s.finding.severity == Severity.MEDIUM)
|
| 406 |
-
low = sum(1 for s in scored if s.finding.severity == Severity.LOW)
|
| 407 |
-
info = sum(1 for s in scored if s.finding.severity == Severity.INFO)
|
| 408 |
-
total = len(scored)
|
| 409 |
-
raw_score = (crit * 10 + high * 7 + med * 4 + low * 1) / total
|
| 410 |
-
overall = round(min(raw_score, 10.0), 1)
|
| 411 |
-
|
| 412 |
-
dt = datetime.now().strftime('%B %d, %Y at %H:%M')
|
| 413 |
-
|
| 414 |
-
markdown = f"""# ClauseGuard Risk Analysis Report
|
| 415 |
-
|
| 416 |
-
**Contract:** sample_employment_agreement.txt (Demo)
|
| 417 |
-
**Type:** Employment
|
| 418 |
-
**Overall Risk Score:** {overall}/10
|
| 419 |
-
**Generated:** {dt}
|
| 420 |
-
|
| 421 |
-
---
|
| 422 |
-
|
| 423 |
-
## Executive Summary
|
| 424 |
-
|
| 425 |
-
This employment agreement contains **{crit} critical** and **{high} high-severity** risks that demand immediate attention before signing. The most severe issues involve an overly broad IP assignment clause that claims ownership of personal projects, mandatory arbitration waiving all court rights, and a worldwide non-compete with unlimited company discretion. We strongly recommend negotiating the top 3 actions below.
|
| 426 |
-
|
| 427 |
-
---
|
| 428 |
-
|
| 429 |
-
## Top 3 Actions Before Signing
|
| 430 |
-
|
| 431 |
-
1. **Restrict IP Assignment** β Demand a carve-out excluding personal projects made on your own time and equipment unrelated to company business.
|
| 432 |
-
2. **Add Arbitration Opt-Out** β Request a 30-day window to opt out of binding arbitration and preserve your right to go to court.
|
| 433 |
-
3. **Limit the Non-Compete** β Reduce duration to 12 months and restrict geographic scope to regions where the company actually operates.
|
| 434 |
-
|
| 435 |
-
---
|
| 436 |
-
|
| 437 |
-
## Risk Summary
|
| 438 |
-
|
| 439 |
-
| Severity | Count |
|
| 440 |
-
|----------|-------|
|
| 441 |
-
| π΄ Critical | {crit} |
|
| 442 |
-
| π High | {high} |
|
| 443 |
-
| π‘ Medium | {med} |
|
| 444 |
-
| π’ Low | {low} |
|
| 445 |
-
| βΉοΈ Info | {info} |
|
| 446 |
-
|
| 447 |
-
**Total Clauses Analyzed:** {total}
|
| 448 |
-
|
| 449 |
-
---
|
| 450 |
-
|
| 451 |
-
## Clause-by-Clause Analysis
|
| 452 |
-
|
| 453 |
-
"""
|
| 454 |
-
|
| 455 |
-
for sc in scored:
|
| 456 |
-
emoji_map = {"CRITICAL": "π΄", "HIGH": "π ", "MEDIUM": "π‘", "LOW": "π’", "INFO": "βΉοΈ"}
|
| 457 |
-
emoji = emoji_map.get(sc.finding.severity.value, "βͺ")
|
| 458 |
-
markdown += f"""### {sc.clause.clause_type.value} β {emoji} {sc.finding.severity.value}
|
| 459 |
-
|
| 460 |
-
**Original Text:**
|
| 461 |
-
{sc.clause.raw_text}
|
| 462 |
-
|
| 463 |
-
**Plain English:**
|
| 464 |
-
{sc.clause.plain_english or 'N/A'}
|
| 465 |
-
|
| 466 |
-
**Risk Assessment:** {sc.finding.risk_reason}
|
| 467 |
-
|
| 468 |
-
**Recommended Action:** {sc.finding.recommended_action}
|
| 469 |
-
|
| 470 |
-
"""
|
| 471 |
-
|
| 472 |
-
if sc.finding.safer_clause_version:
|
| 473 |
-
markdown += f"**Safer Alternative:** {sc.finding.safer_clause_version}\n\n"
|
| 474 |
-
if sc.finding.negotiation_message:
|
| 475 |
-
markdown += f"**Negotiation Script:**\n\n{sc.finding.negotiation_message}\n\n"
|
| 476 |
-
if sc.finding.impact_scenarios:
|
| 477 |
-
markdown += "**Potential Consequences:**\n"
|
| 478 |
-
for impact in sc.finding.impact_scenarios:
|
| 479 |
-
markdown += f"- {impact}\n"
|
| 480 |
-
markdown += "\n"
|
| 481 |
-
markdown += "---\n\n"
|
| 482 |
-
|
| 483 |
-
markdown += "\n*Generated by ClauseGuard AI β’ Powered by Qwen2.5 via vLLM on AMD β’ Not legal advice*\n"
|
| 484 |
-
|
| 485 |
-
return FinalReport(
|
| 486 |
-
contract_name="Sample Employment Agreement (Demo)",
|
| 487 |
-
generated_at=datetime.now(),
|
| 488 |
-
summary={
|
| 489 |
-
"total_clauses": total,
|
| 490 |
-
"critical_count": crit,
|
| 491 |
-
"high_count": high,
|
| 492 |
-
"medium_count": med,
|
| 493 |
-
"low_count": low,
|
| 494 |
-
"overall_score": overall,
|
| 495 |
-
"contract_type": "Employment",
|
| 496 |
-
},
|
| 497 |
-
top_3_actions=[
|
| 498 |
-
"Restrict IP assignment to work directly related to company business, created during work hours using company resources",
|
| 499 |
-
"Add a 30-day opt-out window for binding arbitration to preserve your right to go to court",
|
| 500 |
-
"Limit non-compete to 12 months in specific regions where the company has active operations",
|
| 501 |
-
],
|
| 502 |
-
scored_clauses=scored,
|
| 503 |
-
markdown_report=markdown,
|
| 504 |
-
processed_normally=False,
|
| 505 |
-
)
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
def _load_demo_report() -> None:
|
| 509 |
-
st.session_state.report = _build_demo_report()
|
| 510 |
-
st.session_state.error = None
|
| 511 |
-
st.session_state.uploaded_filename = "sample_nda.txt"
|
| 512 |
-
demo_raw = ""
|
| 513 |
-
for sc in st.session_state.report.scored_clauses:
|
| 514 |
-
heading = sc.clause.section_heading or ""
|
| 515 |
-
text = sc.clause.raw_text
|
| 516 |
-
demo_raw += f"{heading}\n{text}\n\n" if heading else f"{text}\n\n"
|
| 517 |
-
st.session_state.copilot_raw_text = demo_raw.strip()
|
| 518 |
-
st.session_state.active_tab = 0
|
| 519 |
-
st.session_state.copilot_messages = []
|
| 520 |
-
st.session_state.clause_ai_responses = {}
|
| 521 |
-
st.session_state.generated_emails = {}
|
| 522 |
-
st.session_state.copilot_cache_key = None
|
| 523 |
-
st.rerun()
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
def _load_guided_demo() -> None:
|
| 527 |
-
st.session_state.guided_demo = True
|
| 528 |
-
st.session_state.demo_step = 0
|
| 529 |
-
_load_demo_report()
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 533 |
-
# SESSION STATE
|
| 534 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 535 |
-
|
| 536 |
-
def _init_session_state() -> None:
|
| 537 |
-
defaults = {
|
| 538 |
-
"report": None,
|
| 539 |
-
"error": None,
|
| 540 |
-
"analyzing": False,
|
| 541 |
-
"uploaded_filename": None,
|
| 542 |
-
"uploaded_bytes": None,
|
| 543 |
-
"agent_statuses": {a: "pending" for a in AGENT_NAMES},
|
| 544 |
-
"agent_messages": {a: "" for a in AGENT_NAMES},
|
| 545 |
-
"guided_demo": False,
|
| 546 |
-
"demo_step": 0,
|
| 547 |
-
"copilot_messages": [],
|
| 548 |
-
"copilot_context": "",
|
| 549 |
-
"copilot_raw_text": "",
|
| 550 |
-
"copilot_cache_key": None,
|
| 551 |
-
"clause_ai_responses": {},
|
| 552 |
-
"pending_ai_query": None,
|
| 553 |
-
"generated_emails": {},
|
| 554 |
-
"active_tab": 0,
|
| 555 |
-
"highlight_clause_id": None,
|
| 556 |
-
}
|
| 557 |
-
for key, default in defaults.items():
|
| 558 |
-
if key not in st.session_state:
|
| 559 |
-
st.session_state[key] = default
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 563 |
-
# LIVE AGENT EVENT HANDLER
|
| 564 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 565 |
-
|
| 566 |
-
def _on_agent_event(agent: str, status: str, details: dict) -> None:
|
| 567 |
-
st.session_state.agent_statuses[agent] = status
|
| 568 |
-
st.session_state.agent_messages[agent] = details.get("message", "")
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 572 |
-
# ANALYSIS RUNNER
|
| 573 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 574 |
-
|
| 575 |
-
def _run_analysis() -> None:
|
| 576 |
-
file_bytes = st.session_state.uploaded_bytes
|
| 577 |
-
filename = st.session_state.uploaded_filename
|
| 578 |
-
try:
|
| 579 |
-
validate_config()
|
| 580 |
-
except ValueError as e:
|
| 581 |
-
st.session_state.error = str(e)
|
| 582 |
-
st.session_state.analyzing = False
|
| 583 |
-
return
|
| 584 |
-
|
| 585 |
-
for a in AGENT_NAMES:
|
| 586 |
-
st.session_state.agent_statuses[a] = "pending"
|
| 587 |
-
st.session_state.agent_messages[a] = ""
|
| 588 |
-
|
| 589 |
-
set_event_callback(_on_agent_event)
|
| 590 |
-
|
| 591 |
-
progress_bar = st.progress(0)
|
| 592 |
-
status_text = st.empty()
|
| 593 |
-
agent_panel = st.empty()
|
| 594 |
-
|
| 595 |
-
try:
|
| 596 |
-
status_text.markdown("<h3 style='color:#fff'>π Reading file...</h3>", unsafe_allow_html=True)
|
| 597 |
-
raw_text = extract_text(file_bytes, filename)
|
| 598 |
-
st.session_state.copilot_raw_text = raw_text
|
| 599 |
-
|
| 600 |
-
status_text.markdown("<h3 style='color:#8ab4f8'>π Testing model connection...</h3>", unsafe_allow_html=True)
|
| 601 |
-
ok, conn_err = _check_model_connectivity()
|
| 602 |
-
if not ok:
|
| 603 |
-
st.session_state.error = f"Cannot connect to model API: {conn_err}"
|
| 604 |
-
st.session_state.analyzing = False
|
| 605 |
-
progress_bar.empty()
|
| 606 |
-
status_text.empty()
|
| 607 |
-
agent_panel.empty()
|
| 608 |
-
st.rerun()
|
| 609 |
-
return
|
| 610 |
-
|
| 611 |
-
status_text.markdown("<h3 style='color:#8ab4f8'>π€ Running AI analysis pipeline...</h3>", unsafe_allow_html=True)
|
| 612 |
-
|
| 613 |
-
def _render_agent_panel():
|
| 614 |
-
rows = ""
|
| 615 |
-
for a in AGENT_NAMES:
|
| 616 |
-
step = AGENT_STEP_NUMBERS.get(a, "")
|
| 617 |
-
s = st.session_state.agent_statuses[a]
|
| 618 |
-
icon = AGENT_ICONS.get(s, "β³")
|
| 619 |
-
msg = st.session_state.agent_messages.get(a, "")
|
| 620 |
-
if s == "completed":
|
| 621 |
-
color = "#55dd55"
|
| 622 |
-
anim = ""
|
| 623 |
-
elif s == "failed":
|
| 624 |
-
color = "#ff4444"
|
| 625 |
-
anim = ""
|
| 626 |
-
elif s == "running":
|
| 627 |
-
color = "#ffaa44"
|
| 628 |
-
anim = " class='agent-running'"
|
| 629 |
-
else:
|
| 630 |
-
color = "#666"
|
| 631 |
-
anim = ""
|
| 632 |
-
rows += f"<tr{anim}><td style='padding:8px 12px;text-align:center;font-size:1.1rem'>{step}</td><td style='padding:8px 12px'>{icon}</td><td style='padding:8px 12px;color:{color};font-weight:600'>{a}</td><td style='padding:8px 12px;color:#aaa;font-size:0.85rem'>{msg}</td></tr>"
|
| 633 |
-
return f"<div style='background:#1a1a2e;border-radius:14px;padding:1.25rem;border:1px solid rgba(255,255,255,0.08)'><table style='width:100%;border-collapse:collapse'><thead><tr><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px'>Step</th><th style='padding:6px 12px'></th><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px;text-align:left'>Agent</th><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px;text-align:left'>Status</th></tr></thead><tbody>{rows}</tbody></table></div>"
|
| 634 |
-
|
| 635 |
-
agent_panel.markdown(_render_agent_panel(), unsafe_allow_html=True)
|
| 636 |
-
|
| 637 |
-
loop = asyncio.new_event_loop()
|
| 638 |
-
asyncio.set_event_loop(loop)
|
| 639 |
-
try:
|
| 640 |
-
report = loop.run_until_complete(run_pipeline(raw_text, filename))
|
| 641 |
-
finally:
|
| 642 |
-
loop.close()
|
| 643 |
-
|
| 644 |
-
for a in AGENT_NAMES:
|
| 645 |
-
if st.session_state.agent_statuses[a] == "pending":
|
| 646 |
-
st.session_state.agent_statuses[a] = "completed"
|
| 647 |
-
st.session_state.agent_messages[a] = "OK"
|
| 648 |
-
agent_panel.markdown(_render_agent_panel(), unsafe_allow_html=True)
|
| 649 |
-
|
| 650 |
-
progress_bar.progress(1.0)
|
| 651 |
-
|
| 652 |
-
if report.summary.total_clauses == 0:
|
| 653 |
-
logger.error("Pipeline produced 0 clauses β model API may be unreachable or returned errors")
|
| 654 |
-
failed_agents = [
|
| 655 |
-
a for a in AGENT_NAMES
|
| 656 |
-
if st.session_state.agent_statuses.get(a) == "failed"
|
| 657 |
-
]
|
| 658 |
-
if failed_agents:
|
| 659 |
-
st.session_state.error = (
|
| 660 |
-
f"Analysis failed β the {failed_agents[0]} agent could not complete. "
|
| 661 |
-
"The model API may be unreachable or returned malformed responses. "
|
| 662 |
-
"Check that the vLLM endpoint is running at the configured BASE_URL."
|
| 663 |
-
)
|
| 664 |
-
else:
|
| 665 |
-
st.session_state.error = (
|
| 666 |
-
"Analysis could not extract any clauses from the document. "
|
| 667 |
-
"The model may be unavailable or the document format may be unsupported. "
|
| 668 |
-
"Check your model endpoint configuration."
|
| 669 |
-
)
|
| 670 |
-
status_text.markdown("<h3 style='color:#ff4444'>β Analysis failed</h3>", unsafe_allow_html=True)
|
| 671 |
-
st.session_state.report = None
|
| 672 |
-
st.session_state.analyzing = False
|
| 673 |
-
progress_bar.empty()
|
| 674 |
-
status_text.empty()
|
| 675 |
-
agent_panel.empty()
|
| 676 |
-
st.rerun()
|
| 677 |
-
return
|
| 678 |
-
|
| 679 |
-
status_text.markdown("<h3 style='color:#55dd55'>β
Analysis complete!</h3>", unsafe_allow_html=True)
|
| 680 |
-
st.session_state.report = report
|
| 681 |
-
st.session_state.error = None
|
| 682 |
-
st.session_state.copilot_messages = []
|
| 683 |
-
st.session_state.clause_ai_responses = {}
|
| 684 |
-
st.session_state.generated_emails = {}
|
| 685 |
-
|
| 686 |
-
if not report.processed_normally or report.summary.critical_count == 0 and report.summary.high_count == 0 and report.summary.medium_count == 0:
|
| 687 |
-
st.session_state.error = (
|
| 688 |
-
"Analysis completed but no significant risks were detected. "
|
| 689 |
-
"The model responses may have been incomplete β review the "
|
| 690 |
-
f"report ({report.summary.total_clauses} clauses analyzed) carefully."
|
| 691 |
-
)
|
| 692 |
-
|
| 693 |
-
except ValueError as e:
|
| 694 |
-
st.session_state.error = f"Could not process: {e}"
|
| 695 |
-
except Exception as e:
|
| 696 |
-
st.session_state.error = "An unexpected error occurred. Try again."
|
| 697 |
-
logger.error("Analysis error: %s", e)
|
| 698 |
-
finally:
|
| 699 |
-
st.session_state.analyzing = False
|
| 700 |
-
progress_bar.empty()
|
| 701 |
-
status_text.empty()
|
| 702 |
-
agent_panel.empty()
|
| 703 |
-
st.rerun()
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββ
|
| 707 |
-
# FALLBACK GENERATORS FOR NEGOTIATION COPILOT
|
| 708 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 709 |
-
|
| 710 |
-
def _generate_fallback_safer(sc: ScoredClause) -> str:
|
| 711 |
-
ctype = sc.clause.clause_type.value
|
| 712 |
-
fallbacks = {
|
| 713 |
-
"IP_ASSIGNMENT": "Employee assigns only inventions directly related to Company's business, created during working hours using Company resources. Personal projects remain Employee's property.",
|
| 714 |
-
"ARBITRATION": "Either party may opt out of arbitration within 30 days. Both parties retain the right to bring claims in court.",
|
| 715 |
-
"NON_COMPETE": "Non-compete limited to 12 months within specific metro areas where Company operates.",
|
| 716 |
-
"AUTO_RENEWAL": "Agreement renews only with mutual written consent. No automatic renewal.",
|
| 717 |
-
"TERMINATION": "Either party may terminate with 30 days written notice.",
|
| 718 |
-
"INDEMNIFICATION": "Indemnification limited to direct damages caused by negligence or willful misconduct.",
|
| 719 |
-
"LIABILITY_CAP": "Liability capped at the greater of fees paid or $10,000.",
|
| 720 |
-
"DATA_SHARING": "Data shared only with explicit opt-in consent, revocable at any time.",
|
| 721 |
-
"GOVERNING_LAW": "Governing law set to user's home state with optional mediation.",
|
| 722 |
-
"PAYMENT": "Payment due net-30 after invoice receipt. Late fees capped at 5% annually.",
|
| 723 |
-
"CONFIDENTIALITY": "Confidential information excludes publicly available data and independently developed knowledge.",
|
| 724 |
-
"NON_SOLICITATION": "Non-solicitation limited to 12 months and applies only to employees directly worked with.",
|
| 725 |
-
"FORCE_MAJEURE": "Neither party liable for delays due to circumstances beyond reasonable control, with prompt notice.",
|
| 726 |
-
"SEVERABILITY": "If any provision is found unenforceable, remaining provisions stay in full effect.",
|
| 727 |
-
"ASSIGNMENT": "Neither party may assign without written consent, not to be unreasonably withheld.",
|
| 728 |
-
"WAIVER": "Failure to enforce any provision does not constitute waiver. Waivers must be in writing.",
|
| 729 |
-
"SURVIVAL": "Confidentiality, indemnification, and payment obligations survive termination.",
|
| 730 |
-
"NOTICE": "Notices effective upon email delivery with read receipt or 3 days after certified mail.",
|
| 731 |
-
}
|
| 732 |
-
return fallbacks.get(ctype, "Request a mutual agreement: both parties share rights and obligations equally. Remove one-sided provisions.")
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
def _generate_fallback_message(sc: ScoredClause) -> str:
|
| 736 |
-
topic = sc.clause.section_heading or sc.clause.clause_type.value.replace("_", " ").title()
|
| 737 |
-
safer = sc.finding.safer_clause_version or _generate_fallback_safer(sc)
|
| 738 |
-
return (
|
| 739 |
-
f"Hi,\n\nI've reviewed the contract and would like to discuss the {topic} clause. "
|
| 740 |
-
f"I'd suggest the following adjustment:\n\n'{safer}'\n\n"
|
| 741 |
-
f"This ensures both parties are treated fairly. Would you be open to this change?\n\nThanks!"
|
| 742 |
-
)
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
def _build_safer_contract(report: FinalReport) -> str:
|
| 746 |
-
lines: list[str] = []
|
| 747 |
-
lines.append(f"# SAFER VERSION β {report.contract_name}")
|
| 748 |
-
lines.append(f"# Auto-generated by ClauseGuard β replaces {report.summary.critical_count + report.summary.high_count} high-risk clauses")
|
| 749 |
-
lines.append(f"# Original risk score: {report.summary.overall_score}/10")
|
| 750 |
-
lines.append(f"# Generated: {datetime.now().strftime('%B %d, %Y at %H:%M')}")
|
| 751 |
-
lines.append("")
|
| 752 |
-
|
| 753 |
-
replaced_count = 0
|
| 754 |
-
for i, sc in enumerate(report.scored_clauses, 1):
|
| 755 |
-
safer = sc.finding.safer_clause_version
|
| 756 |
-
sev = sc.finding.severity
|
| 757 |
-
|
| 758 |
-
if safer and sev in (Severity.CRITICAL, Severity.HIGH):
|
| 759 |
-
replaced_count += 1
|
| 760 |
-
lines.append(f"# {'β' * 70}")
|
| 761 |
-
lines.append(f"# CLAUSE {i}: REPLACED β {sev.value} Risk β {sc.finding.risk_title}")
|
| 762 |
-
lines.append(f"# {'β' * 70}")
|
| 763 |
-
lines.append(f"# ORIGINAL (RISKY):")
|
| 764 |
-
for orig_line in sc.clause.raw_text.split("\n"):
|
| 765 |
-
lines.append(f"# {orig_line.strip()}")
|
| 766 |
-
lines.append(f"#")
|
| 767 |
-
lines.append(f"# SAFER VERSION:")
|
| 768 |
-
lines.append(f"{i}. {sc.clause.section_heading or 'CLAUSE ' + str(i)}")
|
| 769 |
-
lines.append(f" {safer}")
|
| 770 |
-
lines.append("")
|
| 771 |
-
else:
|
| 772 |
-
heading = sc.clause.section_heading or f"CLAUSE {i}"
|
| 773 |
-
lines.append(f"{i}. {heading}")
|
| 774 |
-
lines.append(f" {sc.clause.raw_text.strip()}")
|
| 775 |
-
lines.append("")
|
| 776 |
-
|
| 777 |
-
lines.append(f"# {'=' * 70}")
|
| 778 |
-
lines.append(f"# END OF SAFER CONTRACT")
|
| 779 |
-
lines.append(f"# {replaced_count} clauses replaced | {report.summary.total_clauses - replaced_count} left unchanged")
|
| 780 |
-
return "\n".join(lines)
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββ
|
| 784 |
-
# UI HELPER FUNCTIONS
|
| 785 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 786 |
-
|
| 787 |
-
def seats(n: int) -> str:
|
| 788 |
-
if n <= 0:
|
| 789 |
-
return "No parties"
|
| 790 |
-
if n == 1:
|
| 791 |
-
return "1 party"
|
| 792 |
-
return f"{n} parties"
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
def _render_info_card(title: str, body: str, icon: str = "βΉοΈ", bg: str = "rgba(30,144,255,0.08)", border: str = "#1e90ff") -> str:
|
| 796 |
-
return f"""<div style="background:{bg};border-left:4px solid {border};border-radius:4px 12px 12px 4px;padding:0.75rem 1rem;margin:0.4rem 0">
|
| 797 |
-
<span style="font-size:0.85rem;font-weight:600;color:#ccc">{icon} {title}</span>
|
| 798 |
-
<div style="font-size:0.82rem;color:#aaa;margin-top:0.25rem;line-height:1.5">{body}</div>
|
| 799 |
-
</div>"""
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
def _render_info_card_raw(html: str) -> None:
|
| 803 |
-
st.markdown(html, unsafe_allow_html=True)
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
def _switch_to_chat_with_prompt(prompt_text: str) -> None:
|
| 807 |
-
st.session_state.active_tab = 3
|
| 808 |
-
st.session_state.pending_ai_query = prompt_text
|
| 809 |
-
st.rerun()
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
def _render_single_clause_card(sc: ScoredClause, style: dict, show_actions: bool = True) -> None:
|
| 813 |
-
s = style
|
| 814 |
-
c = sc.clause
|
| 815 |
-
f = sc.finding
|
| 816 |
-
|
| 817 |
-
st.markdown(f"""
|
| 818 |
-
<div style="
|
| 819 |
-
background: linear-gradient(135deg, {s['bg']} 0%, rgba(20,22,30,0.6) 100%);
|
| 820 |
-
border: 1px solid {s['border']}22;
|
| 821 |
-
border-left: 4px solid {s['border']};
|
| 822 |
-
border-radius: 0 12px 12px 0;
|
| 823 |
-
padding: 1.25rem 1.25rem 0.75rem 1.25rem;
|
| 824 |
-
margin-bottom: 0.5rem;
|
| 825 |
-
">
|
| 826 |
-
<div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.75rem">
|
| 827 |
-
<span style="
|
| 828 |
-
display:inline-flex;align-items:center;gap:4px;
|
| 829 |
-
background:{s['tag_bg']};
|
| 830 |
-
color:{s['color']};
|
| 831 |
-
padding:0.25rem 0.75rem;
|
| 832 |
-
border-radius:20px;
|
| 833 |
-
font-size:0.73rem;
|
| 834 |
-
font-weight:700;
|
| 835 |
-
letter-spacing:0.4px;
|
| 836 |
-
text-transform:uppercase;
|
| 837 |
-
white-space:nowrap;
|
| 838 |
-
">{s['badge']}</span>
|
| 839 |
-
<span style="font-weight:600;color:#e8e8e8;font-size:1rem;line-height:1.3">{f.risk_title}</span>
|
| 840 |
-
</div>
|
| 841 |
-
<div style="display:flex;gap:1rem;margin-bottom:0.6rem">
|
| 842 |
-
<span style="color:#888;font-size:0.75rem">π {c.section_heading or ''}</span>
|
| 843 |
-
<span style="color:#888;font-size:0.75rem">π·οΈ {c.clause_type.value}</span>
|
| 844 |
-
<span style="color:#666;font-size:0.75rem">Clause #{c.id}</span>
|
| 845 |
-
</div>
|
| 846 |
-
</div>""", unsafe_allow_html=True)
|
| 847 |
-
|
| 848 |
-
with st.expander("π View Original Text"):
|
| 849 |
-
st.markdown(f"<div style='background:#1c1d2a;padding:0.85rem;border-radius:8px;font-family:Consolas,monospace;font-size:0.88rem;line-height:1.65;color:#d0d0d0;white-space:pre-wrap'>{c.raw_text}</div>", unsafe_allow_html=True)
|
| 850 |
-
|
| 851 |
-
if c.plain_english:
|
| 852 |
-
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:rgba(30,144,255,0.07);border-radius:8px;border:1px solid rgba(30,144,255,0.12)">
|
| 853 |
-
<span style="font-size:0.95rem;flex-shrink:0">π¬</span>
|
| 854 |
-
<span style="color:#c0cfe0;font-size:0.9rem;line-height:1.5">{c.plain_english}</span>
|
| 855 |
-
</div>""", unsafe_allow_html=True)
|
| 856 |
-
|
| 857 |
-
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:{s['bg']};border-radius:8px;border:1px solid {s['border']}18">
|
| 858 |
-
<span style="font-size:0.95rem;flex-shrink:0">β οΈ</span>
|
| 859 |
-
<div>
|
| 860 |
-
<div style="color:{s['color']};font-size:0.8rem;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:0.2rem">Risk</div>
|
| 861 |
-
<div style="color:#d0d0d0;font-size:0.9rem;line-height:1.55">{f.risk_reason}</div>
|
| 862 |
-
</div>
|
| 863 |
-
</div>""", unsafe_allow_html=True)
|
| 864 |
-
|
| 865 |
-
if f.recommended_action:
|
| 866 |
-
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:rgba(50,205,50,0.06);border-radius:8px;border:1px solid rgba(50,205,50,0.12)">
|
| 867 |
-
<span style="font-size:0.95rem;flex-shrink:0">β
</span>
|
| 868 |
-
<span style="color:#b0d0b0;font-size:0.9rem;line-height:1.5">{f.recommended_action}</span>
|
| 869 |
-
</div>""", unsafe_allow_html=True)
|
| 870 |
-
|
| 871 |
-
if f.impact_scenarios:
|
| 872 |
-
with st.expander("β οΈ What Could Happen If You Sign This"):
|
| 873 |
-
for impact in f.impact_scenarios:
|
| 874 |
-
st.markdown(f"<div style='background:rgba(255,68,68,0.06);padding:0.4rem 0.75rem;margin:0.15rem 0;border-radius:6px;border-left:3px solid {s['border']};font-size:0.85rem;color:#e0a0a0'>β’ {impact}</div>", unsafe_allow_html=True)
|
| 875 |
-
|
| 876 |
-
if show_actions and f.severity not in (Severity.LOW, Severity.INFO):
|
| 877 |
-
if st.button("βοΈ Ask AI to Explain", key=f"explain_{c.id}", use_container_width=True):
|
| 878 |
-
_switch_to_chat_with_prompt(f"Explain clause {c.id} ({f.risk_title}) in simple terms. What does this mean for me?")
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 882 |
-
# HEADER
|
| 883 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 884 |
-
|
| 885 |
-
def render_header() -> None:
|
| 886 |
-
hero_l, hero_r = st.columns([3, 1])
|
| 887 |
-
with hero_l:
|
| 888 |
-
st.markdown("""<div style="background:linear-gradient(135deg,#1e3a5f 0%,#2a5298 100%);padding:1.5rem 2rem;border-radius:16px;margin-bottom:0.5rem">
|
| 889 |
-
<h1 style="margin:0;color:#fff;font-size:2.2rem">π‘οΈ ClauseGuard</h1>
|
| 890 |
-
<p style="margin:0.25rem 0 0 0;color:#c8d8f0;font-size:1.1rem">AI-Powered Contract Clause Risk Analyzer</p>
|
| 891 |
-
</div>""", unsafe_allow_html=True)
|
| 892 |
-
with hero_r:
|
| 893 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 894 |
-
dc1, dc2 = st.columns(2)
|
| 895 |
-
with dc1:
|
| 896 |
-
if st.button("β‘ Instant Demo", use_container_width=True, help="See a pre-analyzed NDA report instantly"):
|
| 897 |
-
_load_demo_report()
|
| 898 |
-
with dc2:
|
| 899 |
-
if st.button("π¬ Guided Tour", use_container_width=True, help="Walk through a demo with highlights"):
|
| 900 |
-
_load_guided_demo()
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 904 |
-
# GUIDED DEMO TOUR
|
| 905 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 906 |
-
|
| 907 |
-
def _render_guided_tour() -> None:
|
| 908 |
-
if not st.session_state.get("guided_demo"):
|
| 909 |
-
return
|
| 910 |
-
step = st.session_state.get("demo_step", 0)
|
| 911 |
-
tour_steps = [
|
| 912 |
-
{
|
| 913 |
-
"title": "π― Welcome to ClauseGuard!",
|
| 914 |
-
"body": "Let's walk through a sample NDA contract analysis. You'll see how 5 AI agents work together to identify risks, explain legal jargon, and help you negotiate better terms. Each agent has a specific role in the pipeline.",
|
| 915 |
-
"tab": 0,
|
| 916 |
-
"icon": "π―",
|
| 917 |
-
},
|
| 918 |
-
{
|
| 919 |
-
"title": "π Step 1: Risk Overview Dashboard",
|
| 920 |
-
"body": "The **Overview tab** shows your contract's risk score, severity breakdown, and the top 3 actions you should take before signing. Check the bar chart to see how many clauses fall into each risk category. The risk score is calculated from 0 (safe) to 10 (extremely risky).",
|
| 921 |
-
"tab": 0,
|
| 922 |
-
"icon": "π",
|
| 923 |
-
},
|
| 924 |
-
{
|
| 925 |
-
"title": "π Step 2: Clause-by-Clause Deep Dive",
|
| 926 |
-
"body": "Switch to the **Clauses tab** to drill into each clause. Critical and High-risk clauses are expanded by default so you see the most dangerous issues first. Each clause card shows: original legal text, plain English translation, the specific risk reason, and a recommended action.",
|
| 927 |
-
"tab": 1,
|
| 928 |
-
"icon": "π",
|
| 929 |
-
},
|
| 930 |
-
{
|
| 931 |
-
"title": "π¬ Step 3: Negotiation Copilot",
|
| 932 |
-
"body": "In the **Negotiation tab**, you'll find side-by-side comparisons: what you signed vs. what you should ask for instead. Each risky clause comes with a pre-written negotiation message and a safer alternative. You can also download a fully rewritten 'Safer Contract' with all high-risk clauses replaced.",
|
| 933 |
-
"tab": 2,
|
| 934 |
-
"icon": "π¬",
|
| 935 |
-
},
|
| 936 |
-
{
|
| 937 |
-
"title": "π€ Step 4: AI Chat Assistant",
|
| 938 |
-
"body": "The **Chat Assistant tab** lets you ask follow-up questions in plain English. The AI has full context of your entire contract and all clause analyses. Try questions like 'Summarize this contract' or 'What's the most dangerous clause and why?' Use the quick-action chips for common questions.",
|
| 939 |
-
"tab": 3,
|
| 940 |
-
"icon": "π€",
|
| 941 |
-
},
|
| 942 |
-
{
|
| 943 |
-
"title": "β
You're Ready!",
|
| 944 |
-
"body": "Now you know your way around ClauseGuard. Use the **Instant Demo** button anytime to revisit this tour, or upload your own contract to run a real analysis with the full 5-agent AI pipeline. Remember: always consult a qualified attorney for final legal review.",
|
| 945 |
-
"tab": 0,
|
| 946 |
-
"icon": "β
",
|
| 947 |
-
},
|
| 948 |
-
]
|
| 949 |
-
|
| 950 |
-
if step < len(tour_steps):
|
| 951 |
-
ts = tour_steps[step]
|
| 952 |
-
progress_pct = (step + 1) / len(tour_steps)
|
| 953 |
-
with st.container():
|
| 954 |
-
st.markdown(f"""<div style="background:linear-gradient(135deg,#1e3a5f 0%,#2a5298 100%);padding:1.25rem 1.5rem;border-radius:14px;margin:0.5rem 0;border:1px solid rgba(255,255,255,0.1)">
|
| 955 |
-
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.25rem">
|
| 956 |
-
<span style="font-size:1.5rem">{ts['icon']}</span>
|
| 957 |
-
<h3 style="margin:0;color:#fff;font-size:1.2rem">{ts['title']}</h3>
|
| 958 |
-
</div>
|
| 959 |
-
<p style="color:#c8d8f0;margin:0.5rem 0;line-height:1.6">{ts['body']}</p>
|
| 960 |
-
<div style="background:rgba(255,255,255,0.1);border-radius:4px;height:4px;margin-top:0.75rem">
|
| 961 |
-
<div style="background:linear-gradient(90deg,#667eea,#764ba2);border-radius:4px;height:100%;width:{progress_pct*100:.0f}%"></div>
|
| 962 |
-
</div>
|
| 963 |
-
<div style="text-align:right;font-size:0.75rem;color:rgba(255,255,255,0.5);margin-top:0.25rem">Step {step + 1} of {len(tour_steps)}</div>
|
| 964 |
-
</div>""", unsafe_allow_html=True)
|
| 965 |
-
|
| 966 |
-
c1, c2, c3 = st.columns([1, 1, 1])
|
| 967 |
-
with c1:
|
| 968 |
-
if step > 0:
|
| 969 |
-
if st.button("β¬
οΈ Previous", key=f"tour_prev_{step}", use_container_width=True):
|
| 970 |
-
st.session_state.demo_step = step - 1
|
| 971 |
-
st.rerun()
|
| 972 |
-
with c3:
|
| 973 |
-
if st.button("Next β‘οΈ" if step < len(tour_steps) - 1 else "β
Finish Tour", key=f"tour_next_{step}", use_container_width=True):
|
| 974 |
-
if step < len(tour_steps) - 1:
|
| 975 |
-
st.session_state.demo_step = step + 1
|
| 976 |
-
tab_idx = tour_steps[step + 1]["tab"]
|
| 977 |
-
st.session_state.active_tab = tab_idx
|
| 978 |
-
else:
|
| 979 |
-
st.session_state.guided_demo = False
|
| 980 |
-
st.rerun()
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 984 |
-
# RISK BANNER
|
| 985 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 986 |
-
|
| 987 |
-
def render_risk_banner() -> None:
|
| 988 |
-
if not st.session_state.report:
|
| 989 |
-
return
|
| 990 |
-
r = st.session_state.report
|
| 991 |
-
s = r.summary
|
| 992 |
-
total_risky = s.critical_count + s.high_count
|
| 993 |
-
|
| 994 |
-
if total_risky >= 3:
|
| 995 |
-
st.error(f"π¨ **HIGH ALERT β {total_risky} critical or high-risk clauses detected!** Review carefully before signing. We strongly recommend negotiating these terms.")
|
| 996 |
-
elif total_risky > 0:
|
| 997 |
-
st.warning(f"β οΈ **This contract has {total_risky} high-risk clause(s)** β review carefully before signing")
|
| 998 |
-
elif s.medium_count > 0:
|
| 999 |
-
st.info(f"βΉοΈ **{s.medium_count} medium-risk clause(s) found** β this contract may need attention before signing")
|
| 1000 |
-
else:
|
| 1001 |
-
st.success("β
**This contract looks clean** β no high or critical risk clauses detected. Still review all terms before signing.")
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1005 |
-
# ISSUES SUMMARY (displays before tabs)
|
| 1006 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1007 |
-
|
| 1008 |
-
def render_issues_summary() -> None:
|
| 1009 |
-
report = st.session_state.report
|
| 1010 |
-
criticals = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.CRITICAL]
|
| 1011 |
-
highs = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.HIGH]
|
| 1012 |
-
mediums = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.MEDIUM]
|
| 1013 |
-
all_issues = criticals + highs + mediums
|
| 1014 |
-
|
| 1015 |
-
if not all_issues:
|
| 1016 |
-
if not report.processed_normally:
|
| 1017 |
-
st.warning(
|
| 1018 |
-
"β οΈ **Analysis was incomplete** β the AI risk scorer could not evaluate these clauses. "
|
| 1019 |
-
"All clauses are marked as MEDIUM 'Needs Human Review'. "
|
| 1020 |
-
"This typically means the model API is having issues. Check your vLLM endpoint configuration."
|
| 1021 |
-
)
|
| 1022 |
-
return
|
| 1023 |
-
st.success("β
No issues found β all clauses look reasonable. Use the tabs below to explore the full analysis.")
|
| 1024 |
-
return
|
| 1025 |
-
|
| 1026 |
-
st.markdown("## π Issues Found")
|
| 1027 |
-
total_labels = []
|
| 1028 |
-
if criticals:
|
| 1029 |
-
total_labels.append(f"{len(criticals)} critical")
|
| 1030 |
-
if highs:
|
| 1031 |
-
total_labels.append(f"{len(highs)} high")
|
| 1032 |
-
if mediums:
|
| 1033 |
-
total_labels.append(f"{len(mediums)} medium")
|
| 1034 |
-
st.caption(f"{len(all_issues)} clauses need attention β {', '.join(total_labels)}")
|
| 1035 |
-
|
| 1036 |
-
issue_cols = st.columns(min(len(all_issues), 3))
|
| 1037 |
-
for idx, sc in enumerate(all_issues):
|
| 1038 |
-
col_idx = idx % 3
|
| 1039 |
-
style = SEVERITY_STYLE.get(sc.finding.severity, SEVERITY_STYLE[Severity.INFO])
|
| 1040 |
-
with issue_cols[col_idx]:
|
| 1041 |
-
reason_preview = sc.finding.risk_reason[:120]
|
| 1042 |
-
if len(sc.finding.risk_reason) > 120:
|
| 1043 |
-
reason_preview += "..."
|
| 1044 |
-
st.markdown(
|
| 1045 |
-
f"""<div style="background:#1e1e2e;border-radius:12px;padding:1rem;margin:0.3rem 0;
|
| 1046 |
-
border-top:3px solid {style['border']};border-left:1px solid #333;border-right:1px solid #333;border-bottom:1px solid #333">
|
| 1047 |
-
<div style="font-weight:700;margin-bottom:0.3rem;font-size:0.8rem">{style['badge']}</div>
|
| 1048 |
-
<div style="font-size:0.9rem;color:#e0e0e0;line-height:1.4;margin-bottom:0.5rem"><b>{sc.finding.risk_title}</b></div>
|
| 1049 |
-
<div style="font-size:0.8rem;color:#aaa;line-height:1.4">{reason_preview}</div>
|
| 1050 |
-
</div>""",
|
| 1051 |
-
unsafe_allow_html=True,
|
| 1052 |
-
)
|
| 1053 |
-
st.markdown("")
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1057 |
-
# TAB 1: OVERVIEW
|
| 1058 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1059 |
-
|
| 1060 |
-
def render_overview_tab() -> None:
|
| 1061 |
-
report = st.session_state.report
|
| 1062 |
-
s = report.summary
|
| 1063 |
-
|
| 1064 |
-
st.markdown("### π Risk Score Dashboard")
|
| 1065 |
-
st.caption(f"Contract Type: **{s.contract_type}** β’ {s.total_clauses} clauses analyzed β’ {s.critical_count + s.high_count + s.medium_count} need attention")
|
| 1066 |
-
|
| 1067 |
-
col_a, col_b, col_c = st.columns([1, 2, 1])
|
| 1068 |
-
with col_a:
|
| 1069 |
-
score = s.overall_score
|
| 1070 |
-
if score >= 7:
|
| 1071 |
-
sc_color = "#ff4444"
|
| 1072 |
-
label = "High Risk"
|
| 1073 |
-
bg_glow = "rgba(255,68,68,0.08)"
|
| 1074 |
-
elif score >= 4:
|
| 1075 |
-
sc_color = "#ff8c00"
|
| 1076 |
-
label = "Medium Risk"
|
| 1077 |
-
bg_glow = "rgba(255,140,0,0.06)"
|
| 1078 |
-
else:
|
| 1079 |
-
sc_color = "#32cd32"
|
| 1080 |
-
label = "Low Risk"
|
| 1081 |
-
bg_glow = "rgba(50,205,50,0.06)"
|
| 1082 |
-
st.markdown(f"""<div style="background:#1e1e2e;border-radius:16px;padding:1.5rem;text-align:center;border:1px solid #333;box-shadow:0 0 30px {bg_glow}">
|
| 1083 |
-
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:2px">Risk Score</div>
|
| 1084 |
-
<div style="font-size:3.5rem;font-weight:900;color:{sc_color};line-height:1.1">{score}<span style="font-size:1.5rem;color:#666">/10</span></div>
|
| 1085 |
-
<div style="font-size:0.85rem;color:{sc_color};margin-top:0.2rem;font-weight:600">{label}</div>
|
| 1086 |
-
<div style="font-size:0.82rem;color:#aaa;margin-top:0.5rem">{s.critical_count}C Β· {s.high_count}H Β· {s.medium_count}M Β· {s.low_count}L</div>
|
| 1087 |
-
</div>""", unsafe_allow_html=True)
|
| 1088 |
-
with col_b:
|
| 1089 |
-
max_val = max(s.critical_count, s.high_count, s.medium_count, s.low_count,
|
| 1090 |
-
s.total_clauses - s.critical_count - s.high_count - s.medium_count - s.low_count, 1)
|
| 1091 |
-
chart_data = pd.DataFrame({
|
| 1092 |
-
"Severity": ["Critical", "High", "Medium", "Low", "Info"],
|
| 1093 |
-
"Count": [s.critical_count, s.high_count, s.medium_count, s.low_count,
|
| 1094 |
-
max(s.total_clauses - s.critical_count - s.high_count - s.medium_count - s.low_count, 0)],
|
| 1095 |
-
})
|
| 1096 |
-
st.bar_chart(chart_data.set_index("Severity"), use_container_width=True, height=220)
|
| 1097 |
-
with col_c:
|
| 1098 |
-
risky = s.critical_count + s.high_count + s.medium_count
|
| 1099 |
-
pct = (risky / s.total_clauses * 100) if s.total_clauses > 0 else 0
|
| 1100 |
-
if pct >= 50:
|
| 1101 |
-
attn_color = "#ff4444"
|
| 1102 |
-
attn_label = "Review Urgently"
|
| 1103 |
-
elif pct >= 25:
|
| 1104 |
-
attn_color = "#ff8c00"
|
| 1105 |
-
attn_label = "Needs Review"
|
| 1106 |
-
else:
|
| 1107 |
-
attn_color = "#32cd32"
|
| 1108 |
-
attn_label = "Mostly Clean"
|
| 1109 |
-
st.markdown(f"""<div style="background:#1e1e2e;border-radius:12px;padding:1.25rem;text-align:center;border:1px solid #333;height:100%">
|
| 1110 |
-
<div style="font-size:0.75rem;color:#888;text-transform:uppercase;letter-spacing:1px">Needs Attention</div>
|
| 1111 |
-
<div style="font-size:2.5rem;font-weight:900;color:{attn_color}">{risky}<span style="font-size:1rem;color:#666">/{s.total_clauses}</span></div>
|
| 1112 |
-
<div style="font-size:0.85rem;color:{attn_color};font-weight:500">{attn_label}</div>
|
| 1113 |
-
<div style="font-size:0.8rem;color:#888;margin-top:0.25rem">{pct:.0f}% of clauses</div>
|
| 1114 |
-
</div>""", unsafe_allow_html=True)
|
| 1115 |
-
|
| 1116 |
-
st.markdown("")
|
| 1117 |
-
st.markdown("### β‘ Top 3 Actions Before Signing")
|
| 1118 |
-
if report.top_3_actions:
|
| 1119 |
-
for i, action in enumerate(report.top_3_actions, 1):
|
| 1120 |
-
colors = ["#ff4444", "#ff8c00", "#ffd700"]
|
| 1121 |
-
emojis = ["β ", "β‘", "β’"]
|
| 1122 |
-
st.markdown(f"""<div style="background:#1e1e2e;border-radius:10px;padding:1rem 1.25rem;margin:0.4rem 0;
|
| 1123 |
-
border-left:4px solid {colors[i-1]}">
|
| 1124 |
-
<b style="color:#8ab4f8;font-size:1.1rem">{emojis[i-1]}</b>
|
| 1125 |
-
<span style="margin-left:0.5rem;color:#e8e8e8">{action}</span></div>""", unsafe_allow_html=True)
|
| 1126 |
-
else:
|
| 1127 |
-
st.info("No specific actions needed β this contract appears well-balanced.")
|
| 1128 |
-
|
| 1129 |
-
criticals = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.CRITICAL]
|
| 1130 |
-
if criticals:
|
| 1131 |
-
st.markdown("")
|
| 1132 |
-
st.markdown("### β οΈ What Could Happen If You Sign This?")
|
| 1133 |
-
st.caption("Realistic AI-generated consequence scenarios based on these clause patterns. These are illustrative examples β consult an attorney for legal advice.")
|
| 1134 |
-
for idx, sc in enumerate(criticals[:3]):
|
| 1135 |
-
scenarios = sc.finding.impact_scenarios
|
| 1136 |
-
if not scenarios:
|
| 1137 |
-
scenarios = ["You may face significant legal or financial consequences from this clause."]
|
| 1138 |
-
st.markdown(f"**{idx + 1}. π΄ {sc.finding.risk_title}**")
|
| 1139 |
-
for scenario in scenarios:
|
| 1140 |
-
st.markdown(f"<div style='background:rgba(255,68,68,0.08);border-left:3px solid #ff4444;padding:0.5rem 0.75rem;margin:0.2rem 0;margin-left:1rem;border-radius:4px;font-size:0.9rem;color:#e0a0a0'>{scenario}</div>", unsafe_allow_html=True)
|
| 1141 |
-
|
| 1142 |
-
high_risks = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.HIGH]
|
| 1143 |
-
if high_risks:
|
| 1144 |
-
st.markdown("")
|
| 1145 |
-
st.markdown("### π High-Risk Clauses at a Glance")
|
| 1146 |
-
for sc in high_risks:
|
| 1147 |
-
style = SEVERITY_STYLE[Severity.HIGH]
|
| 1148 |
-
reason_preview = sc.finding.risk_reason[:120]
|
| 1149 |
-
if len(sc.finding.risk_reason) > 120:
|
| 1150 |
-
reason_preview += "..."
|
| 1151 |
-
st.markdown(f"""<div style="background:{style['bg']};border-left:3px solid {style['border']};padding:0.6rem 0.9rem;margin:0.3rem 0;border-radius:4px">
|
| 1152 |
-
<b style="color:{style['color']}">{sc.finding.risk_title}</b>
|
| 1153 |
-
<span style="color:#aaa;font-size:0.85rem;margin-left:0.5rem">β {reason_preview}</span>
|
| 1154 |
-
</div>""", unsafe_allow_html=True)
|
| 1155 |
-
|
| 1156 |
-
medium_risks = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.MEDIUM]
|
| 1157 |
-
if medium_risks:
|
| 1158 |
-
st.markdown("")
|
| 1159 |
-
st.markdown("### π‘ Medium-Risk Clauses")
|
| 1160 |
-
for sc in medium_risks:
|
| 1161 |
-
style = SEVERITY_STYLE[Severity.MEDIUM]
|
| 1162 |
-
reason_preview = sc.finding.risk_reason[:80]
|
| 1163 |
-
if len(sc.finding.risk_reason) > 80:
|
| 1164 |
-
reason_preview += "..."
|
| 1165 |
-
st.markdown(f"""<div style="background:{style['bg']};border-left:3px solid {style['border']};padding:0.5rem 0.8rem;margin:0.2rem 0;border-radius:4px;font-size:0.9rem">
|
| 1166 |
-
<b style="color:{style['color']}">{sc.finding.risk_title}</b>
|
| 1167 |
-
<span style="color:#999;margin-left:0.3rem">β {reason_preview}</span>
|
| 1168 |
-
</div>""", unsafe_allow_html=True)
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1172 |
-
# TAB 2: CLAUSES
|
| 1173 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1174 |
-
|
| 1175 |
-
def render_clauses_tab() -> None:
|
| 1176 |
-
report = st.session_state.report
|
| 1177 |
-
st.markdown("### π Clause-by-Clause Analysis")
|
| 1178 |
-
st.caption("Each issue below shows the original legal text, plain-English translation, risk assessment, and recommended actions.")
|
| 1179 |
-
|
| 1180 |
-
filter_cols = st.columns(5)
|
| 1181 |
-
show_crit = filter_cols[0].checkbox("π΄ Critical", value=True)
|
| 1182 |
-
show_high = filter_cols[1].checkbox("π High", value=True)
|
| 1183 |
-
show_med = filter_cols[2].checkbox("π‘ Medium", value=True)
|
| 1184 |
-
show_low = filter_cols[3].checkbox("π’ Low", value=False)
|
| 1185 |
-
show_info = filter_cols[4].checkbox("βΉοΈ Info", value=False)
|
| 1186 |
-
|
| 1187 |
-
visible = {Severity.CRITICAL: show_crit, Severity.HIGH: show_high,
|
| 1188 |
-
Severity.MEDIUM: show_med, Severity.LOW: show_low, Severity.INFO: show_info}
|
| 1189 |
-
|
| 1190 |
-
default_s = SEVERITY_STYLE[Severity.INFO]
|
| 1191 |
-
issue_num = 0
|
| 1192 |
-
for sc in report.scored_clauses:
|
| 1193 |
-
sev = sc.finding.severity
|
| 1194 |
-
if not visible.get(sev, False):
|
| 1195 |
-
continue
|
| 1196 |
-
issue_num += 1
|
| 1197 |
-
style = SEVERITY_STYLE.get(sev, default_s)
|
| 1198 |
-
|
| 1199 |
-
st.markdown(f"""
|
| 1200 |
-
<div style="display:flex;align-items:center;gap:0.75rem;margin:1.5rem 0 0.75rem 0">
|
| 1201 |
-
<span style="
|
| 1202 |
-
background:{style['border']};
|
| 1203 |
-
color:#fff;
|
| 1204 |
-
min-width:2rem;height:2rem;
|
| 1205 |
-
border-radius:50%;
|
| 1206 |
-
display:inline-flex;align-items:center;justify-content:center;
|
| 1207 |
-
font-weight:800;font-size:0.9rem;
|
| 1208 |
-
">#{issue_num}</span>
|
| 1209 |
-
<div style="background:linear-gradient(90deg, {style['border']}44 0%, transparent 100%);height:1px;flex:1"></div>
|
| 1210 |
-
</div>""", unsafe_allow_html=True)
|
| 1211 |
-
|
| 1212 |
-
_render_single_clause_card(sc, style, show_actions=True)
|
| 1213 |
-
|
| 1214 |
-
if issue_num == 0:
|
| 1215 |
-
st.info("Select severity levels above to view issues. Try enabling Critical and High to see the most important clauses that need your attention.")
|
| 1216 |
-
else:
|
| 1217 |
-
st.caption(f"Showing {issue_num} of {report.summary.total_clauses} clauses β use severity filters above to adjust view")
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1221 |
-
# TAB 3: NEGOTIATION
|
| 1222 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1223 |
-
|
| 1224 |
-
def _highlight_diff(original: str, safer: str) -> tuple[str, str]:
|
| 1225 |
-
original_span = f"<span style='background:rgba(255,68,68,0.20);padding:0 2px;border-radius:2px;text-decoration:line-through'>{original}</span>"
|
| 1226 |
-
safer_span = f"<span style='background:rgba(50,205,50,0.20);padding:0 2px;border-radius:2px;font-weight:600'>{safer}</span>"
|
| 1227 |
-
return original_span, safer_span
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
def generate_negotiation_email(sc: ScoredClause, recipient: str = "[Other Party]") -> str:
|
| 1231 |
-
topic = sc.clause.section_heading or sc.clause.clause_type.value.replace("_", " ").title()
|
| 1232 |
-
safer = sc.finding.safer_clause_version or _generate_fallback_safer(sc)
|
| 1233 |
-
risk_reason = sc.finding.risk_reason
|
| 1234 |
-
return (
|
| 1235 |
-
f"Subject: Proposed adjustment β {topic} clause\n\n"
|
| 1236 |
-
f"Hi {recipient},\n\n"
|
| 1237 |
-
f"I've reviewed the contract and have a concern about the {topic} clause.\n\n"
|
| 1238 |
-
f"My concern: {risk_reason}\n\n"
|
| 1239 |
-
f"I'd suggest the following alternative language to make this fair for both parties:\n\n"
|
| 1240 |
-
f'"{safer}"\n\n'
|
| 1241 |
-
f"Let me know your thoughts β I'm happy to discuss further.\n\n"
|
| 1242 |
-
f"Best regards"
|
| 1243 |
-
)
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
def _render_email_card(sc: ScoredClause, recipient: str = "[Other Party]") -> None:
|
| 1247 |
-
recipient_input = st.text_input("Recipient name", value=recipient, key=f"recipient_{sc.clause.id}")
|
| 1248 |
-
email_body = generate_negotiation_email(sc, recipient_input)
|
| 1249 |
-
st.markdown("**π§ Formal Email Draft**")
|
| 1250 |
-
st.code(email_body, language=None)
|
| 1251 |
-
col_copy, col_info = st.columns([1, 3])
|
| 1252 |
-
with col_copy:
|
| 1253 |
-
if st.button("π Copy to Clipboard", key=f"copy_email_{sc.clause.id}"):
|
| 1254 |
-
st.toast("Email copied!", icon="π")
|
| 1255 |
-
with col_info:
|
| 1256 |
-
st.caption("Click the code block above to select all text, then Ctrl+C to copy")
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
def render_negotiation_tab() -> None:
|
| 1260 |
-
report = st.session_state.report
|
| 1261 |
-
default_s = SEVERITY_STYLE[Severity.INFO]
|
| 1262 |
-
|
| 1263 |
-
st.markdown("### π¬ Negotiation Copilot")
|
| 1264 |
-
st.caption("Each risky clause shows what you signed vs. a safer alternative, side-by-side. Use the pre-written messages or generate a formal email to send to the other party.")
|
| 1265 |
-
|
| 1266 |
-
negotiable = [sc for sc in report.scored_clauses if sc.finding.severity not in (Severity.LOW, Severity.INFO)]
|
| 1267 |
-
if not negotiable:
|
| 1268 |
-
st.success("β
No actionable risks detected β this contract looks reasonable!")
|
| 1269 |
-
else:
|
| 1270 |
-
st.info(f"π **{len(negotiable)} clauses** flagged for negotiation below")
|
| 1271 |
-
|
| 1272 |
-
for i, sc in enumerate(negotiable):
|
| 1273 |
-
style = SEVERITY_STYLE.get(sc.finding.severity, default_s)
|
| 1274 |
-
sev_label = sc.finding.severity.value
|
| 1275 |
-
|
| 1276 |
-
st.markdown(f"""<div style="background:{style['bg']};border-left:4px solid {style['border']};padding:0.6rem 1rem;border-radius:4px 10px 10px 4px;margin:1.2rem 0 0.5rem 0">
|
| 1277 |
-
<span style="font-weight:700;color:{style['color']}">{style['badge']}</span>
|
| 1278 |
-
<span style="font-weight:600;color:#e0e0e0;margin-left:0.5rem">{sc.finding.risk_title}</span>
|
| 1279 |
-
<span style="color:#888;font-size:0.8rem;margin-left:0.8rem">Clause {sc.clause.id}</span>
|
| 1280 |
-
</div>""", unsafe_allow_html=True)
|
| 1281 |
-
|
| 1282 |
-
st.markdown("**π Why This Matters**")
|
| 1283 |
-
st.markdown(f"<div style='color:#ccc;font-size:0.9rem;line-height:1.55;margin-bottom:0.75rem;padding:0.5rem 0.75rem;background:rgba(255,255,255,0.02);border-radius:8px'>{sc.finding.risk_reason}</div>", unsafe_allow_html=True)
|
| 1284 |
-
|
| 1285 |
-
neg_l, neg_r = st.columns(2)
|
| 1286 |
-
with neg_l:
|
| 1287 |
-
st.markdown("**β οΈ Current Clause (Risky)**")
|
| 1288 |
-
text_to_show = sc.clause.raw_text[:500]
|
| 1289 |
-
if len(sc.clause.raw_text) > 500:
|
| 1290 |
-
text_to_show += "..."
|
| 1291 |
-
st.markdown(f"<div style='background:rgba(255,68,68,0.08);padding:0.75rem;border-radius:8px;border:1px solid rgba(255,68,68,0.2);font-size:0.85rem;line-height:1.6;color:#e0e0e0'>{text_to_show}</div>", unsafe_allow_html=True)
|
| 1292 |
-
|
| 1293 |
-
with neg_r:
|
| 1294 |
-
st.markdown("**π‘ Safer Alternative**")
|
| 1295 |
-
safer = sc.finding.safer_clause_version
|
| 1296 |
-
if not safer:
|
| 1297 |
-
safer = _generate_fallback_safer(sc)
|
| 1298 |
-
st.markdown(f"<div style='background:rgba(50,205,50,0.08);padding:0.75rem;border-radius:8px;border:1px solid rgba(50,205,50,0.2);font-size:0.85rem;line-height:1.6;color:#e0e0e0'>{safer}</div>", unsafe_allow_html=True)
|
| 1299 |
-
|
| 1300 |
-
if sc.finding.recommended_action:
|
| 1301 |
-
st.markdown(f"**β
Recommended:** {sc.finding.recommended_action}")
|
| 1302 |
-
|
| 1303 |
-
neg_msg = sc.finding.negotiation_message
|
| 1304 |
-
if not neg_msg:
|
| 1305 |
-
neg_msg = _generate_fallback_message(sc)
|
| 1306 |
-
st.markdown("**π§ Quick Negotiation Message**")
|
| 1307 |
-
st.code(neg_msg, language=None)
|
| 1308 |
-
|
| 1309 |
-
if sc.finding.impact_scenarios:
|
| 1310 |
-
st.markdown("**β οΈ Consequences of Not Negotiating**")
|
| 1311 |
-
for impact in sc.finding.impact_scenarios:
|
| 1312 |
-
st.markdown(f"<div style='background:rgba(255,68,68,0.06);padding:0.35rem 0.75rem;margin:0.15rem 0;margin-left:0.5rem;border-radius:4px;font-size:0.85rem;color:#ff9999'>β’ {impact}</div>", unsafe_allow_html=True)
|
| 1313 |
-
|
| 1314 |
-
with st.expander("π§ Generate Formal Email to Send"):
|
| 1315 |
-
_render_email_card(sc)
|
| 1316 |
-
|
| 1317 |
-
if i < len(negotiable) - 1:
|
| 1318 |
-
st.divider()
|
| 1319 |
-
|
| 1320 |
-
safe_contract = _build_safer_contract(report)
|
| 1321 |
-
with st.expander("π Preview Safer Contract"):
|
| 1322 |
-
preview_max = 3500
|
| 1323 |
-
preview_text = safe_contract[:preview_max]
|
| 1324 |
-
if len(safe_contract) > preview_max:
|
| 1325 |
-
preview_text += f"\n\n... (showing first {preview_max} chars of {len(safe_contract)} β download full contract at bottom of page)"
|
| 1326 |
-
st.code(preview_text, language=None)
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1330 |
-
# TAB 4: CHAT ASSISTANT
|
| 1331 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1332 |
-
|
| 1333 |
-
def render_chat_tab() -> None:
|
| 1334 |
-
report = st.session_state.report
|
| 1335 |
-
st.markdown("### π€ Chat Assistant")
|
| 1336 |
-
st.caption("Ask questions about your contract in plain English. The AI has full context of every clause, risk assessment, and recommended action β all injected into this conversation automatically.")
|
| 1337 |
-
|
| 1338 |
-
cache_key = id(report)
|
| 1339 |
-
if st.session_state.get("copilot_cache_key") != cache_key:
|
| 1340 |
-
raw_text = st.session_state.get("copilot_raw_text", "")
|
| 1341 |
-
st.session_state.copilot_context = build_contract_context(raw_text, report)
|
| 1342 |
-
st.session_state.copilot_cache_key = cache_key
|
| 1343 |
-
|
| 1344 |
-
copilot_context = st.session_state.copilot_context
|
| 1345 |
-
|
| 1346 |
-
if not st.session_state.copilot_messages:
|
| 1347 |
-
total_risky = report.summary.critical_count + report.summary.high_count
|
| 1348 |
-
if total_risky > 0:
|
| 1349 |
-
welcome = (
|
| 1350 |
-
f"I've analyzed your contract and found **{total_risky} high-risk clause(s)** "
|
| 1351 |
-
f"(risk score: **{report.summary.overall_score}/10**). "
|
| 1352 |
-
"You can ask me to:\n\n"
|
| 1353 |
-
"- Explain any clause in simple terms\n"
|
| 1354 |
-
"- Tell you which clauses are risky and why\n"
|
| 1355 |
-
"- Suggest safer wording for specific clauses\n"
|
| 1356 |
-
"- Help you draft a negotiation message\n"
|
| 1357 |
-
"- Describe what could happen if you sign as-is\n"
|
| 1358 |
-
"- Compare clauses to industry standards\n\n"
|
| 1359 |
-
"What would you like to know?"
|
| 1360 |
-
)
|
| 1361 |
-
else:
|
| 1362 |
-
welcome = (
|
| 1363 |
-
f"I've analyzed your contract and it looks reasonable (risk score: **{report.summary.overall_score}/10**). "
|
| 1364 |
-
"You can ask me to explain any clause, check for potential hidden issues, or compare terms to standard practices. "
|
| 1365 |
-
"What would you like to know?"
|
| 1366 |
-
)
|
| 1367 |
-
with st.chat_message("assistant"):
|
| 1368 |
-
st.markdown(welcome)
|
| 1369 |
-
st.session_state.copilot_messages = [{"role": "assistant", "content": welcome}]
|
| 1370 |
-
|
| 1371 |
-
st.markdown("**π‘ Click a question to ask instantly:**")
|
| 1372 |
-
chip_cols = st.columns(4)
|
| 1373 |
-
quick_prompts = [
|
| 1374 |
-
"Summarize this contract in 3 sentences",
|
| 1375 |
-
"What's the most dangerous clause and why?",
|
| 1376 |
-
"Suggest safer wording for the IP clause",
|
| 1377 |
-
"What should I negotiate first?",
|
| 1378 |
-
"Explain the non-compete in simple English",
|
| 1379 |
-
"Are there any hidden fees, penalties, or traps?",
|
| 1380 |
-
"What happens if I breach this contract?",
|
| 1381 |
-
"Draft an email requesting changes to all risky clauses",
|
| 1382 |
-
]
|
| 1383 |
-
for idx, prompt in enumerate(quick_prompts):
|
| 1384 |
-
col = chip_cols[idx % 4]
|
| 1385 |
-
with col:
|
| 1386 |
-
if st.button(prompt, key=f"chip_{idx}", use_container_width=True):
|
| 1387 |
-
st.session_state.pending_ai_query = prompt
|
| 1388 |
-
st.rerun()
|
| 1389 |
-
|
| 1390 |
-
for msg in st.session_state.copilot_messages:
|
| 1391 |
-
with st.chat_message(msg["role"]):
|
| 1392 |
-
st.markdown(msg["content"])
|
| 1393 |
-
|
| 1394 |
-
if st.session_state.get("pending_ai_query"):
|
| 1395 |
-
query = st.session_state.pending_ai_query
|
| 1396 |
-
st.session_state.pending_ai_query = None
|
| 1397 |
-
if copilot_context:
|
| 1398 |
-
st.session_state.copilot_messages.append({"role": "user", "content": query})
|
| 1399 |
-
with st.chat_message("user"):
|
| 1400 |
-
st.markdown(query)
|
| 1401 |
-
with st.chat_message("assistant"):
|
| 1402 |
-
with st.spinner("Thinking β analyzing contract context..."):
|
| 1403 |
-
chat_history = st.session_state.copilot_messages[:-1]
|
| 1404 |
-
response = run_copilot_sync(copilot_context, chat_history, query)
|
| 1405 |
-
st.markdown(response)
|
| 1406 |
-
st.session_state.copilot_messages.append({"role": "assistant", "content": response})
|
| 1407 |
-
st.rerun()
|
| 1408 |
-
|
| 1409 |
-
if prompt := st.chat_input("Ask about this contract...", key="copilot_chat_input"):
|
| 1410 |
-
if not copilot_context:
|
| 1411 |
-
st.warning("No contract analysis available. Please upload and analyze a contract first.")
|
| 1412 |
-
else:
|
| 1413 |
-
st.session_state.copilot_messages.append({"role": "user", "content": prompt})
|
| 1414 |
-
with st.chat_message("user"):
|
| 1415 |
-
st.markdown(prompt)
|
| 1416 |
-
with st.chat_message("assistant"):
|
| 1417 |
-
with st.spinner("Thinking β analyzing contract context..."):
|
| 1418 |
-
chat_history = st.session_state.copilot_messages[:-1]
|
| 1419 |
-
response = run_copilot_sync(copilot_context, chat_history, prompt)
|
| 1420 |
-
st.markdown(response)
|
| 1421 |
-
st.session_state.copilot_messages.append({"role": "assistant", "content": response})
|
| 1422 |
-
|
| 1423 |
-
if st.session_state.copilot_messages:
|
| 1424 |
-
cc1, cc2, cc3 = st.columns([1, 2, 1])
|
| 1425 |
-
with cc1:
|
| 1426 |
-
if st.button("ποΈ Clear Chat", key="copilot_clear", use_container_width=True):
|
| 1427 |
-
st.session_state.copilot_messages = []
|
| 1428 |
-
st.rerun()
|
| 1429 |
-
with cc3:
|
| 1430 |
-
st.caption(f"{len(st.session_state.copilot_messages)} messages")
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1434 |
-
# SIDEBAR
|
| 1435 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1436 |
-
|
| 1437 |
-
def render_sidebar() -> None:
|
| 1438 |
-
with st.sidebar:
|
| 1439 |
-
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1440 |
-
<h4 style="margin:0 0 0.5rem 0;color:#fff">π― How It Works</h4>
|
| 1441 |
-
<ol style="margin:0;padding-left:1.25rem;font-size:0.9rem;color:#ccc;line-height:2">
|
| 1442 |
-
<li>Upload any contract file (PDF, DOCX, TXT)</li>
|
| 1443 |
-
<li>5 specialized AI agents analyze every clause</li>
|
| 1444 |
-
<li>Get a detailed risk report with plain English explanations</li>
|
| 1445 |
-
<li>Use Negotiation Copilot to draft counter-proposals</li>
|
| 1446 |
-
<li>Chat with the AI Copilot for any follow-up questions</li>
|
| 1447 |
-
</ol>
|
| 1448 |
-
</div>""", unsafe_allow_html=True)
|
| 1449 |
-
|
| 1450 |
-
st.markdown("")
|
| 1451 |
-
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1452 |
-
<h4 style="margin:0 0 0.5rem 0;color:#fff">π€ 5-Agent AI Pipeline</h4>
|
| 1453 |
-
<div style="font-size:0.85rem;color:#ccc;line-height:2">
|
| 1454 |
-
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β Extractor</b> β Segments contract into individual clauses</p>
|
| 1455 |
-
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β‘ Classifier</b> β Labels each clause by legal type</p>
|
| 1456 |
-
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β’ Risk Scorer</b> β Evaluates severity of each clause</p>
|
| 1457 |
-
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β£ Translator</b> β Converts legalese to plain English</p>
|
| 1458 |
-
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β€ Reporter</b> β Compiles the final risk report</p>
|
| 1459 |
-
</div>
|
| 1460 |
-
</div>""", unsafe_allow_html=True)
|
| 1461 |
-
|
| 1462 |
-
if st.session_state.report:
|
| 1463 |
-
s = st.session_state.report.summary
|
| 1464 |
-
total_risky = s.critical_count + s.high_count
|
| 1465 |
-
st.markdown("")
|
| 1466 |
-
st.markdown("#### π Contract Stats")
|
| 1467 |
-
|
| 1468 |
-
risk_delta = f"{total_risky} high-risk" if total_risky > 0 else "Clean"
|
| 1469 |
-
st.metric(
|
| 1470 |
-
"π― Risk Score",
|
| 1471 |
-
f"{s.overall_score}/10",
|
| 1472 |
-
delta=risk_delta,
|
| 1473 |
-
delta_color="inverse" if total_risky > 0 else "normal",
|
| 1474 |
-
)
|
| 1475 |
-
st.metric("π Total Clauses", s.total_clauses)
|
| 1476 |
-
|
| 1477 |
-
has_any_risks = False
|
| 1478 |
-
for icon, label, key in [
|
| 1479 |
-
("π΄", "Critical", "critical_count"),
|
| 1480 |
-
("π ", "High", "high_count"),
|
| 1481 |
-
("π‘", "Medium", "medium_count"),
|
| 1482 |
-
("π’", "Low", "low_count"),
|
| 1483 |
-
]:
|
| 1484 |
-
count = getattr(s, key, 0)
|
| 1485 |
-
if count > 0:
|
| 1486 |
-
has_any_risks = True
|
| 1487 |
-
st.metric(f"{icon} {label}", count)
|
| 1488 |
-
|
| 1489 |
-
st.divider()
|
| 1490 |
-
st.markdown(f"**Contract Type:** {s.contract_type}")
|
| 1491 |
-
st.markdown(f"**Analyzed:** {st.session_state.report.generated_at.strftime('%b %d, %Y at %H:%M')}")
|
| 1492 |
-
|
| 1493 |
-
if not st.session_state.report.processed_normally:
|
| 1494 |
-
st.caption("β οΈ Report may not cover all clauses due to processing constraints.")
|
| 1495 |
-
|
| 1496 |
-
st.markdown("")
|
| 1497 |
-
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1498 |
-
<h4 style="margin:0 0 0.5rem 0;color:#fff">β‘ Powered by</h4>
|
| 1499 |
-
<p style="font-size:0.85rem;color:#ccc;margin:0;line-height:1.8">
|
| 1500 |
-
Qwen2.5 via vLLM on AMD MI300X<br>
|
| 1501 |
-
OpenAI-compatible API<br>
|
| 1502 |
-
Streamlit β’ Python 3.10+
|
| 1503 |
-
</p>
|
| 1504 |
-
<div style="margin-top:0.5rem;padding:0.3rem 0.5rem;background:#1a0533;border-radius:6px;border:1px solid #667eea;text-align:center;font-size:0.7rem;color:#aabbcc">
|
| 1505 |
-
π·οΈ AMD Developer Cloud
|
| 1506 |
-
</div>
|
| 1507 |
-
</div>""", unsafe_allow_html=True)
|
| 1508 |
-
|
| 1509 |
-
st.markdown("")
|
| 1510 |
-
st.markdown("""<div style="font-size:0.7rem;color:#555;text-align:center;margin-top:1rem">
|
| 1511 |
-
<p style="margin:0">β οΈ Not legal advice. AI-generated analysis.</p>
|
| 1512 |
-
<p style="margin:0">Always consult a qualified attorney before signing.</p>
|
| 1513 |
-
</div>""", unsafe_allow_html=True)
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1517 |
-
# MAIN APP ENTRY POINT
|
| 1518 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1519 |
|
| 1520 |
st.set_page_config(
|
| 1521 |
page_title="ClauseGuard β AI Contract Risk Analyzer",
|
|
@@ -1572,72 +70,8 @@ if st.session_state.error:
|
|
| 1572 |
st.info("π‘ To use ClauseGuard, set the `BASE_URL` and `MODEL_NAME` in your `.env` file to point to your Qwen/vLLM endpoint. See the README for setup instructions.")
|
| 1573 |
|
| 1574 |
if st.session_state.report:
|
| 1575 |
-
report = st.session_state.report
|
| 1576 |
-
s = report.summary
|
| 1577 |
-
|
| 1578 |
st.divider()
|
| 1579 |
-
|
| 1580 |
render_issues_summary()
|
| 1581 |
-
|
| 1582 |
-
active_tab = st.radio(
|
| 1583 |
-
"Navigate between sections",
|
| 1584 |
-
TAB_NAMES,
|
| 1585 |
-
index=min(st.session_state.get("active_tab", 0), len(TAB_NAMES) - 1),
|
| 1586 |
-
label_visibility="collapsed",
|
| 1587 |
-
horizontal=True,
|
| 1588 |
-
)
|
| 1589 |
-
st.session_state.active_tab = TAB_NAMES.index(active_tab)
|
| 1590 |
-
tab_index = st.session_state.active_tab
|
| 1591 |
-
|
| 1592 |
-
if tab_index == 0:
|
| 1593 |
-
render_overview_tab()
|
| 1594 |
-
elif tab_index == 1:
|
| 1595 |
-
render_clauses_tab()
|
| 1596 |
-
elif tab_index == 2:
|
| 1597 |
-
render_negotiation_tab()
|
| 1598 |
-
elif tab_index == 3:
|
| 1599 |
-
render_chat_tab()
|
| 1600 |
-
|
| 1601 |
-
st.divider()
|
| 1602 |
-
st.markdown("### π₯ Download Your Report")
|
| 1603 |
-
st.caption("Download the full analysis in your preferred format to share with legal counsel or reference later.")
|
| 1604 |
-
|
| 1605 |
-
dl_cols = st.columns(3)
|
| 1606 |
-
with dl_cols[0]:
|
| 1607 |
-
st.download_button(
|
| 1608 |
-
"π Download Markdown Report",
|
| 1609 |
-
data=report.markdown_report or "# ClauseGuard Report\n\nRun analysis first.",
|
| 1610 |
-
file_name=f"clauseguard_report_{report.contract_name.replace('.','_')}.md",
|
| 1611 |
-
mime="text/markdown",
|
| 1612 |
-
use_container_width=True,
|
| 1613 |
-
)
|
| 1614 |
-
with dl_cols[1]:
|
| 1615 |
-
safe_contract = _build_safer_contract(report)
|
| 1616 |
-
st.download_button(
|
| 1617 |
-
"π‘οΈ Download Safer Contract",
|
| 1618 |
-
data=safe_contract,
|
| 1619 |
-
file_name=f"safer_{report.contract_name.replace('.txt','').replace('.pdf','').replace('.docx','')}.txt",
|
| 1620 |
-
mime="text/plain",
|
| 1621 |
-
use_container_width=True,
|
| 1622 |
-
)
|
| 1623 |
-
with dl_cols[2]:
|
| 1624 |
-
csv_lines = ["Clause ID,Type,Severity,Risk Title,Risk Reason,Recommended Action,Plain English,Negotiation Message"]
|
| 1625 |
-
for sc in report.scored_clauses:
|
| 1626 |
-
csv_lines.append(
|
| 1627 |
-
f'"{sc.clause.id}","{sc.clause.clause_type.value}","{sc.finding.severity.value}","{sc.finding.risk_title}","{sc.finding.risk_reason}","{sc.finding.recommended_action}","{sc.clause.plain_english or ""}","{sc.finding.negotiation_message or ""}"'
|
| 1628 |
-
)
|
| 1629 |
-
st.download_button(
|
| 1630 |
-
"π Download CSV Data",
|
| 1631 |
-
data="\n".join(csv_lines),
|
| 1632 |
-
file_name=f"clauseguard_data_{report.contract_name.replace('.','_')}.csv",
|
| 1633 |
-
mime="text/csv",
|
| 1634 |
-
use_container_width=True,
|
| 1635 |
-
)
|
| 1636 |
-
|
| 1637 |
-
st.caption(
|
| 1638 |
-
f"Generated {report.generated_at.strftime('%B %d, %Y at %H:%M')} "
|
| 1639 |
-
f"β’ {s.contract_type} β’ {s.total_clauses} clauses analyzed"
|
| 1640 |
-
f"{' β’ β οΈ Partial analysis' if not report.processed_normally else ''}"
|
| 1641 |
-
)
|
| 1642 |
|
| 1643 |
render_sidebar()
|
|
|
|
| 1 |
"""ClauseGuard Streamlit UI β redesigned modern SaaS edition."""
|
|
|
|
|
|
|
| 2 |
import sys
|
|
|
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 6 |
|
| 7 |
import streamlit as st
|
|
|
|
| 8 |
|
| 9 |
+
from clauseguard.ui import (
|
| 10 |
+
MAX_FILE_SIZE_MB, MAX_FILE_SIZE_BYTES, ALLOWED_EXTENSIONS,
|
| 11 |
+
AGENT_NAMES, AGENT_ICONS, AGENT_STEP_NUMBERS, SEVERITY_STYLE, CUSTOM_CSS,
|
| 12 |
+
_check_model_connectivity, _build_demo_report, _load_demo_report, _load_guided_demo,
|
| 13 |
+
_init_session_state, _on_agent_event, _run_analysis, _build_safer_contract,
|
| 14 |
+
render_header, _render_guided_tour, render_risk_banner, render_issues_summary,
|
| 15 |
+
render_overview_tab, render_sidebar, render_clauses_tab, render_negotiation_tab, render_chat_tab,
|
| 16 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
st.set_page_config(
|
| 19 |
page_title="ClauseGuard β AI Contract Risk Analyzer",
|
|
|
|
| 70 |
st.info("π‘ To use ClauseGuard, set the `BASE_URL` and `MODEL_NAME` in your `.env` file to point to your Qwen/vLLM endpoint. See the README for setup instructions.")
|
| 71 |
|
| 72 |
if st.session_state.report:
|
|
|
|
|
|
|
|
|
|
| 73 |
st.divider()
|
|
|
|
| 74 |
render_issues_summary()
|
| 75 |
+
render_overview_tab()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
render_sidebar()
|
clauseguard/pages/1_π_Clauses.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; from pathlib import Path; sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from clauseguard.ui import _init_session_state, render_sidebar, render_clauses_tab, CUSTOM_CSS
|
| 4 |
+
|
| 5 |
+
st.set_page_config(page_title="Clauses β ClauseGuard", page_icon="π‘οΈ", layout="wide", initial_sidebar_state="expanded")
|
| 6 |
+
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
|
| 7 |
+
_init_session_state()
|
| 8 |
+
render_sidebar()
|
| 9 |
+
|
| 10 |
+
if st.session_state.report is None:
|
| 11 |
+
st.warning("No analysis yet. Upload a contract on the **Home** page first, or click **Instant Demo**.")
|
| 12 |
+
st.stop()
|
| 13 |
+
|
| 14 |
+
st.markdown("## π Clause-by-Clause Analysis")
|
| 15 |
+
render_clauses_tab()
|
clauseguard/pages/2_π¬_Negotiation.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; from pathlib import Path; sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from clauseguard.ui import _init_session_state, render_sidebar, render_negotiation_tab, CUSTOM_CSS
|
| 4 |
+
|
| 5 |
+
st.set_page_config(page_title="Negotiation β ClauseGuard", page_icon="π‘οΈ", layout="wide", initial_sidebar_state="expanded")
|
| 6 |
+
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
|
| 7 |
+
_init_session_state()
|
| 8 |
+
render_sidebar()
|
| 9 |
+
|
| 10 |
+
if st.session_state.report is None:
|
| 11 |
+
st.warning("No analysis yet. Upload a contract on the **Home** page first, or click **Instant Demo**.")
|
| 12 |
+
st.stop()
|
| 13 |
+
|
| 14 |
+
render_negotiation_tab()
|
clauseguard/pages/3_π€_Chat.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; from pathlib import Path; sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from clauseguard.ui import _init_session_state, render_sidebar, render_chat_tab, CUSTOM_CSS
|
| 4 |
+
|
| 5 |
+
st.set_page_config(page_title="Chat β ClauseGuard", page_icon="π‘οΈ", layout="wide", initial_sidebar_state="expanded")
|
| 6 |
+
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
|
| 7 |
+
_init_session_state()
|
| 8 |
+
render_sidebar()
|
| 9 |
+
|
| 10 |
+
if st.session_state.report is None:
|
| 11 |
+
st.warning("No analysis yet. Upload a contract on the **Home** page first, or click **Instant Demo**.")
|
| 12 |
+
st.stop()
|
| 13 |
+
|
| 14 |
+
render_chat_tab()
|
clauseguard/pages/4_π₯_Download.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; from pathlib import Path; sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from clauseguard.ui import _init_session_state, render_sidebar, _build_safer_contract, CUSTOM_CSS
|
| 4 |
+
|
| 5 |
+
st.set_page_config(page_title="Downloads β ClauseGuard", page_icon="π‘οΈ", layout="wide", initial_sidebar_state="expanded")
|
| 6 |
+
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
|
| 7 |
+
_init_session_state()
|
| 8 |
+
render_sidebar()
|
| 9 |
+
|
| 10 |
+
if st.session_state.report is None:
|
| 11 |
+
st.warning("No analysis yet. Upload a contract on the **Home** page first, or click **Instant Demo**.")
|
| 12 |
+
st.stop()
|
| 13 |
+
|
| 14 |
+
report = st.session_state.report
|
| 15 |
+
s = report.summary
|
| 16 |
+
|
| 17 |
+
st.markdown("## π₯ Download Your Report")
|
| 18 |
+
st.caption("Download the full analysis in your preferred format to share with legal counsel or reference later.")
|
| 19 |
+
|
| 20 |
+
dl_cols = st.columns(3)
|
| 21 |
+
with dl_cols[0]:
|
| 22 |
+
st.download_button(
|
| 23 |
+
"π Download Markdown Report",
|
| 24 |
+
data=report.markdown_report or "# ClauseGuard Report\n\nRun analysis first.",
|
| 25 |
+
file_name=f"clauseguard_report_{report.contract_name.replace('.','_')}.md",
|
| 26 |
+
mime="text/markdown",
|
| 27 |
+
use_container_width=True,
|
| 28 |
+
)
|
| 29 |
+
with dl_cols[1]:
|
| 30 |
+
safe_contract = _build_safer_contract(report)
|
| 31 |
+
st.download_button(
|
| 32 |
+
"π‘οΈ Download Safer Contract",
|
| 33 |
+
data=safe_contract,
|
| 34 |
+
file_name=f"safer_{report.contract_name.replace('.txt','').replace('.pdf','').replace('.docx','')}.txt",
|
| 35 |
+
mime="text/plain",
|
| 36 |
+
use_container_width=True,
|
| 37 |
+
)
|
| 38 |
+
with dl_cols[2]:
|
| 39 |
+
csv_lines = ["Clause ID,Type,Severity,Risk Title,Risk Reason,Recommended Action,Plain English,Negotiation Message"]
|
| 40 |
+
for sc in report.scored_clauses:
|
| 41 |
+
csv_lines.append(
|
| 42 |
+
f'"{sc.clause.id}","{sc.clause.clause_type.value}","{sc.finding.severity.value}","{sc.finding.risk_title}","{sc.finding.risk_reason}","{sc.finding.recommended_action}","{sc.clause.plain_english or ""}","{sc.finding.negotiation_message or ""}"'
|
| 43 |
+
)
|
| 44 |
+
st.download_button(
|
| 45 |
+
"π Download CSV Data",
|
| 46 |
+
data="\n".join(csv_lines),
|
| 47 |
+
file_name=f"clauseguard_data_{report.contract_name.replace('.','_')}.csv",
|
| 48 |
+
mime="text/csv",
|
| 49 |
+
use_container_width=True,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
st.caption(
|
| 53 |
+
f"Generated {report.generated_at.strftime('%B %d, %Y at %H:%M')} "
|
| 54 |
+
f"β’ {s.contract_type} β’ {s.total_clauses} clauses analyzed"
|
| 55 |
+
f"{' β’ β οΈ Partial analysis' if not report.processed_normally else ''}"
|
| 56 |
+
)
|
clauseguard/ui.py
ADDED
|
@@ -0,0 +1,1513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ClauseGuard shared UI functions β constants, helpers, renderers."""
|
| 2 |
+
import asyncio
|
| 3 |
+
import logging
|
| 4 |
+
import sys
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 10 |
+
|
| 11 |
+
import streamlit as st
|
| 12 |
+
import pandas as pd
|
| 13 |
+
|
| 14 |
+
from clauseguard.agents.copilot import build_contract_context, run_copilot_sync
|
| 15 |
+
from clauseguard.agents.orchestrator import run_pipeline, set_event_callback
|
| 16 |
+
from clauseguard.config.settings import validate_config
|
| 17 |
+
from clauseguard.models.findings import RiskFinding, ScoredClause, Severity
|
| 18 |
+
from clauseguard.models.report import FinalReport
|
| 19 |
+
from clauseguard.tools.file_tools import extract_text
|
| 20 |
+
|
| 21 |
+
logging.basicConfig(level=logging.INFO)
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# ββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
|
| 26 |
+
MAX_FILE_SIZE_MB = 10
|
| 27 |
+
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
|
| 28 |
+
ALLOWED_EXTENSIONS = ["pdf", "txt", "docx"]
|
| 29 |
+
|
| 30 |
+
TAB_NAMES = ["π Overview", "π Clauses", "π¬ Negotiation", "π€ Chat Assistant"]
|
| 31 |
+
TAB_SESSION_KEY = "tab_selector_radio"
|
| 32 |
+
|
| 33 |
+
AGENT_NAMES = ["Extractor", "Classifier", "Risk Scorer", "Translator", "Reporter"]
|
| 34 |
+
AGENT_ICONS = {"running": "βοΈ", "completed": "β
", "failed": "β", "pending": "β³"}
|
| 35 |
+
AGENT_STEP_NUMBERS = {"Extractor": "β ", "Classifier": "β‘", "Risk Scorer": "β’",
|
| 36 |
+
"Translator": "β£", "Reporter": "β€"}
|
| 37 |
+
|
| 38 |
+
SEVERITY_STYLE = {
|
| 39 |
+
Severity.CRITICAL: {"badge": "π΄ CRITICAL", "border": "#ff4444", "bg": "rgba(255,68,68,0.12)", "color": "#ff6666", "tag_bg": "rgba(255,68,68,0.18)"},
|
| 40 |
+
Severity.HIGH: {"badge": "π HIGH", "border": "#ff8c00", "bg": "rgba(255,140,0,0.12)", "color": "#ffaa44", "tag_bg": "rgba(255,140,0,0.15)"},
|
| 41 |
+
Severity.MEDIUM: {"badge": "π‘ MEDIUM", "border": "#ffd700", "bg": "rgba(255,215,0,0.12)", "color": "#ffdd55", "tag_bg": "rgba(255,215,0,0.12)"},
|
| 42 |
+
Severity.LOW: {"badge": "π’ LOW", "border": "#32cd32", "bg": "rgba(50,205,50,0.12)", "color": "#55dd55", "tag_bg": "rgba(50,205,50,0.10)"},
|
| 43 |
+
Severity.INFO: {"badge": "βΉοΈ INFO", "border": "#1e90ff", "bg": "rgba(30,144,255,0.08)", "color": "#55aaff", "tag_bg": "rgba(30,144,255,0.08)"},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _check_model_connectivity() -> tuple[bool, str]:
|
| 48 |
+
"""Quick connectivity check against the configured model endpoint.
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
(ok, error_message) β ok is True if the endpoint is reachable.
|
| 52 |
+
"""
|
| 53 |
+
import asyncio
|
| 54 |
+
from clauseguard.services.model_service import get_client
|
| 55 |
+
from clauseguard.config.settings import MODEL_NAME
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
loop = asyncio.new_event_loop()
|
| 59 |
+
asyncio.set_event_loop(loop)
|
| 60 |
+
try:
|
| 61 |
+
client = get_client()
|
| 62 |
+
loop.run_until_complete(
|
| 63 |
+
asyncio.wait_for(
|
| 64 |
+
client.models.list(),
|
| 65 |
+
timeout=10,
|
| 66 |
+
)
|
| 67 |
+
)
|
| 68 |
+
return True, ""
|
| 69 |
+
except asyncio.TimeoutError:
|
| 70 |
+
return False, "Model endpoint timed out β the vLLM server may be offline or unreachable"
|
| 71 |
+
except Exception as e:
|
| 72 |
+
err = str(e)
|
| 73 |
+
if "ConnectionRefusedError" in err or "Connection refused" in err or "ConnectError" in err:
|
| 74 |
+
return False, f"Connection refused β vLLM server is not running at the configured BASE_URL"
|
| 75 |
+
if "Name or service not known" in err or "getaddrinfo" in err.lower():
|
| 76 |
+
return False, f"Cannot resolve host β check that the BASE_URL is correct"
|
| 77 |
+
return False, f"Model endpoint error: {err[:120]}"
|
| 78 |
+
finally:
|
| 79 |
+
loop.close()
|
| 80 |
+
except Exception as e:
|
| 81 |
+
return False, f"Connectivity check failed: {str(e)[:120]}"
|
| 82 |
+
|
| 83 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
# CUSTOM CSS
|
| 85 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
|
| 87 |
+
CUSTOM_CSS = """
|
| 88 |
+
<style>
|
| 89 |
+
.main .block-container { padding-top: 1.5rem; padding-bottom: 2rem; }
|
| 90 |
+
|
| 91 |
+
.stButton > button {
|
| 92 |
+
border-radius: 10px;
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
font-size: 0.95rem;
|
| 95 |
+
padding: 0.65rem 1.5rem;
|
| 96 |
+
transition: all 0.2s ease;
|
| 97 |
+
border: 1px solid rgba(255,255,255,0.08);
|
| 98 |
+
}
|
| 99 |
+
.stButton > button:hover {
|
| 100 |
+
transform: translateY(-1px);
|
| 101 |
+
box-shadow: 0 6px 20px rgba(102,126,234,0.35);
|
| 102 |
+
}
|
| 103 |
+
.stButton > button[kind="primary"] {
|
| 104 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 105 |
+
color: #fff !important;
|
| 106 |
+
border: none !important;
|
| 107 |
+
}
|
| 108 |
+
.stButton > button[kind="secondary"] {
|
| 109 |
+
background: rgba(255,255,255,0.06) !important;
|
| 110 |
+
color: #e0e0e0 !important;
|
| 111 |
+
border: 1px solid rgba(255,255,255,0.12) !important;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.stFileUploader section {
|
| 115 |
+
border: 2px dashed #667eea !important;
|
| 116 |
+
border-radius: 14px !important;
|
| 117 |
+
padding: 1.5rem !important;
|
| 118 |
+
background: rgba(102,126,234,0.03) !important;
|
| 119 |
+
transition: all 0.25s ease;
|
| 120 |
+
}
|
| 121 |
+
.stFileUploader section:hover {
|
| 122 |
+
border-color: #8ab4f8 !important;
|
| 123 |
+
background: rgba(102,126,234,0.08) !important;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
div[role="radiogroup"] {
|
| 127 |
+
display: flex; gap: 4px;
|
| 128 |
+
background: #0e1117; padding: 4px;
|
| 129 |
+
border-radius: 14px; border: 1px solid rgba(255,255,255,0.06);
|
| 130 |
+
margin-bottom: 1rem;
|
| 131 |
+
}
|
| 132 |
+
div[role="radiogroup"] label {
|
| 133 |
+
flex: 1; text-align: center;
|
| 134 |
+
padding: 10px 16px !important;
|
| 135 |
+
border-radius: 10px;
|
| 136 |
+
font-weight: 600; font-size: 0.92rem;
|
| 137 |
+
color: #aaa; cursor: pointer;
|
| 138 |
+
transition: all 0.2s ease;
|
| 139 |
+
}
|
| 140 |
+
div[role="radiogroup"] label:hover { background: rgba(255,255,255,0.04); }
|
| 141 |
+
div[role="radiogroup"] label:has(input:checked) {
|
| 142 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 143 |
+
color: #ffffff !important;
|
| 144 |
+
}
|
| 145 |
+
div[role="radiogroup"] input[type="radio"] {
|
| 146 |
+
position: absolute; opacity: 0; width: 0; height: 0;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
@media (max-width: 768px) {
|
| 150 |
+
div[role="radiogroup"] label { padding: 8px 10px; font-size: 0.78rem; }
|
| 151 |
+
}
|
| 152 |
+
.stTabs [data-baseweb="tab"] {
|
| 153 |
+
font-weight: 600;
|
| 154 |
+
font-size: 0.95rem;
|
| 155 |
+
padding: 10px 20px;
|
| 156 |
+
border-radius: 10px;
|
| 157 |
+
color: #aaa;
|
| 158 |
+
}
|
| 159 |
+
.stTabs [aria-selected="true"] {
|
| 160 |
+
color: #ffffff !important;
|
| 161 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 162 |
+
border-radius: 10px !important;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.stExpander {
|
| 166 |
+
border: 1px solid rgba(255,255,255,0.06) !important;
|
| 167 |
+
border-radius: 10px !important;
|
| 168 |
+
margin-bottom: 0.3rem !important;
|
| 169 |
+
overflow: hidden;
|
| 170 |
+
transition: all 0.15s ease;
|
| 171 |
+
}
|
| 172 |
+
.stExpander:hover {
|
| 173 |
+
border-color: rgba(255,255,255,0.1) !important;
|
| 174 |
+
}
|
| 175 |
+
.stExpander > div:first-child {
|
| 176 |
+
border-radius: 10px !important;
|
| 177 |
+
background: rgba(255,255,255,0.015);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.stChatMessage { border-radius: 12px !important; }
|
| 181 |
+
|
| 182 |
+
.stProgress > div > div > div > div {
|
| 183 |
+
background: linear-gradient(90deg, #667eea, #764ba2) !important;
|
| 184 |
+
border-radius: 4px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
[data-testid="stMetric"] {
|
| 188 |
+
background: rgba(255,255,255,0.03);
|
| 189 |
+
border: 1px solid rgba(255,255,255,0.06);
|
| 190 |
+
border-radius: 12px;
|
| 191 |
+
padding: 0.75rem 1rem;
|
| 192 |
+
}
|
| 193 |
+
[data-testid="stMetric"] label { font-weight: 500 !important; }
|
| 194 |
+
|
| 195 |
+
.stCodeBlock { border-radius: 10px !important; }
|
| 196 |
+
|
| 197 |
+
.cg-card {
|
| 198 |
+
background: linear-gradient(145deg, #12121f 0%, #0e1117 100%);
|
| 199 |
+
border: 1px solid rgba(255,255,255,0.07);
|
| 200 |
+
border-radius: 14px;
|
| 201 |
+
padding: 1.25rem;
|
| 202 |
+
margin-bottom: 1rem;
|
| 203 |
+
transition: border-color 0.2s ease;
|
| 204 |
+
}
|
| 205 |
+
.cg-card:hover { border-color: rgba(255,255,255,0.12); }
|
| 206 |
+
|
| 207 |
+
.cg-badge {
|
| 208 |
+
display: inline-block;
|
| 209 |
+
padding: 0.25rem 0.7rem;
|
| 210 |
+
border-radius: 20px;
|
| 211 |
+
font-size: 0.75rem;
|
| 212 |
+
font-weight: 700;
|
| 213 |
+
letter-spacing: 0.4px;
|
| 214 |
+
text-transform: uppercase;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
@keyframes pulse-glow {
|
| 218 |
+
0%, 100% { opacity: 1; }
|
| 219 |
+
50% { opacity: 0.6; }
|
| 220 |
+
}
|
| 221 |
+
.agent-running {
|
| 222 |
+
animation: pulse-glow 1.4s ease-in-out infinite;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.cg-chip {
|
| 226 |
+
display: inline-block;
|
| 227 |
+
padding: 0.35rem 0.9rem;
|
| 228 |
+
border-radius: 20px;
|
| 229 |
+
font-size: 0.78rem;
|
| 230 |
+
font-weight: 500;
|
| 231 |
+
background: rgba(102,126,234,0.12);
|
| 232 |
+
color: #8ab4f8;
|
| 233 |
+
border: 1px solid rgba(102,126,234,0.2);
|
| 234 |
+
cursor: pointer;
|
| 235 |
+
margin: 0.2rem;
|
| 236 |
+
transition: all 0.15s ease;
|
| 237 |
+
}
|
| 238 |
+
.cg-chip:hover {
|
| 239 |
+
background: rgba(102,126,234,0.25);
|
| 240 |
+
border-color: rgba(102,126,234,0.4);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
@media (max-width: 768px) {
|
| 244 |
+
.stTabs [data-baseweb="tab"] { padding: 8px 12px; font-size: 0.8rem; }
|
| 245 |
+
}
|
| 246 |
+
</style>
|
| 247 |
+
"""
|
| 248 |
+
|
| 249 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 250 |
+
# DEMO REPORT BUILDER
|
| 251 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 252 |
+
|
| 253 |
+
def _build_demo_report() -> FinalReport:
|
| 254 |
+
"""Build a pre-cached demo report showcasing all features with realistic contract data."""
|
| 255 |
+
from clauseguard.models.clause import Clause, ClauseType
|
| 256 |
+
|
| 257 |
+
demo_clauses: list[dict] = [
|
| 258 |
+
{
|
| 259 |
+
"text": "Employee hereby irrevocably assigns to Company all inventions, discoveries, works of authorship, and intellectual property created or conceived by Employee, whether during working hours or on Employee's own time, using Company equipment or Employee's personal equipment, and whether or not related to Company's business. This obligation survives termination of employment for a period of two (2) years.",
|
| 260 |
+
"ctype": "IP_ASSIGNMENT", "sev": "CRITICAL",
|
| 261 |
+
"title": "Overbroad IP Assignment Covering Personal Work",
|
| 262 |
+
"reason": "This clause claims ownership of ALL employee creations made at any time on any equipment, including unrelated personal side projects, and extends the obligation for 2 years after leaving the company β well beyond industry standard.",
|
| 263 |
+
"plain": "You give the company full ownership of everything you ever create β including personal hobbies, side projects, and weekend work done on your own computer β even for two years after you quit or are fired.",
|
| 264 |
+
"action": "Negotiate a strict carve-out limiting IP assignment to work directly related to company business, created during work hours using company resources only.",
|
| 265 |
+
"safer": "Employee assigns to Company all inventions that (a) relate directly to Company's current or planned business, (b) are created during working hours, and (c) use Company resources. Inventions created on Employee's own time using personal equipment and unrelated to Company's business remain Employee's sole property. This obligation ends upon termination of employment.",
|
| 266 |
+
"negotiation": "Hi [Name],\n\nI've reviewed the intellectual property clause and have a concern about its extremely broad scope. As written, it covers personal side projects, hobbies, and work done on my own time with my own equipment β even extending 2 years after I leave. That's well beyond what's standard.\n\nI've drafted an alternative below that protects the company's legitimate interests while respecting my personal creative freedom. The key change: it limits the scope to work actually related to the company's business, done during working hours, using company resources.\n\nWould you be open to this revision?\n\nThanks,\n[Your Name]",
|
| 267 |
+
"impacts": ["You could lose ownership of a personal startup, app, or creative project you build on weekends", "The company could claim royalties or ownership of open-source contributions you make", "Even after quitting, anything you invent for 2 years could be claimed by your former employer"],
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"text": "Any and all disputes, claims, or controversies arising out of or relating to this Agreement shall be resolved exclusively through final and binding arbitration administered by the American Arbitration Association. The Parties hereby expressly waive any right to a trial by jury and waive any right to participate in or bring a class, collective, or representative action. The arbitrator shall have no authority to consolidate claims or conduct class-wide proceedings.",
|
| 271 |
+
"ctype": "ARBITRATION", "sev": "CRITICAL",
|
| 272 |
+
"title": "Mandatory Arbitration Forcing Waiver of All Court Rights",
|
| 273 |
+
"reason": "This clause strips away your right to sue in court, forces mandatory private arbitration, waives your constitutional right to a jury trial, and blocks you from joining any class action β with zero opt-out provision.",
|
| 274 |
+
"plain": "You cannot ever take the company to court. Any dispute β no matter how serious β goes through private arbitration that the company pays for and controls. You also give up your right to join a class-action lawsuit with others who may have been wronged the same way.",
|
| 275 |
+
"action": "Demand an opt-out provision allowing either party to choose court over arbitration. Alternatively, remove the class action waiver entirely.",
|
| 276 |
+
"safer": "Either party may elect to opt out of binding arbitration by providing written notice within thirty (30) days of signing this Agreement. Nothing in this section shall prevent any party from participating in a class, collective, or representative action where permitted by applicable law. Both parties retain the right to seek injunctive or equitable relief in a court of competent jurisdiction.",
|
| 277 |
+
"negotiation": "Hi [Name],\n\nI've reviewed the dispute resolution section and have significant concerns about the mandatory arbitration clause. It completely removes my ability to go to court β even for serious disputes β and blocks class actions entirely.\n\nI'm not opposed to arbitration as an option, but forcing it as the ONLY option with no way out is too one-sided. I've drafted a revised version that adds a 30-day opt-out window (so both parties can choose) and preserves the right to join class actions where the law allows.\n\nThis creates a fair balance. Would you be open to this?\n\nBest,\n[Your Name]",
|
| 278 |
+
"impacts": ["If the company steals your work or fails to pay you, you cannot sue in a public court β you must go through a private arbitrator they help select", "You face the company alone β you cannot pool resources with other employees who were treated the same way", "Arbitration decisions are nearly impossible to appeal, giving you no safety net if the arbitrator makes a mistake"],
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"text": "Employee expressly agrees that during the term of employment and for a period of eighteen (18) months following the termination of employment for any reason, Employee shall not, directly or indirectly, own, manage, operate, control, be employed by, consult for, or render services to any business that is competitive with Company, as determined by Company in its sole discretion, anywhere in the world.",
|
| 282 |
+
"ctype": "NON_COMPETE", "sev": "HIGH",
|
| 283 |
+
"title": "Worldwide Non-Compete with Unlimited Company Discretion",
|
| 284 |
+
"reason": "This 18-month non-compete bans you from working for ANY business the company unilaterally deems 'competitive' β worldwide, with no geographic limit, and the company alone decides who counts as a competitor.",
|
| 285 |
+
"plain": "You cannot work for any company anywhere in the world for 18 months after leaving β and your employer alone gets to decide which companies count as 'competitors.' Even a completely unrelated job could be blocked if the company says so.",
|
| 286 |
+
"action": "Reduce duration to 12 months maximum, limit geographic scope to regions where the company actually operates, and define competitors objectively (not at the company's sole discretion).",
|
| 287 |
+
"safer": "For a period of twelve (12) months following termination, Employee shall not provide services to entities that are direct competitors of Company, limited to the specific metropolitan areas and regions where Company has active and material business operations, and limited to services substantially similar to those Employee performed for Company.",
|
| 288 |
+
"negotiation": "Hi [Name],\n\nThe non-compete clause is exceptionally broad β 18 months, worldwide, covering any business the company chooses to label as a competitor. This would make it nearly impossible for me to find work in my field after leaving.\n\nI've proposed a revised version that's far more reasonable: 12 months, limited to direct competitors (objectively defined), and restricted to regions where the company actually operates. This still protects your legitimate business interests without unfairly restricting my career.\n\nWould this work for you?\n\nThanks,\n[Your Name]",
|
| 289 |
+
"impacts": ["You may be unable to work in your entire industry for a year and a half after leaving β regardless of where you live", "A company in a different country doing vaguely related work could trigger the restriction", "The 'sole discretion' language means the company can retroactively decide you violated it"],
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"text": "Company shall have no liability to Employee for any indirect, incidental, special, consequential, or punitive damages arising out of this Agreement, regardless of the theory of liability, even if Company has been advised of the possibility of such damages. In no event shall Company's total aggregate liability exceed the lesser of (a) $1,000 or (b) one month of Employee's base salary.",
|
| 293 |
+
"ctype": "LIABILITY_CAP", "sev": "HIGH",
|
| 294 |
+
"title": "Extremely Low Liability Cap with Unlimited Damage Waiver",
|
| 295 |
+
"reason": "The company caps its liability at just $1,000 or one month's salary (whichever is lower) and completely waives all indirect, consequential, and punitive damages β even if they knowingly caused harm.",
|
| 296 |
+
"plain": "No matter what the company does to you β even if they intentionally harm you β the most you can ever recover is $1,000 or one month's pay. You cannot claim any additional damages for lost opportunities, emotional distress, or other consequences.",
|
| 297 |
+
"action": "Negotiate liability cap to at least 12 months' salary or the full value of the contract. Remove the blanket waiver of consequential damages for cases of willful misconduct.",
|
| 298 |
+
"safer": "Company's total aggregate liability under this Agreement shall not exceed the greater of (a) twelve (12) months of Employee's base salary or (b) $50,000. This limitation shall not apply to damages arising from Company's willful misconduct, gross negligence, fraud, or violation of applicable law.",
|
| 299 |
+
"negotiation": "Hi [Name],\n\nThe liability cap at $1,000 or one month's salary is extremely low β it basically means the company faces no meaningful consequences even for serious violations of the agreement. I'd like to propose a more balanced cap at 12 months' salary or $50,000, with an exception for willful misconduct and fraud.\n\nThis is standard for contracts like this and ensures both parties have real skin in the game. Let me know your thoughts.\n\nBest,\n[Your Name]",
|
| 300 |
+
"impacts": ["If the company breaches the contract and costs you your career, the most you get is $1,000", "You cannot recover for lost job opportunities, relocation costs, or emotional distress caused by the company's actions"],
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"text": "Company may terminate this Agreement and Employee's engagement at any time, with or without cause, upon providing one (1) day written notice. Upon termination, Employee shall receive no severance, continuation of benefits, or compensation of any kind other than base salary earned through the date of termination.",
|
| 304 |
+
"ctype": "TERMINATION", "sev": "HIGH",
|
| 305 |
+
"title": "No-Notice At-Will Termination with Zero Severance",
|
| 306 |
+
"reason": "The company can fire you with just 1 day notice for any reason β or no reason at all β and you walk away with absolutely nothing: no severance, no benefits continuation, no compensation of any kind.",
|
| 307 |
+
"plain": "The company can fire you tomorrow with one day's notice and pay you nothing beyond what you already earned. No severance, no health insurance continuation, no transition support β you're on your own immediately.",
|
| 308 |
+
"action": "Negotiate a minimum 30-day notice period (or pay in lieu) and at least 2-4 weeks of severance, especially for termination without cause.",
|
| 309 |
+
"safer": "Either party may terminate this Agreement upon thirty (30) days written notice. In the event of termination by Company without cause, Employee shall receive severance equal to four (4) weeks of base salary and continuation of health benefits for thirty (30) days. Termination with cause requires written documentation of the specific cause.",
|
| 310 |
+
"negotiation": "Hi [Name],\n\nI noticed the termination clause allows the company to end the relationship with essentially zero notice and provides no severance or benefits continuation whatsoever. This creates significant financial risk for me.\n\nI'd suggest a more balanced approach: 30 days' notice on both sides, plus a modest severance of 4 weeks' salary if terminated without cause. This is standard practice and ensures stability for both parties.\n\nWould you be open to discussing this?\n\nThanks,\n[Your Name]",
|
| 311 |
+
"impacts": ["You could lose your job with no warning and no financial cushion whatsoever", "You immediately lose health insurance with no COBRA or continuation option provided", "The company can fire you for an arbitrary reason with no documentation required"],
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"text": "This Agreement shall automatically renew for successive one-year terms unless either party provides written notice of non-renewal at least ninety (90) days prior to the end of the then-current term.",
|
| 315 |
+
"ctype": "AUTO_RENEWAL", "sev": "MEDIUM",
|
| 316 |
+
"title": "Auto-Renewal with Long 90-Day Notice Window",
|
| 317 |
+
"reason": "Contract auto-renews annually and requires 90-day notice to cancel β much longer than the standard 30-day notice. Easy to miss the window.",
|
| 318 |
+
"plain": "This agreement renews automatically every year. You must give 90 days' written notice (3 months!) to cancel β miss that window and you're locked in for another full year.",
|
| 319 |
+
"action": "Reduce notice period to 30 days and request automatic email reminders 45 days before each renewal.",
|
| 320 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"text": "The Recipient shall not disclose any Confidential Information to any third party without prior written consent of the Disclosing Party. 'Confidential Information' means all information disclosed by the Disclosing Party, whether oral, written, or in any other form, regardless of whether it is marked 'confidential.' The obligation of confidentiality shall survive termination of this Agreement indefinitely.",
|
| 324 |
+
"ctype": "NDA", "sev": "MEDIUM",
|
| 325 |
+
"title": "Overly Broad and Perpetual Confidentiality Obligation",
|
| 326 |
+
"reason": "Defines confidential information to include ALL information shared β even oral conversations and unmarked documents β and the obligation lasts forever with no expiration.",
|
| 327 |
+
"plain": "Anything the company tells you β even casual conversations or unmarked documents β counts as confidential, and you must keep it secret forever. There's no time limit and no exception for information that becomes public.",
|
| 328 |
+
"action": "Request that only written information marked 'confidential' be covered, and add a reasonable time limit (e.g., 3-5 years) or a public-domain exception.",
|
| 329 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"text": "Employee agrees to indemnify, defend, and hold harmless Company and its officers, directors, employees, and agents from and against any and all claims, damages, losses, liabilities, costs, and expenses (including reasonable attorneys' fees) arising out of or related to Employee's performance under this Agreement, regardless of whether Company was negligent or at fault.",
|
| 333 |
+
"ctype": "INDEMNIFICATION", "sev": "HIGH",
|
| 334 |
+
"title": "One-Sided Indemnification Covering Company's Own Negligence",
|
| 335 |
+
"reason": "You must pay for ALL legal costs and damages β even those caused by the company's own negligence β with no reciprocal obligation from the company. This exposes you to unlimited financial liability.",
|
| 336 |
+
"plain": "If the company does something negligent and gets sued, you have to pay all their legal bills and any damages they owe β even though it was their fault. Meanwhile, the company has no obligation to cover you for anything.",
|
| 337 |
+
"action": "Make indemnification mutual (both parties cover each other) and exclude claims arising from the other party's own negligence or misconduct.",
|
| 338 |
+
"safer": "Each party shall indemnify and hold harmless the other party from claims arising from the indemnifying party's own negligence or willful misconduct. Neither party shall be required to indemnify the other for claims arising from the other party's own fault. This obligation is mutual and reciprocal.",
|
| 339 |
+
"negotiation": "Hi [Name],\n\nThe indemnification clause is entirely one-sided β I'm responsible for covering the company's legal costs even when the company is at fault, but the company covers nothing for me. This creates potentially unlimited financial exposure.\n\nI've proposed a mutual version where each party covers claims arising from their own actions, not the other's. This is standard and fair. Can we discuss?\n\nThanks,\n[Your Name]",
|
| 340 |
+
"impacts": ["You could be forced to pay hundreds of thousands in legal fees for a lawsuit caused by the company's own negligence", "Your personal assets (savings, home) could be at risk with no cap on liability"],
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"text": "The Recipient shall not collect, store, process, or transmit any personal data of third parties without obtaining prior express written consent and implementing reasonable security measures. Any data shared with third-party service providers must be governed by a written data processing agreement.",
|
| 344 |
+
"ctype": "DATA_SHARING", "sev": "LOW",
|
| 345 |
+
"title": "Standard Data Protection Clause",
|
| 346 |
+
"reason": "Standard data protection language requiring consent and security measures before handling personal data β no unusual or risky provisions.",
|
| 347 |
+
"plain": "You must get written permission and use proper security before handling anyone's personal data. This is standard practice and protects everyone involved.",
|
| 348 |
+
"action": "No action needed β this is standard and reasonable.",
|
| 349 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
"text": "Invoices shall be submitted monthly and payment shall be made within sixty (60) days of receipt of a properly submitted invoice. Late payments shall accrue interest at a rate of 1.5% per month.",
|
| 353 |
+
"ctype": "PAYMENT", "sev": "MEDIUM",
|
| 354 |
+
"title": "Net-60 Payment Terms with High Late Interest",
|
| 355 |
+
"reason": "Net-60 payment terms (2 months to get paid) are significantly longer than standard Net-30, and the 1.5% monthly late fee compounds to over 19% annually.",
|
| 356 |
+
"plain": "You submit invoices monthly but the company has 60 days (2 months) to pay you. If they're late, they add 1.5% monthly interest β which sounds good but means you wait a long time for your money.",
|
| 357 |
+
"action": "Negotiate Net-30 payment terms and reduce late interest to a standard rate (e.g., 8-10% annually).",
|
| 358 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"text": "This Agreement shall be governed by and construed in accordance with the laws of the State of New York, without regard to its conflict of laws principles. Any legal action shall be brought exclusively in the state or federal courts located in New York County, New York.",
|
| 362 |
+
"ctype": "GOVERNING_LAW", "sev": "LOW",
|
| 363 |
+
"title": "Standard New York Governing Law and Venue",
|
| 364 |
+
"reason": "Standard choice-of-law and venue clause selecting New York, a common jurisdiction for commercial contracts.",
|
| 365 |
+
"plain": "This agreement is governed by New York law, and any court cases must be handled in New York courts β standard practice for many U.S. contracts.",
|
| 366 |
+
"action": "No action needed unless you're located far from New York and would prefer a more convenient venue.",
|
| 367 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"text": "If any provision of this Agreement is found to be invalid or unenforceable, the remaining provisions shall continue in full force and effect. This Agreement constitutes the entire agreement between the parties and supersedes all prior agreements, whether written or oral. No modification shall be effective unless in writing and signed by both parties.",
|
| 371 |
+
"ctype": "OTHER", "sev": "INFO",
|
| 372 |
+
"title": "Standard Severability and Entire Agreement",
|
| 373 |
+
"reason": "Standard boilerplate provisions covering severability (invalid parts don't void the whole agreement), entire agreement (this document is the complete deal), and written modification requirement.",
|
| 374 |
+
"plain": "Standard legal wrap-up: if one part of this contract is found invalid, the rest still stands. This document is the complete agreement, and any changes must be in writing and signed.",
|
| 375 |
+
"action": "No action needed β these are standard boilerplate provisions found in virtually every contract.",
|
| 376 |
+
"safer": "", "negotiation": "", "impacts": [],
|
| 377 |
+
},
|
| 378 |
+
]
|
| 379 |
+
|
| 380 |
+
scored = []
|
| 381 |
+
for i, d in enumerate(demo_clauses, 1):
|
| 382 |
+
clause = Clause(
|
| 383 |
+
id=i,
|
| 384 |
+
raw_text=d["text"],
|
| 385 |
+
plain_english=d["plain"],
|
| 386 |
+
clause_type=ClauseType(d["ctype"]),
|
| 387 |
+
section_heading=d["ctype"].replace("_", " "),
|
| 388 |
+
position=i,
|
| 389 |
+
confidence_score=0.95,
|
| 390 |
+
)
|
| 391 |
+
finding = RiskFinding(
|
| 392 |
+
clause_id=i,
|
| 393 |
+
severity=Severity(d["sev"]),
|
| 394 |
+
risk_title=d["title"],
|
| 395 |
+
risk_reason=d["reason"],
|
| 396 |
+
recommended_action=d["action"],
|
| 397 |
+
safer_clause_version=d["safer"],
|
| 398 |
+
negotiation_message=d["negotiation"],
|
| 399 |
+
impact_scenarios=d["impacts"],
|
| 400 |
+
)
|
| 401 |
+
scored.append(ScoredClause(clause=clause, finding=finding))
|
| 402 |
+
|
| 403 |
+
crit = sum(1 for s in scored if s.finding.severity == Severity.CRITICAL)
|
| 404 |
+
high = sum(1 for s in scored if s.finding.severity == Severity.HIGH)
|
| 405 |
+
med = sum(1 for s in scored if s.finding.severity == Severity.MEDIUM)
|
| 406 |
+
low = sum(1 for s in scored if s.finding.severity == Severity.LOW)
|
| 407 |
+
info = sum(1 for s in scored if s.finding.severity == Severity.INFO)
|
| 408 |
+
total = len(scored)
|
| 409 |
+
raw_score = (crit * 10 + high * 7 + med * 4 + low * 1) / total
|
| 410 |
+
overall = round(min(raw_score, 10.0), 1)
|
| 411 |
+
|
| 412 |
+
dt = datetime.now().strftime('%B %d, %Y at %H:%M')
|
| 413 |
+
|
| 414 |
+
markdown = f"""# ClauseGuard Risk Analysis Report
|
| 415 |
+
|
| 416 |
+
**Contract:** sample_employment_agreement.txt (Demo)
|
| 417 |
+
**Type:** Employment
|
| 418 |
+
**Overall Risk Score:** {overall}/10
|
| 419 |
+
**Generated:** {dt}
|
| 420 |
+
|
| 421 |
+
---
|
| 422 |
+
|
| 423 |
+
## Executive Summary
|
| 424 |
+
|
| 425 |
+
This employment agreement contains **{crit} critical** and **{high} high-severity** risks that demand immediate attention before signing. The most severe issues involve an overly broad IP assignment clause that claims ownership of personal projects, mandatory arbitration waiving all court rights, and a worldwide non-compete with unlimited company discretion. We strongly recommend negotiating the top 3 actions below.
|
| 426 |
+
|
| 427 |
+
---
|
| 428 |
+
|
| 429 |
+
## Top 3 Actions Before Signing
|
| 430 |
+
|
| 431 |
+
1. **Restrict IP Assignment** β Demand a carve-out excluding personal projects made on your own time and equipment unrelated to company business.
|
| 432 |
+
2. **Add Arbitration Opt-Out** β Request a 30-day window to opt out of binding arbitration and preserve your right to go to court.
|
| 433 |
+
3. **Limit the Non-Compete** β Reduce duration to 12 months and restrict geographic scope to regions where the company actually operates.
|
| 434 |
+
|
| 435 |
+
---
|
| 436 |
+
|
| 437 |
+
## Risk Summary
|
| 438 |
+
|
| 439 |
+
| Severity | Count |
|
| 440 |
+
|----------|-------|
|
| 441 |
+
| π΄ Critical | {crit} |
|
| 442 |
+
| π High | {high} |
|
| 443 |
+
| π‘ Medium | {med} |
|
| 444 |
+
| π’ Low | {low} |
|
| 445 |
+
| βΉοΈ Info | {info} |
|
| 446 |
+
|
| 447 |
+
**Total Clauses Analyzed:** {total}
|
| 448 |
+
|
| 449 |
+
---
|
| 450 |
+
|
| 451 |
+
## Clause-by-Clause Analysis
|
| 452 |
+
|
| 453 |
+
"""
|
| 454 |
+
|
| 455 |
+
for sc in scored:
|
| 456 |
+
emoji_map = {"CRITICAL": "π΄", "HIGH": "π ", "MEDIUM": "π‘", "LOW": "π’", "INFO": "βΉοΈ"}
|
| 457 |
+
emoji = emoji_map.get(sc.finding.severity.value, "βͺ")
|
| 458 |
+
markdown += f"""### {sc.clause.clause_type.value} β {emoji} {sc.finding.severity.value}
|
| 459 |
+
|
| 460 |
+
**Original Text:**
|
| 461 |
+
{sc.clause.raw_text}
|
| 462 |
+
|
| 463 |
+
**Plain English:**
|
| 464 |
+
{sc.clause.plain_english or 'N/A'}
|
| 465 |
+
|
| 466 |
+
**Risk Assessment:** {sc.finding.risk_reason}
|
| 467 |
+
|
| 468 |
+
**Recommended Action:** {sc.finding.recommended_action}
|
| 469 |
+
|
| 470 |
+
"""
|
| 471 |
+
|
| 472 |
+
if sc.finding.safer_clause_version:
|
| 473 |
+
markdown += f"**Safer Alternative:** {sc.finding.safer_clause_version}\n\n"
|
| 474 |
+
if sc.finding.negotiation_message:
|
| 475 |
+
markdown += f"**Negotiation Script:**\n\n{sc.finding.negotiation_message}\n\n"
|
| 476 |
+
if sc.finding.impact_scenarios:
|
| 477 |
+
markdown += "**Potential Consequences:**\n"
|
| 478 |
+
for impact in sc.finding.impact_scenarios:
|
| 479 |
+
markdown += f"- {impact}\n"
|
| 480 |
+
markdown += "\n"
|
| 481 |
+
markdown += "---\n\n"
|
| 482 |
+
|
| 483 |
+
markdown += "\n*Generated by ClauseGuard AI β’ Powered by Qwen2.5 via vLLM on AMD β’ Not legal advice*\n"
|
| 484 |
+
|
| 485 |
+
return FinalReport(
|
| 486 |
+
contract_name="Sample Employment Agreement (Demo)",
|
| 487 |
+
generated_at=datetime.now(),
|
| 488 |
+
summary={
|
| 489 |
+
"total_clauses": total,
|
| 490 |
+
"critical_count": crit,
|
| 491 |
+
"high_count": high,
|
| 492 |
+
"medium_count": med,
|
| 493 |
+
"low_count": low,
|
| 494 |
+
"overall_score": overall,
|
| 495 |
+
"contract_type": "Employment",
|
| 496 |
+
},
|
| 497 |
+
top_3_actions=[
|
| 498 |
+
"Restrict IP assignment to work directly related to company business, created during work hours using company resources",
|
| 499 |
+
"Add a 30-day opt-out window for binding arbitration to preserve your right to go to court",
|
| 500 |
+
"Limit non-compete to 12 months in specific regions where the company has active operations",
|
| 501 |
+
],
|
| 502 |
+
scored_clauses=scored,
|
| 503 |
+
markdown_report=markdown,
|
| 504 |
+
processed_normally=False,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def _load_demo_report() -> None:
|
| 509 |
+
st.session_state.report = _build_demo_report()
|
| 510 |
+
st.session_state.error = None
|
| 511 |
+
st.session_state.uploaded_filename = "sample_nda.txt"
|
| 512 |
+
demo_raw = ""
|
| 513 |
+
for sc in st.session_state.report.scored_clauses:
|
| 514 |
+
heading = sc.clause.section_heading or ""
|
| 515 |
+
text = sc.clause.raw_text
|
| 516 |
+
demo_raw += f"{heading}\n{text}\n\n" if heading else f"{text}\n\n"
|
| 517 |
+
st.session_state.copilot_raw_text = demo_raw.strip()
|
| 518 |
+
st.session_state.active_tab = 0
|
| 519 |
+
st.session_state.copilot_messages = []
|
| 520 |
+
st.session_state.clause_ai_responses = {}
|
| 521 |
+
st.session_state.generated_emails = {}
|
| 522 |
+
st.session_state.copilot_cache_key = None
|
| 523 |
+
st.rerun()
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def _load_guided_demo() -> None:
|
| 527 |
+
st.session_state.guided_demo = True
|
| 528 |
+
st.session_state.demo_step = 0
|
| 529 |
+
_load_demo_report()
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 533 |
+
# SESSION STATE
|
| 534 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 535 |
+
|
| 536 |
+
def _init_session_state() -> None:
|
| 537 |
+
defaults = {
|
| 538 |
+
"report": None,
|
| 539 |
+
"error": None,
|
| 540 |
+
"analyzing": False,
|
| 541 |
+
"uploaded_filename": None,
|
| 542 |
+
"uploaded_bytes": None,
|
| 543 |
+
"agent_statuses": {a: "pending" for a in AGENT_NAMES},
|
| 544 |
+
"agent_messages": {a: "" for a in AGENT_NAMES},
|
| 545 |
+
"guided_demo": False,
|
| 546 |
+
"demo_step": 0,
|
| 547 |
+
"copilot_messages": [],
|
| 548 |
+
"copilot_context": "",
|
| 549 |
+
"copilot_raw_text": "",
|
| 550 |
+
"copilot_cache_key": None,
|
| 551 |
+
"clause_ai_responses": {},
|
| 552 |
+
"pending_ai_query": None,
|
| 553 |
+
"generated_emails": {},
|
| 554 |
+
"active_tab": 0,
|
| 555 |
+
"highlight_clause_id": None,
|
| 556 |
+
}
|
| 557 |
+
for key, default in defaults.items():
|
| 558 |
+
if key not in st.session_state:
|
| 559 |
+
st.session_state[key] = default
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 563 |
+
# LIVE AGENT EVENT HANDLER
|
| 564 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 565 |
+
|
| 566 |
+
def _on_agent_event(agent: str, status: str, details: dict) -> None:
|
| 567 |
+
st.session_state.agent_statuses[agent] = status
|
| 568 |
+
st.session_state.agent_messages[agent] = details.get("message", "")
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 572 |
+
# ANALYSIS RUNNER
|
| 573 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 574 |
+
|
| 575 |
+
def _run_analysis() -> None:
|
| 576 |
+
file_bytes = st.session_state.uploaded_bytes
|
| 577 |
+
filename = st.session_state.uploaded_filename
|
| 578 |
+
try:
|
| 579 |
+
validate_config()
|
| 580 |
+
except ValueError as e:
|
| 581 |
+
st.session_state.error = str(e)
|
| 582 |
+
st.session_state.analyzing = False
|
| 583 |
+
return
|
| 584 |
+
|
| 585 |
+
for a in AGENT_NAMES:
|
| 586 |
+
st.session_state.agent_statuses[a] = "pending"
|
| 587 |
+
st.session_state.agent_messages[a] = ""
|
| 588 |
+
|
| 589 |
+
set_event_callback(_on_agent_event)
|
| 590 |
+
|
| 591 |
+
progress_bar = st.progress(0)
|
| 592 |
+
status_text = st.empty()
|
| 593 |
+
agent_panel = st.empty()
|
| 594 |
+
|
| 595 |
+
try:
|
| 596 |
+
status_text.markdown("<h3 style='color:#fff'>π Reading file...</h3>", unsafe_allow_html=True)
|
| 597 |
+
raw_text = extract_text(file_bytes, filename)
|
| 598 |
+
st.session_state.copilot_raw_text = raw_text
|
| 599 |
+
|
| 600 |
+
status_text.markdown("<h3 style='color:#8ab4f8'>π Testing model connection...</h3>", unsafe_allow_html=True)
|
| 601 |
+
ok, conn_err = _check_model_connectivity()
|
| 602 |
+
if not ok:
|
| 603 |
+
st.session_state.error = f"Cannot connect to model API: {conn_err}"
|
| 604 |
+
st.session_state.analyzing = False
|
| 605 |
+
progress_bar.empty()
|
| 606 |
+
status_text.empty()
|
| 607 |
+
agent_panel.empty()
|
| 608 |
+
st.rerun()
|
| 609 |
+
return
|
| 610 |
+
|
| 611 |
+
status_text.markdown("<h3 style='color:#8ab4f8'>π€ Running AI analysis pipeline...</h3>", unsafe_allow_html=True)
|
| 612 |
+
|
| 613 |
+
def _render_agent_panel():
|
| 614 |
+
rows = ""
|
| 615 |
+
for a in AGENT_NAMES:
|
| 616 |
+
step = AGENT_STEP_NUMBERS.get(a, "")
|
| 617 |
+
s = st.session_state.agent_statuses[a]
|
| 618 |
+
icon = AGENT_ICONS.get(s, "β³")
|
| 619 |
+
msg = st.session_state.agent_messages.get(a, "")
|
| 620 |
+
if s == "completed":
|
| 621 |
+
color = "#55dd55"
|
| 622 |
+
anim = ""
|
| 623 |
+
elif s == "failed":
|
| 624 |
+
color = "#ff4444"
|
| 625 |
+
anim = ""
|
| 626 |
+
elif s == "running":
|
| 627 |
+
color = "#ffaa44"
|
| 628 |
+
anim = " class='agent-running'"
|
| 629 |
+
else:
|
| 630 |
+
color = "#666"
|
| 631 |
+
anim = ""
|
| 632 |
+
rows += f"<tr{anim}><td style='padding:8px 12px;text-align:center;font-size:1.1rem'>{step}</td><td style='padding:8px 12px'>{icon}</td><td style='padding:8px 12px;color:{color};font-weight:600'>{a}</td><td style='padding:8px 12px;color:#aaa;font-size:0.85rem'>{msg}</td></tr>"
|
| 633 |
+
return f"<div style='background:#1a1a2e;border-radius:14px;padding:1.25rem;border:1px solid rgba(255,255,255,0.08)'><table style='width:100%;border-collapse:collapse'><thead><tr><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px'>Step</th><th style='padding:6px 12px'></th><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px;text-align:left'>Agent</th><th style='padding:6px 12px;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:1px;text-align:left'>Status</th></tr></thead><tbody>{rows}</tbody></table></div>"
|
| 634 |
+
|
| 635 |
+
agent_panel.markdown(_render_agent_panel(), unsafe_allow_html=True)
|
| 636 |
+
|
| 637 |
+
loop = asyncio.new_event_loop()
|
| 638 |
+
asyncio.set_event_loop(loop)
|
| 639 |
+
try:
|
| 640 |
+
report = loop.run_until_complete(run_pipeline(raw_text, filename))
|
| 641 |
+
finally:
|
| 642 |
+
loop.close()
|
| 643 |
+
|
| 644 |
+
for a in AGENT_NAMES:
|
| 645 |
+
if st.session_state.agent_statuses[a] == "pending":
|
| 646 |
+
st.session_state.agent_statuses[a] = "completed"
|
| 647 |
+
st.session_state.agent_messages[a] = "OK"
|
| 648 |
+
agent_panel.markdown(_render_agent_panel(), unsafe_allow_html=True)
|
| 649 |
+
|
| 650 |
+
progress_bar.progress(1.0)
|
| 651 |
+
|
| 652 |
+
if report.summary.total_clauses == 0:
|
| 653 |
+
logger.error("Pipeline produced 0 clauses β model API may be unreachable or returned errors")
|
| 654 |
+
failed_agents = [
|
| 655 |
+
a for a in AGENT_NAMES
|
| 656 |
+
if st.session_state.agent_statuses.get(a) == "failed"
|
| 657 |
+
]
|
| 658 |
+
if failed_agents:
|
| 659 |
+
st.session_state.error = (
|
| 660 |
+
f"Analysis failed β the {failed_agents[0]} agent could not complete. "
|
| 661 |
+
"The model API may be unreachable or returned malformed responses. "
|
| 662 |
+
"Check that the vLLM endpoint is running at the configured BASE_URL."
|
| 663 |
+
)
|
| 664 |
+
else:
|
| 665 |
+
st.session_state.error = (
|
| 666 |
+
"Analysis could not extract any clauses from the document. "
|
| 667 |
+
"The model may be unavailable or the document format may be unsupported. "
|
| 668 |
+
"Check your model endpoint configuration."
|
| 669 |
+
)
|
| 670 |
+
status_text.markdown("<h3 style='color:#ff4444'>β Analysis failed</h3>", unsafe_allow_html=True)
|
| 671 |
+
st.session_state.report = None
|
| 672 |
+
st.session_state.analyzing = False
|
| 673 |
+
progress_bar.empty()
|
| 674 |
+
status_text.empty()
|
| 675 |
+
agent_panel.empty()
|
| 676 |
+
st.rerun()
|
| 677 |
+
return
|
| 678 |
+
|
| 679 |
+
status_text.markdown("<h3 style='color:#55dd55'>β
Analysis complete!</h3>", unsafe_allow_html=True)
|
| 680 |
+
st.session_state.report = report
|
| 681 |
+
st.session_state.error = None
|
| 682 |
+
st.session_state.copilot_messages = []
|
| 683 |
+
st.session_state.clause_ai_responses = {}
|
| 684 |
+
st.session_state.generated_emails = {}
|
| 685 |
+
|
| 686 |
+
if not report.processed_normally or report.summary.critical_count == 0 and report.summary.high_count == 0 and report.summary.medium_count == 0:
|
| 687 |
+
st.session_state.error = (
|
| 688 |
+
"Analysis completed but no significant risks were detected. "
|
| 689 |
+
"The model responses may have been incomplete β review the "
|
| 690 |
+
f"report ({report.summary.total_clauses} clauses analyzed) carefully."
|
| 691 |
+
)
|
| 692 |
+
|
| 693 |
+
except ValueError as e:
|
| 694 |
+
st.session_state.error = f"Could not process: {e}"
|
| 695 |
+
except Exception as e:
|
| 696 |
+
st.session_state.error = "An unexpected error occurred. Try again."
|
| 697 |
+
logger.error("Analysis error: %s", e)
|
| 698 |
+
finally:
|
| 699 |
+
st.session_state.analyzing = False
|
| 700 |
+
progress_bar.empty()
|
| 701 |
+
status_text.empty()
|
| 702 |
+
agent_panel.empty()
|
| 703 |
+
st.rerun()
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 707 |
+
# FALLBACK GENERATORS FOR NEGOTIATION COPILOT
|
| 708 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 709 |
+
|
| 710 |
+
def _generate_fallback_safer(sc: ScoredClause) -> str:
|
| 711 |
+
ctype = sc.clause.clause_type.value
|
| 712 |
+
fallbacks = {
|
| 713 |
+
"IP_ASSIGNMENT": "Employee assigns only inventions directly related to Company's business, created during working hours using Company resources. Personal projects remain Employee's property.",
|
| 714 |
+
"ARBITRATION": "Either party may opt out of arbitration within 30 days. Both parties retain the right to bring claims in court.",
|
| 715 |
+
"NON_COMPETE": "Non-compete limited to 12 months within specific metro areas where Company operates.",
|
| 716 |
+
"AUTO_RENEWAL": "Agreement renews only with mutual written consent. No automatic renewal.",
|
| 717 |
+
"TERMINATION": "Either party may terminate with 30 days written notice.",
|
| 718 |
+
"INDEMNIFICATION": "Indemnification limited to direct damages caused by negligence or willful misconduct.",
|
| 719 |
+
"LIABILITY_CAP": "Liability capped at the greater of fees paid or $10,000.",
|
| 720 |
+
"DATA_SHARING": "Data shared only with explicit opt-in consent, revocable at any time.",
|
| 721 |
+
"GOVERNING_LAW": "Governing law set to user's home state with optional mediation.",
|
| 722 |
+
"PAYMENT": "Payment due net-30 after invoice receipt. Late fees capped at 5% annually.",
|
| 723 |
+
"CONFIDENTIALITY": "Confidential information excludes publicly available data and independently developed knowledge.",
|
| 724 |
+
"NON_SOLICITATION": "Non-solicitation limited to 12 months and applies only to employees directly worked with.",
|
| 725 |
+
"FORCE_MAJEURE": "Neither party liable for delays due to circumstances beyond reasonable control, with prompt notice.",
|
| 726 |
+
"SEVERABILITY": "If any provision is found unenforceable, remaining provisions stay in full effect.",
|
| 727 |
+
"ASSIGNMENT": "Neither party may assign without written consent, not to be unreasonably withheld.",
|
| 728 |
+
"WAIVER": "Failure to enforce any provision does not constitute waiver. Waivers must be in writing.",
|
| 729 |
+
"SURVIVAL": "Confidentiality, indemnification, and payment obligations survive termination.",
|
| 730 |
+
"NOTICE": "Notices effective upon email delivery with read receipt or 3 days after certified mail.",
|
| 731 |
+
}
|
| 732 |
+
return fallbacks.get(ctype, "Request a mutual agreement: both parties share rights and obligations equally. Remove one-sided provisions.")
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
def _generate_fallback_message(sc: ScoredClause) -> str:
|
| 736 |
+
topic = sc.clause.section_heading or sc.clause.clause_type.value.replace("_", " ").title()
|
| 737 |
+
safer = sc.finding.safer_clause_version or _generate_fallback_safer(sc)
|
| 738 |
+
return (
|
| 739 |
+
f"Hi,\n\nI've reviewed the contract and would like to discuss the {topic} clause. "
|
| 740 |
+
f"I'd suggest the following adjustment:\n\n'{safer}'\n\n"
|
| 741 |
+
f"This ensures both parties are treated fairly. Would you be open to this change?\n\nThanks!"
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def _build_safer_contract(report: FinalReport) -> str:
|
| 746 |
+
lines: list[str] = []
|
| 747 |
+
lines.append(f"# SAFER VERSION β {report.contract_name}")
|
| 748 |
+
lines.append(f"# Auto-generated by ClauseGuard β replaces {report.summary.critical_count + report.summary.high_count} high-risk clauses")
|
| 749 |
+
lines.append(f"# Original risk score: {report.summary.overall_score}/10")
|
| 750 |
+
lines.append(f"# Generated: {datetime.now().strftime('%B %d, %Y at %H:%M')}")
|
| 751 |
+
lines.append("")
|
| 752 |
+
|
| 753 |
+
replaced_count = 0
|
| 754 |
+
for i, sc in enumerate(report.scored_clauses, 1):
|
| 755 |
+
safer = sc.finding.safer_clause_version
|
| 756 |
+
sev = sc.finding.severity
|
| 757 |
+
|
| 758 |
+
if safer and sev in (Severity.CRITICAL, Severity.HIGH):
|
| 759 |
+
replaced_count += 1
|
| 760 |
+
lines.append(f"# {'β' * 70}")
|
| 761 |
+
lines.append(f"# CLAUSE {i}: REPLACED β {sev.value} Risk β {sc.finding.risk_title}")
|
| 762 |
+
lines.append(f"# {'β' * 70}")
|
| 763 |
+
lines.append(f"# ORIGINAL (RISKY):")
|
| 764 |
+
for orig_line in sc.clause.raw_text.split("\n"):
|
| 765 |
+
lines.append(f"# {orig_line.strip()}")
|
| 766 |
+
lines.append(f"#")
|
| 767 |
+
lines.append(f"# SAFER VERSION:")
|
| 768 |
+
lines.append(f"{i}. {sc.clause.section_heading or 'CLAUSE ' + str(i)}")
|
| 769 |
+
lines.append(f" {safer}")
|
| 770 |
+
lines.append("")
|
| 771 |
+
else:
|
| 772 |
+
heading = sc.clause.section_heading or f"CLAUSE {i}"
|
| 773 |
+
lines.append(f"{i}. {heading}")
|
| 774 |
+
lines.append(f" {sc.clause.raw_text.strip()}")
|
| 775 |
+
lines.append("")
|
| 776 |
+
|
| 777 |
+
lines.append(f"# {'=' * 70}")
|
| 778 |
+
lines.append(f"# END OF SAFER CONTRACT")
|
| 779 |
+
lines.append(f"# {replaced_count} clauses replaced | {report.summary.total_clauses - replaced_count} left unchanged")
|
| 780 |
+
return "\n".join(lines)
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββ
|
| 784 |
+
# UI HELPER FUNCTIONS
|
| 785 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 786 |
+
|
| 787 |
+
def seats(n: int) -> str:
|
| 788 |
+
if n <= 0:
|
| 789 |
+
return "No parties"
|
| 790 |
+
if n == 1:
|
| 791 |
+
return "1 party"
|
| 792 |
+
return f"{n} parties"
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
def _render_info_card(title: str, body: str, icon: str = "βΉοΈ", bg: str = "rgba(30,144,255,0.08)", border: str = "#1e90ff") -> str:
|
| 796 |
+
return f"""<div style="background:{bg};border-left:4px solid {border};border-radius:4px 12px 12px 4px;padding:0.75rem 1rem;margin:0.4rem 0">
|
| 797 |
+
<span style="font-size:0.85rem;font-weight:600;color:#ccc">{icon} {title}</span>
|
| 798 |
+
<div style="font-size:0.82rem;color:#aaa;margin-top:0.25rem;line-height:1.5">{body}</div>
|
| 799 |
+
</div>"""
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
def _render_info_card_raw(html: str) -> None:
|
| 803 |
+
st.markdown(html, unsafe_allow_html=True)
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _switch_to_chat_with_prompt(prompt_text: str) -> None:
|
| 807 |
+
st.session_state.active_tab = 3
|
| 808 |
+
st.session_state.pending_ai_query = prompt_text
|
| 809 |
+
st.rerun()
|
| 810 |
+
|
| 811 |
+
|
| 812 |
+
def _render_single_clause_card(sc: ScoredClause, style: dict, show_actions: bool = True) -> None:
|
| 813 |
+
s = style
|
| 814 |
+
c = sc.clause
|
| 815 |
+
f = sc.finding
|
| 816 |
+
|
| 817 |
+
st.markdown(f"""
|
| 818 |
+
<div style="
|
| 819 |
+
background: linear-gradient(135deg, {s['bg']} 0%, rgba(20,22,30,0.6) 100%);
|
| 820 |
+
border: 1px solid {s['border']}22;
|
| 821 |
+
border-left: 4px solid {s['border']};
|
| 822 |
+
border-radius: 0 12px 12px 0;
|
| 823 |
+
padding: 1.25rem 1.25rem 0.75rem 1.25rem;
|
| 824 |
+
margin-bottom: 0.5rem;
|
| 825 |
+
">
|
| 826 |
+
<div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.75rem">
|
| 827 |
+
<span style="
|
| 828 |
+
display:inline-flex;align-items:center;gap:4px;
|
| 829 |
+
background:{s['tag_bg']};
|
| 830 |
+
color:{s['color']};
|
| 831 |
+
padding:0.25rem 0.75rem;
|
| 832 |
+
border-radius:20px;
|
| 833 |
+
font-size:0.73rem;
|
| 834 |
+
font-weight:700;
|
| 835 |
+
letter-spacing:0.4px;
|
| 836 |
+
text-transform:uppercase;
|
| 837 |
+
white-space:nowrap;
|
| 838 |
+
">{s['badge']}</span>
|
| 839 |
+
<span style="font-weight:600;color:#e8e8e8;font-size:1rem;line-height:1.3">{f.risk_title}</span>
|
| 840 |
+
</div>
|
| 841 |
+
<div style="display:flex;gap:1rem;margin-bottom:0.6rem">
|
| 842 |
+
<span style="color:#888;font-size:0.75rem">π {c.section_heading or ''}</span>
|
| 843 |
+
<span style="color:#888;font-size:0.75rem">π·οΈ {c.clause_type.value}</span>
|
| 844 |
+
<span style="color:#666;font-size:0.75rem">Clause #{c.id}</span>
|
| 845 |
+
</div>
|
| 846 |
+
</div>""", unsafe_allow_html=True)
|
| 847 |
+
|
| 848 |
+
with st.expander("π View Original Text"):
|
| 849 |
+
st.markdown(f"<div style='background:#1c1d2a;padding:0.85rem;border-radius:8px;font-family:Consolas,monospace;font-size:0.88rem;line-height:1.65;color:#d0d0d0;white-space:pre-wrap'>{c.raw_text}</div>", unsafe_allow_html=True)
|
| 850 |
+
|
| 851 |
+
if c.plain_english:
|
| 852 |
+
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:rgba(30,144,255,0.07);border-radius:8px;border:1px solid rgba(30,144,255,0.12)">
|
| 853 |
+
<span style="font-size:0.95rem;flex-shrink:0">π¬</span>
|
| 854 |
+
<span style="color:#c0cfe0;font-size:0.9rem;line-height:1.5">{c.plain_english}</span>
|
| 855 |
+
</div>""", unsafe_allow_html=True)
|
| 856 |
+
|
| 857 |
+
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:{s['bg']};border-radius:8px;border:1px solid {s['border']}18">
|
| 858 |
+
<span style="font-size:0.95rem;flex-shrink:0">β οΈ</span>
|
| 859 |
+
<div>
|
| 860 |
+
<div style="color:{s['color']};font-size:0.8rem;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:0.2rem">Risk</div>
|
| 861 |
+
<div style="color:#d0d0d0;font-size:0.9rem;line-height:1.55">{f.risk_reason}</div>
|
| 862 |
+
</div>
|
| 863 |
+
</div>""", unsafe_allow_html=True)
|
| 864 |
+
|
| 865 |
+
if f.recommended_action:
|
| 866 |
+
st.markdown(f"""<div style="display:flex;gap:0.5rem;align-items:flex-start;margin:0.5rem 0;padding:0.6rem 0.85rem;background:rgba(50,205,50,0.06);border-radius:8px;border:1px solid rgba(50,205,50,0.12)">
|
| 867 |
+
<span style="font-size:0.95rem;flex-shrink:0">β
</span>
|
| 868 |
+
<span style="color:#b0d0b0;font-size:0.9rem;line-height:1.5">{f.recommended_action}</span>
|
| 869 |
+
</div>""", unsafe_allow_html=True)
|
| 870 |
+
|
| 871 |
+
if f.impact_scenarios:
|
| 872 |
+
with st.expander("β οΈ What Could Happen If You Sign This"):
|
| 873 |
+
for impact in f.impact_scenarios:
|
| 874 |
+
st.markdown(f"<div style='background:rgba(255,68,68,0.06);padding:0.4rem 0.75rem;margin:0.15rem 0;border-radius:6px;border-left:3px solid {s['border']};font-size:0.85rem;color:#e0a0a0'>β’ {impact}</div>", unsafe_allow_html=True)
|
| 875 |
+
|
| 876 |
+
if show_actions and f.severity not in (Severity.LOW, Severity.INFO):
|
| 877 |
+
if st.button("βοΈ Ask AI to Explain", key=f"explain_{c.id}", use_container_width=True):
|
| 878 |
+
_switch_to_chat_with_prompt(f"Explain clause {c.id} ({f.risk_title}) in simple terms. What does this mean for me?")
|
| 879 |
+
|
| 880 |
+
|
| 881 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 882 |
+
# HEADER
|
| 883 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 884 |
+
|
| 885 |
+
def render_header() -> None:
|
| 886 |
+
hero_l, hero_r = st.columns([3, 1])
|
| 887 |
+
with hero_l:
|
| 888 |
+
st.markdown("""<div style="background:linear-gradient(135deg,#1e3a5f 0%,#2a5298 100%);padding:1.5rem 2rem;border-radius:16px;margin-bottom:0.5rem">
|
| 889 |
+
<h1 style="margin:0;color:#fff;font-size:2.2rem">π‘οΈ ClauseGuard</h1>
|
| 890 |
+
<p style="margin:0.25rem 0 0 0;color:#c8d8f0;font-size:1.1rem">AI-Powered Contract Clause Risk Analyzer</p>
|
| 891 |
+
</div>""", unsafe_allow_html=True)
|
| 892 |
+
with hero_r:
|
| 893 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 894 |
+
dc1, dc2 = st.columns(2)
|
| 895 |
+
with dc1:
|
| 896 |
+
if st.button("β‘ Instant Demo", use_container_width=True, help="See a pre-analyzed NDA report instantly"):
|
| 897 |
+
_load_demo_report()
|
| 898 |
+
with dc2:
|
| 899 |
+
if st.button("π¬ Guided Tour", use_container_width=True, help="Walk through a demo with highlights"):
|
| 900 |
+
_load_guided_demo()
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 904 |
+
# GUIDED DEMO TOUR
|
| 905 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 906 |
+
|
| 907 |
+
def _render_guided_tour() -> None:
|
| 908 |
+
if not st.session_state.get("guided_demo"):
|
| 909 |
+
return
|
| 910 |
+
step = st.session_state.get("demo_step", 0)
|
| 911 |
+
tour_steps = [
|
| 912 |
+
{
|
| 913 |
+
"title": "π― Welcome to ClauseGuard!",
|
| 914 |
+
"body": "Let's walk through a sample NDA contract analysis. You'll see how 5 AI agents work together to identify risks, explain legal jargon, and help you negotiate better terms. Each agent has a specific role in the pipeline.",
|
| 915 |
+
"tab": 0,
|
| 916 |
+
"icon": "π―",
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"title": "π Step 1: Risk Overview Dashboard",
|
| 920 |
+
"body": "The **Overview tab** shows your contract's risk score, severity breakdown, and the top 3 actions you should take before signing. Check the bar chart to see how many clauses fall into each risk category. The risk score is calculated from 0 (safe) to 10 (extremely risky).",
|
| 921 |
+
"tab": 0,
|
| 922 |
+
"icon": "π",
|
| 923 |
+
},
|
| 924 |
+
{
|
| 925 |
+
"title": "π Step 2: Clause-by-Clause Deep Dive",
|
| 926 |
+
"body": "Switch to the **Clauses tab** to drill into each clause. Critical and High-risk clauses are expanded by default so you see the most dangerous issues first. Each clause card shows: original legal text, plain English translation, the specific risk reason, and a recommended action.",
|
| 927 |
+
"tab": 1,
|
| 928 |
+
"icon": "π",
|
| 929 |
+
},
|
| 930 |
+
{
|
| 931 |
+
"title": "π¬ Step 3: Negotiation Copilot",
|
| 932 |
+
"body": "In the **Negotiation tab**, you'll find side-by-side comparisons: what you signed vs. what you should ask for instead. Each risky clause comes with a pre-written negotiation message and a safer alternative. You can also download a fully rewritten 'Safer Contract' with all high-risk clauses replaced.",
|
| 933 |
+
"tab": 2,
|
| 934 |
+
"icon": "π¬",
|
| 935 |
+
},
|
| 936 |
+
{
|
| 937 |
+
"title": "π€ Step 4: AI Chat Assistant",
|
| 938 |
+
"body": "The **Chat Assistant tab** lets you ask follow-up questions in plain English. The AI has full context of your entire contract and all clause analyses. Try questions like 'Summarize this contract' or 'What's the most dangerous clause and why?' Use the quick-action chips for common questions.",
|
| 939 |
+
"tab": 3,
|
| 940 |
+
"icon": "π€",
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"title": "β
You're Ready!",
|
| 944 |
+
"body": "Now you know your way around ClauseGuard. Use the **Instant Demo** button anytime to revisit this tour, or upload your own contract to run a real analysis with the full 5-agent AI pipeline. Remember: always consult a qualified attorney for final legal review.",
|
| 945 |
+
"tab": 0,
|
| 946 |
+
"icon": "β
",
|
| 947 |
+
},
|
| 948 |
+
]
|
| 949 |
+
|
| 950 |
+
if step < len(tour_steps):
|
| 951 |
+
ts = tour_steps[step]
|
| 952 |
+
progress_pct = (step + 1) / len(tour_steps)
|
| 953 |
+
with st.container():
|
| 954 |
+
st.markdown(f"""<div style="background:linear-gradient(135deg,#1e3a5f 0%,#2a5298 100%);padding:1.25rem 1.5rem;border-radius:14px;margin:0.5rem 0;border:1px solid rgba(255,255,255,0.1)">
|
| 955 |
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.25rem">
|
| 956 |
+
<span style="font-size:1.5rem">{ts['icon']}</span>
|
| 957 |
+
<h3 style="margin:0;color:#fff;font-size:1.2rem">{ts['title']}</h3>
|
| 958 |
+
</div>
|
| 959 |
+
<p style="color:#c8d8f0;margin:0.5rem 0;line-height:1.6">{ts['body']}</p>
|
| 960 |
+
<div style="background:rgba(255,255,255,0.1);border-radius:4px;height:4px;margin-top:0.75rem">
|
| 961 |
+
<div style="background:linear-gradient(90deg,#667eea,#764ba2);border-radius:4px;height:100%;width:{progress_pct*100:.0f}%"></div>
|
| 962 |
+
</div>
|
| 963 |
+
<div style="text-align:right;font-size:0.75rem;color:rgba(255,255,255,0.5);margin-top:0.25rem">Step {step + 1} of {len(tour_steps)}</div>
|
| 964 |
+
</div>""", unsafe_allow_html=True)
|
| 965 |
+
|
| 966 |
+
c1, c2, c3 = st.columns([1, 1, 1])
|
| 967 |
+
with c1:
|
| 968 |
+
if step > 0:
|
| 969 |
+
if st.button("β¬
οΈ Previous", key=f"tour_prev_{step}", use_container_width=True):
|
| 970 |
+
st.session_state.demo_step = step - 1
|
| 971 |
+
st.rerun()
|
| 972 |
+
with c3:
|
| 973 |
+
if st.button("Next β‘οΈ" if step < len(tour_steps) - 1 else "β
Finish Tour", key=f"tour_next_{step}", use_container_width=True):
|
| 974 |
+
if step < len(tour_steps) - 1:
|
| 975 |
+
st.session_state.demo_step = step + 1
|
| 976 |
+
tab_idx = tour_steps[step + 1]["tab"]
|
| 977 |
+
st.session_state.active_tab = tab_idx
|
| 978 |
+
else:
|
| 979 |
+
st.session_state.guided_demo = False
|
| 980 |
+
st.rerun()
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 984 |
+
# RISK BANNER
|
| 985 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 986 |
+
|
| 987 |
+
def render_risk_banner() -> None:
|
| 988 |
+
if not st.session_state.report:
|
| 989 |
+
return
|
| 990 |
+
r = st.session_state.report
|
| 991 |
+
s = r.summary
|
| 992 |
+
total_risky = s.critical_count + s.high_count
|
| 993 |
+
|
| 994 |
+
if total_risky >= 3:
|
| 995 |
+
st.error(f"π¨ **HIGH ALERT β {total_risky} critical or high-risk clauses detected!** Review carefully before signing. We strongly recommend negotiating these terms.")
|
| 996 |
+
elif total_risky > 0:
|
| 997 |
+
st.warning(f"β οΈ **This contract has {total_risky} high-risk clause(s)** β review carefully before signing")
|
| 998 |
+
elif s.medium_count > 0:
|
| 999 |
+
st.info(f"βΉοΈ **{s.medium_count} medium-risk clause(s) found** β this contract may need attention before signing")
|
| 1000 |
+
else:
|
| 1001 |
+
st.success("β
**This contract looks clean** β no high or critical risk clauses detected. Still review all terms before signing.")
|
| 1002 |
+
|
| 1003 |
+
|
| 1004 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1005 |
+
# ISSUES SUMMARY (displays before tabs)
|
| 1006 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1007 |
+
|
| 1008 |
+
def render_issues_summary() -> None:
|
| 1009 |
+
report = st.session_state.report
|
| 1010 |
+
criticals = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.CRITICAL]
|
| 1011 |
+
highs = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.HIGH]
|
| 1012 |
+
mediums = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.MEDIUM]
|
| 1013 |
+
all_issues = criticals + highs + mediums
|
| 1014 |
+
|
| 1015 |
+
if not all_issues:
|
| 1016 |
+
if not report.processed_normally:
|
| 1017 |
+
st.warning(
|
| 1018 |
+
"β οΈ **Analysis was incomplete** β the AI risk scorer could not evaluate these clauses. "
|
| 1019 |
+
"All clauses are marked as MEDIUM 'Needs Human Review'. "
|
| 1020 |
+
"This typically means the model API is having issues. Check your vLLM endpoint configuration."
|
| 1021 |
+
)
|
| 1022 |
+
return
|
| 1023 |
+
st.success("β
No issues found β all clauses look reasonable. Use the tabs below to explore the full analysis.")
|
| 1024 |
+
return
|
| 1025 |
+
|
| 1026 |
+
st.markdown("## π Issues Found")
|
| 1027 |
+
total_labels = []
|
| 1028 |
+
if criticals:
|
| 1029 |
+
total_labels.append(f"{len(criticals)} critical")
|
| 1030 |
+
if highs:
|
| 1031 |
+
total_labels.append(f"{len(highs)} high")
|
| 1032 |
+
if mediums:
|
| 1033 |
+
total_labels.append(f"{len(mediums)} medium")
|
| 1034 |
+
st.caption(f"{len(all_issues)} clauses need attention β {', '.join(total_labels)}")
|
| 1035 |
+
|
| 1036 |
+
issue_cols = st.columns(min(len(all_issues), 3))
|
| 1037 |
+
for idx, sc in enumerate(all_issues):
|
| 1038 |
+
col_idx = idx % 3
|
| 1039 |
+
style = SEVERITY_STYLE.get(sc.finding.severity, SEVERITY_STYLE[Severity.INFO])
|
| 1040 |
+
with issue_cols[col_idx]:
|
| 1041 |
+
reason_preview = sc.finding.risk_reason[:120]
|
| 1042 |
+
if len(sc.finding.risk_reason) > 120:
|
| 1043 |
+
reason_preview += "..."
|
| 1044 |
+
st.markdown(
|
| 1045 |
+
f"""<div style="background:#1e1e2e;border-radius:12px;padding:1rem;margin:0.3rem 0;
|
| 1046 |
+
border-top:3px solid {style['border']};border-left:1px solid #333;border-right:1px solid #333;border-bottom:1px solid #333">
|
| 1047 |
+
<div style="font-weight:700;margin-bottom:0.3rem;font-size:0.8rem">{style['badge']}</div>
|
| 1048 |
+
<div style="font-size:0.9rem;color:#e0e0e0;line-height:1.4;margin-bottom:0.5rem"><b>{sc.finding.risk_title}</b></div>
|
| 1049 |
+
<div style="font-size:0.8rem;color:#aaa;line-height:1.4">{reason_preview}</div>
|
| 1050 |
+
</div>""",
|
| 1051 |
+
unsafe_allow_html=True,
|
| 1052 |
+
)
|
| 1053 |
+
st.markdown("")
|
| 1054 |
+
|
| 1055 |
+
|
| 1056 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1057 |
+
# TAB 1: OVERVIEW
|
| 1058 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1059 |
+
|
| 1060 |
+
def render_overview_tab() -> None:
|
| 1061 |
+
report = st.session_state.report
|
| 1062 |
+
s = report.summary
|
| 1063 |
+
|
| 1064 |
+
st.markdown("### π Risk Score Dashboard")
|
| 1065 |
+
st.caption(f"Contract Type: **{s.contract_type}** β’ {s.total_clauses} clauses analyzed β’ {s.critical_count + s.high_count + s.medium_count} need attention")
|
| 1066 |
+
|
| 1067 |
+
col_a, col_b, col_c = st.columns([1, 2, 1])
|
| 1068 |
+
with col_a:
|
| 1069 |
+
score = s.overall_score
|
| 1070 |
+
if score >= 7:
|
| 1071 |
+
sc_color = "#ff4444"
|
| 1072 |
+
label = "High Risk"
|
| 1073 |
+
bg_glow = "rgba(255,68,68,0.08)"
|
| 1074 |
+
elif score >= 4:
|
| 1075 |
+
sc_color = "#ff8c00"
|
| 1076 |
+
label = "Medium Risk"
|
| 1077 |
+
bg_glow = "rgba(255,140,0,0.06)"
|
| 1078 |
+
else:
|
| 1079 |
+
sc_color = "#32cd32"
|
| 1080 |
+
label = "Low Risk"
|
| 1081 |
+
bg_glow = "rgba(50,205,50,0.06)"
|
| 1082 |
+
st.markdown(f"""<div style="background:#1e1e2e;border-radius:16px;padding:1.5rem;text-align:center;border:1px solid #333;box-shadow:0 0 30px {bg_glow}">
|
| 1083 |
+
<div style="font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:2px">Risk Score</div>
|
| 1084 |
+
<div style="font-size:3.5rem;font-weight:900;color:{sc_color};line-height:1.1">{score}<span style="font-size:1.5rem;color:#666">/10</span></div>
|
| 1085 |
+
<div style="font-size:0.85rem;color:{sc_color};margin-top:0.2rem;font-weight:600">{label}</div>
|
| 1086 |
+
<div style="font-size:0.82rem;color:#aaa;margin-top:0.5rem">{s.critical_count}C Β· {s.high_count}H Β· {s.medium_count}M Β· {s.low_count}L</div>
|
| 1087 |
+
</div>""", unsafe_allow_html=True)
|
| 1088 |
+
with col_b:
|
| 1089 |
+
max_val = max(s.critical_count, s.high_count, s.medium_count, s.low_count,
|
| 1090 |
+
s.total_clauses - s.critical_count - s.high_count - s.medium_count - s.low_count, 1)
|
| 1091 |
+
chart_data = pd.DataFrame({
|
| 1092 |
+
"Severity": ["Critical", "High", "Medium", "Low", "Info"],
|
| 1093 |
+
"Count": [s.critical_count, s.high_count, s.medium_count, s.low_count,
|
| 1094 |
+
max(s.total_clauses - s.critical_count - s.high_count - s.medium_count - s.low_count, 0)],
|
| 1095 |
+
})
|
| 1096 |
+
st.bar_chart(chart_data.set_index("Severity"), use_container_width=True, height=220)
|
| 1097 |
+
with col_c:
|
| 1098 |
+
risky = s.critical_count + s.high_count + s.medium_count
|
| 1099 |
+
pct = (risky / s.total_clauses * 100) if s.total_clauses > 0 else 0
|
| 1100 |
+
if pct >= 50:
|
| 1101 |
+
attn_color = "#ff4444"
|
| 1102 |
+
attn_label = "Review Urgently"
|
| 1103 |
+
elif pct >= 25:
|
| 1104 |
+
attn_color = "#ff8c00"
|
| 1105 |
+
attn_label = "Needs Review"
|
| 1106 |
+
else:
|
| 1107 |
+
attn_color = "#32cd32"
|
| 1108 |
+
attn_label = "Mostly Clean"
|
| 1109 |
+
st.markdown(f"""<div style="background:#1e1e2e;border-radius:12px;padding:1.25rem;text-align:center;border:1px solid #333;height:100%">
|
| 1110 |
+
<div style="font-size:0.75rem;color:#888;text-transform:uppercase;letter-spacing:1px">Needs Attention</div>
|
| 1111 |
+
<div style="font-size:2.5rem;font-weight:900;color:{attn_color}">{risky}<span style="font-size:1rem;color:#666">/{s.total_clauses}</span></div>
|
| 1112 |
+
<div style="font-size:0.85rem;color:{attn_color};font-weight:500">{attn_label}</div>
|
| 1113 |
+
<div style="font-size:0.8rem;color:#888;margin-top:0.25rem">{pct:.0f}% of clauses</div>
|
| 1114 |
+
</div>""", unsafe_allow_html=True)
|
| 1115 |
+
|
| 1116 |
+
st.markdown("")
|
| 1117 |
+
st.markdown("### β‘ Top 3 Actions Before Signing")
|
| 1118 |
+
if report.top_3_actions:
|
| 1119 |
+
for i, action in enumerate(report.top_3_actions, 1):
|
| 1120 |
+
colors = ["#ff4444", "#ff8c00", "#ffd700"]
|
| 1121 |
+
emojis = ["β ", "β‘", "β’"]
|
| 1122 |
+
st.markdown(f"""<div style="background:#1e1e2e;border-radius:10px;padding:1rem 1.25rem;margin:0.4rem 0;
|
| 1123 |
+
border-left:4px solid {colors[i-1]}">
|
| 1124 |
+
<b style="color:#8ab4f8;font-size:1.1rem">{emojis[i-1]}</b>
|
| 1125 |
+
<span style="margin-left:0.5rem;color:#e8e8e8">{action}</span></div>""", unsafe_allow_html=True)
|
| 1126 |
+
else:
|
| 1127 |
+
st.info("No specific actions needed β this contract appears well-balanced.")
|
| 1128 |
+
|
| 1129 |
+
criticals = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.CRITICAL]
|
| 1130 |
+
if criticals:
|
| 1131 |
+
st.markdown("")
|
| 1132 |
+
st.markdown("### β οΈ What Could Happen If You Sign This?")
|
| 1133 |
+
st.caption("Realistic AI-generated consequence scenarios based on these clause patterns. These are illustrative examples β consult an attorney for legal advice.")
|
| 1134 |
+
for idx, sc in enumerate(criticals[:3]):
|
| 1135 |
+
scenarios = sc.finding.impact_scenarios
|
| 1136 |
+
if not scenarios:
|
| 1137 |
+
scenarios = ["You may face significant legal or financial consequences from this clause."]
|
| 1138 |
+
st.markdown(f"**{idx + 1}. π΄ {sc.finding.risk_title}**")
|
| 1139 |
+
for scenario in scenarios:
|
| 1140 |
+
st.markdown(f"<div style='background:rgba(255,68,68,0.08);border-left:3px solid #ff4444;padding:0.5rem 0.75rem;margin:0.2rem 0;margin-left:1rem;border-radius:4px;font-size:0.9rem;color:#e0a0a0'>{scenario}</div>", unsafe_allow_html=True)
|
| 1141 |
+
|
| 1142 |
+
high_risks = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.HIGH]
|
| 1143 |
+
if high_risks:
|
| 1144 |
+
st.markdown("")
|
| 1145 |
+
st.markdown("### π High-Risk Clauses at a Glance")
|
| 1146 |
+
for sc in high_risks:
|
| 1147 |
+
style = SEVERITY_STYLE[Severity.HIGH]
|
| 1148 |
+
reason_preview = sc.finding.risk_reason[:120]
|
| 1149 |
+
if len(sc.finding.risk_reason) > 120:
|
| 1150 |
+
reason_preview += "..."
|
| 1151 |
+
st.markdown(f"""<div style="background:{style['bg']};border-left:3px solid {style['border']};padding:0.6rem 0.9rem;margin:0.3rem 0;border-radius:4px">
|
| 1152 |
+
<b style="color:{style['color']}">{sc.finding.risk_title}</b>
|
| 1153 |
+
<span style="color:#aaa;font-size:0.85rem;margin-left:0.5rem">β {reason_preview}</span>
|
| 1154 |
+
</div>""", unsafe_allow_html=True)
|
| 1155 |
+
|
| 1156 |
+
medium_risks = [sc for sc in report.scored_clauses if sc.finding.severity == Severity.MEDIUM]
|
| 1157 |
+
if medium_risks:
|
| 1158 |
+
st.markdown("")
|
| 1159 |
+
st.markdown("### π‘ Medium-Risk Clauses")
|
| 1160 |
+
for sc in medium_risks:
|
| 1161 |
+
style = SEVERITY_STYLE[Severity.MEDIUM]
|
| 1162 |
+
reason_preview = sc.finding.risk_reason[:80]
|
| 1163 |
+
if len(sc.finding.risk_reason) > 80:
|
| 1164 |
+
reason_preview += "..."
|
| 1165 |
+
st.markdown(f"""<div style="background:{style['bg']};border-left:3px solid {style['border']};padding:0.5rem 0.8rem;margin:0.2rem 0;border-radius:4px;font-size:0.9rem">
|
| 1166 |
+
<b style="color:{style['color']}">{sc.finding.risk_title}</b>
|
| 1167 |
+
<span style="color:#999;margin-left:0.3rem">β {reason_preview}</span>
|
| 1168 |
+
</div>""", unsafe_allow_html=True)
|
| 1169 |
+
|
| 1170 |
+
|
| 1171 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1172 |
+
# TAB 2: CLAUSES
|
| 1173 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1174 |
+
|
| 1175 |
+
def render_clauses_tab() -> None:
|
| 1176 |
+
report = st.session_state.report
|
| 1177 |
+
st.markdown("### π Clause-by-Clause Analysis")
|
| 1178 |
+
st.caption("Each issue below shows the original legal text, plain-English translation, risk assessment, and recommended actions.")
|
| 1179 |
+
|
| 1180 |
+
filter_cols = st.columns(5)
|
| 1181 |
+
show_crit = filter_cols[0].checkbox("π΄ Critical", value=True)
|
| 1182 |
+
show_high = filter_cols[1].checkbox("π High", value=True)
|
| 1183 |
+
show_med = filter_cols[2].checkbox("π‘ Medium", value=True)
|
| 1184 |
+
show_low = filter_cols[3].checkbox("π’ Low", value=False)
|
| 1185 |
+
show_info = filter_cols[4].checkbox("βΉοΈ Info", value=False)
|
| 1186 |
+
|
| 1187 |
+
visible = {Severity.CRITICAL: show_crit, Severity.HIGH: show_high,
|
| 1188 |
+
Severity.MEDIUM: show_med, Severity.LOW: show_low, Severity.INFO: show_info}
|
| 1189 |
+
|
| 1190 |
+
default_s = SEVERITY_STYLE[Severity.INFO]
|
| 1191 |
+
issue_num = 0
|
| 1192 |
+
for sc in report.scored_clauses:
|
| 1193 |
+
sev = sc.finding.severity
|
| 1194 |
+
if not visible.get(sev, False):
|
| 1195 |
+
continue
|
| 1196 |
+
issue_num += 1
|
| 1197 |
+
style = SEVERITY_STYLE.get(sev, default_s)
|
| 1198 |
+
|
| 1199 |
+
st.markdown(f"""
|
| 1200 |
+
<div style="display:flex;align-items:center;gap:0.75rem;margin:1.5rem 0 0.75rem 0">
|
| 1201 |
+
<span style="
|
| 1202 |
+
background:{style['border']};
|
| 1203 |
+
color:#fff;
|
| 1204 |
+
min-width:2rem;height:2rem;
|
| 1205 |
+
border-radius:50%;
|
| 1206 |
+
display:inline-flex;align-items:center;justify-content:center;
|
| 1207 |
+
font-weight:800;font-size:0.9rem;
|
| 1208 |
+
">#{issue_num}</span>
|
| 1209 |
+
<div style="background:linear-gradient(90deg, {style['border']}44 0%, transparent 100%);height:1px;flex:1"></div>
|
| 1210 |
+
</div>""", unsafe_allow_html=True)
|
| 1211 |
+
|
| 1212 |
+
_render_single_clause_card(sc, style, show_actions=True)
|
| 1213 |
+
|
| 1214 |
+
if issue_num == 0:
|
| 1215 |
+
st.info("Select severity levels above to view issues. Try enabling Critical and High to see the most important clauses that need your attention.")
|
| 1216 |
+
else:
|
| 1217 |
+
st.caption(f"Showing {issue_num} of {report.summary.total_clauses} clauses β use severity filters above to adjust view")
|
| 1218 |
+
|
| 1219 |
+
|
| 1220 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1221 |
+
# TAB 3: NEGOTIATION
|
| 1222 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1223 |
+
|
| 1224 |
+
def _highlight_diff(original: str, safer: str) -> tuple[str, str]:
|
| 1225 |
+
original_span = f"<span style='background:rgba(255,68,68,0.20);padding:0 2px;border-radius:2px;text-decoration:line-through'>{original}</span>"
|
| 1226 |
+
safer_span = f"<span style='background:rgba(50,205,50,0.20);padding:0 2px;border-radius:2px;font-weight:600'>{safer}</span>"
|
| 1227 |
+
return original_span, safer_span
|
| 1228 |
+
|
| 1229 |
+
|
| 1230 |
+
def generate_negotiation_email(sc: ScoredClause, recipient: str = "[Other Party]") -> str:
|
| 1231 |
+
topic = sc.clause.section_heading or sc.clause.clause_type.value.replace("_", " ").title()
|
| 1232 |
+
safer = sc.finding.safer_clause_version or _generate_fallback_safer(sc)
|
| 1233 |
+
risk_reason = sc.finding.risk_reason
|
| 1234 |
+
return (
|
| 1235 |
+
f"Subject: Proposed adjustment β {topic} clause\n\n"
|
| 1236 |
+
f"Hi {recipient},\n\n"
|
| 1237 |
+
f"I've reviewed the contract and have a concern about the {topic} clause.\n\n"
|
| 1238 |
+
f"My concern: {risk_reason}\n\n"
|
| 1239 |
+
f"I'd suggest the following alternative language to make this fair for both parties:\n\n"
|
| 1240 |
+
f'"{safer}"\n\n'
|
| 1241 |
+
f"Let me know your thoughts β I'm happy to discuss further.\n\n"
|
| 1242 |
+
f"Best regards"
|
| 1243 |
+
)
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
def _render_email_card(sc: ScoredClause, recipient: str = "[Other Party]") -> None:
|
| 1247 |
+
recipient_input = st.text_input("Recipient name", value=recipient, key=f"recipient_{sc.clause.id}")
|
| 1248 |
+
email_body = generate_negotiation_email(sc, recipient_input)
|
| 1249 |
+
st.markdown("**π§ Formal Email Draft**")
|
| 1250 |
+
st.code(email_body, language=None)
|
| 1251 |
+
col_copy, col_info = st.columns([1, 3])
|
| 1252 |
+
with col_copy:
|
| 1253 |
+
if st.button("π Copy to Clipboard", key=f"copy_email_{sc.clause.id}"):
|
| 1254 |
+
st.toast("Email copied!", icon="π")
|
| 1255 |
+
with col_info:
|
| 1256 |
+
st.caption("Click the code block above to select all text, then Ctrl+C to copy")
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def render_negotiation_tab() -> None:
|
| 1260 |
+
report = st.session_state.report
|
| 1261 |
+
default_s = SEVERITY_STYLE[Severity.INFO]
|
| 1262 |
+
|
| 1263 |
+
st.markdown("### π¬ Negotiation Copilot")
|
| 1264 |
+
st.caption("Each risky clause shows what you signed vs. a safer alternative, side-by-side. Use the pre-written messages or generate a formal email to send to the other party.")
|
| 1265 |
+
|
| 1266 |
+
negotiable = [sc for sc in report.scored_clauses if sc.finding.severity not in (Severity.LOW, Severity.INFO)]
|
| 1267 |
+
if not negotiable:
|
| 1268 |
+
st.success("β
No actionable risks detected β this contract looks reasonable!")
|
| 1269 |
+
else:
|
| 1270 |
+
st.info(f"π **{len(negotiable)} clauses** flagged for negotiation below")
|
| 1271 |
+
|
| 1272 |
+
for i, sc in enumerate(negotiable):
|
| 1273 |
+
style = SEVERITY_STYLE.get(sc.finding.severity, default_s)
|
| 1274 |
+
sev_label = sc.finding.severity.value
|
| 1275 |
+
|
| 1276 |
+
st.markdown(f"""<div style="background:{style['bg']};border-left:4px solid {style['border']};padding:0.6rem 1rem;border-radius:4px 10px 10px 4px;margin:1.2rem 0 0.5rem 0">
|
| 1277 |
+
<span style="font-weight:700;color:{style['color']}">{style['badge']}</span>
|
| 1278 |
+
<span style="font-weight:600;color:#e0e0e0;margin-left:0.5rem">{sc.finding.risk_title}</span>
|
| 1279 |
+
<span style="color:#888;font-size:0.8rem;margin-left:0.8rem">Clause {sc.clause.id}</span>
|
| 1280 |
+
</div>""", unsafe_allow_html=True)
|
| 1281 |
+
|
| 1282 |
+
st.markdown("**π Why This Matters**")
|
| 1283 |
+
st.markdown(f"<div style='color:#ccc;font-size:0.9rem;line-height:1.55;margin-bottom:0.75rem;padding:0.5rem 0.75rem;background:rgba(255,255,255,0.02);border-radius:8px'>{sc.finding.risk_reason}</div>", unsafe_allow_html=True)
|
| 1284 |
+
|
| 1285 |
+
neg_l, neg_r = st.columns(2)
|
| 1286 |
+
with neg_l:
|
| 1287 |
+
st.markdown("**β οΈ Current Clause (Risky)**")
|
| 1288 |
+
text_to_show = sc.clause.raw_text[:500]
|
| 1289 |
+
if len(sc.clause.raw_text) > 500:
|
| 1290 |
+
text_to_show += "..."
|
| 1291 |
+
st.markdown(f"<div style='background:rgba(255,68,68,0.08);padding:0.75rem;border-radius:8px;border:1px solid rgba(255,68,68,0.2);font-size:0.85rem;line-height:1.6;color:#e0e0e0'>{text_to_show}</div>", unsafe_allow_html=True)
|
| 1292 |
+
|
| 1293 |
+
with neg_r:
|
| 1294 |
+
st.markdown("**π‘ Safer Alternative**")
|
| 1295 |
+
safer = sc.finding.safer_clause_version
|
| 1296 |
+
if not safer:
|
| 1297 |
+
safer = _generate_fallback_safer(sc)
|
| 1298 |
+
st.markdown(f"<div style='background:rgba(50,205,50,0.08);padding:0.75rem;border-radius:8px;border:1px solid rgba(50,205,50,0.2);font-size:0.85rem;line-height:1.6;color:#e0e0e0'>{safer}</div>", unsafe_allow_html=True)
|
| 1299 |
+
|
| 1300 |
+
if sc.finding.recommended_action:
|
| 1301 |
+
st.markdown(f"**β
Recommended:** {sc.finding.recommended_action}")
|
| 1302 |
+
|
| 1303 |
+
neg_msg = sc.finding.negotiation_message
|
| 1304 |
+
if not neg_msg:
|
| 1305 |
+
neg_msg = _generate_fallback_message(sc)
|
| 1306 |
+
st.markdown("**π§ Quick Negotiation Message**")
|
| 1307 |
+
st.code(neg_msg, language=None)
|
| 1308 |
+
|
| 1309 |
+
if sc.finding.impact_scenarios:
|
| 1310 |
+
st.markdown("**β οΈ Consequences of Not Negotiating**")
|
| 1311 |
+
for impact in sc.finding.impact_scenarios:
|
| 1312 |
+
st.markdown(f"<div style='background:rgba(255,68,68,0.06);padding:0.35rem 0.75rem;margin:0.15rem 0;margin-left:0.5rem;border-radius:4px;font-size:0.85rem;color:#ff9999'>β’ {impact}</div>", unsafe_allow_html=True)
|
| 1313 |
+
|
| 1314 |
+
with st.expander("π§ Generate Formal Email to Send"):
|
| 1315 |
+
_render_email_card(sc)
|
| 1316 |
+
|
| 1317 |
+
if i < len(negotiable) - 1:
|
| 1318 |
+
st.divider()
|
| 1319 |
+
|
| 1320 |
+
safe_contract = _build_safer_contract(report)
|
| 1321 |
+
with st.expander("π Preview Safer Contract"):
|
| 1322 |
+
preview_max = 3500
|
| 1323 |
+
preview_text = safe_contract[:preview_max]
|
| 1324 |
+
if len(safe_contract) > preview_max:
|
| 1325 |
+
preview_text += f"\n\n... (showing first {preview_max} chars of {len(safe_contract)} β download full contract at bottom of page)"
|
| 1326 |
+
st.code(preview_text, language=None)
|
| 1327 |
+
|
| 1328 |
+
|
| 1329 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1330 |
+
# TAB 4: CHAT ASSISTANT
|
| 1331 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1332 |
+
|
| 1333 |
+
def render_chat_tab() -> None:
|
| 1334 |
+
report = st.session_state.report
|
| 1335 |
+
st.markdown("### π€ Chat Assistant")
|
| 1336 |
+
st.caption("Ask questions about your contract in plain English. The AI has full context of every clause, risk assessment, and recommended action β all injected into this conversation automatically.")
|
| 1337 |
+
|
| 1338 |
+
cache_key = id(report)
|
| 1339 |
+
if st.session_state.get("copilot_cache_key") != cache_key:
|
| 1340 |
+
raw_text = st.session_state.get("copilot_raw_text", "")
|
| 1341 |
+
st.session_state.copilot_context = build_contract_context(raw_text, report)
|
| 1342 |
+
st.session_state.copilot_cache_key = cache_key
|
| 1343 |
+
|
| 1344 |
+
copilot_context = st.session_state.copilot_context
|
| 1345 |
+
|
| 1346 |
+
if not st.session_state.copilot_messages:
|
| 1347 |
+
total_risky = report.summary.critical_count + report.summary.high_count
|
| 1348 |
+
if total_risky > 0:
|
| 1349 |
+
welcome = (
|
| 1350 |
+
f"I've analyzed your contract and found **{total_risky} high-risk clause(s)** "
|
| 1351 |
+
f"(risk score: **{report.summary.overall_score}/10**). "
|
| 1352 |
+
"You can ask me to:\n\n"
|
| 1353 |
+
"- Explain any clause in simple terms\n"
|
| 1354 |
+
"- Tell you which clauses are risky and why\n"
|
| 1355 |
+
"- Suggest safer wording for specific clauses\n"
|
| 1356 |
+
"- Help you draft a negotiation message\n"
|
| 1357 |
+
"- Describe what could happen if you sign as-is\n"
|
| 1358 |
+
"- Compare clauses to industry standards\n\n"
|
| 1359 |
+
"What would you like to know?"
|
| 1360 |
+
)
|
| 1361 |
+
else:
|
| 1362 |
+
welcome = (
|
| 1363 |
+
f"I've analyzed your contract and it looks reasonable (risk score: **{report.summary.overall_score}/10**). "
|
| 1364 |
+
"You can ask me to explain any clause, check for potential hidden issues, or compare terms to standard practices. "
|
| 1365 |
+
"What would you like to know?"
|
| 1366 |
+
)
|
| 1367 |
+
with st.chat_message("assistant"):
|
| 1368 |
+
st.markdown(welcome)
|
| 1369 |
+
st.session_state.copilot_messages = [{"role": "assistant", "content": welcome}]
|
| 1370 |
+
|
| 1371 |
+
st.markdown("**π‘ Click a question to ask instantly:**")
|
| 1372 |
+
chip_cols = st.columns(4)
|
| 1373 |
+
quick_prompts = [
|
| 1374 |
+
"Summarize this contract in 3 sentences",
|
| 1375 |
+
"What's the most dangerous clause and why?",
|
| 1376 |
+
"Suggest safer wording for the IP clause",
|
| 1377 |
+
"What should I negotiate first?",
|
| 1378 |
+
"Explain the non-compete in simple English",
|
| 1379 |
+
"Are there any hidden fees, penalties, or traps?",
|
| 1380 |
+
"What happens if I breach this contract?",
|
| 1381 |
+
"Draft an email requesting changes to all risky clauses",
|
| 1382 |
+
]
|
| 1383 |
+
for idx, prompt in enumerate(quick_prompts):
|
| 1384 |
+
col = chip_cols[idx % 4]
|
| 1385 |
+
with col:
|
| 1386 |
+
if st.button(prompt, key=f"chip_{idx}", use_container_width=True):
|
| 1387 |
+
st.session_state.pending_ai_query = prompt
|
| 1388 |
+
st.rerun()
|
| 1389 |
+
|
| 1390 |
+
for msg in st.session_state.copilot_messages:
|
| 1391 |
+
with st.chat_message(msg["role"]):
|
| 1392 |
+
st.markdown(msg["content"])
|
| 1393 |
+
|
| 1394 |
+
if st.session_state.get("pending_ai_query"):
|
| 1395 |
+
query = st.session_state.pending_ai_query
|
| 1396 |
+
st.session_state.pending_ai_query = None
|
| 1397 |
+
if copilot_context:
|
| 1398 |
+
st.session_state.copilot_messages.append({"role": "user", "content": query})
|
| 1399 |
+
with st.chat_message("user"):
|
| 1400 |
+
st.markdown(query)
|
| 1401 |
+
with st.chat_message("assistant"):
|
| 1402 |
+
with st.spinner("Thinking β analyzing contract context..."):
|
| 1403 |
+
chat_history = st.session_state.copilot_messages[:-1]
|
| 1404 |
+
response = run_copilot_sync(copilot_context, chat_history, query)
|
| 1405 |
+
st.markdown(response)
|
| 1406 |
+
st.session_state.copilot_messages.append({"role": "assistant", "content": response})
|
| 1407 |
+
st.rerun()
|
| 1408 |
+
|
| 1409 |
+
if prompt := st.chat_input("Ask about this contract...", key="copilot_chat_input"):
|
| 1410 |
+
if not copilot_context:
|
| 1411 |
+
st.warning("No contract analysis available. Please upload and analyze a contract first.")
|
| 1412 |
+
else:
|
| 1413 |
+
st.session_state.copilot_messages.append({"role": "user", "content": prompt})
|
| 1414 |
+
with st.chat_message("user"):
|
| 1415 |
+
st.markdown(prompt)
|
| 1416 |
+
with st.chat_message("assistant"):
|
| 1417 |
+
with st.spinner("Thinking β analyzing contract context..."):
|
| 1418 |
+
chat_history = st.session_state.copilot_messages[:-1]
|
| 1419 |
+
response = run_copilot_sync(copilot_context, chat_history, prompt)
|
| 1420 |
+
st.markdown(response)
|
| 1421 |
+
st.session_state.copilot_messages.append({"role": "assistant", "content": response})
|
| 1422 |
+
|
| 1423 |
+
if st.session_state.copilot_messages:
|
| 1424 |
+
cc1, cc2, cc3 = st.columns([1, 2, 1])
|
| 1425 |
+
with cc1:
|
| 1426 |
+
if st.button("ποΈ Clear Chat", key="copilot_clear", use_container_width=True):
|
| 1427 |
+
st.session_state.copilot_messages = []
|
| 1428 |
+
st.rerun()
|
| 1429 |
+
with cc3:
|
| 1430 |
+
st.caption(f"{len(st.session_state.copilot_messages)} messages")
|
| 1431 |
+
|
| 1432 |
+
|
| 1433 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1434 |
+
# SIDEBAR
|
| 1435 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1436 |
+
|
| 1437 |
+
def render_sidebar() -> None:
|
| 1438 |
+
with st.sidebar:
|
| 1439 |
+
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1440 |
+
<h4 style="margin:0 0 0.5rem 0;color:#fff">π― How It Works</h4>
|
| 1441 |
+
<ol style="margin:0;padding-left:1.25rem;font-size:0.9rem;color:#ccc;line-height:2">
|
| 1442 |
+
<li>Upload any contract file (PDF, DOCX, TXT)</li>
|
| 1443 |
+
<li>5 specialized AI agents analyze every clause</li>
|
| 1444 |
+
<li>Get a detailed risk report with plain English explanations</li>
|
| 1445 |
+
<li>Use Negotiation Copilot to draft counter-proposals</li>
|
| 1446 |
+
<li>Chat with the AI Copilot for any follow-up questions</li>
|
| 1447 |
+
</ol>
|
| 1448 |
+
</div>""", unsafe_allow_html=True)
|
| 1449 |
+
|
| 1450 |
+
st.markdown("")
|
| 1451 |
+
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1452 |
+
<h4 style="margin:0 0 0.5rem 0;color:#fff">π€ 5-Agent AI Pipeline</h4>
|
| 1453 |
+
<div style="font-size:0.85rem;color:#ccc;line-height:2">
|
| 1454 |
+
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β Extractor</b> β Segments contract into individual clauses</p>
|
| 1455 |
+
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β‘ Classifier</b> β Labels each clause by legal type</p>
|
| 1456 |
+
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β’ Risk Scorer</b> β Evaluates severity of each clause</p>
|
| 1457 |
+
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β£ Translator</b> β Converts legalese to plain English</p>
|
| 1458 |
+
<p style="margin:0.2rem 0"><b style="color:#8ab4f8">β€ Reporter</b> β Compiles the final risk report</p>
|
| 1459 |
+
</div>
|
| 1460 |
+
</div>""", unsafe_allow_html=True)
|
| 1461 |
+
|
| 1462 |
+
if st.session_state.report:
|
| 1463 |
+
s = st.session_state.report.summary
|
| 1464 |
+
total_risky = s.critical_count + s.high_count
|
| 1465 |
+
st.markdown("")
|
| 1466 |
+
st.markdown("#### π Contract Stats")
|
| 1467 |
+
|
| 1468 |
+
risk_delta = f"{total_risky} high-risk" if total_risky > 0 else "Clean"
|
| 1469 |
+
st.metric(
|
| 1470 |
+
"π― Risk Score",
|
| 1471 |
+
f"{s.overall_score}/10",
|
| 1472 |
+
delta=risk_delta,
|
| 1473 |
+
delta_color="inverse" if total_risky > 0 else "normal",
|
| 1474 |
+
)
|
| 1475 |
+
st.metric("π Total Clauses", s.total_clauses)
|
| 1476 |
+
|
| 1477 |
+
has_any_risks = False
|
| 1478 |
+
for icon, label, key in [
|
| 1479 |
+
("π΄", "Critical", "critical_count"),
|
| 1480 |
+
("π ", "High", "high_count"),
|
| 1481 |
+
("π‘", "Medium", "medium_count"),
|
| 1482 |
+
("π’", "Low", "low_count"),
|
| 1483 |
+
]:
|
| 1484 |
+
count = getattr(s, key, 0)
|
| 1485 |
+
if count > 0:
|
| 1486 |
+
has_any_risks = True
|
| 1487 |
+
st.metric(f"{icon} {label}", count)
|
| 1488 |
+
|
| 1489 |
+
st.divider()
|
| 1490 |
+
st.markdown(f"**Contract Type:** {s.contract_type}")
|
| 1491 |
+
st.markdown(f"**Analyzed:** {st.session_state.report.generated_at.strftime('%b %d, %Y at %H:%M')}")
|
| 1492 |
+
|
| 1493 |
+
if not st.session_state.report.processed_normally:
|
| 1494 |
+
st.caption("β οΈ Report may not cover all clauses due to processing constraints.")
|
| 1495 |
+
|
| 1496 |
+
st.markdown("")
|
| 1497 |
+
st.markdown("""<div style="background:#1e2a3a;border-radius:12px;padding:1.25rem;border:1px solid #3a4a5a">
|
| 1498 |
+
<h4 style="margin:0 0 0.5rem 0;color:#fff">β‘ Powered by</h4>
|
| 1499 |
+
<p style="font-size:0.85rem;color:#ccc;margin:0;line-height:1.8">
|
| 1500 |
+
Qwen2.5 via vLLM on AMD MI300X<br>
|
| 1501 |
+
OpenAI-compatible API<br>
|
| 1502 |
+
Streamlit β’ Python 3.10+
|
| 1503 |
+
</p>
|
| 1504 |
+
<div style="margin-top:0.5rem;padding:0.3rem 0.5rem;background:#1a0533;border-radius:6px;border:1px solid #667eea;text-align:center;font-size:0.7rem;color:#aabbcc">
|
| 1505 |
+
π·οΈ AMD Developer Cloud
|
| 1506 |
+
</div>
|
| 1507 |
+
</div>""", unsafe_allow_html=True)
|
| 1508 |
+
|
| 1509 |
+
st.markdown("")
|
| 1510 |
+
st.markdown("""<div style="font-size:0.7rem;color:#555;text-align:center;margin-top:1rem">
|
| 1511 |
+
<p style="margin:0">β οΈ Not legal advice. AI-generated analysis.</p>
|
| 1512 |
+
<p style="margin:0">Always consult a qualified attorney before signing.</p>
|
| 1513 |
+
</div>""", unsafe_allow_html=True)
|