Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- api/court_schemas.py +89 -0
- api/main.py +254 -1
- frontend/court/court.css +837 -0
- frontend/court/court.html +756 -0
- frontend/court/court.js +784 -0
- frontend/index.html +5 -0
- src/court/__init__.py +1 -0
- src/court/brief.py +154 -0
- src/court/judge.py +218 -0
- src/court/opposing.py +355 -0
- src/court/orchestrator.py +852 -0
- src/court/registrar.py +118 -0
- src/court/session.py +469 -0
- src/court/summariser.py +253 -0
- src/system_prompt.py +10 -0
- src/verify.py +23 -12
api/court_schemas.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic schemas for all court API endpoints.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from typing import Optional, List, Dict, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class NewSessionRequest(BaseModel):
|
| 10 |
+
case_title: str
|
| 11 |
+
user_side: str # petitioner | respondent
|
| 12 |
+
user_client: str
|
| 13 |
+
opposing_party: str
|
| 14 |
+
legal_issues: List[str]
|
| 15 |
+
brief_facts: str
|
| 16 |
+
jurisdiction: str = "supreme_court"
|
| 17 |
+
bench_composition: str = "division" # single | division | constitutional
|
| 18 |
+
difficulty: str = "standard" # moot | standard | adversarial
|
| 19 |
+
session_length: str = "standard" # brief | standard | extended
|
| 20 |
+
show_trap_warnings: bool = True
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ImportSessionRequest(BaseModel):
|
| 24 |
+
research_session_id: str
|
| 25 |
+
user_side: str
|
| 26 |
+
bench_composition: str = "division"
|
| 27 |
+
difficulty: str = "standard"
|
| 28 |
+
session_length: str = "standard"
|
| 29 |
+
show_trap_warnings: bool = True
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ArgueRequest(BaseModel):
|
| 33 |
+
session_id: str
|
| 34 |
+
argument: str = Field(..., min_length=20, max_length=2000)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class ObjectionRequest(BaseModel):
|
| 38 |
+
session_id: str
|
| 39 |
+
objection_type: str
|
| 40 |
+
objection_text: str = ""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class DocumentRequest(BaseModel):
|
| 44 |
+
session_id: str
|
| 45 |
+
doc_type: str
|
| 46 |
+
for_side: str = "yours" # yours | opposing | court_record
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class EndSessionRequest(BaseModel):
|
| 50 |
+
session_id: str
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class TranscriptEntry(BaseModel):
|
| 54 |
+
speaker: str
|
| 55 |
+
role_label: str
|
| 56 |
+
content: str
|
| 57 |
+
round_number: int
|
| 58 |
+
phase: str
|
| 59 |
+
timestamp: str
|
| 60 |
+
entry_type: str
|
| 61 |
+
metadata: Dict = {}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class RoundResponse(BaseModel):
|
| 65 |
+
opposing_response: str
|
| 66 |
+
judge_question: str
|
| 67 |
+
registrar_note: str
|
| 68 |
+
trap_detected: bool
|
| 69 |
+
trap_warning: str
|
| 70 |
+
new_concessions: List[Dict]
|
| 71 |
+
round_number: int
|
| 72 |
+
phase: str
|
| 73 |
+
cross_exam_complete: bool = False
|
| 74 |
+
ready_for_analysis: bool = False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class SessionSummary(BaseModel):
|
| 78 |
+
session_id: str
|
| 79 |
+
case_title: str
|
| 80 |
+
user_side: str
|
| 81 |
+
phase: str
|
| 82 |
+
current_round: int
|
| 83 |
+
max_rounds: int
|
| 84 |
+
created_at: str
|
| 85 |
+
updated_at: str
|
| 86 |
+
outcome_prediction: Optional[str]
|
| 87 |
+
performance_score: Optional[float]
|
| 88 |
+
concession_count: int
|
| 89 |
+
trap_count: int
|
api/main.py
CHANGED
|
@@ -5,6 +5,10 @@ V2 agent with conversation memory and 3-pass reasoning.
|
|
| 5 |
Port 7860 for HuggingFace Spaces compatibility.
|
| 6 |
"""
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
|
@@ -112,6 +116,10 @@ load_reranker()
|
|
| 112 |
from src.citation_graph import load_citation_graph
|
| 113 |
load_citation_graph()
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
AGENT_VERSION = os.getenv("AGENT_VERSION", "v2")
|
| 116 |
|
| 117 |
if AGENT_VERSION == "v2":
|
|
@@ -265,4 +273,249 @@ def analytics():
|
|
| 265 |
"stage_distribution": dict(stages),
|
| 266 |
"entity_type_frequency": entity_freq,
|
| 267 |
"recent_latencies": latencies[-20:],
|
| 268 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Port 7860 for HuggingFace Spaces compatibility.
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
# Load environment variables from .env file
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
| 116 |
from src.citation_graph import load_citation_graph
|
| 117 |
load_citation_graph()
|
| 118 |
|
| 119 |
+
# Load court sessions from HuggingFace dataset on startup
|
| 120 |
+
from src.court.session import load_sessions_from_hf
|
| 121 |
+
load_sessions_from_hf()
|
| 122 |
+
|
| 123 |
AGENT_VERSION = os.getenv("AGENT_VERSION", "v2")
|
| 124 |
|
| 125 |
if AGENT_VERSION == "v2":
|
|
|
|
| 273 |
"stage_distribution": dict(stages),
|
| 274 |
"entity_type_frequency": entity_freq,
|
| 275 |
"recent_latencies": latencies[-20:],
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# ── COURT ENDPOINTS ────────────────────────────────────────────
|
| 280 |
+
|
| 281 |
+
from api.court_schemas import (
|
| 282 |
+
NewSessionRequest, ImportSessionRequest, ArgueRequest,
|
| 283 |
+
ObjectionRequest, DocumentRequest, EndSessionRequest,
|
| 284 |
+
RoundResponse, SessionSummary
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@app.post("/court/new")
|
| 289 |
+
def court_new_session(request: NewSessionRequest):
|
| 290 |
+
"""Start a fresh moot court session."""
|
| 291 |
+
from src.court.session import create_session
|
| 292 |
+
from src.court.brief import generate_fresh_brief
|
| 293 |
+
from src.court.registrar import build_round_announcement
|
| 294 |
+
|
| 295 |
+
case_brief = generate_fresh_brief(
|
| 296 |
+
case_title=request.case_title,
|
| 297 |
+
user_side=request.user_side,
|
| 298 |
+
user_client=request.user_client,
|
| 299 |
+
opposing_party=request.opposing_party,
|
| 300 |
+
legal_issues=request.legal_issues,
|
| 301 |
+
brief_facts=request.brief_facts,
|
| 302 |
+
jurisdiction=request.jurisdiction,
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
session_id = create_session(
|
| 306 |
+
case_title=request.case_title,
|
| 307 |
+
user_side=request.user_side,
|
| 308 |
+
user_client=request.user_client,
|
| 309 |
+
opposing_party=request.opposing_party,
|
| 310 |
+
legal_issues=request.legal_issues,
|
| 311 |
+
brief_facts=request.brief_facts,
|
| 312 |
+
jurisdiction=request.jurisdiction,
|
| 313 |
+
bench_composition=request.bench_composition,
|
| 314 |
+
difficulty=request.difficulty,
|
| 315 |
+
session_length=request.session_length,
|
| 316 |
+
show_trap_warnings=request.show_trap_warnings,
|
| 317 |
+
case_brief=case_brief,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
# Registrar opens the session
|
| 321 |
+
from src.court.session import get_session, add_transcript_entry
|
| 322 |
+
session = get_session(session_id)
|
| 323 |
+
opening = build_round_announcement(session, 0, "briefing")
|
| 324 |
+
add_transcript_entry(
|
| 325 |
+
session_id=session_id,
|
| 326 |
+
speaker="REGISTRAR",
|
| 327 |
+
role_label="COURT REGISTRAR",
|
| 328 |
+
content=opening,
|
| 329 |
+
entry_type="announcement",
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
return {
|
| 333 |
+
"session_id": session_id,
|
| 334 |
+
"case_brief": case_brief,
|
| 335 |
+
"opening_announcement": opening,
|
| 336 |
+
"phase": "briefing",
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@app.post("/court/import")
|
| 341 |
+
def court_import_session(request: ImportSessionRequest):
|
| 342 |
+
"""Import a NyayaSetu research session into Moot Court."""
|
| 343 |
+
from src.court.session import create_session, add_transcript_entry
|
| 344 |
+
from src.court.brief import generate_case_brief
|
| 345 |
+
from src.court.registrar import build_round_announcement
|
| 346 |
+
from src.agent_v2 import sessions as research_sessions
|
| 347 |
+
|
| 348 |
+
research_session = research_sessions.get(request.research_session_id)
|
| 349 |
+
if not research_session:
|
| 350 |
+
raise HTTPException(status_code=404, detail="Research session not found")
|
| 351 |
+
|
| 352 |
+
case_state = research_session.get("case_state", {})
|
| 353 |
+
|
| 354 |
+
case_brief = generate_case_brief(research_session, request.user_side)
|
| 355 |
+
|
| 356 |
+
# Extract case details from research session
|
| 357 |
+
parties = case_state.get("parties", [])
|
| 358 |
+
case_title = f"{parties[0]} vs {parties[1]}" if len(parties) >= 2 else "Present Matter"
|
| 359 |
+
legal_issues_raw = research_session.get("case_state", {}).get("disputes", [])
|
| 360 |
+
|
| 361 |
+
session_id = create_session(
|
| 362 |
+
case_title=case_title,
|
| 363 |
+
user_side=request.user_side,
|
| 364 |
+
user_client=parties[0] if parties else "Petitioner",
|
| 365 |
+
opposing_party=parties[1] if len(parties) > 1 else "Respondent",
|
| 366 |
+
legal_issues=legal_issues_raw[:5],
|
| 367 |
+
brief_facts=research_session.get("summary", ""),
|
| 368 |
+
jurisdiction="supreme_court",
|
| 369 |
+
bench_composition=request.bench_composition,
|
| 370 |
+
difficulty=request.difficulty,
|
| 371 |
+
session_length=request.session_length,
|
| 372 |
+
show_trap_warnings=request.show_trap_warnings,
|
| 373 |
+
imported_from_session=request.research_session_id,
|
| 374 |
+
case_brief=case_brief,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
from src.court.session import get_session
|
| 378 |
+
session = get_session(session_id)
|
| 379 |
+
opening = build_round_announcement(session, 0, "briefing")
|
| 380 |
+
add_transcript_entry(
|
| 381 |
+
session_id=session_id,
|
| 382 |
+
speaker="REGISTRAR",
|
| 383 |
+
role_label="COURT REGISTRAR",
|
| 384 |
+
content=opening,
|
| 385 |
+
entry_type="announcement",
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
return {
|
| 389 |
+
"session_id": session_id,
|
| 390 |
+
"case_brief": case_brief,
|
| 391 |
+
"opening_announcement": opening,
|
| 392 |
+
"phase": "briefing",
|
| 393 |
+
"imported_from": request.research_session_id,
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
@app.post("/court/argue")
|
| 398 |
+
def court_argue(request: ArgueRequest):
|
| 399 |
+
"""Submit an argument or answer during the session."""
|
| 400 |
+
from src.court.orchestrator import process_user_argument
|
| 401 |
+
|
| 402 |
+
if not request.session_id or not request.argument.strip():
|
| 403 |
+
raise HTTPException(status_code=400, detail="Session ID and argument required")
|
| 404 |
+
|
| 405 |
+
result = process_user_argument(request.session_id, request.argument)
|
| 406 |
+
|
| 407 |
+
if "error" in result:
|
| 408 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 409 |
+
|
| 410 |
+
return result
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
@app.post("/court/object")
|
| 414 |
+
def court_object(request: ObjectionRequest):
|
| 415 |
+
"""Raise an objection."""
|
| 416 |
+
from src.court.orchestrator import process_objection
|
| 417 |
+
|
| 418 |
+
result = process_objection(
|
| 419 |
+
request.session_id,
|
| 420 |
+
request.objection_type,
|
| 421 |
+
request.objection_text,
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
if "error" in result:
|
| 425 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 426 |
+
|
| 427 |
+
return result
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
@app.post("/court/document")
|
| 431 |
+
def court_document(request: DocumentRequest):
|
| 432 |
+
"""Generate and produce a legal document."""
|
| 433 |
+
from src.court.orchestrator import process_document_request
|
| 434 |
+
|
| 435 |
+
result = process_document_request(
|
| 436 |
+
request.session_id,
|
| 437 |
+
request.doc_type,
|
| 438 |
+
request.for_side,
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
if "error" in result:
|
| 442 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 443 |
+
|
| 444 |
+
return result
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
@app.post("/court/end")
|
| 448 |
+
def court_end_session(request: EndSessionRequest):
|
| 449 |
+
"""End the session and generate full analysis."""
|
| 450 |
+
from src.court.orchestrator import generate_session_analysis
|
| 451 |
+
from src.court.session import get_session
|
| 452 |
+
|
| 453 |
+
session = get_session(request.session_id)
|
| 454 |
+
if not session:
|
| 455 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 456 |
+
|
| 457 |
+
if session["phase"] != "completed":
|
| 458 |
+
raise HTTPException(
|
| 459 |
+
status_code=400,
|
| 460 |
+
detail=f"Session is in phase '{session['phase']}' — complete closing arguments first"
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
analysis = generate_session_analysis(request.session_id)
|
| 464 |
+
|
| 465 |
+
if "error" in analysis:
|
| 466 |
+
raise HTTPException(status_code=500, detail=analysis["error"])
|
| 467 |
+
|
| 468 |
+
return analysis
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
@app.get("/court/session/{session_id}")
|
| 472 |
+
def court_get_session(session_id: str):
|
| 473 |
+
"""Get full session data including transcript."""
|
| 474 |
+
from src.court.session import get_session
|
| 475 |
+
|
| 476 |
+
session = get_session(session_id)
|
| 477 |
+
if not session:
|
| 478 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 479 |
+
|
| 480 |
+
return session
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@app.get("/court/sessions")
|
| 484 |
+
def court_list_sessions():
|
| 485 |
+
"""List all sessions."""
|
| 486 |
+
from src.court.session import get_all_sessions
|
| 487 |
+
|
| 488 |
+
sessions = get_all_sessions()
|
| 489 |
+
|
| 490 |
+
# Return summary only
|
| 491 |
+
summaries = []
|
| 492 |
+
for s in sessions:
|
| 493 |
+
summaries.append({
|
| 494 |
+
"session_id": s["session_id"],
|
| 495 |
+
"case_title": s["case_title"],
|
| 496 |
+
"user_side": s["user_side"],
|
| 497 |
+
"phase": s["phase"],
|
| 498 |
+
"current_round": s["current_round"],
|
| 499 |
+
"max_rounds": s["max_rounds"],
|
| 500 |
+
"created_at": s["created_at"],
|
| 501 |
+
"updated_at": s["updated_at"],
|
| 502 |
+
"outcome_prediction": s.get("outcome_prediction"),
|
| 503 |
+
"performance_score": s.get("performance_score"),
|
| 504 |
+
"concession_count": len(s.get("concessions", [])),
|
| 505 |
+
"trap_count": len(s.get("trap_events", [])),
|
| 506 |
+
})
|
| 507 |
+
|
| 508 |
+
return {"sessions": summaries, "total": len(summaries)}
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
@app.post("/court/cross_exam/start")
|
| 512 |
+
def court_start_cross_exam(session_id: str):
|
| 513 |
+
"""Manually trigger cross-examination phase."""
|
| 514 |
+
from src.court.orchestrator import start_cross_examination
|
| 515 |
+
|
| 516 |
+
result = start_cross_examination(session_id)
|
| 517 |
+
|
| 518 |
+
if "error" in result:
|
| 519 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 520 |
+
|
| 521 |
+
return result
|
frontend/court/court.css
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 2 |
+
/* CLAY MORPHISM DESIGN SYSTEM */
|
| 3 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--clay-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
| 7 |
+
--clay-shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
| 8 |
+
--clay-shadow-inset: inset 0 8px 16px rgba(0, 0, 0, 0.06), inset -2px -2px 8px rgba(0, 0, 0, 0.04);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/* Typography overrides */
|
| 12 |
+
body {
|
| 13 |
+
font-family: "Plus Jakarta Sans", sans-serif;
|
| 14 |
+
-webkit-font-smoothing: antialiased;
|
| 15 |
+
-moz-osx-font-smoothing: grayscale;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
h1, h2, h3, h4, h5, h6 {
|
| 19 |
+
font-family: "Cormorant Garamond", serif;
|
| 20 |
+
letter-spacing: -0.02em;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 24 |
+
/* CLAY MORPHISM CARDS */
|
| 25 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 26 |
+
|
| 27 |
+
.clay-card {
|
| 28 |
+
background: linear-gradient(145deg, #FAF7F1, #F5F0E8);
|
| 29 |
+
border: 1px solid rgba(255, 255, 255, 0.6);
|
| 30 |
+
box-shadow: var(--clay-shadow);
|
| 31 |
+
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.clay-card:hover {
|
| 35 |
+
box-shadow: var(--clay-shadow-hover);
|
| 36 |
+
border-color: rgba(255, 255, 255, 0.8);
|
| 37 |
+
transform: translateY(-2px);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.clay-card:active {
|
| 41 |
+
box-shadow: var(--clay-shadow-inset);
|
| 42 |
+
transform: translateY(0);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.clay-puffy {
|
| 46 |
+
background: linear-gradient(145deg, #F8F5F0, #F2EDE5);
|
| 47 |
+
border: 1px solid rgba(255, 255, 255, 0.7);
|
| 48 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.6), inset -2px -2px 8px rgba(0, 0, 0, 0.04);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.clay-inset {
|
| 52 |
+
background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.03));
|
| 53 |
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
| 54 |
+
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.04);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.clay-inset:focus-within {
|
| 58 |
+
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.06), 0 0 0 3px rgba(21, 51, 40, 0.1);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Inputs inside inset */
|
| 62 |
+
.clay-inset textarea,
|
| 63 |
+
.clay-inset input {
|
| 64 |
+
background: transparent !important;
|
| 65 |
+
border: none !important;
|
| 66 |
+
outline: none !important;
|
| 67 |
+
color: #1d1c17;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.clay-inset textarea::placeholder,
|
| 71 |
+
.clay-inset input::placeholder {
|
| 72 |
+
color: #c1c8c3;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 76 |
+
/* CLAY BUTTONS */
|
| 77 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 78 |
+
|
| 79 |
+
.clay-btn-primary {
|
| 80 |
+
background: linear-gradient(145deg, #1f4335, #153328);
|
| 81 |
+
border: none;
|
| 82 |
+
box-shadow: 0 4px 12px rgba(21, 51, 40, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 83 |
+
font-weight: 600;
|
| 84 |
+
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
position: relative;
|
| 87 |
+
overflow: hidden;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.clay-btn-primary:hover {
|
| 91 |
+
box-shadow: 0 8px 20px rgba(21, 51, 40, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 92 |
+
transform: translateY(-2px);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.clay-btn-primary:active {
|
| 96 |
+
transform: translateY(0);
|
| 97 |
+
box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.1);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.clay-btn-primary:disabled {
|
| 101 |
+
opacity: 0.5;
|
| 102 |
+
cursor: not-allowed;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.clay-btn-secondary {
|
| 106 |
+
background: linear-gradient(145deg, #FCF4E5, #F8EDD6);
|
| 107 |
+
border: 2px solid #d8a95e;
|
| 108 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
| 109 |
+
font-weight: 600;
|
| 110 |
+
color: #795900;
|
| 111 |
+
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 112 |
+
cursor: pointer;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.clay-btn-secondary:hover {
|
| 116 |
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
| 117 |
+
transform: translateY(-2px);
|
| 118 |
+
border-color: #c4934d;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.clay-btn-secondary:active {
|
| 122 |
+
transform: translateY(0);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.clay-btn-ghost {
|
| 126 |
+
background: transparent;
|
| 127 |
+
border: 1px solid #c1c8c3;
|
| 128 |
+
box-shadow: none;
|
| 129 |
+
color: #1d1c17;
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
cursor: pointer;
|
| 132 |
+
transition: all 0.3s ease;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.clay-btn-ghost:hover {
|
| 136 |
+
background: #f2ede5;
|
| 137 |
+
border-color: #1d1c17;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 141 |
+
/* BANNER STYLES */
|
| 142 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 143 |
+
|
| 144 |
+
.clay-banner-warning {
|
| 145 |
+
background: linear-gradient(145deg, #FFF4E0, #FCE5CC);
|
| 146 |
+
border: 1px solid #fed174;
|
| 147 |
+
box-shadow: 0 4px 12px rgba(254, 209, 116, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
| 148 |
+
animation: slideInUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 152 |
+
/* INPUTS */
|
| 153 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 154 |
+
|
| 155 |
+
.clay-input {
|
| 156 |
+
background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.4), rgba(0, 0, 0, 0.02));
|
| 157 |
+
border: 1px solid #d4cfc7;
|
| 158 |
+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04);
|
| 159 |
+
color: #1d1c17;
|
| 160 |
+
font-family: "Plus Jakarta Sans", sans-serif;
|
| 161 |
+
transition: all 0.3s ease;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.clay-input:focus {
|
| 165 |
+
outline: none;
|
| 166 |
+
border-color: #153328;
|
| 167 |
+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04), 0 0 0 3px rgba(21, 51, 40, 0.1);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.clay-input::placeholder {
|
| 171 |
+
color: #a9a9a2;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 175 |
+
/* ANIMATIONS */
|
| 176 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 177 |
+
|
| 178 |
+
@keyframes fadeIn {
|
| 179 |
+
from {
|
| 180 |
+
opacity: 0;
|
| 181 |
+
}
|
| 182 |
+
to {
|
| 183 |
+
opacity: 1;
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
@keyframes fadeInUp {
|
| 188 |
+
from {
|
| 189 |
+
opacity: 0;
|
| 190 |
+
transform: translateY(16px);
|
| 191 |
+
}
|
| 192 |
+
to {
|
| 193 |
+
opacity: 1;
|
| 194 |
+
transform: translateY(0);
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
@keyframes slideInUp {
|
| 199 |
+
from {
|
| 200 |
+
transform: translateY(100%);
|
| 201 |
+
opacity: 0;
|
| 202 |
+
}
|
| 203 |
+
to {
|
| 204 |
+
transform: translateY(0);
|
| 205 |
+
opacity: 1;
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
@keyframes slideInDown {
|
| 210 |
+
from {
|
| 211 |
+
transform: translateY(-100%);
|
| 212 |
+
opacity: 0;
|
| 213 |
+
}
|
| 214 |
+
to {
|
| 215 |
+
transform: translateY(0);
|
| 216 |
+
opacity: 1;
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
@keyframes pulse {
|
| 221 |
+
0%, 100% {
|
| 222 |
+
transform: scale(1);
|
| 223 |
+
}
|
| 224 |
+
50% {
|
| 225 |
+
transform: scale(1.1);
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
@keyframes spin {
|
| 230 |
+
from {
|
| 231 |
+
transform: rotate(0deg);
|
| 232 |
+
}
|
| 233 |
+
to {
|
| 234 |
+
transform: rotate(360deg);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.pulsing {
|
| 239 |
+
animation: pulse 2s ease-in-out infinite;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 243 |
+
/* SCREEN MANAGEMENT */
|
| 244 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 245 |
+
|
| 246 |
+
.screen {
|
| 247 |
+
display: none;
|
| 248 |
+
width: 100%;
|
| 249 |
+
height: 100%;
|
| 250 |
+
min-height: 100vh;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.screen.active {
|
| 254 |
+
display: flex;
|
| 255 |
+
flex-direction: column;
|
| 256 |
+
animation: fadeIn 0.3s ease-out;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 260 |
+
/* SETUP WIZARD */
|
| 261 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 262 |
+
|
| 263 |
+
.setup-step {
|
| 264 |
+
animation: fadeInUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.setup-step.hidden {
|
| 268 |
+
display: none;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.step-indicator {
|
| 272 |
+
display: flex;
|
| 273 |
+
flex-direction: column;
|
| 274 |
+
align-items: center;
|
| 275 |
+
gap: 8px;
|
| 276 |
+
opacity: 0.4;
|
| 277 |
+
transition: all 0.3s ease;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.step-indicator.active {
|
| 281 |
+
opacity: 1;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.step-num {
|
| 285 |
+
display: flex;
|
| 286 |
+
align-items: center;
|
| 287 |
+
justify-content: center;
|
| 288 |
+
width: 40px;
|
| 289 |
+
height: 40px;
|
| 290 |
+
border-radius: 50%;
|
| 291 |
+
background: linear-gradient(145deg, #2c4a3e, #153328);
|
| 292 |
+
color: white;
|
| 293 |
+
font-weight: bold;
|
| 294 |
+
font-size: 18px;
|
| 295 |
+
box-shadow: var(--clay-shadow);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.step-indicator.active .step-num {
|
| 299 |
+
background: linear-gradient(145deg, #1f4335, #0f2620);
|
| 300 |
+
box-shadow: 0 6px 16px rgba(21, 51, 40, 0.3);
|
| 301 |
+
transform: scale(1.1);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.side-card {
|
| 305 |
+
border: 2px solid transparent;
|
| 306 |
+
transition: all 0.3s ease;
|
| 307 |
+
cursor: pointer;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.side-card:hover {
|
| 311 |
+
border-color: rgba(21, 51, 40, 0.2);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.side-card.selected {
|
| 315 |
+
border-color: #153328;
|
| 316 |
+
box-shadow: 0 8px 20px rgba(21, 51, 40, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
| 317 |
+
background: linear-gradient(145deg, #F2F8F6, #EAF0ED);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.option-card {
|
| 321 |
+
padding: 16px;
|
| 322 |
+
border-radius: 12px;
|
| 323 |
+
border: 2px solid #d4cfc7;
|
| 324 |
+
background: #f8f3eb;
|
| 325 |
+
cursor: pointer;
|
| 326 |
+
transition: all 0.3s ease;
|
| 327 |
+
text-align: center;
|
| 328 |
+
font-weight: 500;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.option-card:hover {
|
| 332 |
+
border-color: #153328;
|
| 333 |
+
box-shadow: 0 4px 12px rgba(21, 51, 40, 0.1);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.option-card.selected {
|
| 337 |
+
background: linear-gradient(145deg, #2c4a3e, #153328);
|
| 338 |
+
color: white;
|
| 339 |
+
border-color: #153328;
|
| 340 |
+
box-shadow: 0 6px 16px rgba(21, 51, 40, 0.2);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
/* ════════════════════════════════════════════════════════��═══ */
|
| 344 |
+
/* TOGGLE SWITCH */
|
| 345 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 346 |
+
|
| 347 |
+
.toggle-switch {
|
| 348 |
+
position: relative;
|
| 349 |
+
display: inline-block;
|
| 350 |
+
width: 56px;
|
| 351 |
+
height: 32px;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.toggle-switch input {
|
| 355 |
+
opacity: 0;
|
| 356 |
+
width: 0;
|
| 357 |
+
height: 0;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.toggle-slider {
|
| 361 |
+
position: absolute;
|
| 362 |
+
cursor: pointer;
|
| 363 |
+
top: 0;
|
| 364 |
+
left: 0;
|
| 365 |
+
right: 0;
|
| 366 |
+
bottom: 0;
|
| 367 |
+
background-color: #d4cfc7;
|
| 368 |
+
transition: 0.4s;
|
| 369 |
+
border-radius: 32px;
|
| 370 |
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.toggle-slider:before {
|
| 374 |
+
position: absolute;
|
| 375 |
+
content: "";
|
| 376 |
+
height: 26px;
|
| 377 |
+
width: 26px;
|
| 378 |
+
left: 3px;
|
| 379 |
+
bottom: 3px;
|
| 380 |
+
background-color: white;
|
| 381 |
+
transition: 0.4s;
|
| 382 |
+
border-radius: 50%;
|
| 383 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
input:checked + .toggle-slider {
|
| 387 |
+
background-color: #153328;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
input:checked + .toggle-slider:before {
|
| 391 |
+
transform: translateX(24px);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 395 |
+
/* COURTROOM LAYOUT */
|
| 396 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 397 |
+
|
| 398 |
+
#argument-input {
|
| 399 |
+
font-size: 18px;
|
| 400 |
+
letter-spacing: -0.01em;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
#argument-input::placeholder {
|
| 404 |
+
color: #c1c8c3;
|
| 405 |
+
opacity: 0.6;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 409 |
+
/* TRANSCRIPT STYLING */
|
| 410 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 411 |
+
|
| 412 |
+
.transcript-entry {
|
| 413 |
+
display: grid;
|
| 414 |
+
grid-template-columns: 80px 1fr;
|
| 415 |
+
gap: 12px;
|
| 416 |
+
padding: 12px 0;
|
| 417 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
| 418 |
+
animation: fadeInUp 0.4s ease-out;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.transcript-entry:last-child {
|
| 422 |
+
border-bottom: none;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.speaker-pill {
|
| 426 |
+
display: inline-flex;
|
| 427 |
+
align-items: center;
|
| 428 |
+
justify-content: center;
|
| 429 |
+
padding: 6px 12px;
|
| 430 |
+
border-radius: 20px;
|
| 431 |
+
font-size: 11px;
|
| 432 |
+
font-weight: bold;
|
| 433 |
+
margin-bottom: 8px;
|
| 434 |
+
width: fit-content;
|
| 435 |
+
font-family: "JetBrains Mono", monospace;
|
| 436 |
+
text-transform: uppercase;
|
| 437 |
+
letter-spacing: 0.5px;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.speaker-judge {
|
| 441 |
+
background: linear-gradient(145deg, #e8f1ef, #d4e8e3);
|
| 442 |
+
color: #0b3b2d;
|
| 443 |
+
border: 1px solid rgba(21, 51, 40, 0.2);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.speaker-opposing {
|
| 447 |
+
background: linear-gradient(145deg, #ffe8e8, #ffc7c7);
|
| 448 |
+
color: #8b2500;
|
| 449 |
+
border: 1px solid rgba(186, 26, 26, 0.2);
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.speaker-user {
|
| 453 |
+
background: linear-gradient(145deg, #e0e8ff, #c7d7ff);
|
| 454 |
+
color: #0a3366;
|
| 455 |
+
border: 1px solid rgba(33, 99, 204, 0.2);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.speaker-registrar {
|
| 459 |
+
background: linear-gradient(145deg, #f0e8f8, #e8d4f0);
|
| 460 |
+
color: #4a2971;
|
| 461 |
+
border: 1px solid rgba(52, 37, 84, 0.2);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.transcript-text {
|
| 465 |
+
font-family: "Cormorant Garamond", serif;
|
| 466 |
+
font-size: 18px;
|
| 467 |
+
line-height: 1.6;
|
| 468 |
+
color: #1d1c17;
|
| 469 |
+
text-align: left;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.transcript-text em {
|
| 473 |
+
font-style: italic;
|
| 474 |
+
color: #153328;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.transcript-text strong {
|
| 478 |
+
font-weight: 700;
|
| 479 |
+
color: #153328;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 483 |
+
/* CONCESSION TRACKER */
|
| 484 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 485 |
+
|
| 486 |
+
.concession-item {
|
| 487 |
+
padding: 8px;
|
| 488 |
+
border-left: 3px solid #ba1a1a;
|
| 489 |
+
background: rgba(186, 26, 26, 0.05);
|
| 490 |
+
border-radius: 4px;
|
| 491 |
+
font-size: 13px;
|
| 492 |
+
line-height: 1.4;
|
| 493 |
+
animation: slideInUp 0.3s ease;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 497 |
+
/* MODALS */
|
| 498 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 499 |
+
|
| 500 |
+
.modal-overlay {
|
| 501 |
+
position: fixed;
|
| 502 |
+
top: 0;
|
| 503 |
+
left: 0;
|
| 504 |
+
right: 0;
|
| 505 |
+
bottom: 0;
|
| 506 |
+
background: rgba(0, 0, 0, 0.4);
|
| 507 |
+
display: flex;
|
| 508 |
+
align-items: center;
|
| 509 |
+
justify-content: center;
|
| 510 |
+
z-index: 1000;
|
| 511 |
+
backdrop-filter: blur(4px);
|
| 512 |
+
animation: fadeIn 0.2s ease-out;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.modal-overlay.hidden {
|
| 516 |
+
display: none;
|
| 517 |
+
animation: none;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.modal-overlay .clay-card {
|
| 521 |
+
max-height: 90vh;
|
| 522 |
+
overflow-y: auto;
|
| 523 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
/* ═══════════════════════════════════════════════════════���════ */
|
| 527 |
+
/* ANALYSIS ACCORDION */
|
| 528 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 529 |
+
|
| 530 |
+
.accordion-item {
|
| 531 |
+
border: 1px solid #d4cfc7;
|
| 532 |
+
border-radius: 12px;
|
| 533 |
+
margin-bottom: 12px;
|
| 534 |
+
overflow: hidden;
|
| 535 |
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
| 536 |
+
transition: all 0.3s ease;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.accordion-item.open {
|
| 540 |
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.accordion-header {
|
| 544 |
+
display: flex;
|
| 545 |
+
justify-content: space-between;
|
| 546 |
+
align-items: center;
|
| 547 |
+
padding: 16px;
|
| 548 |
+
cursor: pointer;
|
| 549 |
+
background: linear-gradient(145deg, #f8f3eb, #f2ede5);
|
| 550 |
+
border-bottom: 1px solid transparent;
|
| 551 |
+
transition: all 0.3s ease;
|
| 552 |
+
user-select: none;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.accordion-item.open .accordion-header {
|
| 556 |
+
background: linear-gradient(145deg, #f2ede5, #ece8e0);
|
| 557 |
+
border-bottom-color: #d4cfc7;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.accordion-header:hover {
|
| 561 |
+
background: linear-gradient(145deg, #f5f0e8, #ece8e0);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.accordion-title {
|
| 565 |
+
font-weight: 600;
|
| 566 |
+
color: #1d1c17;
|
| 567 |
+
font-family: "Cormorant Garamond", serif;
|
| 568 |
+
font-size: 18px;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.accordion-icon {
|
| 572 |
+
transition: transform 0.3s ease;
|
| 573 |
+
color: #153328;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.accordion-item.open .accordion-icon {
|
| 577 |
+
transform: rotate(180deg);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.accordion-content {
|
| 581 |
+
display: none;
|
| 582 |
+
padding: 16px;
|
| 583 |
+
background: #fef9f1;
|
| 584 |
+
font-size: 15px;
|
| 585 |
+
line-height: 1.7;
|
| 586 |
+
color: #1d1c17;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.accordion-item.open .accordion-content {
|
| 590 |
+
display: block;
|
| 591 |
+
animation: slideInDown 0.3s ease-out;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.accordion-content ul {
|
| 595 |
+
list-style: none;
|
| 596 |
+
padding: 0;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.accordion-content li {
|
| 600 |
+
padding: 8px 0;
|
| 601 |
+
padding-left: 24px;
|
| 602 |
+
position: relative;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
.accordion-content li:before {
|
| 606 |
+
content: "▸";
|
| 607 |
+
position: absolute;
|
| 608 |
+
left: 0;
|
| 609 |
+
color: #795900;
|
| 610 |
+
font-weight: bold;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 614 |
+
/* ANALYSIS TABS */
|
| 615 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 616 |
+
|
| 617 |
+
.analysis-tab {
|
| 618 |
+
cursor: pointer;
|
| 619 |
+
transition: all 0.2s ease;
|
| 620 |
+
border: none;
|
| 621 |
+
background: transparent;
|
| 622 |
+
color: #1d1c17;
|
| 623 |
+
text-transform: none;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.analysis-tab:hover {
|
| 627 |
+
opacity: 0.7;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.analysis-tab.active {
|
| 631 |
+
color: #153328;
|
| 632 |
+
font-weight: 700;
|
| 633 |
+
border-bottom: 2px solid #153328;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 637 |
+
/* SESSION CARDS */
|
| 638 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 639 |
+
|
| 640 |
+
.session-card {
|
| 641 |
+
cursor: pointer;
|
| 642 |
+
transition: all 0.3s ease;
|
| 643 |
+
border: 2px solid transparent;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.session-card:hover {
|
| 647 |
+
border-color: #153328;
|
| 648 |
+
box-shadow: var(--clay-shadow-hover);
|
| 649 |
+
transform: translateY(-4px);
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.session-meta {
|
| 653 |
+
display: grid;
|
| 654 |
+
grid-template-columns: 1fr 1fr;
|
| 655 |
+
gap: 8px;
|
| 656 |
+
font-size: 13px;
|
| 657 |
+
margin-top: 12px;
|
| 658 |
+
padding-top: 12px;
|
| 659 |
+
border-top: 1px solid #d4cfc7;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
.meta-item {
|
| 663 |
+
display: flex;
|
| 664 |
+
align-items: center;
|
| 665 |
+
gap: 6px;
|
| 666 |
+
color: #153328;
|
| 667 |
+
font-weight: 500;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 671 |
+
/* LOADING SCREEN */
|
| 672 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 673 |
+
|
| 674 |
+
.loading-step {
|
| 675 |
+
display: flex;
|
| 676 |
+
align-items: center;
|
| 677 |
+
gap: 16px;
|
| 678 |
+
padding: 16px;
|
| 679 |
+
background: #f8f3eb;
|
| 680 |
+
border-radius: 12px;
|
| 681 |
+
animation: fadeInUp 0.5s ease-out;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.step-icon {
|
| 685 |
+
width: 48px;
|
| 686 |
+
height: 48px;
|
| 687 |
+
border-radius: 50%;
|
| 688 |
+
background: linear-gradient(145deg, #2c4a3e, #153328);
|
| 689 |
+
display: flex;
|
| 690 |
+
align-items: center;
|
| 691 |
+
justify-content: center;
|
| 692 |
+
color: white;
|
| 693 |
+
flex-shrink: 0;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.step-content {
|
| 697 |
+
flex: 1;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.progress-bar {
|
| 701 |
+
width: 100%;
|
| 702 |
+
height: 6px;
|
| 703 |
+
background: #d4cfc7;
|
| 704 |
+
border-radius: 3px;
|
| 705 |
+
overflow: hidden;
|
| 706 |
+
margin-top: 4px;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.progress-fill {
|
| 710 |
+
height: 100%;
|
| 711 |
+
background: linear-gradient(90deg, #153328, #2c4a3e);
|
| 712 |
+
transition: width 0.6s ease-out;
|
| 713 |
+
border-radius: 3px;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 717 |
+
/* WATERCOLOR BG */
|
| 718 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 719 |
+
|
| 720 |
+
.watercolor-bg {
|
| 721 |
+
position: absolute;
|
| 722 |
+
top: 0;
|
| 723 |
+
left: 0;
|
| 724 |
+
width: 100%;
|
| 725 |
+
height: 100%;
|
| 726 |
+
background: radial-gradient(ellipse 900px 600px at 20% 30%, rgba(44, 74, 62, 0.06), transparent),
|
| 727 |
+
radial-gradient(ellipse 800px 400px at 80% 70%, rgba(254, 209, 116, 0.04), transparent),
|
| 728 |
+
radial-gradient(ellipse 600px 500px at 50% 10%, rgba(232, 221, 255, 0.05), transparent);
|
| 729 |
+
pointer-events: none;
|
| 730 |
+
z-index: 0;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 734 |
+
/* RESPONSIVE */
|
| 735 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 736 |
+
|
| 737 |
+
@media (max-width: 1024px) {
|
| 738 |
+
#screen-courtroom {
|
| 739 |
+
display: flex !important;
|
| 740 |
+
flex-direction: column;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
#screen-courtroom > aside {
|
| 744 |
+
width: 100% !important;
|
| 745 |
+
flex-direction: row !important;
|
| 746 |
+
padding: 12px;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
#screen-courtroom > main {
|
| 750 |
+
flex: 1 !important;
|
| 751 |
+
}
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
@media (max-width: 768px) {
|
| 755 |
+
body {
|
| 756 |
+
font-size: 14px;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.transcript-text {
|
| 760 |
+
font-size: 16px;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.modal-overlay .clay-card {
|
| 764 |
+
margin: 16px;
|
| 765 |
+
max-width: none;
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 770 |
+
/* UTILITY CLASSES */
|
| 771 |
+
/* ════════════════════════════════════════════════════════════ */
|
| 772 |
+
|
| 773 |
+
.flex {
|
| 774 |
+
display: flex;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.items-center {
|
| 778 |
+
align-items: center;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
.justify-center {
|
| 782 |
+
justify-content: center;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
.gap-2 {
|
| 786 |
+
gap: 8px;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.gap-3 {
|
| 790 |
+
gap: 12px;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.gap-4 {
|
| 794 |
+
gap: 16px;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.gap-6 {
|
| 798 |
+
gap: 24px;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
.truncate {
|
| 802 |
+
white-space: nowrap;
|
| 803 |
+
overflow: hidden;
|
| 804 |
+
text-overflow: ellipsis;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.italic {
|
| 808 |
+
font-style: italic;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.font-bold {
|
| 812 |
+
font-weight: 700;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.text-sm {
|
| 816 |
+
font-size: 14px;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
.text-xs {
|
| 820 |
+
font-size: 12px;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.opacity-50 {
|
| 824 |
+
opacity: 0.5;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.cursor-pointer {
|
| 828 |
+
cursor: pointer;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
.transition-colors {
|
| 832 |
+
transition: color 0.3s ease, background-color 0.3s ease;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.transition-transform {
|
| 836 |
+
transition: transform 0.3s ease;
|
| 837 |
+
}
|
frontend/court/court.html
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>NyayaSetu — Moot Court</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400;1,700&family=DM+Sans:wght@400;500;700&family=JetBrains+Mono&family=Newsreader:opsz,wght@6..72,400;6..72,600;6..72,700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
+
<link rel="stylesheet" href="/static/court/court.css"/>
|
| 11 |
+
<script id="tailwind-config">
|
| 12 |
+
tailwind.config = {
|
| 13 |
+
darkMode: "class",
|
| 14 |
+
theme: {
|
| 15 |
+
extend: {
|
| 16 |
+
colors: {
|
| 17 |
+
"primary": "#153328",
|
| 18 |
+
"primary-container": "#2c4a3e",
|
| 19 |
+
"on-primary": "#ffffff",
|
| 20 |
+
"on-primary-container": "#98b9a9",
|
| 21 |
+
"secondary": "#795900",
|
| 22 |
+
"secondary-container": "#fed174",
|
| 23 |
+
"on-secondary": "#ffffff",
|
| 24 |
+
"on-secondary-container": "#785800",
|
| 25 |
+
"tertiary": "#342554",
|
| 26 |
+
"tertiary-container": "#4b3b6c",
|
| 27 |
+
"on-tertiary": "#ffffff",
|
| 28 |
+
"error": "#ba1a1a",
|
| 29 |
+
"error-container": "#ffdad6",
|
| 30 |
+
"on-error-container": "#93000a",
|
| 31 |
+
"background": "#fef9f1",
|
| 32 |
+
"surface": "#fef9f1",
|
| 33 |
+
"surface-bright": "#fef9f1",
|
| 34 |
+
"surface-dim": "#ded9d2",
|
| 35 |
+
"surface-variant": "#e7e2da",
|
| 36 |
+
"surface-container": "#f2ede5",
|
| 37 |
+
"surface-container-low": "#f8f3eb",
|
| 38 |
+
"surface-container-high": "#ece8e0",
|
| 39 |
+
"surface-container-highest": "#e7e2da",
|
| 40 |
+
"surface-container-lowest": "#ffffff",
|
| 41 |
+
"on-surface": "#1d1c17",
|
| 42 |
+
"on-surface-variant": "#414845",
|
| 43 |
+
"outline": "#727974",
|
| 44 |
+
"outline-variant": "#c1c8c3",
|
| 45 |
+
"inverse-surface": "#32302b",
|
| 46 |
+
"inverse-on-surface": "#f5f0e8",
|
| 47 |
+
"secondary-fixed": "#ffdfa0",
|
| 48 |
+
"secondary-fixed-dim": "#ecc165",
|
| 49 |
+
"tertiary-fixed": "#eaddff",
|
| 50 |
+
"tertiary-fixed-dim": "#d1bdf7",
|
| 51 |
+
"on-tertiary-fixed": "#221141",
|
| 52 |
+
},
|
| 53 |
+
fontFamily: {
|
| 54 |
+
"serif": ["Cormorant Garamond", "serif"],
|
| 55 |
+
"sans": ["DM Sans", "sans-serif"],
|
| 56 |
+
"headline": ["Newsreader", "serif"],
|
| 57 |
+
"body": ["Plus Jakarta Sans", "sans-serif"],
|
| 58 |
+
"mono": ["JetBrains Mono", "monospace"],
|
| 59 |
+
},
|
| 60 |
+
borderRadius: {
|
| 61 |
+
"DEFAULT": "1rem",
|
| 62 |
+
"lg": "2rem",
|
| 63 |
+
"xl": "3rem",
|
| 64 |
+
"full": "9999px",
|
| 65 |
+
},
|
| 66 |
+
},
|
| 67 |
+
},
|
| 68 |
+
}
|
| 69 |
+
</script>
|
| 70 |
+
</head>
|
| 71 |
+
<body class="bg-[#F5F0E8] text-on-surface font-body min-h-screen overflow-hidden">
|
| 72 |
+
|
| 73 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 74 |
+
<!-- SCREEN: LOBBY -->
|
| 75 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 76 |
+
<div id="screen-lobby" class="screen active">
|
| 77 |
+
<!-- Watercolor bg -->
|
| 78 |
+
<div class="watercolor-bg"></div>
|
| 79 |
+
|
| 80 |
+
<!-- Nav -->
|
| 81 |
+
<header class="fixed top-0 left-0 right-0 z-50 bg-[#F5F0E8]/90 backdrop-blur-md shadow-[0_8px_20px_rgba(0,0,0,0.08)]">
|
| 82 |
+
<nav class="flex justify-between items-center w-full px-12 py-4 max-w-[1920px] mx-auto">
|
| 83 |
+
<div class="flex items-center gap-8">
|
| 84 |
+
<a href="/static/index.html" class="font-serif text-2xl font-bold text-primary italic">NyayaSetu</a>
|
| 85 |
+
<div class="hidden md:flex gap-8 items-center">
|
| 86 |
+
<a href="/static/index.html" class="font-serif font-bold text-lg text-primary/70 hover:text-primary transition-colors">Research</a>
|
| 87 |
+
<span class="font-serif font-bold text-lg text-secondary border-b-2 border-secondary pb-1">Moot Court</span>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="flex items-center gap-4">
|
| 91 |
+
<span class="font-mono text-xs text-primary/40 bg-surface-container px-3 py-1 rounded-full">Educational Simulation Only</span>
|
| 92 |
+
</div>
|
| 93 |
+
</nav>
|
| 94 |
+
</header>
|
| 95 |
+
|
| 96 |
+
<main class="relative z-10 pt-32 pb-20 px-12 grid grid-cols-12 gap-12 max-w-7xl mx-auto items-center min-h-screen">
|
| 97 |
+
<!-- Centre card -->
|
| 98 |
+
<div class="col-span-12 lg:col-span-7 flex justify-center items-center">
|
| 99 |
+
<div class="clay-card p-12 lg:p-16 rounded-xl w-full max-w-2xl text-center">
|
| 100 |
+
<!-- Gavel icon -->
|
| 101 |
+
<div class="mb-8 inline-flex items-center justify-center w-20 h-20 rounded-full bg-surface-container clay-puffy">
|
| 102 |
+
<span class="material-symbols-outlined text-secondary text-4xl" style="font-variation-settings:'FILL' 1">gavel</span>
|
| 103 |
+
</div>
|
| 104 |
+
<h1 class="font-serif text-5xl lg:text-6xl font-bold text-primary tracking-tight mb-4">NYAYASETU MOOT COURT</h1>
|
| 105 |
+
<p class="font-sans text-lg text-primary/60 mb-12 uppercase tracking-widest">Practice. Argue. Prepare.</p>
|
| 106 |
+
|
| 107 |
+
<div class="flex flex-col sm:flex-row gap-6 justify-center mb-10">
|
| 108 |
+
<button onclick="showScreen('setup')" class="clay-btn-primary flex items-center justify-center gap-3 px-8 py-5 rounded-xl font-sans font-bold text-lg text-white">
|
| 109 |
+
<span class="material-symbols-outlined">scale</span>
|
| 110 |
+
Start New Case
|
| 111 |
+
</button>
|
| 112 |
+
<button onclick="showImportFlow()" class="clay-btn-secondary flex items-center justify-center gap-3 border-2 border-secondary/30 px-8 py-5 rounded-xl font-sans font-bold text-lg text-secondary">
|
| 113 |
+
<span class="material-symbols-outlined">folder_open</span>
|
| 114 |
+
Continue Research Session
|
| 115 |
+
</button>
|
| 116 |
+
</div>
|
| 117 |
+
<a onclick="showScreen('sessions')" class="text-secondary font-sans font-bold text-base hover:underline flex items-center justify-center gap-2 mb-12 cursor-pointer">
|
| 118 |
+
<span class="material-symbols-outlined text-sm">list_alt</span>
|
| 119 |
+
View Past Sessions
|
| 120 |
+
</a>
|
| 121 |
+
<div class="pt-8 border-t border-primary/5">
|
| 122 |
+
<p class="text-primary/40 text-sm font-sans italic">For educational simulation only. Not legal advice.</p>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<!-- Recent sessions sidebar -->
|
| 128 |
+
<div class="col-span-12 lg:col-span-5 flex flex-col gap-6">
|
| 129 |
+
<div class="flex items-center justify-between px-4">
|
| 130 |
+
<h3 class="font-serif text-2xl font-bold text-primary">Recent Sessions</h3>
|
| 131 |
+
<span class="text-xs font-mono text-secondary bg-secondary-fixed/30 px-2 py-1 rounded">LIVE</span>
|
| 132 |
+
</div>
|
| 133 |
+
<div id="recent-sessions-list" class="space-y-4">
|
| 134 |
+
<div class="clay-card p-6 rounded-xl text-center text-primary/40 text-sm font-sans">
|
| 135 |
+
Loading sessions...
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</main>
|
| 140 |
+
|
| 141 |
+
<!-- Registrar clock -->
|
| 142 |
+
<div class="fixed bottom-12 right-12 z-50">
|
| 143 |
+
<div class="clay-card bg-white/80 backdrop-blur-xl p-4 rounded-xl">
|
| 144 |
+
<div class="flex items-center gap-4">
|
| 145 |
+
<div class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
| 146 |
+
<div class="flex flex-col">
|
| 147 |
+
<span class="font-mono text-[10px] text-primary/40 leading-none">REGISTRAR CLOCK</span>
|
| 148 |
+
<span id="lobby-clock" class="font-sans font-bold text-primary">--:--:--</span>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 157 |
+
<!-- SCREEN: CASE SETUP -->
|
| 158 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 159 |
+
<div id="screen-setup" class="screen">
|
| 160 |
+
<header class="fixed top-0 left-0 right-0 z-50 bg-[#F5F0E8]/90 backdrop-blur-md shadow-[0_8px_20px_rgba(0,0,0,0.08)]">
|
| 161 |
+
<nav class="flex justify-between items-center px-12 py-4">
|
| 162 |
+
<span class="font-serif text-2xl font-bold text-primary italic">NyayaSetu Moot Court</span>
|
| 163 |
+
<button onclick="showScreen('lobby')" class="text-primary/60 hover:text-primary font-sans text-sm flex items-center gap-2">
|
| 164 |
+
<span class="material-symbols-outlined text-sm">arrow_back</span> Back to Lobby
|
| 165 |
+
</button>
|
| 166 |
+
</nav>
|
| 167 |
+
</header>
|
| 168 |
+
|
| 169 |
+
<main class="pt-28 pb-16 px-12 max-w-4xl mx-auto">
|
| 170 |
+
<div class="mb-10">
|
| 171 |
+
<h1 class="font-serif text-4xl font-bold text-primary mb-2">Case Setup</h1>
|
| 172 |
+
<p class="text-primary/60 font-sans">Configure your moot court session before entering the courtroom.</p>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<!-- Step indicators -->
|
| 176 |
+
<div class="flex items-center gap-4 mb-10">
|
| 177 |
+
<div id="step-1-ind" class="step-indicator active">
|
| 178 |
+
<span class="step-num">1</span>
|
| 179 |
+
<span class="font-sans text-sm font-medium">Your Side</span>
|
| 180 |
+
</div>
|
| 181 |
+
<div class="h-px flex-1 bg-outline-variant"></div>
|
| 182 |
+
<div id="step-2-ind" class="step-indicator">
|
| 183 |
+
<span class="step-num">2</span>
|
| 184 |
+
<span class="font-sans text-sm font-medium">The Case</span>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="h-px flex-1 bg-outline-variant"></div>
|
| 187 |
+
<div id="step-3-ind" class="step-indicator">
|
| 188 |
+
<span class="step-num">3</span>
|
| 189 |
+
<span class="font-sans text-sm font-medium">Courtroom Setup</span>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<!-- Step 1: Your Side -->
|
| 194 |
+
<div id="setup-step-1" class="setup-step">
|
| 195 |
+
<h2 class="font-serif text-2xl font-bold text-primary mb-8">Who are you representing?</h2>
|
| 196 |
+
<div class="grid grid-cols-2 gap-6 mb-10">
|
| 197 |
+
<div id="side-petitioner" onclick="selectSide('petitioner')" class="side-card clay-card p-8 rounded-xl cursor-pointer text-center">
|
| 198 |
+
<div class="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center mx-auto mb-4">
|
| 199 |
+
<span class="material-symbols-outlined text-blue-600 text-3xl">person</span>
|
| 200 |
+
</div>
|
| 201 |
+
<h3 class="font-serif text-xl font-bold text-primary mb-2">Petitioner</h3>
|
| 202 |
+
<p class="text-primary/60 font-sans text-sm">You argue FOR the case. You brought this matter before the court.</p>
|
| 203 |
+
</div>
|
| 204 |
+
<div id="side-respondent" onclick="selectSide('respondent')" class="side-card clay-card p-8 rounded-xl cursor-pointer text-center">
|
| 205 |
+
<div class="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
|
| 206 |
+
<span class="material-symbols-outlined text-red-600 text-3xl">shield</span>
|
| 207 |
+
</div>
|
| 208 |
+
<h3 class="font-serif text-xl font-bold text-primary mb-2">Respondent</h3>
|
| 209 |
+
<p class="text-primary/60 font-sans text-sm">You argue AGAINST the case. You are defending against the petition.</p>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
<button onclick="goToStep(2)" class="clay-btn-primary px-8 py-4 rounded-xl font-sans font-bold text-white">Continue →</button>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<!-- Step 2: The Case -->
|
| 216 |
+
<div id="setup-step-2" class="setup-step hidden">
|
| 217 |
+
<h2 class="font-serif text-2xl font-bold text-primary mb-8">Define the Case</h2>
|
| 218 |
+
<div class="space-y-6">
|
| 219 |
+
<div>
|
| 220 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Case Title</label>
|
| 221 |
+
<input id="case-title" type="text" placeholder="e.g. State of Maharashtra vs Ramesh Kumar" class="clay-input w-full px-5 py-4 rounded-xl font-sans"/>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="grid grid-cols-2 gap-6">
|
| 224 |
+
<div>
|
| 225 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Your Client</label>
|
| 226 |
+
<input id="user-client" type="text" placeholder="Client name" class="clay-input w-full px-5 py-4 rounded-xl font-sans"/>
|
| 227 |
+
</div>
|
| 228 |
+
<div>
|
| 229 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Opposing Party</label>
|
| 230 |
+
<input id="opposing-party" type="text" placeholder="Opposing party name" class="clay-input w-full px-5 py-4 rounded-xl font-sans"/>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
<div>
|
| 234 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Legal Issues</label>
|
| 235 |
+
<div id="issues-list" class="flex flex-wrap gap-2 mb-3"></div>
|
| 236 |
+
<div class="flex gap-3">
|
| 237 |
+
<input id="issue-input" type="text" placeholder="Add a legal issue and press Enter" class="clay-input flex-1 px-5 py-3 rounded-xl font-sans text-sm"/>
|
| 238 |
+
<button onclick="addIssue()" class="clay-btn-secondary px-4 py-3 rounded-xl font-sans font-bold text-secondary">Add</button>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div>
|
| 242 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Brief Facts</label>
|
| 243 |
+
<textarea id="brief-facts" rows="4" placeholder="Briefly describe the facts of the case..." class="clay-input w-full px-5 py-4 rounded-xl font-sans resize-none"></textarea>
|
| 244 |
+
</div>
|
| 245 |
+
<div>
|
| 246 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-2">Jurisdiction</label>
|
| 247 |
+
<select id="jurisdiction" class="clay-input w-full px-5 py-4 rounded-xl font-sans">
|
| 248 |
+
<option value="supreme_court">Supreme Court of India</option>
|
| 249 |
+
<option value="high_court">High Court</option>
|
| 250 |
+
<option value="district_court">District Court</option>
|
| 251 |
+
</select>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="flex gap-4 mt-8">
|
| 255 |
+
<button onclick="goToStep(1)" class="clay-btn-ghost px-8 py-4 rounded-xl font-sans font-bold">← Back</button>
|
| 256 |
+
<button onclick="goToStep(3)" class="clay-btn-primary px-8 py-4 rounded-xl font-sans font-bold text-white">Continue →</button>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<!-- Step 3: Courtroom Setup -->
|
| 261 |
+
<div id="setup-step-3" class="setup-step hidden">
|
| 262 |
+
<h2 class="font-serif text-2xl font-bold text-primary mb-8">Courtroom Configuration</h2>
|
| 263 |
+
<div class="space-y-8">
|
| 264 |
+
<!-- Bench -->
|
| 265 |
+
<div>
|
| 266 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-4">Bench Composition</label>
|
| 267 |
+
<div class="grid grid-cols-3 gap-4">
|
| 268 |
+
<div class="option-card" id="bench-single" onclick="selectOption('bench','single')">
|
| 269 |
+
<span class="font-sans font-medium">Single Judge</span>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="option-card selected" id="bench-division" onclick="selectOption('bench','division')">
|
| 272 |
+
<span class="font-sans font-medium">Division Bench</span>
|
| 273 |
+
</div>
|
| 274 |
+
<div class="option-card" id="bench-constitutional" onclick="selectOption('bench','constitutional')">
|
| 275 |
+
<span class="font-sans font-medium">Constitutional Bench</span>
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
<!-- Difficulty -->
|
| 280 |
+
<div>
|
| 281 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-4">Difficulty</label>
|
| 282 |
+
<div class="grid grid-cols-3 gap-4">
|
| 283 |
+
<div class="option-card" id="diff-moot" onclick="selectOption('diff','moot')">
|
| 284 |
+
<span class="font-sans font-medium">Moot (Guided)</span>
|
| 285 |
+
</div>
|
| 286 |
+
<div class="option-card selected" id="diff-standard" onclick="selectOption('diff','standard')">
|
| 287 |
+
<span class="font-sans font-medium">Standard</span>
|
| 288 |
+
</div>
|
| 289 |
+
<div class="option-card" id="diff-adversarial" onclick="selectOption('diff','adversarial')">
|
| 290 |
+
<span class="font-sans font-medium">Adversarial</span>
|
| 291 |
+
<span class="text-xs text-error ml-1">(No mercy)</span>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
<!-- Session Length -->
|
| 296 |
+
<div>
|
| 297 |
+
<label class="font-serif text-lg font-semibold text-primary block mb-4">Session Length</label>
|
| 298 |
+
<div class="grid grid-cols-3 gap-4">
|
| 299 |
+
<div class="option-card" id="len-brief" onclick="selectOption('len','brief')">
|
| 300 |
+
<span class="font-sans font-medium">Brief</span>
|
| 301 |
+
<span class="text-xs text-primary/50 block">3 rounds</span>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="option-card selected" id="len-standard" onclick="selectOption('len','standard')">
|
| 304 |
+
<span class="font-sans font-medium">Standard</span>
|
| 305 |
+
<span class="text-xs text-primary/50 block">5 rounds</span>
|
| 306 |
+
</div>
|
| 307 |
+
<div class="option-card" id="len-extended" onclick="selectOption('len','extended')">
|
| 308 |
+
<span class="font-sans font-medium">Extended</span>
|
| 309 |
+
<span class="text-xs text-primary/50 block">8 rounds</span>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
<!-- Trap warnings toggle -->
|
| 314 |
+
<div class="flex items-center justify-between clay-card p-5 rounded-xl">
|
| 315 |
+
<div>
|
| 316 |
+
<h4 class="font-sans font-bold text-primary">Enable Trap Detection Warnings</h4>
|
| 317 |
+
<p class="text-sm text-primary/60">Show a warning when opposing counsel may be setting a trap</p>
|
| 318 |
+
</div>
|
| 319 |
+
<label class="toggle-switch">
|
| 320 |
+
<input type="checkbox" id="trap-warnings" checked/>
|
| 321 |
+
<span class="toggle-slider"></span>
|
| 322 |
+
</label>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
<div class="flex gap-4 mt-10">
|
| 326 |
+
<button onclick="goToStep(2)" class="clay-btn-ghost px-8 py-4 rounded-xl font-sans font-bold">← Back</button>
|
| 327 |
+
<button onclick="enterCourtroom()" class="clay-btn-primary px-10 py-5 rounded-xl font-sans font-bold text-white text-lg flex items-center gap-3">
|
| 328 |
+
<span class="material-symbols-outlined">balance</span>
|
| 329 |
+
Enter the Courtroom
|
| 330 |
+
</button>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
</main>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 338 |
+
<!-- SCREEN: COURTROOM (Main) -->
|
| 339 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 340 |
+
<div id="screen-courtroom" class="screen">
|
| 341 |
+
<!-- Top nav -->
|
| 342 |
+
<nav class="fixed top-0 left-0 right-0 z-50 bg-[#F5F0E8]/90 backdrop-blur-md shadow-[0_8px_20px_rgba(0,0,0,0.08)]">
|
| 343 |
+
<div class="flex justify-between items-center px-12 py-4">
|
| 344 |
+
<div class="flex items-center gap-4">
|
| 345 |
+
<span class="font-serif text-xl font-bold text-primary italic">NyayaSetu</span>
|
| 346 |
+
<span class="text-primary/30">|</span>
|
| 347 |
+
<span id="court-case-title" class="font-sans text-sm text-primary/70 truncate max-w-xs">Moot Court</span>
|
| 348 |
+
</div>
|
| 349 |
+
<div class="flex items-center gap-4">
|
| 350 |
+
<div id="round-indicator" class="px-4 py-2 bg-surface-container rounded-xl flex items-center gap-2">
|
| 351 |
+
<span class="material-symbols-outlined text-secondary text-sm">timer</span>
|
| 352 |
+
<span class="font-mono text-primary text-sm font-bold">Round 1</span>
|
| 353 |
+
</div>
|
| 354 |
+
<div id="phase-badge" class="px-3 py-1 bg-primary text-white rounded-full font-mono text-xs">BRIEFING</div>
|
| 355 |
+
<button onclick="confirmEndSession()" class="px-4 py-2 rounded-xl border border-error/30 text-error text-sm font-sans font-medium hover:bg-error-container transition-colors">
|
| 356 |
+
End Session
|
| 357 |
+
</button>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</nav>
|
| 361 |
+
|
| 362 |
+
<div class="pt-20 flex h-screen">
|
| 363 |
+
<!-- Left panel: Bench + Concessions + Precedents -->
|
| 364 |
+
<aside class="w-72 flex-shrink-0 flex flex-col gap-4 p-4 overflow-y-auto bg-[#F5F0E8] shadow-[inset_-4px_0_8px_rgba(0,0,0,0.04)]">
|
| 365 |
+
|
| 366 |
+
<!-- Judge panel -->
|
| 367 |
+
<div class="clay-card p-5 rounded-xl">
|
| 368 |
+
<div class="flex items-center gap-3 mb-3">
|
| 369 |
+
<div class="w-10 h-10 rounded-full bg-primary-container flex items-center justify-center">
|
| 370 |
+
<span class="material-symbols-outlined text-on-primary-container text-lg">account_balance</span>
|
| 371 |
+
</div>
|
| 372 |
+
<div>
|
| 373 |
+
<p class="font-mono text-[10px] text-primary/40 uppercase tracking-widest">HON'BLE BENCH</p>
|
| 374 |
+
<p class="font-sans text-sm font-bold text-primary" id="bench-label">Division Bench</p>
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
<div id="user-side-badge" class="text-xs font-mono bg-blue-100 text-blue-700 px-2 py-1 rounded-full inline-block">
|
| 378 |
+
You: Petitioner
|
| 379 |
+
</div>
|
| 380 |
+
</div>
|
| 381 |
+
|
| 382 |
+
<!-- Concession tracker -->
|
| 383 |
+
<div class="clay-card p-5 rounded-xl flex-1">
|
| 384 |
+
<div class="flex items-center justify-between mb-4">
|
| 385 |
+
<h3 class="font-serif text-sm font-bold text-primary uppercase tracking-wider">Concessions</h3>
|
| 386 |
+
<span id="concession-count" class="font-mono text-xs bg-secondary-container text-on-secondary-container px-2 py-0.5 rounded-full">0</span>
|
| 387 |
+
</div>
|
| 388 |
+
<div id="concessions-list" class="space-y-3 text-xs">
|
| 389 |
+
<p class="text-primary/40 italic font-sans">No concessions recorded yet.</p>
|
| 390 |
+
</div>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
+
<!-- Cited precedents -->
|
| 394 |
+
<div class="clay-card p-5 rounded-xl">
|
| 395 |
+
<h3 class="font-serif text-sm font-bold text-primary uppercase tracking-wider mb-3">Cited in Session</h3>
|
| 396 |
+
<div id="precedents-list" class="space-y-2 text-xs">
|
| 397 |
+
<p class="text-primary/40 italic font-sans">None yet.</p>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
</aside>
|
| 402 |
+
|
| 403 |
+
<!-- Centre: Transcript + Input -->
|
| 404 |
+
<main class="flex-1 flex flex-col overflow-hidden">
|
| 405 |
+
|
| 406 |
+
<!-- Trap warning banner -->
|
| 407 |
+
<div id="trap-banner" class="hidden clay-banner-warning mx-4 mt-4 p-5 rounded-xl flex items-center justify-between">
|
| 408 |
+
<div class="flex items-center gap-4">
|
| 409 |
+
<div class="w-12 h-12 bg-white/40 rounded-full flex items-center justify-center">
|
| 410 |
+
<span class="material-symbols-outlined text-secondary text-2xl" style="font-variation-settings:'FILL' 1">bolt</span>
|
| 411 |
+
</div>
|
| 412 |
+
<div>
|
| 413 |
+
<h3 class="font-serif text-lg font-bold text-[#5c4300]">⚡ TRAP DETECTED</h3>
|
| 414 |
+
<p id="trap-warning-text" class="text-[#795900] font-sans text-sm">Opposing counsel's last statement may contain a trap. Read carefully.</p>
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
<button onclick="dismissTrap()" class="bg-secondary text-white px-6 py-2 rounded-xl font-sans font-bold text-sm">Dismiss</button>
|
| 418 |
+
</div>
|
| 419 |
+
|
| 420 |
+
<!-- Transcript area -->
|
| 421 |
+
<div class="flex-1 overflow-y-auto p-6" id="transcript-container">
|
| 422 |
+
<div id="transcript" class="max-w-3xl mx-auto space-y-8 pb-8">
|
| 423 |
+
<!-- Entries injected here by JS -->
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<!-- Input area -->
|
| 428 |
+
<div class="border-t border-outline-variant/20 bg-[#F5F0E8] p-4">
|
| 429 |
+
<div class="max-w-3xl mx-auto">
|
| 430 |
+
<label id="input-label" class="block text-xs font-mono text-outline uppercase tracking-widest mb-3">Your Argument</label>
|
| 431 |
+
<div class="clay-inset p-4 rounded-xl flex gap-3 items-end">
|
| 432 |
+
<textarea
|
| 433 |
+
id="argument-input"
|
| 434 |
+
rows="3"
|
| 435 |
+
placeholder="State your argument to the court..."
|
| 436 |
+
class="flex-1 bg-transparent border-none focus:ring-0 font-serif text-lg text-on-surface placeholder:text-outline-variant resize-none"
|
| 437 |
+
></textarea>
|
| 438 |
+
<div class="flex flex-col gap-2">
|
| 439 |
+
<div class="text-xs font-mono text-outline text-right" id="word-count">0 / 500</div>
|
| 440 |
+
<div class="flex gap-2">
|
| 441 |
+
<button onclick="openObjectionModal()" class="w-10 h-10 clay-card flex items-center justify-center text-secondary hover:scale-105 transition-transform" title="Raise Objection">
|
| 442 |
+
<span class="material-symbols-outlined text-sm">warning</span>
|
| 443 |
+
</button>
|
| 444 |
+
<button onclick="openDocumentModal()" class="w-10 h-10 clay-card flex items-center justify-center text-tertiary hover:scale-105 transition-transform" title="Produce Document">
|
| 445 |
+
<span class="material-symbols-outlined text-sm">description</span>
|
| 446 |
+
</button>
|
| 447 |
+
<button onclick="submitArgument()" id="submit-btn" class="clay-btn-primary w-10 h-10 flex items-center justify-center text-white rounded-xl">
|
| 448 |
+
<span class="material-symbols-outlined text-sm">send</span>
|
| 449 |
+
</button>
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
<div class="flex justify-between mt-2 text-xs text-primary/40 font-mono">
|
| 454 |
+
<span id="phase-hint">Round 0 — Opening submissions</span>
|
| 455 |
+
<span>ARGUE | OBJECT ⚡ | PRODUCE 📄</span>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
</div>
|
| 459 |
+
</main>
|
| 460 |
+
|
| 461 |
+
<!-- Right panel: Case file + Documents + Round nav -->
|
| 462 |
+
<aside class="w-64 flex-shrink-0 flex flex-col gap-4 p-4 overflow-y-auto bg-[#F5F0E8] shadow-[inset_4px_0_8px_rgba(0,0,0,0.04)]">
|
| 463 |
+
|
| 464 |
+
<!-- Case details -->
|
| 465 |
+
<div class="clay-card p-5 rounded-xl">
|
| 466 |
+
<h3 class="font-mono text-[10px] text-primary/40 uppercase tracking-widest mb-3">Case File</h3>
|
| 467 |
+
<p id="panel-case-title" class="font-serif text-sm font-bold text-primary mb-2">-</p>
|
| 468 |
+
<div id="panel-issues" class="flex flex-wrap gap-1">
|
| 469 |
+
<!-- Issue tags -->
|
| 470 |
+
</div>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<!-- Documents produced -->
|
| 474 |
+
<div class="clay-card p-5 rounded-xl flex-1">
|
| 475 |
+
<h3 class="font-mono text-[10px] text-primary/40 uppercase tracking-widest mb-3">Documents</h3>
|
| 476 |
+
<div id="documents-list" class="space-y-2">
|
| 477 |
+
<p class="text-primary/40 italic font-sans text-xs">None produced.</p>
|
| 478 |
+
</div>
|
| 479 |
+
</div>
|
| 480 |
+
|
| 481 |
+
<!-- Round navigator -->
|
| 482 |
+
<div class="clay-card p-5 rounded-xl">
|
| 483 |
+
<h3 class="font-mono text-[10px] text-primary/40 uppercase tracking-widest mb-3">Rounds</h3>
|
| 484 |
+
<div id="round-nav" class="flex flex-wrap gap-2">
|
| 485 |
+
<!-- Round dots injected here -->
|
| 486 |
+
</div>
|
| 487 |
+
</div>
|
| 488 |
+
|
| 489 |
+
<!-- End session button -->
|
| 490 |
+
<button onclick="confirmEndSession()" class="w-full py-3 rounded-xl border border-primary/20 text-primary/60 font-sans text-sm font-medium hover:border-error hover:text-error transition-colors">
|
| 491 |
+
End Session →
|
| 492 |
+
</button>
|
| 493 |
+
|
| 494 |
+
</aside>
|
| 495 |
+
</div>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 500 |
+
<!-- SCREEN: ANALYSIS LOADING -->
|
| 501 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 502 |
+
<div id="screen-loading" class="screen">
|
| 503 |
+
<main class="min-h-screen flex flex-col items-center justify-center px-8">
|
| 504 |
+
<div class="relative mb-12">
|
| 505 |
+
<div class="w-32 h-32 rounded-full bg-surface-container-lowest clay-puffy flex items-center justify-center">
|
| 506 |
+
<span class="material-symbols-outlined text-secondary text-6xl pulsing" style="font-variation-settings:'FILL' 1">gavel</span>
|
| 507 |
+
</div>
|
| 508 |
+
<div class="absolute -top-4 -right-4 w-12 h-12 bg-tertiary-fixed-dim rounded-full flex items-center justify-center clay-puffy">
|
| 509 |
+
<span class="material-symbols-outlined text-on-tertiary-fixed text-2xl" style="font-variation-settings:'FILL' 1">auto_awesome</span>
|
| 510 |
+
</div>
|
| 511 |
+
</div>
|
| 512 |
+
<div class="text-center mb-16">
|
| 513 |
+
<h1 class="font-serif text-4xl md:text-5xl text-primary font-bold italic tracking-tight mb-4">The court is in deliberation...</h1>
|
| 514 |
+
<p class="font-sans text-primary/60 text-lg">Processing legal nuances and judicial precedents.</p>
|
| 515 |
+
</div>
|
| 516 |
+
<div class="w-full max-w-lg space-y-4" id="loading-steps">
|
| 517 |
+
<div class="loading-step" id="ls-1">
|
| 518 |
+
<div class="step-icon"><span class="material-symbols-outlined text-white text-lg">hourglass_empty</span></div>
|
| 519 |
+
<div class="step-content">
|
| 520 |
+
<div class="flex justify-between mb-2"><span class="font-sans font-medium text-primary">Reviewing submissions</span><span class="font-mono text-xs text-secondary font-bold" id="ls-1-pct">0%</span></div>
|
| 521 |
+
<div class="progress-bar"><div class="progress-fill" id="ls-1-bar" style="width:0%"></div></div>
|
| 522 |
+
</div>
|
| 523 |
+
</div>
|
| 524 |
+
<div class="loading-step" id="ls-2">
|
| 525 |
+
<div class="step-icon"><span class="material-symbols-outlined text-white text-lg">hourglass_empty</span></div>
|
| 526 |
+
<div class="step-content">
|
| 527 |
+
<div class="flex justify-between mb-2"><span class="font-sans font-medium text-primary">Analysing argument strength</span><span class="font-mono text-xs text-secondary font-bold" id="ls-2-pct">0%</span></div>
|
| 528 |
+
<div class="progress-bar"><div class="progress-fill" id="ls-2-bar" style="width:0%"></div></div>
|
| 529 |
+
</div>
|
| 530 |
+
</div>
|
| 531 |
+
<div class="loading-step" id="ls-3">
|
| 532 |
+
<div class="step-icon"><span class="material-symbols-outlined text-white text-lg">hourglass_empty</span></div>
|
| 533 |
+
<div class="step-content">
|
| 534 |
+
<div class="flex justify-between mb-2"><span class="font-sans font-medium text-primary">Identifying missed opportunities</span><span class="font-mono text-xs text-secondary font-bold" id="ls-3-pct">0%</span></div>
|
| 535 |
+
<div class="progress-bar"><div class="progress-fill" id="ls-3-bar" style="width:0%"></div></div>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
<div class="loading-step" id="ls-4">
|
| 539 |
+
<div class="step-icon"><span class="material-symbols-outlined text-white text-lg">hourglass_empty</span></div>
|
| 540 |
+
<div class="step-content">
|
| 541 |
+
<div class="flex justify-between mb-2"><span class="font-sans font-medium text-primary">Generating case analysis</span><span class="font-mono text-xs text-secondary font-bold" id="ls-4-pct">0%</span></div>
|
| 542 |
+
<div class="progress-bar"><div class="progress-fill" id="ls-4-bar" style="width:0%"></div></div>
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
</main>
|
| 547 |
+
</div>
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
<!-- ════════════��═════════════════════════════════════════════ -->
|
| 551 |
+
<!-- SCREEN: SESSION ANALYSIS -->
|
| 552 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 553 |
+
<div id="screen-analysis" class="screen">
|
| 554 |
+
<header class="fixed top-0 left-0 right-0 z-50 bg-[#F5F0E8]/90 backdrop-blur-md shadow-[0_8px_20px_rgba(0,0,0,0.08)]">
|
| 555 |
+
<nav class="flex justify-between items-center px-12 py-4">
|
| 556 |
+
<span class="font-serif text-2xl font-bold text-primary italic">Session Analysis</span>
|
| 557 |
+
<div class="flex gap-4">
|
| 558 |
+
<button onclick="showScreen('lobby')" class="clay-btn-ghost px-5 py-2 rounded-xl font-sans text-sm">← Back to Lobby</button>
|
| 559 |
+
<button onclick="showScreen('setup')" class="clay-btn-primary px-5 py-2 rounded-xl font-sans text-sm text-white">New Session</button>
|
| 560 |
+
</div>
|
| 561 |
+
</nav>
|
| 562 |
+
</header>
|
| 563 |
+
|
| 564 |
+
<main class="pt-24 pb-16 px-12 max-w-5xl mx-auto">
|
| 565 |
+
<!-- Outcome card -->
|
| 566 |
+
<div id="outcome-card" class="clay-card p-8 rounded-xl mb-8 flex items-center justify-between">
|
| 567 |
+
<div>
|
| 568 |
+
<p class="font-mono text-xs text-primary/40 uppercase tracking-widest mb-2">Session Outcome Prediction</p>
|
| 569 |
+
<h2 id="outcome-text" class="font-serif text-4xl font-bold text-primary mb-1">—</h2>
|
| 570 |
+
<p id="outcome-reasoning" class="text-primary/60 font-sans text-sm">Generating analysis...</p>
|
| 571 |
+
</div>
|
| 572 |
+
<div class="text-right">
|
| 573 |
+
<p class="font-mono text-xs text-primary/40 mb-1">Performance Score</p>
|
| 574 |
+
<p id="score-display" class="font-serif text-5xl font-bold text-secondary">—</p>
|
| 575 |
+
<p class="font-mono text-xs text-primary/40">out of 10</p>
|
| 576 |
+
</div>
|
| 577 |
+
</div>
|
| 578 |
+
|
| 579 |
+
<!-- Stats grid -->
|
| 580 |
+
<div class="grid grid-cols-4 gap-4 mb-8">
|
| 581 |
+
<div class="clay-card p-5 rounded-xl text-center">
|
| 582 |
+
<p id="stat-strong" class="font-serif text-3xl font-bold text-primary mb-1">0</p>
|
| 583 |
+
<p class="font-sans text-xs text-primary/60">Strong Arguments</p>
|
| 584 |
+
</div>
|
| 585 |
+
<div class="clay-card p-5 rounded-xl text-center">
|
| 586 |
+
<p id="stat-weak" class="font-serif text-3xl font-bold text-error mb-1">0</p>
|
| 587 |
+
<p class="font-sans text-xs text-primary/60">Weak Arguments</p>
|
| 588 |
+
</div>
|
| 589 |
+
<div class="clay-card p-5 rounded-xl text-center">
|
| 590 |
+
<p id="stat-traps" class="font-serif text-3xl font-bold text-secondary mb-1">0</p>
|
| 591 |
+
<p class="font-sans text-xs text-primary/60">Traps Detected</p>
|
| 592 |
+
</div>
|
| 593 |
+
<div class="clay-card p-5 rounded-xl text-center">
|
| 594 |
+
<p id="stat-concessions" class="font-serif text-3xl font-bold text-tertiary mb-1">0</p>
|
| 595 |
+
<p class="font-sans text-xs text-primary/60">Concessions Made</p>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
+
<!-- Tabs -->
|
| 600 |
+
<div class="flex gap-1 mb-6 bg-surface-container p-1 rounded-xl w-fit">
|
| 601 |
+
<button onclick="switchAnalysisTab('analysis')" id="tab-analysis" class="analysis-tab active px-5 py-2 rounded-lg font-sans text-sm font-medium">📊 Analysis</button>
|
| 602 |
+
<button onclick="switchAnalysisTab('transcript')" id="tab-transcript" class="analysis-tab px-5 py-2 rounded-lg font-sans text-sm font-medium">📜 Full Transcript</button>
|
| 603 |
+
<button onclick="switchAnalysisTab('documents')" id="tab-documents" class="analysis-tab px-5 py-2 rounded-lg font-sans text-sm font-medium">📄 Documents</button>
|
| 604 |
+
</div>
|
| 605 |
+
|
| 606 |
+
<!-- Analysis tab content -->
|
| 607 |
+
<div id="analysis-tab-content">
|
| 608 |
+
<div class="space-y-4" id="analysis-accordion">
|
| 609 |
+
<!-- Accordions injected by JS -->
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
|
| 613 |
+
<!-- Transcript tab content -->
|
| 614 |
+
<div id="transcript-tab-content" class="hidden">
|
| 615 |
+
<div class="clay-card p-8 rounded-xl">
|
| 616 |
+
<pre id="full-transcript-text" class="font-serif text-sm text-on-surface leading-relaxed whitespace-pre-wrap"></pre>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
<!-- Documents tab content -->
|
| 621 |
+
<div id="documents-tab-content" class="hidden">
|
| 622 |
+
<div id="analysis-documents-list" class="space-y-4">
|
| 623 |
+
<p class="text-primary/40 font-sans text-sm">No documents produced in this session.</p>
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
</main>
|
| 627 |
+
</div>
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 631 |
+
<!-- SCREEN: PAST SESSIONS -->
|
| 632 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 633 |
+
<div id="screen-sessions" class="screen">
|
| 634 |
+
<header class="fixed top-0 left-0 right-0 z-50 bg-[#F5F0E8]/90 backdrop-blur-md shadow-[0_8px_20px_rgba(0,0,0,0.08)]">
|
| 635 |
+
<nav class="flex justify-between items-center px-12 py-4">
|
| 636 |
+
<span class="font-serif text-2xl font-bold text-primary italic">Past Sessions</span>
|
| 637 |
+
<button onclick="showScreen('lobby')" class="clay-btn-ghost px-5 py-2 rounded-xl font-sans text-sm">← Back</button>
|
| 638 |
+
</nav>
|
| 639 |
+
</header>
|
| 640 |
+
<main class="pt-24 pb-16 px-12 max-w-4xl mx-auto">
|
| 641 |
+
<div id="all-sessions-list" class="space-y-4">
|
| 642 |
+
<div class="clay-card p-8 rounded-xl text-center text-primary/40 font-sans">Loading sessions...</div>
|
| 643 |
+
</div>
|
| 644 |
+
</main>
|
| 645 |
+
</div>
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 649 |
+
<!-- MODALS -->
|
| 650 |
+
<!-- ══════════════════════════════════════════════════════════ -->
|
| 651 |
+
|
| 652 |
+
<!-- Objection Modal -->
|
| 653 |
+
<div id="objection-modal" class="modal-overlay hidden">
|
| 654 |
+
<div class="clay-card w-full max-w-lg rounded-xl p-8 relative">
|
| 655 |
+
<button onclick="closeModal('objection-modal')" class="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-dim">
|
| 656 |
+
<span class="material-symbols-outlined text-sm">close</span>
|
| 657 |
+
</button>
|
| 658 |
+
<h2 class="font-serif text-2xl font-bold text-primary mb-2">Raise an Objection</h2>
|
| 659 |
+
<p class="text-primary/60 font-sans text-sm mb-6">Select the basis for your objection.</p>
|
| 660 |
+
<div class="space-y-3 mb-6" id="objection-types">
|
| 661 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 662 |
+
<input type="radio" name="obj-type" value="irrelevant" class="text-secondary"/>
|
| 663 |
+
<span class="font-sans font-medium">Irrelevant — no bearing on the issue</span>
|
| 664 |
+
</label>
|
| 665 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 666 |
+
<input type="radio" name="obj-type" value="misleading" class="text-secondary"/>
|
| 667 |
+
<span class="font-sans font-medium">Misleading — misrepresenting the law</span>
|
| 668 |
+
</label>
|
| 669 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 670 |
+
<input type="radio" name="obj-type" value="citation_error" class="text-secondary"/>
|
| 671 |
+
<span class="font-sans font-medium">Citation error — case does not support the proposition</span>
|
| 672 |
+
</label>
|
| 673 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 674 |
+
<input type="radio" name="obj-type" value="custom" class="text-secondary"/>
|
| 675 |
+
<span class="font-sans font-medium">Other (specify below)</span>
|
| 676 |
+
</label>
|
| 677 |
+
<input type="text" id="custom-objection" placeholder="Describe your objection..." class="clay-input w-full px-4 py-3 rounded-xl font-sans text-sm"/>
|
| 678 |
+
</div>
|
| 679 |
+
<button onclick="submitObjection()" class="clay-btn-primary w-full py-4 rounded-xl font-sans font-bold text-white">Raise Objection</button>
|
| 680 |
+
</div>
|
| 681 |
+
</div>
|
| 682 |
+
|
| 683 |
+
<!-- Document Modal -->
|
| 684 |
+
<div id="document-modal" class="modal-overlay hidden">
|
| 685 |
+
<div class="clay-card w-full max-w-xl rounded-xl p-8 relative">
|
| 686 |
+
<button onclick="closeModal('document-modal')" class="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-dim">
|
| 687 |
+
<span class="material-symbols-outlined text-sm">close</span>
|
| 688 |
+
</button>
|
| 689 |
+
<h2 class="font-serif text-2xl font-bold text-primary mb-2">Produce Legal Document</h2>
|
| 690 |
+
<p class="text-primary/60 font-sans text-sm mb-6">Select the template and purpose.</p>
|
| 691 |
+
<div class="grid grid-cols-2 gap-3 mb-6">
|
| 692 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 693 |
+
<input type="radio" name="doc-type" value="Written Submissions" checked class="text-secondary"/>
|
| 694 |
+
<span class="font-sans font-medium text-sm">Written Submissions</span>
|
| 695 |
+
</label>
|
| 696 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 697 |
+
<input type="radio" name="doc-type" value="Legal Notice" class="text-secondary"/>
|
| 698 |
+
<span class="font-sans font-medium text-sm">Legal Notice</span>
|
| 699 |
+
</label>
|
| 700 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 701 |
+
<input type="radio" name="doc-type" value="Affidavit" class="text-secondary"/>
|
| 702 |
+
<span class="font-sans font-medium text-sm">Affidavit</span>
|
| 703 |
+
</label>
|
| 704 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer">
|
| 705 |
+
<input type="radio" name="doc-type" value="Bail Application" class="text-secondary"/>
|
| 706 |
+
<span class="font-sans font-medium text-sm">Bail Application</span>
|
| 707 |
+
</label>
|
| 708 |
+
<label class="flex items-center gap-3 p-4 rounded-xl clay-inset cursor-pointer col-span-2">
|
| 709 |
+
<input type="radio" name="doc-type" value="Writ Petition" class="text-secondary"/>
|
| 710 |
+
<span class="font-sans font-medium text-sm">Writ Petition</span>
|
| 711 |
+
</label>
|
| 712 |
+
</div>
|
| 713 |
+
<div class="mb-6">
|
| 714 |
+
<label class="font-serif text-sm font-semibold text-primary block mb-3">Filed by:</label>
|
| 715 |
+
<div class="flex gap-4">
|
| 716 |
+
<label class="flex items-center gap-2 cursor-pointer">
|
| 717 |
+
<input type="radio" name="doc-side" value="yours" checked class="text-secondary"/>
|
| 718 |
+
<span class="font-sans text-sm">Your side</span>
|
| 719 |
+
</label>
|
| 720 |
+
<label class="flex items-center gap-2 cursor-pointer">
|
| 721 |
+
<input type="radio" name="doc-side" value="opposing" class="text-secondary"/>
|
| 722 |
+
<span class="font-sans text-sm">Opposing side</span>
|
| 723 |
+
</label>
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
<button onclick="submitDocumentRequest()" class="clay-btn-primary w-full py-4 rounded-xl font-sans font-bold text-white flex items-center justify-center gap-3">
|
| 727 |
+
<span class="material-symbols-outlined" style="font-variation-settings:'FILL' 1">auto_awesome</span>
|
| 728 |
+
Generate Document
|
| 729 |
+
</button>
|
| 730 |
+
</div>
|
| 731 |
+
</div>
|
| 732 |
+
|
| 733 |
+
<!-- Document Viewer Modal -->
|
| 734 |
+
<div id="document-viewer-modal" class="modal-overlay hidden">
|
| 735 |
+
<div class="clay-card w-full max-w-3xl rounded-xl overflow-hidden">
|
| 736 |
+
<div class="flex justify-between items-center p-6 border-b border-outline-variant/20">
|
| 737 |
+
<div>
|
| 738 |
+
<h3 id="doc-viewer-title" class="font-serif text-xl font-bold text-primary">Document</h3>
|
| 739 |
+
<p id="doc-viewer-meta" class="font-mono text-xs text-primary/40">Filed by Counsel</p>
|
| 740 |
+
</div>
|
| 741 |
+
<div class="flex gap-3">
|
| 742 |
+
<button onclick="copyDocument()" class="clay-btn-ghost px-4 py-2 rounded-xl font-sans text-sm">Copy</button>
|
| 743 |
+
<button onclick="closeModal('document-viewer-modal')" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-dim">
|
| 744 |
+
<span class="material-symbols-outlined text-sm">close</span>
|
| 745 |
+
</button>
|
| 746 |
+
</div>
|
| 747 |
+
</div>
|
| 748 |
+
<div class="p-8 max-h-[70vh] overflow-y-auto">
|
| 749 |
+
<pre id="doc-viewer-content" class="font-serif text-sm leading-relaxed whitespace-pre-wrap text-on-surface"></pre>
|
| 750 |
+
</div>
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
|
| 754 |
+
<script src="/static/court/court.js"></script>
|
| 755 |
+
</body>
|
| 756 |
+
</html>
|
frontend/court/court.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ════════════════════════════════════════════════════════════
|
| 2 |
+
// GLOBAL STATE
|
| 3 |
+
// ════════════════════════════════════════════════════════════
|
| 4 |
+
|
| 5 |
+
const state = {
|
| 6 |
+
currentSession: null,
|
| 7 |
+
currentScreen: 'lobby',
|
| 8 |
+
setupStep: 1,
|
| 9 |
+
setupData: {
|
| 10 |
+
side: null,
|
| 11 |
+
title: '',
|
| 12 |
+
userClient: '',
|
| 13 |
+
opposingParty: '',
|
| 14 |
+
issues: [],
|
| 15 |
+
facts: '',
|
| 16 |
+
jurisdiction: 'supreme_court',
|
| 17 |
+
bench: 'division',
|
| 18 |
+
difficulty: 'standard',
|
| 19 |
+
Length: 'standard',
|
| 20 |
+
trapWarnings: true,
|
| 21 |
+
},
|
| 22 |
+
currentRound: 0,
|
| 23 |
+
transcript: [],
|
| 24 |
+
concessions: [],
|
| 25 |
+
documents: [],
|
| 26 |
+
traps: [],
|
| 27 |
+
isWaitingForResponse: false,
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
// ════════════════════════════════════════════════════════════
|
| 31 |
+
// INITIALIZATION
|
| 32 |
+
// ════════════════════════════════════════════════════════════
|
| 33 |
+
|
| 34 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 35 |
+
loadRecentSessions();
|
| 36 |
+
updateLobbyTime();
|
| 37 |
+
setInterval(updateLobbyTime, 1000);
|
| 38 |
+
document.getElementById('argument-input').addEventListener('input', updateWordCount);
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
// ════════════════════════════════════════════════════════════
|
| 42 |
+
// SCREEN MANAGEMENT
|
| 43 |
+
// ════════════════════════════════════════════════════════════
|
| 44 |
+
|
| 45 |
+
function showScreen(screenId) {
|
| 46 |
+
// Hide all screens
|
| 47 |
+
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
| 48 |
+
|
| 49 |
+
// Show target screen
|
| 50 |
+
const targetScreen = document.getElementById(`screen-${screenId}`);
|
| 51 |
+
if (targetScreen) {
|
| 52 |
+
targetScreen.classList.add('active');
|
| 53 |
+
state.currentScreen = screenId;
|
| 54 |
+
|
| 55 |
+
// Screen-specific initialization
|
| 56 |
+
if (screenId === 'lobby') {
|
| 57 |
+
loadRecentSessions();
|
| 58 |
+
} else if (screenId === 'courtroom') {
|
| 59 |
+
initializeCourtroom();
|
| 60 |
+
} else if (screenId === 'analysis') {
|
| 61 |
+
renderAnalysis();
|
| 62 |
+
} else if (screenId === 'sessions') {
|
| 63 |
+
loadAllSessions();
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// ════════════════════════════════════════════════════════════
|
| 69 |
+
// SETUP WIZARD
|
| 70 |
+
// ════════════════════════════════════════════════════════════
|
| 71 |
+
|
| 72 |
+
function selectSide(side) {
|
| 73 |
+
state.setupData.side = side;
|
| 74 |
+
document.querySelectorAll('.side-card').forEach(c => c.classList.remove('selected'));
|
| 75 |
+
document.getElementById(`side-${side}`).classList.add('selected');
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function selectOption(category, value) {
|
| 79 |
+
if (category === 'bench') state.setupData.bench = value;
|
| 80 |
+
if (category === 'diff') state.setupData.difficulty = value;
|
| 81 |
+
if (category === 'len') state.setupData.Length = value;
|
| 82 |
+
|
| 83 |
+
document.querySelectorAll(`[id^="${category}-"]`).forEach(c => c.classList.remove('selected'));
|
| 84 |
+
document.getElementById(`${category}-${value}`).classList.add('selected');
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function addIssue() {
|
| 88 |
+
const input = document.getElementById('issue-input');
|
| 89 |
+
const issue = input.value.trim();
|
| 90 |
+
if (issue) {
|
| 91 |
+
state.setupData.issues.push(issue);
|
| 92 |
+
input.value = '';
|
| 93 |
+
renderIssues();
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function renderIssues() {
|
| 98 |
+
const list = document.getElementById('issues-list');
|
| 99 |
+
list.innerHTML = state.setupData.issues.map(issue =>
|
| 100 |
+
`<span class="px-3 py-1 bg-secondary-fixed text-secondary rounded-full text-sm font-sans flex items-center gap-2">
|
| 101 |
+
${issue}
|
| 102 |
+
<button onclick="removeIssue('${issue}')" class="cursor-pointer">✕</button>
|
| 103 |
+
</span>`
|
| 104 |
+
).join('');
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function removeIssue(issue) {
|
| 108 |
+
state.setupData.issues = state.setupData.issues.filter(i => i !== issue);
|
| 109 |
+
renderIssues();
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function goToStep(step) {
|
| 113 |
+
// Validate current step
|
| 114 |
+
if (state.setupStep === 1 && !state.setupData.side) {
|
| 115 |
+
alert('Please select your side');
|
| 116 |
+
return;
|
| 117 |
+
}
|
| 118 |
+
if (state.setupStep === 2) {
|
| 119 |
+
if (!document.getElementById('case-title').value.trim()) {
|
| 120 |
+
alert('Please enter case title');
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
state.setupData.title = document.getElementById('case-title').value;
|
| 124 |
+
state.setupData.userClient = document.getElementById('user-client').value;
|
| 125 |
+
state.setupData.opposingParty = document.getElementById('opposing-party').value;
|
| 126 |
+
state.setupData.facts = document.getElementById('brief-facts').value;
|
| 127 |
+
state.setupData.jurisdiction = document.getElementById('jurisdiction').value;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Update steps
|
| 131 |
+
state.setupStep = step;
|
| 132 |
+
document.querySelectorAll('.setup-step').forEach(s => s.classList.add('hidden'));
|
| 133 |
+
document.getElementById(`setup-step-${step}`).classList.remove('hidden');
|
| 134 |
+
|
| 135 |
+
// Update indicators
|
| 136 |
+
document.querySelectorAll('.step-indicator').forEach((ind, i) => {
|
| 137 |
+
ind.classList.toggle('active', i + 1 <= step);
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function enterCourtroom() {
|
| 142 |
+
state.setupData.trapWarnings = document.getElementById('trap-warnings').checked;
|
| 143 |
+
createNewSession();
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// ════════════════════════════════════════════════════════════
|
| 147 |
+
// API INTEGRATION
|
| 148 |
+
// ════════════════════════════════════════════════════════════
|
| 149 |
+
|
| 150 |
+
async function createNewSession() {
|
| 151 |
+
try {
|
| 152 |
+
const briefingPhase = {
|
| 153 |
+
round: 0,
|
| 154 |
+
parties: {
|
| 155 |
+
user: { side: state.setupData.side, client: state.setupData.userClient },
|
| 156 |
+
opposing: { client: state.setupData.opposingParty }
|
| 157 |
+
},
|
| 158 |
+
legalIssues: state.setupData.issues,
|
| 159 |
+
jurisdiction: state.setupData.jurisdiction,
|
| 160 |
+
facts: state.setupData.facts
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
const requestBody = {
|
| 164 |
+
case_title: state.setupData.title,
|
| 165 |
+
jurisdiction: state.setupData.jurisdiction,
|
| 166 |
+
user_side: state.setupData.side,
|
| 167 |
+
user_client: state.setupData.userClient,
|
| 168 |
+
opposing_party: state.setupData.opposingParty,
|
| 169 |
+
legal_issues: state.setupData.issues,
|
| 170 |
+
case_facts: state.setupData.facts,
|
| 171 |
+
difficulty: state.setupData.difficulty,
|
| 172 |
+
bench_type: state.setupData.bench,
|
| 173 |
+
max_rounds: state.setupData.Length === 'brief' ? 3 : state.setupData.Length === 'extended' ? 8 : 5
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
const response = await fetch('/court/new', {
|
| 177 |
+
method: 'POST',
|
| 178 |
+
headers: { 'Content-Type': 'application/json' },
|
| 179 |
+
body: JSON.stringify(requestBody)
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
if (!response.ok) throw new Error('Failed to create session');
|
| 183 |
+
|
| 184 |
+
const data = await response.json();
|
| 185 |
+
state.currentSession = data.session_id;
|
| 186 |
+
|
| 187 |
+
// Initialize courtroom with opening statement
|
| 188 |
+
showScreen('courtroom');
|
| 189 |
+
await loadSessionData();
|
| 190 |
+
|
| 191 |
+
// Add opening statement to transcript
|
| 192 |
+
setTimeout(() => {
|
| 193 |
+
addTranscriptEntry('registrar', 'The court is now in session. ' + data.opening_statement || 'Proceedings begin.');
|
| 194 |
+
}, 300);
|
| 195 |
+
|
| 196 |
+
} catch (error) {
|
| 197 |
+
console.error('Error creating session:', error);
|
| 198 |
+
alert('Failed to create session. Please try again.');
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
async function loadSessionData() {
|
| 203 |
+
try {
|
| 204 |
+
const response = await fetch(`/court/session/${state.currentSession}`);
|
| 205 |
+
const data = await response.json();
|
| 206 |
+
|
| 207 |
+
// Update UI with session info
|
| 208 |
+
document.getElementById('court-case-title').textContent = data.case_title;
|
| 209 |
+
document.getElementById('panel-case-title').textContent = data.case_title;
|
| 210 |
+
document.getElementById('panel-issues').innerHTML = data.legal_issues
|
| 211 |
+
.map(issue => `<span class="text-xs px-2 py-1 bg-primary-container text-on-primary-container rounded-full">${issue}</span>`)
|
| 212 |
+
.join('');
|
| 213 |
+
|
| 214 |
+
// Current bench
|
| 215 |
+
const benchLabel = data.bench_type === 'single' ? 'Single Judge' :
|
| 216 |
+
data.bench_type === 'division' ? 'Division Bench' : 'Constitutional Bench';
|
| 217 |
+
document.getElementById('bench-label').textContent = benchLabel;
|
| 218 |
+
|
| 219 |
+
// User side badge
|
| 220 |
+
const sideText = data.user_side === 'petitioner' ? 'Petitioner' : 'Respondent';
|
| 221 |
+
document.getElementById('user-side-badge').textContent = `You: ${sideText}`;
|
| 222 |
+
|
| 223 |
+
} catch (error) {
|
| 224 |
+
console.error('Error loading session:', error);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
async function submitArgument() {
|
| 229 |
+
const textarea = document.getElementById('argument-input');
|
| 230 |
+
const argument = textarea.value.trim();
|
| 231 |
+
|
| 232 |
+
if (!argument) return;
|
| 233 |
+
if (state.isWaitingForResponse) return;
|
| 234 |
+
|
| 235 |
+
// Add user's argument to transcript
|
| 236 |
+
addTranscriptEntry('user', argument);
|
| 237 |
+
|
| 238 |
+
textarea.value = '';
|
| 239 |
+
updateWordCount();
|
| 240 |
+
state.isWaitingForResponse = true;
|
| 241 |
+
|
| 242 |
+
try {
|
| 243 |
+
// Show loading screen
|
| 244 |
+
showScreen('loading');
|
| 245 |
+
finishLoadingAnimation();
|
| 246 |
+
|
| 247 |
+
const response = await fetch('/court/argue', {
|
| 248 |
+
method: 'POST',
|
| 249 |
+
headers: { 'Content-Type': 'application/json' },
|
| 250 |
+
body: JSON.stringify({
|
| 251 |
+
session_id: state.currentSession,
|
| 252 |
+
user_argument: argument
|
| 253 |
+
})
|
| 254 |
+
});
|
| 255 |
+
|
| 256 |
+
if (!response.ok) throw new Error('Argument submission failed');
|
| 257 |
+
|
| 258 |
+
const data = await response.json();
|
| 259 |
+
|
| 260 |
+
// Process responses
|
| 261 |
+
if (data.trap_detected && state.setupData.trapWarnings) {
|
| 262 |
+
showTrapWarning(data.trap_description);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Add responses to transcript
|
| 266 |
+
if (data.opposing_response) {
|
| 267 |
+
setTimeout(() => addTranscriptEntry('opposing', data.opposing_response), 500);
|
| 268 |
+
}
|
| 269 |
+
if (data.judge_question) {
|
| 270 |
+
setTimeout(() => addTranscriptEntry('judge', data.judge_question), 1000);
|
| 271 |
+
}
|
| 272 |
+
if (data.registrar_note) {
|
| 273 |
+
setTimeout(() => addTranscriptEntry('registrar', data.registrar_note), 1500);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// Update metrics
|
| 277 |
+
if (data.concessions) {
|
| 278 |
+
data.concessions.forEach(c => {
|
| 279 |
+
if (!state.concessions.includes(c)) {
|
| 280 |
+
state.concessions.push(c);
|
| 281 |
+
addConcession(c);
|
| 282 |
+
}
|
| 283 |
+
});
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// Update round
|
| 287 |
+
state.currentRound = data.round;
|
| 288 |
+
updateRoundIndicator();
|
| 289 |
+
|
| 290 |
+
// Check if session should end
|
| 291 |
+
if (data.session_ended) {
|
| 292 |
+
setTimeout(() => {
|
| 293 |
+
showScreen('loading');
|
| 294 |
+
finishLoadingAnimation();
|
| 295 |
+
state.currentSession = data.session_id;
|
| 296 |
+
showScreen('analysis');
|
| 297 |
+
}, 2500);
|
| 298 |
+
} else {
|
| 299 |
+
setTimeout(() => showScreen('courtroom'), 2500);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
} catch (error) {
|
| 303 |
+
console.error('Error submitting argument:', error);
|
| 304 |
+
alert('Error submitting argument. Please try again.');
|
| 305 |
+
state.isWaitingForResponse = false;
|
| 306 |
+
showScreen('courtroom');
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
async function submitObjection() {
|
| 311 |
+
const selectedType = document.querySelector('input[name="obj-type"]:checked');
|
| 312 |
+
if (!selectedType) return;
|
| 313 |
+
|
| 314 |
+
const objectionType = selectedType.value;
|
| 315 |
+
let objectionText = objectionType;
|
| 316 |
+
|
| 317 |
+
if (objectionType === 'custom') {
|
| 318 |
+
objectionText = document.getElementById('custom-objection').value.trim();
|
| 319 |
+
if (!objectionText) return;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
try {
|
| 323 |
+
const response = await fetch('/court/object', {
|
| 324 |
+
method: 'POST',
|
| 325 |
+
headers: { 'Content-Type': 'application/json' },
|
| 326 |
+
body: JSON.stringify({
|
| 327 |
+
session_id: state.currentSession,
|
| 328 |
+
objection_type: objectionType,
|
| 329 |
+
objection_text: objectionText
|
| 330 |
+
})
|
| 331 |
+
});
|
| 332 |
+
|
| 333 |
+
if (response.ok) {
|
| 334 |
+
const data = await response.json();
|
| 335 |
+
addTranscriptEntry('user', `[OBJECTION: ${objectionText}]`);
|
| 336 |
+
if (data.judge_response) {
|
| 337 |
+
addTranscriptEntry('judge', data.judge_response);
|
| 338 |
+
}
|
| 339 |
+
closeModal('objection-modal');
|
| 340 |
+
}
|
| 341 |
+
} catch (error) {
|
| 342 |
+
console.error('Objection error:', error);
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
async function submitDocumentRequest() {
|
| 347 |
+
const docType = document.querySelector('input[name="doc-type"]:checked').value;
|
| 348 |
+
const docSide = document.querySelector('input[name="doc-side"]:checked').value;
|
| 349 |
+
|
| 350 |
+
try {
|
| 351 |
+
state.isWaitingForResponse = true;
|
| 352 |
+
const response = await fetch('/court/document', {
|
| 353 |
+
method: 'POST',
|
| 354 |
+
headers: { 'Content-Type': 'application/json' },
|
| 355 |
+
body: JSON.stringify({
|
| 356 |
+
session_id: state.currentSession,
|
| 357 |
+
document_type: docType,
|
| 358 |
+
filed_by: docSide
|
| 359 |
+
})
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
if (response.ok) {
|
| 363 |
+
const data = await response.json();
|
| 364 |
+
state.documents.push({
|
| 365 |
+
id: state.documents.length,
|
| 366 |
+
title: docType,
|
| 367 |
+
content: data.document_content,
|
| 368 |
+
filedBy: docSide,
|
| 369 |
+
timestamp: new Date()
|
| 370 |
+
});
|
| 371 |
+
|
| 372 |
+
addTranscriptEntry('user', `[DOCUMENT PRODUCED: ${docType}]`);
|
| 373 |
+
renderDocumentsList();
|
| 374 |
+
closeModal('document-modal');
|
| 375 |
+
}
|
| 376 |
+
} catch (error) {
|
| 377 |
+
console.error('Document error:', error);
|
| 378 |
+
} finally {
|
| 379 |
+
state.isWaitingForResponse = false;
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// ════════════════════════════════════════════════════════════
|
| 384 |
+
// TRANSCRIPT & UI UPDATES
|
| 385 |
+
// ════════════════════════════════════════════════════════════
|
| 386 |
+
|
| 387 |
+
function addTranscriptEntry(speaker, text) {
|
| 388 |
+
const entry = {
|
| 389 |
+
speaker,
|
| 390 |
+
text,
|
| 391 |
+
timestamp: new Date(),
|
| 392 |
+
round: state.currentRound
|
| 393 |
+
};
|
| 394 |
+
state.transcript.push(entry);
|
| 395 |
+
|
| 396 |
+
const container = document.getElementById('transcript');
|
| 397 |
+
const entryEl = document.createElement('div');
|
| 398 |
+
entryEl.className = 'transcript-entry';
|
| 399 |
+
|
| 400 |
+
const speakerClass = `speaker-${speaker}`;
|
| 401 |
+
const speakerLabel = {
|
| 402 |
+
'judge': 'HON\'BLE BENCH',
|
| 403 |
+
'opposing': 'OPPOSING COUNSEL',
|
| 404 |
+
'user': 'YOUR SUBMISSION',
|
| 405 |
+
'registrar': 'REGISTRAR'
|
| 406 |
+
}[speaker] || speaker.toUpperCase();
|
| 407 |
+
|
| 408 |
+
entryEl.innerHTML = `
|
| 409 |
+
<div>
|
| 410 |
+
<div class="speaker-pill ${speakerClass}">${speakerLabel}</div>
|
| 411 |
+
</div>
|
| 412 |
+
<div class="transcript-text">${text}</div>
|
| 413 |
+
`;
|
| 414 |
+
|
| 415 |
+
container.appendChild(entryEl);
|
| 416 |
+
container.parentElement.scrollTop = container.parentElement.scrollHeight;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
function addConcession(text) {
|
| 420 |
+
const list = document.getElementById('concessions-list');
|
| 421 |
+
if (list.textContent.includes('No concessions')) {
|
| 422 |
+
list.innerHTML = '';
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
const item = document.createElement('div');
|
| 426 |
+
item.className = 'concession-item';
|
| 427 |
+
item.innerHTML = `✓ ${text}`;
|
| 428 |
+
list.appendChild(item);
|
| 429 |
+
|
| 430 |
+
const count = state.concessions.length;
|
| 431 |
+
document.getElementById('concession-count').textContent = count;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
function updateWordCount() {
|
| 435 |
+
const textarea = document.getElementById('argument-input');
|
| 436 |
+
const count = textarea.value.length;
|
| 437 |
+
document.getElementById('word-count').textContent = `${count} / 500`;
|
| 438 |
+
|
| 439 |
+
if (count > 500) {
|
| 440 |
+
textarea.value = textarea.value.substring(0, 500);
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
function updateRoundIndicator() {
|
| 445 |
+
document.getElementById('round-indicator').innerHTML = `
|
| 446 |
+
<span class="material-symbols-outlined text-secondary text-sm">timer</span>
|
| 447 |
+
<span class="font-mono text-primary text-sm font-bold">Round ${state.currentRound}</span>
|
| 448 |
+
`;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
function showTrapWarning(description) {
|
| 452 |
+
const banner = document.getElementById('trap-banner');
|
| 453 |
+
document.getElementById('trap-warning-text').textContent = description || 'Opposing counsel\'s last statement may contain a trap. Read carefully.';
|
| 454 |
+
banner.classList.remove('hidden');
|
| 455 |
+
|
| 456 |
+
setTimeout(() => {
|
| 457 |
+
banner.classList.add('hidden');
|
| 458 |
+
}, 8000);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function dismissTrap() {
|
| 462 |
+
document.getElementById('trap-banner').classList.add('hidden');
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// ════════════════════════════════════════════════════════════
|
| 466 |
+
// COURTROOM INITIALIZATION
|
| 467 |
+
// ════════════════════════════════════════════════════════════
|
| 468 |
+
|
| 469 |
+
function initializeCourtroom() {
|
| 470 |
+
renderDocumentsList();
|
| 471 |
+
updatePhaseHint();
|
| 472 |
+
if (state.transcript.length === 0) {
|
| 473 |
+
addTranscriptEntry('registrar', 'The court is now in session. Opening submissions begin. State your argument when ready.');
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function updatePhaseHint() {
|
| 478 |
+
const hints = {
|
| 479 |
+
0: 'Round 1 — Opening submissions',
|
| 480 |
+
1: 'Round 2 — Main arguments',
|
| 481 |
+
2: 'Round 3 — Counter-arguments',
|
| 482 |
+
3: 'Round 4 — Rebuttal',
|
| 483 |
+
4: 'Round 5 — Final submissions'
|
| 484 |
+
};
|
| 485 |
+
const maxRounds = state.setupData.Length === 'brief' ? 3 : state.setupData.Length === 'extended' ? 8 : 5;
|
| 486 |
+
const hint = hints[state.currentRound] || `Round ${state.currentRound + 1} of ${maxRounds}`;
|
| 487 |
+
document.getElementById('phase-hint').textContent = hint;
|
| 488 |
+
document.getElementById('input-label').textContent =
|
| 489 |
+
state.currentRound === 0 ? 'YOUR OPENING STATEMENT' :
|
| 490 |
+
state.currentRound === maxRounds - 1 ? 'YOUR CLOSING SUBMISSIONS' :
|
| 491 |
+
'YOUR ARGUMENT';
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
function renderDocumentsList() {
|
| 495 |
+
const list = document.getElementById('documents-list');
|
| 496 |
+
if (state.documents.length === 0) {
|
| 497 |
+
list.innerHTML = '<p class="text-primary/40 italic font-sans text-xs">None produced.</p>';
|
| 498 |
+
} else {
|
| 499 |
+
list.innerHTML = state.documents.map(doc => `
|
| 500 |
+
<div onclick="viewDocument(${doc.id})" class="p-3 bg-white border border-outline-variant rounded-lg cursor-pointer hover:border-secondary text-xs">
|
| 501 |
+
<p class="font-sans font-bold text-primary">${doc.title}</p>
|
| 502 |
+
<p class="text-primary/50 text-[10px]">Filed by: ${doc.filedBy}</p>
|
| 503 |
+
</div>
|
| 504 |
+
`).join('');
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
// ════════════════════════════════════════════════════════════
|
| 509 |
+
// MODAL MANAGEMENT
|
| 510 |
+
// ════════════════════════════════════════════════════════════
|
| 511 |
+
|
| 512 |
+
function openObjectionModal() {
|
| 513 |
+
document.getElementById('objection-modal').classList.remove('hidden');
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
function openDocumentModal() {
|
| 517 |
+
document.getElementById('document-modal').classList.remove('hidden');
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function viewDocument(id) {
|
| 521 |
+
const doc = state.documents.find(d => d.id === id);
|
| 522 |
+
if (!doc) return;
|
| 523 |
+
|
| 524 |
+
document.getElementById('doc-viewer-title').textContent = doc.title;
|
| 525 |
+
document.getElementById('doc-viewer-meta').textContent = `Filed by: ${doc.filedBy === 'yours' ? 'Your Counsel' : 'Opposing Counsel'} — ${doc.timestamp.toLocaleString()}`;
|
| 526 |
+
document.getElementById('doc-viewer-content').textContent = doc.content;
|
| 527 |
+
document.getElementById('document-viewer-modal').classList.remove('hidden');
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
function closeModal(modalId) {
|
| 531 |
+
document.getElementById(modalId).classList.add('hidden');
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
function copyDocument() {
|
| 535 |
+
const text = document.getElementById('doc-viewer-content').textContent;
|
| 536 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 537 |
+
alert('Document copied to clipboard');
|
| 538 |
+
});
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
// ════════════════════════════════════════════════════════════
|
| 542 |
+
// ANALYSIS SCREEN
|
| 543 |
+
// ════════════════════════════════════════════════════════════
|
| 544 |
+
|
| 545 |
+
async function renderAnalysis() {
|
| 546 |
+
try {
|
| 547 |
+
const response = await fetch(`/court/session/${state.currentSession}`);
|
| 548 |
+
const sessionData = await response.json();
|
| 549 |
+
|
| 550 |
+
// Parse analysis
|
| 551 |
+
const analysis = sessionData.analysis || {};
|
| 552 |
+
|
| 553 |
+
// Outcome and score
|
| 554 |
+
document.getElementById('outcome-text').textContent = analysis.outcome || 'ANALYSIS PENDING';
|
| 555 |
+
document.getElementById('outcome-reasoning').textContent = analysis.outcome_reasoning || 'Processing...';
|
| 556 |
+
document.getElementById('score-display').textContent = analysis.score || '—';
|
| 557 |
+
|
| 558 |
+
// Stats
|
| 559 |
+
document.getElementById('stat-strong').textContent = analysis.strong_arguments_count || 0;
|
| 560 |
+
document.getElementById('stat-weak').textContent = analysis.weak_arguments_count || 0;
|
| 561 |
+
document.getElementById('stat-traps').textContent = analysis.traps_detected_count || 0;
|
| 562 |
+
document.getElementById('stat-concessions').textContent = state.concessions.length;
|
| 563 |
+
|
| 564 |
+
// Accordion sections
|
| 565 |
+
const accordion = document.getElementById('analysis-accordion');
|
| 566 |
+
accordion.innerHTML = '';
|
| 567 |
+
|
| 568 |
+
const sections = analysis.sections || [
|
| 569 |
+
{ title: 'Argument Quality', content: 'Analysis generating...' },
|
| 570 |
+
{ title: 'Judge Perspective', content: 'Processing...' },
|
| 571 |
+
{ title: 'Weaknesses Identified', content: 'Analyzing...' }
|
| 572 |
+
];
|
| 573 |
+
|
| 574 |
+
sections.forEach((section, idx) => {
|
| 575 |
+
const item = document.createElement('div');
|
| 576 |
+
item.className = 'accordion-item' + (idx === 0 ? ' open' : '');
|
| 577 |
+
item.innerHTML = `
|
| 578 |
+
<div class="accordion-header" onclick="toggleAccordion(this)">
|
| 579 |
+
<div class="accordion-title">${section.title}</div>
|
| 580 |
+
<span class="material-symbols-outlined accordion-icon">expand_more</span>
|
| 581 |
+
</div>
|
| 582 |
+
<div class="accordion-content">${section.content || 'No content available.'}</div>
|
| 583 |
+
`;
|
| 584 |
+
accordion.appendChild(item);
|
| 585 |
+
});
|
| 586 |
+
|
| 587 |
+
// Full transcript
|
| 588 |
+
document.getElementById('full-transcript-text').textContent = state.transcript
|
| 589 |
+
.map(e => `[${e.speaker.toUpperCase()}]\n${e.text}\n`)
|
| 590 |
+
.join('\n');
|
| 591 |
+
|
| 592 |
+
// Documents
|
| 593 |
+
const docList = document.getElementById('analysis-documents-list');
|
| 594 |
+
if (state.documents.length === 0) {
|
| 595 |
+
docList.innerHTML = '<p class="text-primary/40 font-sans text-sm">No documents produced during this session.</p>';
|
| 596 |
+
} else {
|
| 597 |
+
docList.innerHTML = state.documents.map(doc => `
|
| 598 |
+
<div class="clay-card p-5 rounded-xl cursor-pointer" onclick="viewDocument(${doc.id})">
|
| 599 |
+
<h3 class="font-serif text-lg font-bold text-primary">${doc.title}</h3>
|
| 600 |
+
<p class="text-primary/60 font-sans text-xs">Filed by ${doc.filedBy === 'yours' ? 'your counsel' : 'opposing counsel'}</p>
|
| 601 |
+
</div>
|
| 602 |
+
`).join('');
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
} catch (error) {
|
| 606 |
+
console.error('Error rendering analysis:', error);
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
function switchAnalysisTab(tab) {
|
| 611 |
+
// Hide all
|
| 612 |
+
document.getElementById('analysis-tab-content').style.display = 'none';
|
| 613 |
+
document.getElementById('transcript-tab-content').style.display = 'none';
|
| 614 |
+
document.getElementById('documents-tab-content').style.display = 'none';
|
| 615 |
+
|
| 616 |
+
// Show target
|
| 617 |
+
if (tab === 'analysis') {
|
| 618 |
+
document.getElementById('analysis-tab-content').style.display = 'block';
|
| 619 |
+
} else if (tab === 'transcript') {
|
| 620 |
+
document.getElementById('transcript-tab-content').style.display = 'block';
|
| 621 |
+
} else if (tab === 'documents') {
|
| 622 |
+
document.getElementById('documents-tab-content').style.display = 'block';
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// Update tab styling
|
| 626 |
+
document.querySelectorAll('.analysis-tab').forEach(t => t.classList.remove('active'));
|
| 627 |
+
document.getElementById(`tab-${tab}`).classList.add('active');
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
function toggleAccordion(header) {
|
| 631 |
+
const item = header.parentElement;
|
| 632 |
+
item.classList.toggle('open');
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
// ════════════════════════════════════════════════════════════
|
| 636 |
+
// SESSIONS MANAGEMENT
|
| 637 |
+
// ════════════════════════════════════════════════════════════
|
| 638 |
+
|
| 639 |
+
async function loadRecentSessions() {
|
| 640 |
+
try {
|
| 641 |
+
const response = await fetch('/court/sessions');
|
| 642 |
+
const sessions = await response.json();
|
| 643 |
+
|
| 644 |
+
const list = document.getElementById('recent-sessions-list');
|
| 645 |
+
if (!sessions || sessions.length === 0) {
|
| 646 |
+
list.innerHTML = '<div class="clay-card p-6 rounded-xl text-center text-primary/40 text-sm font-sans">No sessions yet.</div>';
|
| 647 |
+
return;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
list.innerHTML = sessions.slice(0, 3).map(session => `
|
| 651 |
+
<div class="clay-card p-5 rounded-xl session-card" onclick="loadSessionForReview('${session.id}')">
|
| 652 |
+
<h3 class="font-serif text-lg font-bold text-primary truncate">${session.case_title}</h3>
|
| 653 |
+
<p class="text-primary/60 font-sans text-xs">${session.user_side === 'petitioner' ? 'Petitioner' : 'Respondent'}</p>
|
| 654 |
+
<div class="session-meta">
|
| 655 |
+
<div class="meta-item">
|
| 656 |
+
<span class="material-symbols-outlined text-sm">timer</span>
|
| 657 |
+
Round ${session.current_round}
|
| 658 |
+
</div>
|
| 659 |
+
<div class="meta-item">
|
| 660 |
+
<span class="material-symbols-outlined text-sm">balance</span>
|
| 661 |
+
${session.bench_type || 'Division'}
|
| 662 |
+
</div>
|
| 663 |
+
</div>
|
| 664 |
+
</div>
|
| 665 |
+
`).join('');
|
| 666 |
+
} catch (error) {
|
| 667 |
+
console.error('Error loading sessions:', error);
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
async function loadAllSessions() {
|
| 672 |
+
try {
|
| 673 |
+
const response = await fetch('/court/sessions');
|
| 674 |
+
const sessions = await response.json();
|
| 675 |
+
|
| 676 |
+
const list = document.getElementById('all-sessions-list');
|
| 677 |
+
if (!sessions || sessions.length === 0) {
|
| 678 |
+
list.innerHTML = '<div class="clay-card p-8 rounded-xl text-center text-primary/40 font-sans">No sessions found. Start your first moot court.</div>';
|
| 679 |
+
return;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
list.innerHTML = sessions.map(session => `
|
| 683 |
+
<div class="clay-card p-6 rounded-xl session-card" onclick="loadSessionForReview('${session.id}')">
|
| 684 |
+
<h2 class="font-serif text-xl font-bold text-primary mb-2">${session.case_title}</h2>
|
| 685 |
+
<div class="flex gap-6 mb-4 text-sm">
|
| 686 |
+
<span class="font-sans"><strong>Your Role:</strong> ${session.user_side === 'petitioner' ? 'Petitioner' : 'Respondent'}</span>
|
| 687 |
+
<span class="font-sans"><strong>Bench:</strong> ${session.bench_type || 'Division Bench'}</span>
|
| 688 |
+
</div>
|
| 689 |
+
<div class="flex gap-6 text-xs text-primary/60">
|
| 690 |
+
<span>Rounds: ${session.current_round || 0}</span>
|
| 691 |
+
<span>Status: ${session.is_completed ? 'Completed' : 'In Progress'}</span>
|
| 692 |
+
</div>
|
| 693 |
+
</div>
|
| 694 |
+
`).join('');
|
| 695 |
+
} catch (error) {
|
| 696 |
+
console.error('Error loading all sessions:', error);
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
async function loadSessionForReview(sessionId) {
|
| 701 |
+
try {
|
| 702 |
+
const response = await fetch(`/court/session/${sessionId}`);
|
| 703 |
+
const data = await response.json();
|
| 704 |
+
|
| 705 |
+
state.currentSession = sessionId;
|
| 706 |
+
state.currentRound = data.current_round;
|
| 707 |
+
state.concessions = data.concessions || [];
|
| 708 |
+
state.transcript = data.transcript || [];
|
| 709 |
+
state.documents = data.documents || [];
|
| 710 |
+
|
| 711 |
+
showScreen('analysis');
|
| 712 |
+
} catch (error) {
|
| 713 |
+
console.error('Error loading session:', error);
|
| 714 |
+
}
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
function showImportFlow() {
|
| 718 |
+
alert('Research session import feature coming soon. For now, start a new case directly.');
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
function confirmEndSession() {
|
| 722 |
+
if (confirm('End this session and generate analysis? You cannot continue after this.')) {
|
| 723 |
+
endSession();
|
| 724 |
+
}
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
async function endSession() {
|
| 728 |
+
try {
|
| 729 |
+
const response = await fetch('/court/end', {
|
| 730 |
+
method: 'POST',
|
| 731 |
+
headers: { 'Content-Type': 'application/json' },
|
| 732 |
+
body: JSON.stringify({ session_id: state.currentSession })
|
| 733 |
+
});
|
| 734 |
+
|
| 735 |
+
if (response.ok) {
|
| 736 |
+
showScreen('loading');
|
| 737 |
+
finishLoadingAnimation();
|
| 738 |
+
setTimeout(() => showScreen('analysis'), 2000);
|
| 739 |
+
}
|
| 740 |
+
} catch (error) {
|
| 741 |
+
console.error('Error ending session:', error);
|
| 742 |
+
alert('Error ending session.');
|
| 743 |
+
}
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
// ════════════════════════════════════════════════════════════
|
| 747 |
+
// LOADING ANIMATION
|
| 748 |
+
// ════════════════════════════════════════════════════════════
|
| 749 |
+
|
| 750 |
+
function finishLoadingAnimation() {
|
| 751 |
+
for (let i = 1; i <= 4; i++) {
|
| 752 |
+
setTimeout(() => {
|
| 753 |
+
const pct = Math.min(100, 20 + i * 20);
|
| 754 |
+
document.getElementById(`ls-${i}-bar`).style.width = `${pct}%`;
|
| 755 |
+
document.getElementById(`ls-${i}-pct`).textContent = `${pct}%`;
|
| 756 |
+
}, i * 400);
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// ════════════════════════════════════════════════════════════
|
| 761 |
+
// UTILITIES
|
| 762 |
+
// ════════════════════════════════════════════════════════════
|
| 763 |
+
|
| 764 |
+
function updateLobbyTime() {
|
| 765 |
+
const now = new Date();
|
| 766 |
+
const time = now.toLocaleTimeString('en-US', {
|
| 767 |
+
hour12: true,
|
| 768 |
+
hour: '2-digit',
|
| 769 |
+
minute: '2-digit',
|
| 770 |
+
second: '2-digit'
|
| 771 |
+
});
|
| 772 |
+
const element = document.getElementById('lobby-clock');
|
| 773 |
+
if (element) element.textContent = time;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// ════════════════════════════════════════════════════════════
|
| 777 |
+
// PAGE LOAD HELPERS
|
| 778 |
+
// ════════════════════════════════════════════════════════════
|
| 779 |
+
|
| 780 |
+
document.addEventListener('keydown', (e) => {
|
| 781 |
+
if (e.key === 'Enter' && e.ctrlKey && state.currentScreen === 'courtroom') {
|
| 782 |
+
submitArgument();
|
| 783 |
+
}
|
| 784 |
+
});
|
frontend/index.html
CHANGED
|
@@ -32,6 +32,11 @@
|
|
| 32 |
System Analytics
|
| 33 |
</button>
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
<div class="sidebar-section-label">SESSIONS</div>
|
| 36 |
<div id="sessions-list" class="sessions-list">
|
| 37 |
<div class="sessions-empty">No sessions yet</div>
|
|
|
|
| 32 |
System Analytics
|
| 33 |
</button>
|
| 34 |
|
| 35 |
+
<a href="/static/court/court.html" class="analytics-btn" style="text-decoration: none; color: inherit;">
|
| 36 |
+
<span class="analytics-icon">⚖️</span>
|
| 37 |
+
Moot Court
|
| 38 |
+
</a>
|
| 39 |
+
|
| 40 |
<div class="sidebar-section-label">SESSIONS</div>
|
| 41 |
<div id="sessions-list" class="sessions-list">
|
| 42 |
<div class="sessions-empty">No sessions yet</div>
|
src/court/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Makes src/court a Python package
|
src/court/brief.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Case Brief Generator.
|
| 3 |
+
|
| 4 |
+
When a user imports a NyayaSetu research session into Moot Court,
|
| 5 |
+
this module reads the session state and generates a structured Case Brief.
|
| 6 |
+
|
| 7 |
+
The Case Brief is what opposing counsel reads before the hearing starts.
|
| 8 |
+
It tells them:
|
| 9 |
+
- What facts have been established
|
| 10 |
+
- What legal issues the user identified
|
| 11 |
+
- What precedents they retrieved
|
| 12 |
+
- Where the gaps are in their case
|
| 13 |
+
|
| 14 |
+
This is what makes the simulation genuinely adversarial —
|
| 15 |
+
opposing counsel knows your research.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import logging
|
| 19 |
+
from typing import Dict, Optional
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def generate_case_brief(
|
| 25 |
+
research_session: Dict,
|
| 26 |
+
user_side: str,
|
| 27 |
+
) -> str:
|
| 28 |
+
"""
|
| 29 |
+
Generate a structured Case Brief from a NyayaSetu research session.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
research_session: The session dict from NyayaSetu's sessions store
|
| 33 |
+
user_side: "petitioner" or "respondent"
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
Formatted case brief string ready for LLM consumption
|
| 37 |
+
"""
|
| 38 |
+
cs = research_session.get("case_state", {})
|
| 39 |
+
summary = research_session.get("summary", "")
|
| 40 |
+
|
| 41 |
+
# Extract structured data
|
| 42 |
+
parties = cs.get("parties", [])
|
| 43 |
+
events = cs.get("events", [])
|
| 44 |
+
documents = cs.get("documents", [])
|
| 45 |
+
disputes = cs.get("disputes", [])
|
| 46 |
+
hypotheses = cs.get("hypotheses", [])
|
| 47 |
+
facts_missing = cs.get("facts_missing", [])
|
| 48 |
+
|
| 49 |
+
# Extract precedents from last messages
|
| 50 |
+
last_messages = research_session.get("last_3_messages", [])
|
| 51 |
+
|
| 52 |
+
sections = [
|
| 53 |
+
"CASE BRIEF",
|
| 54 |
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
| 55 |
+
f"Generated from NyayaSetu Research Session",
|
| 56 |
+
f"User Position: {user_side.upper()}",
|
| 57 |
+
"",
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
if summary:
|
| 61 |
+
sections += [
|
| 62 |
+
"SITUATION SUMMARY:",
|
| 63 |
+
summary,
|
| 64 |
+
"",
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
if parties:
|
| 68 |
+
sections += [
|
| 69 |
+
"PARTIES IDENTIFIED:",
|
| 70 |
+
*[f" • {p}" for p in parties],
|
| 71 |
+
"",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
if events:
|
| 75 |
+
sections += [
|
| 76 |
+
"KEY EVENTS:",
|
| 77 |
+
*[f" • {e}" for e in events],
|
| 78 |
+
"",
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
if documents:
|
| 82 |
+
sections += [
|
| 83 |
+
"EVIDENCE/DOCUMENTS MENTIONED:",
|
| 84 |
+
*[f" • {d}" for d in documents],
|
| 85 |
+
"",
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
if disputes:
|
| 89 |
+
sections += [
|
| 90 |
+
"CORE DISPUTES:",
|
| 91 |
+
*[f" • {d}" for d in disputes],
|
| 92 |
+
"",
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
if hypotheses:
|
| 96 |
+
sections += ["LEGAL HYPOTHESES FORMED:"]
|
| 97 |
+
for h in hypotheses[:5]:
|
| 98 |
+
claim = h.get("claim", "")
|
| 99 |
+
confidence = h.get("confidence", "unknown")
|
| 100 |
+
evidence = h.get("evidence", [])
|
| 101 |
+
sections.append(f" • [{confidence.upper()}] {claim}")
|
| 102 |
+
if evidence:
|
| 103 |
+
sections.append(f" Evidence: {', '.join(evidence[:3])}")
|
| 104 |
+
sections.append("")
|
| 105 |
+
|
| 106 |
+
if facts_missing:
|
| 107 |
+
sections += [
|
| 108 |
+
"KNOWN GAPS IN THE CASE (Critical for opposing counsel):",
|
| 109 |
+
*[f" ⚠ {f}" for f in facts_missing],
|
| 110 |
+
"",
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
sections += [
|
| 114 |
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
| 115 |
+
"NOTE: This brief was generated from the user's research session.",
|
| 116 |
+
"Opposing counsel has access to all information above.",
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
return "\n".join(sections)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def generate_fresh_brief(
|
| 123 |
+
case_title: str,
|
| 124 |
+
user_side: str,
|
| 125 |
+
user_client: str,
|
| 126 |
+
opposing_party: str,
|
| 127 |
+
legal_issues: list,
|
| 128 |
+
brief_facts: str,
|
| 129 |
+
jurisdiction: str,
|
| 130 |
+
) -> str:
|
| 131 |
+
"""
|
| 132 |
+
Generate a case brief from scratch when user enters details manually.
|
| 133 |
+
"""
|
| 134 |
+
user_role = "Petitioner" if user_side == "petitioner" else "Respondent"
|
| 135 |
+
opposing_role = "Respondent" if user_side == "petitioner" else "Petitioner"
|
| 136 |
+
|
| 137 |
+
sections = [
|
| 138 |
+
"CASE BRIEF",
|
| 139 |
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
| 140 |
+
f"Case Title: {case_title}",
|
| 141 |
+
f"Jurisdiction: {jurisdiction.replace('_', ' ').title()}",
|
| 142 |
+
"",
|
| 143 |
+
f"{user_role} ({user_client}) vs {opposing_role} ({opposing_party})",
|
| 144 |
+
"",
|
| 145 |
+
"BRIEF FACTS:",
|
| 146 |
+
brief_facts,
|
| 147 |
+
"",
|
| 148 |
+
"LEGAL ISSUES:",
|
| 149 |
+
*[f" {i+1}. {issue}" for i, issue in enumerate(legal_issues)],
|
| 150 |
+
"",
|
| 151 |
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
return "\n".join(sections)
|
src/court/judge.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Judge Agent.
|
| 3 |
+
|
| 4 |
+
Neutral but dangerous. Finds logical gaps in every argument.
|
| 5 |
+
Asks pointed questions that expose weakness.
|
| 6 |
+
Controls session flow.
|
| 7 |
+
|
| 8 |
+
The judge never helps either side — asks what exposes weakness.
|
| 9 |
+
Questions get sharper as the session progresses because the judge
|
| 10 |
+
tracks all concessions and inconsistencies.
|
| 11 |
+
|
| 12 |
+
WHY a dedicated judge module?
|
| 13 |
+
The judge has completely different objectives from opposing counsel.
|
| 14 |
+
Keeping them separate means their prompts never contaminate each other.
|
| 15 |
+
The judge is neutral. Opposing counsel is adversarial. These must stay clean.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import logging
|
| 19 |
+
import json
|
| 20 |
+
from typing import Dict, List, Optional
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
JUDGE_SYSTEM_PROMPT = """You are the presiding judge in an Indian Supreme Court moot court simulation.
|
| 25 |
+
|
| 26 |
+
YOUR ROLE:
|
| 27 |
+
You are neutral but intellectually rigorous. Your job is not to help either party — it is to find the logical gaps, unanswered questions, and weaknesses in whatever argument was just made.
|
| 28 |
+
|
| 29 |
+
YOUR PERSONALITY:
|
| 30 |
+
- Formal and precise. Never casual.
|
| 31 |
+
- Slightly impatient with vague or uncited arguments.
|
| 32 |
+
- Deeply knowledgeable about Indian constitutional law, criminal law, and procedure.
|
| 33 |
+
- You ask exactly ONE question per turn — the most important one.
|
| 34 |
+
- Your questions are surgical: "Counsel, how does your argument survive the test in Maneka Gandhi?" not "Can you explain more?"
|
| 35 |
+
|
| 36 |
+
YOUR LANGUAGE:
|
| 37 |
+
- Always address user as "Counsel" or "Learned Counsel"
|
| 38 |
+
- Refer to yourself as "the Court" or "this Court" — never "I" or "me"
|
| 39 |
+
- Use phrases like: "The court is not satisfied...", "Counsel has not addressed...", "This court wishes to know..."
|
| 40 |
+
- Never use casual language. Every sentence sounds like it belongs in a court record.
|
| 41 |
+
|
| 42 |
+
YOUR QUESTIONING STRATEGY:
|
| 43 |
+
1. Find the weakest logical point in the last argument made
|
| 44 |
+
2. Check if any previous concessions can be pressed further
|
| 45 |
+
3. Identify what legal authority was NOT cited that should have been
|
| 46 |
+
4. Ask the ONE question that most challenges the user's position
|
| 47 |
+
|
| 48 |
+
QUESTION FORMAT:
|
| 49 |
+
Keep your observation to 1-2 sentences, then ask your question.
|
| 50 |
+
Total response: 3-5 sentences maximum.
|
| 51 |
+
Never give a speech. The court asks. Counsel answers.
|
| 52 |
+
|
| 53 |
+
IMPORTANT: You are NOT legal advice. This is a simulation."""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
JUDGE_CLOSING_OBSERVATION_PROMPT = """You are the presiding judge delivering final observations after hearing closing arguments.
|
| 57 |
+
|
| 58 |
+
This is the most important moment in the simulation. Your observations signal which way the court is leaning.
|
| 59 |
+
|
| 60 |
+
Based on the complete transcript, deliver:
|
| 61 |
+
1. Your observation on the strongest argument made in this hearing (2 sentences)
|
| 62 |
+
2. The weakest point that was not adequately addressed (2 sentences)
|
| 63 |
+
3. A final observation that signals the likely outcome WITHOUT explicitly stating it (2 sentences)
|
| 64 |
+
|
| 65 |
+
Total: 6 sentences maximum. Formal judicial language throughout.
|
| 66 |
+
Remain neutral in tone even while signalling the likely outcome through your choice of emphasis."""
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_judge_prompt(
|
| 70 |
+
session: Dict,
|
| 71 |
+
last_user_argument: str,
|
| 72 |
+
retrieved_context: str,
|
| 73 |
+
) -> List[Dict]:
|
| 74 |
+
"""
|
| 75 |
+
Build the messages list for the judge LLM call.
|
| 76 |
+
|
| 77 |
+
The judge sees:
|
| 78 |
+
- Full session transcript (compressed)
|
| 79 |
+
- Last user argument
|
| 80 |
+
- Relevant retrieved precedents
|
| 81 |
+
- All concessions made so far
|
| 82 |
+
- Current round number
|
| 83 |
+
"""
|
| 84 |
+
cs_summary = _build_case_summary(session)
|
| 85 |
+
concessions_text = _format_concessions(session.get("concessions", []))
|
| 86 |
+
transcript_recent = _get_recent_transcript(session, last_n=6)
|
| 87 |
+
|
| 88 |
+
user_content = f"""CASE SUMMARY:
|
| 89 |
+
{cs_summary}
|
| 90 |
+
|
| 91 |
+
RECENT TRANSCRIPT (last 6 entries):
|
| 92 |
+
{transcript_recent}
|
| 93 |
+
|
| 94 |
+
{concessions_text}
|
| 95 |
+
|
| 96 |
+
RELEVANT LEGAL AUTHORITIES (from retrieval):
|
| 97 |
+
{retrieved_context[:1500] if retrieved_context else "No additional precedents retrieved."}
|
| 98 |
+
|
| 99 |
+
LAST ARGUMENT BY COUNSEL:
|
| 100 |
+
{last_user_argument}
|
| 101 |
+
|
| 102 |
+
Round {session.get('current_round', 1)} of {session.get('max_rounds', 5)}.
|
| 103 |
+
Difficulty: {session.get('difficulty', 'standard')}.
|
| 104 |
+
|
| 105 |
+
Now ask your ONE most important question. Be precise. Be judicial. Challenge the weakness."""
|
| 106 |
+
|
| 107 |
+
return [
|
| 108 |
+
{"role": "system", "content": JUDGE_SYSTEM_PROMPT},
|
| 109 |
+
{"role": "user", "content": user_content}
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def build_judge_closing_prompt(session: Dict) -> List[Dict]:
|
| 114 |
+
"""Build prompt for judge's final closing observations."""
|
| 115 |
+
|
| 116 |
+
transcript_full = _get_full_transcript_summary(session)
|
| 117 |
+
concessions = _format_concessions(session.get("concessions", []))
|
| 118 |
+
|
| 119 |
+
user_content = f"""COMPLETE SESSION SUMMARY:
|
| 120 |
+
{transcript_full}
|
| 121 |
+
|
| 122 |
+
{concessions}
|
| 123 |
+
|
| 124 |
+
User argued as: {session.get('user_side', 'petitioner').upper()}
|
| 125 |
+
Case: {session.get('case_title', '')}
|
| 126 |
+
|
| 127 |
+
Deliver your final judicial observations."""
|
| 128 |
+
|
| 129 |
+
return [
|
| 130 |
+
{"role": "system", "content": JUDGE_CLOSING_OBSERVATION_PROMPT},
|
| 131 |
+
{"role": "user", "content": user_content}
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def build_objection_ruling_prompt(
|
| 136 |
+
session: Dict,
|
| 137 |
+
objection_type: str,
|
| 138 |
+
objection_text: str,
|
| 139 |
+
what_was_objected_to: str,
|
| 140 |
+
) -> List[Dict]:
|
| 141 |
+
"""Build prompt for judge ruling on an objection."""
|
| 142 |
+
|
| 143 |
+
system = """You are a Supreme Court judge ruling on an objection raised by counsel.
|
| 144 |
+
|
| 145 |
+
Rule on the objection in 2-3 sentences:
|
| 146 |
+
1. State whether it is sustained or overruled
|
| 147 |
+
2. Give brief legal reasoning
|
| 148 |
+
3. Direct counsel to proceed
|
| 149 |
+
|
| 150 |
+
Be decisive. No hedging. The court rules."""
|
| 151 |
+
|
| 152 |
+
user_content = f"""Objection raised: {objection_type}
|
| 153 |
+
Counsel stated: {objection_text}
|
| 154 |
+
Regarding: {what_was_objected_to}
|
| 155 |
+
|
| 156 |
+
Rule on this objection."""
|
| 157 |
+
|
| 158 |
+
return [
|
| 159 |
+
{"role": "system", "content": system},
|
| 160 |
+
{"role": "user", "content": user_content}
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _build_case_summary(session: Dict) -> str:
|
| 165 |
+
return (
|
| 166 |
+
f"Case: {session.get('case_title', '')}\n"
|
| 167 |
+
f"User side: {session.get('user_side', '').upper()}\n"
|
| 168 |
+
f"Legal issues: {', '.join(session.get('legal_issues', []))}\n"
|
| 169 |
+
f"Phase: {session.get('phase', '')} | Round: {session.get('current_round', 0)}"
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _format_concessions(concessions: List[Dict]) -> str:
|
| 174 |
+
if not concessions:
|
| 175 |
+
return ""
|
| 176 |
+
|
| 177 |
+
lines = ["CONCESSIONS ON RECORD:"]
|
| 178 |
+
for c in concessions:
|
| 179 |
+
lines.append(f" Round {c['round_number']}: \"{c['exact_quote'][:100]}\"")
|
| 180 |
+
lines.append(f" Significance: {c['legal_significance'][:100]}")
|
| 181 |
+
|
| 182 |
+
return "\n".join(lines)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _get_recent_transcript(session: Dict, last_n: int = 6) -> str:
|
| 186 |
+
transcript = session.get("transcript", [])
|
| 187 |
+
recent = transcript[-last_n:] if len(transcript) > last_n else transcript
|
| 188 |
+
|
| 189 |
+
lines = []
|
| 190 |
+
for entry in recent:
|
| 191 |
+
lines.append(f"{entry['role_label'].upper()}: {entry['content'][:300]}")
|
| 192 |
+
lines.append("")
|
| 193 |
+
|
| 194 |
+
return "\n".join(lines) if lines else "No transcript yet."
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _get_full_transcript_summary(session: Dict) -> str:
|
| 198 |
+
"""Compressed full transcript for closing observations."""
|
| 199 |
+
transcript = session.get("transcript", [])
|
| 200 |
+
|
| 201 |
+
if not transcript:
|
| 202 |
+
return "No transcript available."
|
| 203 |
+
|
| 204 |
+
# Group by round
|
| 205 |
+
rounds = {}
|
| 206 |
+
for entry in transcript:
|
| 207 |
+
r = entry.get("round_number", 0)
|
| 208 |
+
if r not in rounds:
|
| 209 |
+
rounds[r] = []
|
| 210 |
+
rounds[r].append(f"{entry['role_label']}: {entry['content'][:200]}")
|
| 211 |
+
|
| 212 |
+
lines = []
|
| 213 |
+
for round_num in sorted(rounds.keys()):
|
| 214 |
+
lines.append(f"--- Round {round_num} ---")
|
| 215 |
+
lines.extend(rounds[round_num])
|
| 216 |
+
lines.append("")
|
| 217 |
+
|
| 218 |
+
return "\n".join(lines)
|
src/court/opposing.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Opposing Counsel Agent.
|
| 3 |
+
|
| 4 |
+
Adversarial. Strategic. Never helps the user.
|
| 5 |
+
|
| 6 |
+
Three trap types:
|
| 7 |
+
1. Admission trap — phrase a statement to elicit a damaging concession
|
| 8 |
+
2. Precedent trap — cite a case that superficially sounds helpful but supports opposition
|
| 9 |
+
3. Internal inconsistency trap — catch the user contradicting themselves
|
| 10 |
+
|
| 11 |
+
The opposing counsel reads everything the user has researched via the case brief.
|
| 12 |
+
This is what makes the simulation genuinely adversarial.
|
| 13 |
+
|
| 14 |
+
Difficulty levels change the aggressiveness:
|
| 15 |
+
- moot: measured, educational, somewhat forgiving
|
| 16 |
+
- standard: sharp, strategic, exploits weaknesses
|
| 17 |
+
- adversarial: ruthless, traps constantly, gives no quarter
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import logging
|
| 21 |
+
import re
|
| 22 |
+
from typing import Dict, List, Optional, Tuple
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# ── Difficulty-specific personality modifiers ──────────────────
|
| 27 |
+
|
| 28 |
+
DIFFICULTY_MODIFIERS = {
|
| 29 |
+
"moot": """
|
| 30 |
+
You are firm but educational in your opposition. While you argue against the user,
|
| 31 |
+
you allow them to recover from weak arguments without immediately exploiting every gap.
|
| 32 |
+
Your goal is to challenge, not to destroy.""",
|
| 33 |
+
|
| 34 |
+
"standard": """
|
| 35 |
+
You are sharp and strategic. You exploit weaknesses directly.
|
| 36 |
+
You set traps when opportunities arise.
|
| 37 |
+
You cite contradictions when you spot them.
|
| 38 |
+
You are a formidable opponent but a realistic one.""",
|
| 39 |
+
|
| 40 |
+
"adversarial": """
|
| 41 |
+
You are ruthless. You exploit every weakness immediately.
|
| 42 |
+
You set traps constantly. You never let a concession pass unexploited.
|
| 43 |
+
You cite every contradiction. You are what a top SC senior advocate
|
| 44 |
+
looks like at their most aggressive. The user will have to fight for every inch.""",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
OPPOSING_SYSTEM_PROMPT = """You are opposing counsel in an Indian Supreme Court moot court simulation.
|
| 49 |
+
|
| 50 |
+
YOUR ROLE:
|
| 51 |
+
You argue AGAINST the user's position. You work FOR the opposing party.
|
| 52 |
+
Your job is to WIN — find the angle, exploit the weakness, set the trap.
|
| 53 |
+
|
| 54 |
+
YOUR PERSONALITY:
|
| 55 |
+
- You are a senior advocate with 20+ years at the Supreme Court bar
|
| 56 |
+
- Sharp, prepared, slightly aggressive
|
| 57 |
+
- You have read every case the user researched (their Case Brief is your preparation)
|
| 58 |
+
- You know their weaknesses better than they do
|
| 59 |
+
- You NEVER inadvertently help the user. Every sentence advances your client's case.
|
| 60 |
+
|
| 61 |
+
YOUR LANGUAGE:
|
| 62 |
+
- Address the bench as "My Lords" or "Hon'ble Court"
|
| 63 |
+
- Address user as "My learned friend" with a slightly dismissive edge
|
| 64 |
+
- Use phrases like "With respect, my learned friend's submission is misconceived...",
|
| 65 |
+
"The settled position in law, as Your Lordships are aware, is...",
|
| 66 |
+
"My learned friend has conveniently overlooked..."
|
| 67 |
+
- Cite cases with their full citation when possible: "(2017) 10 SCC 1"
|
| 68 |
+
|
| 69 |
+
YOUR THREE WEAPONS:
|
| 70 |
+
1. DIRECT COUNTER: State the opposing legal position clearly with authority
|
| 71 |
+
2. CITATION COUNTER: Cite a case that directly contradicts the user's position
|
| 72 |
+
3. TRAP: Set one of the three trap types when the opportunity arises
|
| 73 |
+
|
| 74 |
+
TRAP TYPES (use strategically, not every turn):
|
| 75 |
+
- ADMISSION TRAP: Make a statement that sounds reasonable but forces a damaging concession if agreed to
|
| 76 |
+
Example: "My Lords, surely my learned friend would not dispute that the right in question is subject to reasonable restrictions?"
|
| 77 |
+
|
| 78 |
+
- PRECEDENT TRAP: Cite a case that sounds helpful to the user but actually supports you when read carefully
|
| 79 |
+
Example: Cite Puttaswamy but focus on the proportionality test which the user's case fails
|
| 80 |
+
|
| 81 |
+
- INCONSISTENCY TRAP: If user has contradicted themselves across rounds, call it out explicitly
|
| 82 |
+
Example: "My Lords, in Round 2 my learned friend submitted X. Now my learned friend submits Y. These positions are irreconcilable."
|
| 83 |
+
|
| 84 |
+
RESPONSE LENGTH:
|
| 85 |
+
Keep your counter-argument to 4-6 sentences. Courtroom arguments are precise, not lengthy.
|
| 86 |
+
End with either a direct statement OR a trap question — not both.
|
| 87 |
+
|
| 88 |
+
IMPORTANT: This is a simulation. You are not providing legal advice."""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def build_opposing_prompt(
|
| 92 |
+
session: Dict,
|
| 93 |
+
user_argument: str,
|
| 94 |
+
retrieved_context: str,
|
| 95 |
+
trap_opportunity: Optional[str] = None,
|
| 96 |
+
) -> List[Dict]:
|
| 97 |
+
"""
|
| 98 |
+
Build the messages list for opposing counsel LLM call.
|
| 99 |
+
|
| 100 |
+
The opposing counsel sees:
|
| 101 |
+
- Case brief (user's research, gaps)
|
| 102 |
+
- Full recent transcript
|
| 103 |
+
- User's latest argument
|
| 104 |
+
- Retrieved precedents to use against user
|
| 105 |
+
- Any detected trap opportunities
|
| 106 |
+
- All concessions made so far
|
| 107 |
+
"""
|
| 108 |
+
difficulty = session.get("difficulty", "standard")
|
| 109 |
+
difficulty_modifier = DIFFICULTY_MODIFIERS.get(difficulty, DIFFICULTY_MODIFIERS["standard"])
|
| 110 |
+
|
| 111 |
+
case_brief = session.get("case_brief", "")
|
| 112 |
+
concessions = _format_concessions(session.get("concessions", []))
|
| 113 |
+
inconsistencies = _detect_inconsistencies(session.get("user_arguments", []))
|
| 114 |
+
transcript_recent = _get_recent_transcript(session, last_n=4)
|
| 115 |
+
|
| 116 |
+
trap_instruction = ""
|
| 117 |
+
if trap_opportunity:
|
| 118 |
+
trap_instruction = f"\nTRAP OPPORTUNITY DETECTED: {trap_opportunity}\nConsider exploiting this in your response."
|
| 119 |
+
elif inconsistencies:
|
| 120 |
+
trap_instruction = f"\nINCONSISTENCY DETECTED: {inconsistencies}\nConsider using the inconsistency trap."
|
| 121 |
+
|
| 122 |
+
user_content = f"""CASE BRIEF (your preparation before court):
|
| 123 |
+
{case_brief[:1500]}
|
| 124 |
+
|
| 125 |
+
RECENT TRANSCRIPT:
|
| 126 |
+
{transcript_recent}
|
| 127 |
+
|
| 128 |
+
{concessions}
|
| 129 |
+
|
| 130 |
+
RETRIEVED LEGAL AUTHORITIES (use these against the user):
|
| 131 |
+
{retrieved_context[:2000] if retrieved_context else "Use your general legal knowledge."}
|
| 132 |
+
|
| 133 |
+
USER'S LATEST ARGUMENT:
|
| 134 |
+
{user_argument}
|
| 135 |
+
|
| 136 |
+
Round {session.get('current_round', 1)} of {session.get('max_rounds', 5)}.
|
| 137 |
+
{trap_instruction}
|
| 138 |
+
|
| 139 |
+
{difficulty_modifier}
|
| 140 |
+
|
| 141 |
+
Now respond as opposing counsel. Counter this argument."""
|
| 142 |
+
|
| 143 |
+
return [
|
| 144 |
+
{"role": "system", "content": OPPOSING_SYSTEM_PROMPT},
|
| 145 |
+
{"role": "user", "content": user_content}
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def build_cross_examination_prompt(
|
| 150 |
+
session: Dict,
|
| 151 |
+
question_number: int,
|
| 152 |
+
retrieved_context: str,
|
| 153 |
+
) -> List[Dict]:
|
| 154 |
+
"""
|
| 155 |
+
Build prompt for cross-examination phase.
|
| 156 |
+
Opposing counsel asks pointed questions, not arguments.
|
| 157 |
+
"""
|
| 158 |
+
system = f"""You are opposing counsel conducting cross-examination in an Indian Supreme Court moot court.
|
| 159 |
+
|
| 160 |
+
This is Question {question_number} of 3 in your cross-examination.
|
| 161 |
+
|
| 162 |
+
YOUR OBJECTIVE:
|
| 163 |
+
Ask ONE precise question that:
|
| 164 |
+
1. Forces the user to admit a weakness in their case, OR
|
| 165 |
+
2. Challenges the factual basis of their position, OR
|
| 166 |
+
3. Sets up an admission you can use in your closing argument
|
| 167 |
+
|
| 168 |
+
CROSS-EXAMINATION RULES:
|
| 169 |
+
- Ask only closed questions (yes/no or specific fact questions)
|
| 170 |
+
- Never ask open-ended questions that let them explain freely
|
| 171 |
+
- Build from previous answers — each question should box them in further
|
| 172 |
+
- Your question must be specific, not general
|
| 173 |
+
|
| 174 |
+
Format: One sentence question only. No preamble.
|
| 175 |
+
|
| 176 |
+
{DIFFICULTY_MODIFIERS.get(session.get('difficulty', 'standard'), '')}"""
|
| 177 |
+
|
| 178 |
+
case_brief = session.get("case_brief", "")
|
| 179 |
+
concessions = _format_concessions(session.get("concessions", []))
|
| 180 |
+
transcript = _get_recent_transcript(session, last_n=8)
|
| 181 |
+
|
| 182 |
+
user_content = f"""CASE BRIEF:
|
| 183 |
+
{case_brief[:800]}
|
| 184 |
+
|
| 185 |
+
RECENT TRANSCRIPT:
|
| 186 |
+
{transcript}
|
| 187 |
+
|
| 188 |
+
{concessions}
|
| 189 |
+
|
| 190 |
+
RELEVANT AUTHORITIES:
|
| 191 |
+
{retrieved_context[:1000] if retrieved_context else "Use general legal knowledge."}
|
| 192 |
+
|
| 193 |
+
This is cross-examination Question {question_number} of 3.
|
| 194 |
+
Ask your most damaging question for this position in the examination."""
|
| 195 |
+
|
| 196 |
+
return [
|
| 197 |
+
{"role": "system", "content": system},
|
| 198 |
+
{"role": "user", "content": user_content}
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def build_opposing_closing_prompt(session: Dict) -> List[Dict]:
|
| 203 |
+
"""Build prompt for opposing counsel's closing argument."""
|
| 204 |
+
|
| 205 |
+
system = """You are opposing counsel delivering your closing argument.
|
| 206 |
+
|
| 207 |
+
This is your opportunity to:
|
| 208 |
+
1. Summarise the strongest 3 arguments you made
|
| 209 |
+
2. Point out what the user FAILED to establish
|
| 210 |
+
3. Highlight every concession they made
|
| 211 |
+
4. Tell the court why they should rule in your client's favour
|
| 212 |
+
|
| 213 |
+
Length: 6-8 sentences. Formal. Decisive. Leave no doubt.
|
| 214 |
+
|
| 215 |
+
End with: "For these reasons, we respectfully submit that [the petition/the appeal] be [dismissed/allowed]." """
|
| 216 |
+
|
| 217 |
+
case_brief = session.get("case_brief", "")
|
| 218 |
+
concessions = _format_concessions(session.get("concessions", []))
|
| 219 |
+
transcript_summary = _get_full_transcript_summary(session)
|
| 220 |
+
|
| 221 |
+
user_content = f"""CASE BRIEF:
|
| 222 |
+
{case_brief[:800]}
|
| 223 |
+
|
| 224 |
+
COMPLETE TRANSCRIPT SUMMARY:
|
| 225 |
+
{transcript_summary[:2000]}
|
| 226 |
+
|
| 227 |
+
{concessions}
|
| 228 |
+
|
| 229 |
+
Deliver your closing argument."""
|
| 230 |
+
|
| 231 |
+
return [
|
| 232 |
+
{"role": "system", "content": system},
|
| 233 |
+
{"role": "user", "content": user_content}
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def detect_trap_opportunity(
|
| 238 |
+
user_argument: str,
|
| 239 |
+
previous_arguments: List[Dict],
|
| 240 |
+
session: Dict,
|
| 241 |
+
) -> Optional[Tuple[str, str]]:
|
| 242 |
+
"""
|
| 243 |
+
Analyse user's argument to detect trap opportunities.
|
| 244 |
+
|
| 245 |
+
Returns (trap_type, description) or None.
|
| 246 |
+
|
| 247 |
+
This runs before the LLM call so we can include
|
| 248 |
+
trap instruction in the prompt when relevant.
|
| 249 |
+
"""
|
| 250 |
+
arg_lower = user_argument.lower()
|
| 251 |
+
|
| 252 |
+
# ── Check for admission trap opportunities ─────────────────
|
| 253 |
+
# User makes absolute claims that can be challenged
|
| 254 |
+
absolute_markers = [
|
| 255 |
+
("absolute right", "admission_trap", "User claims absolute right — trap: agree no right is absolute"),
|
| 256 |
+
("always", "admission_trap", "User uses 'always' — trap: get them to admit exceptions exist"),
|
| 257 |
+
("cannot be restricted", "admission_trap", "User claims right cannot be restricted — trap: Article 19(2) reasonable restrictions"),
|
| 258 |
+
("unlimited", "admission_trap", "User claims unlimited right — trap: all rights have limits"),
|
| 259 |
+
("no exception", "admission_trap", "User claims no exception — trap: every rule has exceptions"),
|
| 260 |
+
]
|
| 261 |
+
|
| 262 |
+
for marker, trap_type, description in absolute_markers:
|
| 263 |
+
if marker in arg_lower:
|
| 264 |
+
return (trap_type, description)
|
| 265 |
+
|
| 266 |
+
# ── Check for internal inconsistency ──��───────────────────
|
| 267 |
+
if len(previous_arguments) >= 2:
|
| 268 |
+
inconsistency = _detect_inconsistencies(previous_arguments + [{
|
| 269 |
+
"round": session.get("current_round", 0),
|
| 270 |
+
"text": user_argument,
|
| 271 |
+
"key_claims": [],
|
| 272 |
+
}])
|
| 273 |
+
if inconsistency:
|
| 274 |
+
return ("inconsistency_trap", inconsistency)
|
| 275 |
+
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def _detect_inconsistencies(user_arguments: List[Dict]) -> Optional[str]:
|
| 280 |
+
"""
|
| 281 |
+
Simple rule-based inconsistency detector.
|
| 282 |
+
Checks for contradictory claims across rounds.
|
| 283 |
+
"""
|
| 284 |
+
if len(user_arguments) < 2:
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
# Pairs of contradictory markers
|
| 288 |
+
contradiction_pairs = [
|
| 289 |
+
(["not guilty", "innocent", "no offence"], ["committed", "did take", "admitted"]),
|
| 290 |
+
(["no consent required", "no permission needed"], ["consent was given", "permission was obtained"]),
|
| 291 |
+
(["fundamental right", "absolute"], ["subject to restriction", "can be limited"]),
|
| 292 |
+
(["no notice", "without notice"], ["notice was given", "was informed"]),
|
| 293 |
+
(["private party", "private company"], ["government", "state", "public authority"]),
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
all_texts = [a["text"].lower() for a in user_arguments]
|
| 297 |
+
|
| 298 |
+
for positive_markers, negative_markers in contradiction_pairs:
|
| 299 |
+
found_positive_round = None
|
| 300 |
+
found_negative_round = None
|
| 301 |
+
|
| 302 |
+
for i, text in enumerate(all_texts):
|
| 303 |
+
if any(m in text for m in positive_markers):
|
| 304 |
+
found_positive_round = i
|
| 305 |
+
if any(m in text for m in negative_markers):
|
| 306 |
+
found_negative_round = i
|
| 307 |
+
|
| 308 |
+
if found_positive_round is not None and found_negative_round is not None:
|
| 309 |
+
if found_positive_round != found_negative_round:
|
| 310 |
+
pos_arg = user_arguments[found_positive_round]
|
| 311 |
+
neg_arg = user_arguments[found_negative_round]
|
| 312 |
+
return (
|
| 313 |
+
f"Round {pos_arg['round']}: user argued position A. "
|
| 314 |
+
f"Round {neg_arg['round']}: user argued contradictory position B. "
|
| 315 |
+
f"These cannot coexist."
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def _format_concessions(concessions: List[Dict]) -> str:
|
| 322 |
+
if not concessions:
|
| 323 |
+
return ""
|
| 324 |
+
lines = ["CONCESSIONS ALREADY MADE (exploit these):"]
|
| 325 |
+
for c in concessions:
|
| 326 |
+
lines.append(f" Round {c['round_number']}: \"{c['exact_quote'][:100]}\"")
|
| 327 |
+
return "\n".join(lines)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def _get_recent_transcript(session: Dict, last_n: int = 4) -> str:
|
| 331 |
+
transcript = session.get("transcript", [])
|
| 332 |
+
recent = transcript[-last_n:] if len(transcript) > last_n else transcript
|
| 333 |
+
lines = []
|
| 334 |
+
for entry in recent:
|
| 335 |
+
lines.append(f"{entry['role_label'].upper()}: {entry['content'][:250]}")
|
| 336 |
+
lines.append("")
|
| 337 |
+
return "\n".join(lines) if lines else "No transcript yet."
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _get_full_transcript_summary(session: Dict) -> str:
|
| 341 |
+
transcript = session.get("transcript", [])
|
| 342 |
+
if not transcript:
|
| 343 |
+
return "No transcript."
|
| 344 |
+
rounds = {}
|
| 345 |
+
for entry in transcript:
|
| 346 |
+
r = entry.get("round_number", 0)
|
| 347 |
+
if r not in rounds:
|
| 348 |
+
rounds[r] = []
|
| 349 |
+
rounds[r].append(f"{entry['role_label']}: {entry['content'][:200]}")
|
| 350 |
+
lines = []
|
| 351 |
+
for round_num in sorted(rounds.keys()):
|
| 352 |
+
lines.append(f"--- Round {round_num} ---")
|
| 353 |
+
lines.extend(rounds[round_num])
|
| 354 |
+
lines.append("")
|
| 355 |
+
return "\n".join(lines)
|
src/court/orchestrator.py
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Court Orchestrator.
|
| 3 |
+
|
| 4 |
+
Coordinates all four agents for each user action.
|
| 5 |
+
Single entry point for all court operations.
|
| 6 |
+
|
| 7 |
+
Per round flow:
|
| 8 |
+
1. User submits argument
|
| 9 |
+
2. Opposing counsel responds (retrieval + LLM)
|
| 10 |
+
3. Judge asks question (retrieval + LLM)
|
| 11 |
+
4. Registrar announces (deterministic)
|
| 12 |
+
5. Concession detection runs
|
| 13 |
+
6. Trap detection runs
|
| 14 |
+
7. Session updated
|
| 15 |
+
8. Response assembled
|
| 16 |
+
|
| 17 |
+
This module owns the 3-LLM-calls-per-round budget.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import logging
|
| 21 |
+
import json
|
| 22 |
+
from typing import Dict, List, Optional, Tuple
|
| 23 |
+
|
| 24 |
+
from src.court.session import (
|
| 25 |
+
get_session,
|
| 26 |
+
add_transcript_entry,
|
| 27 |
+
add_concession,
|
| 28 |
+
add_trap_event,
|
| 29 |
+
add_user_argument,
|
| 30 |
+
advance_round,
|
| 31 |
+
advance_phase,
|
| 32 |
+
update_session,
|
| 33 |
+
)
|
| 34 |
+
from src.court.judge import (
|
| 35 |
+
build_judge_prompt,
|
| 36 |
+
build_judge_closing_prompt,
|
| 37 |
+
build_objection_ruling_prompt,
|
| 38 |
+
)
|
| 39 |
+
from src.court.opposing import (
|
| 40 |
+
build_opposing_prompt,
|
| 41 |
+
build_cross_examination_prompt,
|
| 42 |
+
build_opposing_closing_prompt,
|
| 43 |
+
detect_trap_opportunity,
|
| 44 |
+
)
|
| 45 |
+
from src.court.registrar import (
|
| 46 |
+
build_round_announcement,
|
| 47 |
+
build_accountability_note,
|
| 48 |
+
get_document_announcement,
|
| 49 |
+
)
|
| 50 |
+
from src.court.summariser import (
|
| 51 |
+
build_summariser_prompt,
|
| 52 |
+
parse_analysis,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
logger = logging.getLogger(__name__)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _call_llm(messages: List[Dict]) -> str:
|
| 59 |
+
"""
|
| 60 |
+
Call the LLM. Uses the same llm.py as the main agent.
|
| 61 |
+
"""
|
| 62 |
+
from src.llm import call_llm_raw
|
| 63 |
+
return call_llm_raw(messages)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _retrieve_for_court(query: str, session: Dict) -> str:
|
| 67 |
+
"""
|
| 68 |
+
Retrieve relevant precedents for court use.
|
| 69 |
+
Uses the same FAISS retrieval as main agent.
|
| 70 |
+
Returns formatted context string.
|
| 71 |
+
"""
|
| 72 |
+
try:
|
| 73 |
+
from src.embed import embed_text
|
| 74 |
+
from src.retrieval import retrieve
|
| 75 |
+
|
| 76 |
+
embedding = embed_text(query)
|
| 77 |
+
chunks = retrieve(embedding, top_k=3)
|
| 78 |
+
|
| 79 |
+
if not chunks:
|
| 80 |
+
return ""
|
| 81 |
+
|
| 82 |
+
context_parts = []
|
| 83 |
+
for chunk in chunks:
|
| 84 |
+
title = chunk.get("title", "")
|
| 85 |
+
year = chunk.get("year", "")
|
| 86 |
+
text = chunk.get("expanded_context") or chunk.get("chunk_text") or ""
|
| 87 |
+
context_parts.append(f"[{title} | {year}]\n{text[:600]}")
|
| 88 |
+
|
| 89 |
+
return "\n\n".join(context_parts)
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.warning(f"Court retrieval failed: {e}")
|
| 93 |
+
return ""
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def process_user_argument(
|
| 97 |
+
session_id: str,
|
| 98 |
+
user_argument: str,
|
| 99 |
+
) -> Dict:
|
| 100 |
+
"""
|
| 101 |
+
Main function called when user submits an argument during rounds.
|
| 102 |
+
|
| 103 |
+
Returns dict with:
|
| 104 |
+
- opposing_response: str
|
| 105 |
+
- judge_question: str
|
| 106 |
+
- registrar_note: str
|
| 107 |
+
- trap_detected: bool
|
| 108 |
+
- trap_warning: str (empty if no trap)
|
| 109 |
+
- new_concessions: list
|
| 110 |
+
- round_number: int
|
| 111 |
+
- phase: str
|
| 112 |
+
"""
|
| 113 |
+
session = get_session(session_id)
|
| 114 |
+
if not session:
|
| 115 |
+
return {"error": "Session not found"}
|
| 116 |
+
|
| 117 |
+
phase = session["phase"]
|
| 118 |
+
|
| 119 |
+
# Route to appropriate handler
|
| 120 |
+
if phase == "briefing":
|
| 121 |
+
return _handle_briefing(session_id, user_argument, session)
|
| 122 |
+
elif phase == "rounds":
|
| 123 |
+
return _handle_round(session_id, user_argument, session)
|
| 124 |
+
elif phase == "cross_examination":
|
| 125 |
+
return _handle_cross_exam_answer(session_id, user_argument, session)
|
| 126 |
+
elif phase == "closing":
|
| 127 |
+
return _handle_closing(session_id, user_argument, session)
|
| 128 |
+
else:
|
| 129 |
+
return {"error": f"Cannot process argument in phase: {phase}"}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _handle_briefing(session_id: str, user_argument: str, session: Dict) -> Dict:
|
| 133 |
+
"""
|
| 134 |
+
Handle the first submission — opening argument / briefing phase.
|
| 135 |
+
Transitions to rounds phase after.
|
| 136 |
+
"""
|
| 137 |
+
# Add user's opening to transcript
|
| 138 |
+
user_label = (
|
| 139 |
+
"PETITIONER'S COUNSEL"
|
| 140 |
+
if session["user_side"] == "petitioner"
|
| 141 |
+
else "RESPONDENT'S COUNSEL"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
add_transcript_entry(
|
| 145 |
+
session_id=session_id,
|
| 146 |
+
speaker=session["user_side"].upper(),
|
| 147 |
+
role_label=user_label,
|
| 148 |
+
content=user_argument,
|
| 149 |
+
entry_type="argument",
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
add_user_argument(session_id, user_argument, [])
|
| 153 |
+
|
| 154 |
+
# Retrieve context for both agents
|
| 155 |
+
query = f"{session.get('case_title', '')} {' '.join(session.get('legal_issues', []))}"
|
| 156 |
+
retrieved_context = _retrieve_for_court(query, session)
|
| 157 |
+
|
| 158 |
+
# Check for trap opportunity
|
| 159 |
+
trap_info = detect_trap_opportunity(user_argument, [], session)
|
| 160 |
+
|
| 161 |
+
# Opposing counsel responds
|
| 162 |
+
opposing_messages = build_opposing_prompt(
|
| 163 |
+
session=session,
|
| 164 |
+
user_argument=user_argument,
|
| 165 |
+
retrieved_context=retrieved_context,
|
| 166 |
+
trap_opportunity=trap_info[1] if trap_info else None,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
opposing_response = _call_llm(opposing_messages)
|
| 171 |
+
except Exception as e:
|
| 172 |
+
logger.error(f"Opposing counsel LLM failed: {e}")
|
| 173 |
+
opposing_response = (
|
| 174 |
+
"With respect, My Lords, the submission of my learned friend "
|
| 175 |
+
"lacks the necessary legal foundation. We shall address this in detail."
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
add_transcript_entry(
|
| 179 |
+
session_id=session_id,
|
| 180 |
+
speaker="OPPOSING_COUNSEL",
|
| 181 |
+
role_label="RESPONDENT'S COUNSEL" if session["user_side"] == "petitioner" else "PETITIONER'S COUNSEL",
|
| 182 |
+
content=opposing_response,
|
| 183 |
+
entry_type="argument",
|
| 184 |
+
metadata={"trap_type": trap_info[0] if trap_info else None},
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Judge asks first question
|
| 188 |
+
judge_messages = build_judge_prompt(
|
| 189 |
+
session=get_session(session_id), # Fresh session after updates
|
| 190 |
+
last_user_argument=user_argument,
|
| 191 |
+
retrieved_context=retrieved_context,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
judge_question = _call_llm(judge_messages)
|
| 196 |
+
except Exception as e:
|
| 197 |
+
logger.error(f"Judge LLM failed: {e}")
|
| 198 |
+
judge_question = (
|
| 199 |
+
"Counsel, the court wishes to understand the precise legal foundation "
|
| 200 |
+
"for your submission. What authority do you rely upon?"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
add_transcript_entry(
|
| 204 |
+
session_id=session_id,
|
| 205 |
+
speaker="JUDGE",
|
| 206 |
+
role_label="HON'BLE COURT",
|
| 207 |
+
content=judge_question,
|
| 208 |
+
entry_type="question",
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
# Registrar announces Round 1
|
| 212 |
+
advance_phase(session_id)
|
| 213 |
+
advance_round(session_id)
|
| 214 |
+
registrar_note = build_round_announcement(session, 1, "rounds")
|
| 215 |
+
|
| 216 |
+
add_transcript_entry(
|
| 217 |
+
session_id=session_id,
|
| 218 |
+
speaker="REGISTRAR",
|
| 219 |
+
role_label="COURT REGISTRAR",
|
| 220 |
+
content=registrar_note,
|
| 221 |
+
entry_type="announcement",
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Record trap event if applicable
|
| 225 |
+
if trap_info:
|
| 226 |
+
add_trap_event(
|
| 227 |
+
session_id=session_id,
|
| 228 |
+
trap_type=trap_info[0],
|
| 229 |
+
trap_text=opposing_response,
|
| 230 |
+
user_fell_in=False,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
return {
|
| 234 |
+
"opposing_response": opposing_response,
|
| 235 |
+
"judge_question": judge_question,
|
| 236 |
+
"registrar_note": registrar_note,
|
| 237 |
+
"trap_detected": bool(trap_info),
|
| 238 |
+
"trap_warning": f"Trap detected: {trap_info[1]}" if trap_info and session.get("show_trap_warnings") else "",
|
| 239 |
+
"new_concessions": [],
|
| 240 |
+
"round_number": 1,
|
| 241 |
+
"phase": "rounds",
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _handle_round(session_id: str, user_argument: str, session: Dict) -> Dict:
|
| 246 |
+
"""Handle a standard argument round."""
|
| 247 |
+
|
| 248 |
+
current_round = session["current_round"]
|
| 249 |
+
max_rounds = session["max_rounds"]
|
| 250 |
+
user_side = session["user_side"]
|
| 251 |
+
user_label = "PETITIONER'S COUNSEL" if user_side == "petitioner" else "RESPONDENT'S COUNSEL"
|
| 252 |
+
|
| 253 |
+
# Add user argument to transcript
|
| 254 |
+
add_transcript_entry(
|
| 255 |
+
session_id=session_id,
|
| 256 |
+
speaker=user_side.upper(),
|
| 257 |
+
role_label=user_label,
|
| 258 |
+
content=user_argument,
|
| 259 |
+
entry_type="argument",
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
add_user_argument(
|
| 263 |
+
session_id=session_id,
|
| 264 |
+
argument_text=user_argument,
|
| 265 |
+
key_claims=_extract_key_claims(user_argument),
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Retrieve context
|
| 269 |
+
legal_issues = " ".join(session.get("legal_issues", []))
|
| 270 |
+
query = f"{user_argument[:200]} {legal_issues}"
|
| 271 |
+
retrieved_context = _retrieve_for_court(query, session)
|
| 272 |
+
|
| 273 |
+
# Detect traps
|
| 274 |
+
trap_info = detect_trap_opportunity(
|
| 275 |
+
user_argument,
|
| 276 |
+
session.get("user_arguments", []),
|
| 277 |
+
session,
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
# ── LLM Call 1: Opposing counsel ──────────────────────────
|
| 281 |
+
fresh_session = get_session(session_id)
|
| 282 |
+
opposing_messages = build_opposing_prompt(
|
| 283 |
+
session=fresh_session,
|
| 284 |
+
user_argument=user_argument,
|
| 285 |
+
retrieved_context=retrieved_context,
|
| 286 |
+
trap_opportunity=trap_info[1] if trap_info else None,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
try:
|
| 290 |
+
opposing_response = _call_llm(opposing_messages)
|
| 291 |
+
except Exception as e:
|
| 292 |
+
logger.error(f"Opposing LLM failed: {e}")
|
| 293 |
+
opposing_response = (
|
| 294 |
+
"My Lords, with respect, my learned friend's submission overlooks "
|
| 295 |
+
"the settled legal position on this point."
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
add_transcript_entry(
|
| 299 |
+
session_id=session_id,
|
| 300 |
+
speaker="OPPOSING_COUNSEL",
|
| 301 |
+
role_label="RESPONDENT'S COUNSEL" if user_side == "petitioner" else "PETITIONER'S COUNSEL",
|
| 302 |
+
content=opposing_response,
|
| 303 |
+
entry_type="argument",
|
| 304 |
+
metadata={"trap_type": trap_info[0] if trap_info else None},
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
if trap_info:
|
| 308 |
+
add_trap_event(
|
| 309 |
+
session_id=session_id,
|
| 310 |
+
trap_type=trap_info[0],
|
| 311 |
+
trap_text=opposing_response,
|
| 312 |
+
user_fell_in=_did_user_fall_in_trap(user_argument, trap_info[0]),
|
| 313 |
+
user_response=user_argument,
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
# ── LLM Call 2: Judge question ────────────────────────────
|
| 317 |
+
fresh_session = get_session(session_id)
|
| 318 |
+
judge_messages = build_judge_prompt(
|
| 319 |
+
session=fresh_session,
|
| 320 |
+
last_user_argument=user_argument,
|
| 321 |
+
retrieved_context=retrieved_context,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
try:
|
| 325 |
+
judge_question = _call_llm(judge_messages)
|
| 326 |
+
except Exception as e:
|
| 327 |
+
logger.error(f"Judge LLM failed: {e}")
|
| 328 |
+
judge_question = (
|
| 329 |
+
"Counsel, the court requires further elaboration on the legal basis of your submission."
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
add_transcript_entry(
|
| 333 |
+
session_id=session_id,
|
| 334 |
+
speaker="JUDGE",
|
| 335 |
+
role_label="HON'BLE COURT",
|
| 336 |
+
content=judge_question,
|
| 337 |
+
entry_type="question",
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
# ── Advance round / phase ─────────────────────────────────
|
| 341 |
+
new_round = advance_round(session_id)
|
| 342 |
+
fresh_session = get_session(session_id)
|
| 343 |
+
new_phase = fresh_session["phase"]
|
| 344 |
+
|
| 345 |
+
# Registrar announcement
|
| 346 |
+
if new_phase == "cross_examination":
|
| 347 |
+
registrar_note = build_round_announcement(session, new_round, "cross_examination")
|
| 348 |
+
elif new_round <= max_rounds:
|
| 349 |
+
registrar_note = build_round_announcement(session, new_round, "rounds")
|
| 350 |
+
else:
|
| 351 |
+
registrar_note = build_round_announcement(session, new_round, "closing")
|
| 352 |
+
|
| 353 |
+
add_transcript_entry(
|
| 354 |
+
session_id=session_id,
|
| 355 |
+
speaker="REGISTRAR",
|
| 356 |
+
role_label="COURT REGISTRAR",
|
| 357 |
+
content=registrar_note,
|
| 358 |
+
entry_type="announcement",
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# Detect concessions
|
| 362 |
+
new_concessions = _detect_concessions(user_argument, session_id, current_round)
|
| 363 |
+
|
| 364 |
+
return {
|
| 365 |
+
"opposing_response": opposing_response,
|
| 366 |
+
"judge_question": judge_question,
|
| 367 |
+
"registrar_note": registrar_note,
|
| 368 |
+
"trap_detected": bool(trap_info),
|
| 369 |
+
"trap_warning": f"Potential trap in opposing counsel's last statement" if trap_info and session.get("show_trap_warnings") else "",
|
| 370 |
+
"new_concessions": new_concessions,
|
| 371 |
+
"round_number": new_round,
|
| 372 |
+
"phase": new_phase,
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def _handle_cross_exam_answer(
|
| 377 |
+
session_id: str,
|
| 378 |
+
user_answer: str,
|
| 379 |
+
session: Dict,
|
| 380 |
+
) -> Dict:
|
| 381 |
+
"""Handle user's answer during cross-examination."""
|
| 382 |
+
|
| 383 |
+
user_side = session["user_side"]
|
| 384 |
+
user_label = "PETITIONER'S COUNSEL" if user_side == "petitioner" else "RESPONDENT'S COUNSEL"
|
| 385 |
+
|
| 386 |
+
# Count how many cross-exam questions have been asked
|
| 387 |
+
cross_entries = [
|
| 388 |
+
e for e in session.get("transcript", [])
|
| 389 |
+
if e.get("phase") == "cross_examination"
|
| 390 |
+
]
|
| 391 |
+
question_number = len([e for e in cross_entries if e.get("speaker") == "OPPOSING_COUNSEL"]) + 1
|
| 392 |
+
|
| 393 |
+
# Add user's answer
|
| 394 |
+
add_transcript_entry(
|
| 395 |
+
session_id=session_id,
|
| 396 |
+
speaker=user_side.upper(),
|
| 397 |
+
role_label=user_label,
|
| 398 |
+
content=user_answer,
|
| 399 |
+
entry_type="answer",
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
# Detect concessions in answer
|
| 403 |
+
new_concessions = _detect_concessions(user_answer, session_id, session["current_round"])
|
| 404 |
+
|
| 405 |
+
# If more questions remaining (max 3), get next question
|
| 406 |
+
if question_number < 3:
|
| 407 |
+
query = " ".join(session.get("legal_issues", []))
|
| 408 |
+
retrieved_context = _retrieve_for_court(query, session)
|
| 409 |
+
|
| 410 |
+
fresh_session = get_session(session_id)
|
| 411 |
+
cross_messages = build_cross_examination_prompt(
|
| 412 |
+
session=fresh_session,
|
| 413 |
+
question_number=question_number + 1,
|
| 414 |
+
retrieved_context=retrieved_context,
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
try:
|
| 418 |
+
next_question = _call_llm(cross_messages)
|
| 419 |
+
except Exception as e:
|
| 420 |
+
next_question = f"Question {question_number + 1}: Counsel, would you agree that [question]?"
|
| 421 |
+
|
| 422 |
+
add_transcript_entry(
|
| 423 |
+
session_id=session_id,
|
| 424 |
+
speaker="OPPOSING_COUNSEL",
|
| 425 |
+
role_label="RESPONDENT'S COUNSEL" if user_side == "petitioner" else "PETITIONER'S COUNSEL",
|
| 426 |
+
content=next_question,
|
| 427 |
+
entry_type="question",
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
return {
|
| 431 |
+
"opposing_response": next_question,
|
| 432 |
+
"judge_question": "",
|
| 433 |
+
"registrar_note": f"Question {question_number + 1} of 3.",
|
| 434 |
+
"trap_detected": False,
|
| 435 |
+
"trap_warning": "",
|
| 436 |
+
"new_concessions": new_concessions,
|
| 437 |
+
"round_number": session["current_round"],
|
| 438 |
+
"phase": "cross_examination",
|
| 439 |
+
"cross_exam_complete": False,
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
else:
|
| 443 |
+
# Cross-examination complete — advance to closing
|
| 444 |
+
advance_phase(session_id)
|
| 445 |
+
registrar_note = build_round_announcement(session, session["current_round"], "closing")
|
| 446 |
+
|
| 447 |
+
add_transcript_entry(
|
| 448 |
+
session_id=session_id,
|
| 449 |
+
speaker="REGISTRAR",
|
| 450 |
+
role_label="COURT REGISTRAR",
|
| 451 |
+
content=registrar_note,
|
| 452 |
+
entry_type="announcement",
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
return {
|
| 456 |
+
"opposing_response": "",
|
| 457 |
+
"judge_question": "",
|
| 458 |
+
"registrar_note": registrar_note,
|
| 459 |
+
"trap_detected": False,
|
| 460 |
+
"trap_warning": "",
|
| 461 |
+
"new_concessions": new_concessions,
|
| 462 |
+
"round_number": session["current_round"],
|
| 463 |
+
"phase": "closing",
|
| 464 |
+
"cross_exam_complete": True,
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _handle_closing(session_id: str, user_closing: str, session: Dict) -> Dict:
|
| 469 |
+
"""Handle closing arguments from both sides, then generate analysis."""
|
| 470 |
+
|
| 471 |
+
user_side = session["user_side"]
|
| 472 |
+
user_label = "PETITIONER'S COUNSEL" if user_side == "petitioner" else "RESPONDENT'S COUNSEL"
|
| 473 |
+
|
| 474 |
+
# Add user's closing
|
| 475 |
+
add_transcript_entry(
|
| 476 |
+
session_id=session_id,
|
| 477 |
+
speaker=user_side.upper(),
|
| 478 |
+
role_label=user_label,
|
| 479 |
+
content=user_closing,
|
| 480 |
+
entry_type="closing_argument",
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
fresh_session = get_session(session_id)
|
| 484 |
+
|
| 485 |
+
# Opposing counsel's closing
|
| 486 |
+
closing_messages = build_opposing_closing_prompt(fresh_session)
|
| 487 |
+
try:
|
| 488 |
+
opposing_closing = _call_llm(closing_messages)
|
| 489 |
+
except Exception as e:
|
| 490 |
+
opposing_closing = (
|
| 491 |
+
"My Lords, for the reasons submitted throughout these proceedings, "
|
| 492 |
+
"we respectfully submit that the petition deserves to be dismissed."
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
add_transcript_entry(
|
| 496 |
+
session_id=session_id,
|
| 497 |
+
speaker="OPPOSING_COUNSEL",
|
| 498 |
+
role_label="RESPONDENT'S COUNSEL" if user_side == "petitioner" else "PETITIONER'S COUNSEL",
|
| 499 |
+
content=opposing_closing,
|
| 500 |
+
entry_type="closing_argument",
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
# Judge's final observations
|
| 504 |
+
fresh_session = get_session(session_id)
|
| 505 |
+
judge_closing_messages = build_judge_closing_prompt(fresh_session)
|
| 506 |
+
try:
|
| 507 |
+
judge_final = _call_llm(judge_closing_messages)
|
| 508 |
+
except Exception as e:
|
| 509 |
+
judge_final = (
|
| 510 |
+
"The court has heard submissions from both sides. "
|
| 511 |
+
"The matter is reserved for orders."
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
add_transcript_entry(
|
| 515 |
+
session_id=session_id,
|
| 516 |
+
speaker="JUDGE",
|
| 517 |
+
role_label="HON'BLE COURT",
|
| 518 |
+
content=judge_final,
|
| 519 |
+
entry_type="observation",
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
# Registrar closes session
|
| 523 |
+
registrar_final = build_round_announcement(session, session["current_round"], "completed")
|
| 524 |
+
add_transcript_entry(
|
| 525 |
+
session_id=session_id,
|
| 526 |
+
speaker="REGISTRAR",
|
| 527 |
+
role_label="COURT REGISTRAR",
|
| 528 |
+
content=registrar_final,
|
| 529 |
+
entry_type="announcement",
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
advance_phase(session_id)
|
| 533 |
+
|
| 534 |
+
return {
|
| 535 |
+
"opposing_response": opposing_closing,
|
| 536 |
+
"judge_question": judge_final,
|
| 537 |
+
"registrar_note": registrar_final,
|
| 538 |
+
"trap_detected": False,
|
| 539 |
+
"trap_warning": "",
|
| 540 |
+
"new_concessions": [],
|
| 541 |
+
"round_number": session["current_round"],
|
| 542 |
+
"phase": "completed",
|
| 543 |
+
"ready_for_analysis": True,
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def generate_session_analysis(session_id: str) -> Dict:
|
| 548 |
+
"""
|
| 549 |
+
Call the Summariser agent to generate full session analysis.
|
| 550 |
+
Called after phase == "completed".
|
| 551 |
+
"""
|
| 552 |
+
session = get_session(session_id)
|
| 553 |
+
if not session:
|
| 554 |
+
return {"error": "Session not found"}
|
| 555 |
+
|
| 556 |
+
if session["phase"] != "completed":
|
| 557 |
+
return {"error": "Session not yet completed"}
|
| 558 |
+
|
| 559 |
+
summariser_messages = build_summariser_prompt(session)
|
| 560 |
+
|
| 561 |
+
try:
|
| 562 |
+
raw_analysis = _call_llm(summariser_messages)
|
| 563 |
+
except Exception as e:
|
| 564 |
+
logger.error(f"Summariser LLM failed: {e}")
|
| 565 |
+
raw_analysis = (
|
| 566 |
+
"## OVERALL ASSESSMENT\n"
|
| 567 |
+
"The moot court session has concluded. Analysis generation encountered an error.\n\n"
|
| 568 |
+
"## PERFORMANCE SCORE\n5.0/10\n\n"
|
| 569 |
+
"## OUTCOME PREDICTION\nUNKNOWN\n\n"
|
| 570 |
+
"## FULL TRANSCRIPT\n" + _get_transcript_text(session)
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
parsed = parse_analysis(raw_analysis, session)
|
| 574 |
+
|
| 575 |
+
update_session(session_id, {
|
| 576 |
+
"analysis": parsed,
|
| 577 |
+
"outcome_prediction": parsed.get("outcome_prediction", "unknown"),
|
| 578 |
+
"performance_score": parsed.get("performance_score", 0.0),
|
| 579 |
+
})
|
| 580 |
+
|
| 581 |
+
return parsed
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def process_objection(
|
| 585 |
+
session_id: str,
|
| 586 |
+
objection_type: str,
|
| 587 |
+
objection_text: str,
|
| 588 |
+
) -> Dict:
|
| 589 |
+
"""Handle a user-raised objection."""
|
| 590 |
+
session = get_session(session_id)
|
| 591 |
+
if not session:
|
| 592 |
+
return {"error": "Session not found"}
|
| 593 |
+
|
| 594 |
+
# Get what the objection is about from last transcript entry
|
| 595 |
+
transcript = session.get("transcript", [])
|
| 596 |
+
last_entry = transcript[-1] if transcript else {}
|
| 597 |
+
objected_to = last_entry.get("content", "the last submission")[:200]
|
| 598 |
+
|
| 599 |
+
# Judge rules on objection
|
| 600 |
+
ruling_messages = build_objection_ruling_prompt(
|
| 601 |
+
session=session,
|
| 602 |
+
objection_type=objection_type,
|
| 603 |
+
objection_text=objection_text,
|
| 604 |
+
what_was_objected_to=objected_to,
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
try:
|
| 608 |
+
ruling = _call_llm(ruling_messages)
|
| 609 |
+
except Exception as e:
|
| 610 |
+
ruling = "Objection overruled. Counsel may proceed."
|
| 611 |
+
|
| 612 |
+
# Add to transcript
|
| 613 |
+
add_transcript_entry(
|
| 614 |
+
session_id=session_id,
|
| 615 |
+
speaker="JUDGE",
|
| 616 |
+
role_label="HON'BLE COURT",
|
| 617 |
+
content=ruling,
|
| 618 |
+
entry_type="ruling",
|
| 619 |
+
metadata={"objection_type": objection_type},
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
sustained = "sustained" in ruling.lower()
|
| 623 |
+
|
| 624 |
+
return {
|
| 625 |
+
"ruling": ruling,
|
| 626 |
+
"sustained": sustained,
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def process_document_request(
|
| 631 |
+
session_id: str,
|
| 632 |
+
doc_type: str,
|
| 633 |
+
for_side: str,
|
| 634 |
+
) -> Dict:
|
| 635 |
+
"""Generate a legal document for the session."""
|
| 636 |
+
session = get_session(session_id)
|
| 637 |
+
if not session:
|
| 638 |
+
return {"error": "Session not found"}
|
| 639 |
+
|
| 640 |
+
system = f"""You are a legal document generator specializing in Indian court documents.
|
| 641 |
+
|
| 642 |
+
Generate a complete, properly formatted {doc_type} for a Supreme Court of India proceeding.
|
| 643 |
+
|
| 644 |
+
FORMAT REQUIREMENTS:
|
| 645 |
+
- Use proper Indian court document format
|
| 646 |
+
- Include: IN THE SUPREME COURT OF INDIA header
|
| 647 |
+
- Include: Case title, W.P./Crl.A./C.A. number placeholder
|
| 648 |
+
- Include: Parties section
|
| 649 |
+
- Include: Prayer/relief sought
|
| 650 |
+
- Include: Verification clause where applicable
|
| 651 |
+
- End with: Signature block with "Counsel for the [Petitioner/Respondent]"
|
| 652 |
+
|
| 653 |
+
The document must look like a real Indian court document.
|
| 654 |
+
Use the case details provided. Be specific — no placeholders except where genuinely needed."""
|
| 655 |
+
|
| 656 |
+
case_brief = session.get("case_brief", "")
|
| 657 |
+
user_side = session.get("user_side", "petitioner")
|
| 658 |
+
|
| 659 |
+
filer = for_side if for_side != "yours" else user_side
|
| 660 |
+
|
| 661 |
+
user_content = f"""Generate a {doc_type} for the following case:
|
| 662 |
+
|
| 663 |
+
{case_brief[:800]}
|
| 664 |
+
|
| 665 |
+
Document is filed by: {filer}
|
| 666 |
+
Legal issues: {', '.join(session.get('legal_issues', []))}
|
| 667 |
+
|
| 668 |
+
Generate the complete document."""
|
| 669 |
+
|
| 670 |
+
try:
|
| 671 |
+
document_text = _call_llm([
|
| 672 |
+
{"role": "system", "content": system},
|
| 673 |
+
{"role": "user", "content": user_content}
|
| 674 |
+
])
|
| 675 |
+
except Exception as e:
|
| 676 |
+
logger.error(f"Document generation failed: {e}")
|
| 677 |
+
document_text = f"[Document generation failed: {e}]"
|
| 678 |
+
|
| 679 |
+
doc_entry = {
|
| 680 |
+
"type": doc_type,
|
| 681 |
+
"for_side": for_side,
|
| 682 |
+
"content": document_text,
|
| 683 |
+
"generated_at": __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
| 684 |
+
"round": session.get("current_round", 0),
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
session_obj = get_session(session_id)
|
| 688 |
+
docs = session_obj.get("documents_produced", [])
|
| 689 |
+
docs.append(doc_entry)
|
| 690 |
+
update_session(session_id, {"documents_produced": docs})
|
| 691 |
+
|
| 692 |
+
# Registrar announcement
|
| 693 |
+
registrar_note = get_document_announcement(doc_type, f"{filer}'s counsel")
|
| 694 |
+
add_transcript_entry(
|
| 695 |
+
session_id=session_id,
|
| 696 |
+
speaker="REGISTRAR",
|
| 697 |
+
role_label="COURT REGISTRAR",
|
| 698 |
+
content=registrar_note,
|
| 699 |
+
entry_type="document",
|
| 700 |
+
metadata={"doc_type": doc_type},
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
return {
|
| 704 |
+
"document": document_text,
|
| 705 |
+
"doc_type": doc_type,
|
| 706 |
+
"registrar_note": registrar_note,
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def start_cross_examination(session_id: str) -> Dict:
|
| 711 |
+
"""
|
| 712 |
+
Initiate cross-examination phase.
|
| 713 |
+
Opposing counsel asks the first question.
|
| 714 |
+
"""
|
| 715 |
+
session = get_session(session_id)
|
| 716 |
+
if not session:
|
| 717 |
+
return {"error": "Session not found"}
|
| 718 |
+
|
| 719 |
+
advance_phase(session_id)
|
| 720 |
+
fresh_session = get_session(session_id)
|
| 721 |
+
|
| 722 |
+
query = " ".join(fresh_session.get("legal_issues", []))
|
| 723 |
+
retrieved_context = _retrieve_for_court(query, fresh_session)
|
| 724 |
+
|
| 725 |
+
cross_messages = build_cross_examination_prompt(
|
| 726 |
+
session=fresh_session,
|
| 727 |
+
question_number=1,
|
| 728 |
+
retrieved_context=retrieved_context,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
try:
|
| 732 |
+
first_question = _call_llm(cross_messages)
|
| 733 |
+
except Exception as e:
|
| 734 |
+
first_question = "Counsel, would you agree that the right you rely upon is subject to reasonable restrictions under the Constitution?"
|
| 735 |
+
|
| 736 |
+
user_side = session.get("user_side", "petitioner")
|
| 737 |
+
add_transcript_entry(
|
| 738 |
+
session_id=session_id,
|
| 739 |
+
speaker="OPPOSING_COUNSEL",
|
| 740 |
+
role_label="RESPONDENT'S COUNSEL" if user_side == "petitioner" else "PETITIONER'S COUNSEL",
|
| 741 |
+
content=first_question,
|
| 742 |
+
entry_type="question",
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
registrar_note = build_round_announcement(session, session["current_round"], "cross_examination")
|
| 746 |
+
add_transcript_entry(
|
| 747 |
+
session_id=session_id,
|
| 748 |
+
speaker="REGISTRAR",
|
| 749 |
+
role_label="COURT REGISTRAR",
|
| 750 |
+
content=registrar_note,
|
| 751 |
+
entry_type="announcement",
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
return {
|
| 755 |
+
"first_question": first_question,
|
| 756 |
+
"registrar_note": registrar_note,
|
| 757 |
+
"phase": "cross_examination",
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
# ── Helper functions ───────────────────────────────────────────
|
| 762 |
+
|
| 763 |
+
def _extract_key_claims(argument_text: str) -> List[str]:
|
| 764 |
+
"""Extract key legal claims from argument text for inconsistency tracking."""
|
| 765 |
+
import re
|
| 766 |
+
|
| 767 |
+
claim_patterns = [
|
| 768 |
+
r'(?:the\s+)?(?:right|provision|section|article)\s+(?:to\s+)?[\w\s]{5,40}',
|
| 769 |
+
r'(?:is|was|are|were)\s+(?:not|never|always|clearly)\s+[\w\s]{5,30}',
|
| 770 |
+
r'(?:cannot|shall not|must not)\s+[\w\s]{5,30}',
|
| 771 |
+
]
|
| 772 |
+
|
| 773 |
+
claims = []
|
| 774 |
+
text_lower = argument_text.lower()
|
| 775 |
+
|
| 776 |
+
for pattern in claim_patterns:
|
| 777 |
+
matches = re.findall(pattern, text_lower)
|
| 778 |
+
claims.extend([m.strip()[:80] for m in matches[:2]])
|
| 779 |
+
|
| 780 |
+
return claims[:5]
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def _detect_concessions(
|
| 784 |
+
text: str,
|
| 785 |
+
session_id: str,
|
| 786 |
+
round_number: int,
|
| 787 |
+
) -> List[Dict]:
|
| 788 |
+
"""
|
| 789 |
+
Simple rule-based concession detector.
|
| 790 |
+
Looks for concession language patterns.
|
| 791 |
+
"""
|
| 792 |
+
import re
|
| 793 |
+
|
| 794 |
+
concession_patterns = [
|
| 795 |
+
(r'(?:i\s+)?(?:accept|concede|admit|acknowledge)\s+that\s+([^.]{20,200})', "Direct concession"),
|
| 796 |
+
(r'(?:you\s+are\s+right|that\s+is\s+correct|i\s+agree)\s+(?:that\s+)?([^.]{20,200})', "Agreement concession"),
|
| 797 |
+
(r'(?:while|although|even\s+though)\s+(?:i\s+)?(?:accept|concede|admit)\s+([^.]{20,200})', "Qualified concession"),
|
| 798 |
+
(r'(?:for\s+the\s+purposes\s+of\s+this\s+argument|without\s+prejudice)\s+([^.]{20,200})', "Argumentative concession"),
|
| 799 |
+
]
|
| 800 |
+
|
| 801 |
+
new_concessions = []
|
| 802 |
+
text_lower = text.lower()
|
| 803 |
+
|
| 804 |
+
for pattern, significance_prefix in concession_patterns:
|
| 805 |
+
matches = re.finditer(pattern, text_lower)
|
| 806 |
+
for match in matches:
|
| 807 |
+
quote = match.group(0)[:150]
|
| 808 |
+
conceded_point = match.group(1)[:100] if match.lastindex else quote
|
| 809 |
+
|
| 810 |
+
add_concession(
|
| 811 |
+
session_id=session_id,
|
| 812 |
+
exact_quote=quote,
|
| 813 |
+
legal_significance=f"{significance_prefix}: {conceded_point}",
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
new_concessions.append({
|
| 817 |
+
"quote": quote,
|
| 818 |
+
"significance": f"{significance_prefix}: {conceded_point}",
|
| 819 |
+
"round": round_number,
|
| 820 |
+
})
|
| 821 |
+
|
| 822 |
+
return new_concessions
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def _did_user_fall_in_trap(user_argument: str, trap_type: str) -> bool:
|
| 826 |
+
"""
|
| 827 |
+
Heuristic to determine if user fell into a trap.
|
| 828 |
+
Returns True if user's argument shows they took the bait.
|
| 829 |
+
"""
|
| 830 |
+
arg_lower = user_argument.lower()
|
| 831 |
+
|
| 832 |
+
if trap_type == "admission_trap":
|
| 833 |
+
# User fell in if they agreed with the trap statement
|
| 834 |
+
fall_markers = ["yes", "agree", "correct", "right", "indeed", "certainly", "that is true"]
|
| 835 |
+
return any(marker in arg_lower[:200] for marker in fall_markers)
|
| 836 |
+
|
| 837 |
+
elif trap_type == "inconsistency_trap":
|
| 838 |
+
# User fell in if they tried to reconcile the inconsistency poorly
|
| 839 |
+
fall_markers = ["both", "however", "but in this case", "that was different"]
|
| 840 |
+
return any(marker in arg_lower[:200] for marker in fall_markers)
|
| 841 |
+
|
| 842 |
+
return False
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
def _get_transcript_text(session: Dict) -> str:
|
| 846 |
+
"""Simple transcript formatter for fallback."""
|
| 847 |
+
transcript = session.get("transcript", [])
|
| 848 |
+
lines = []
|
| 849 |
+
for entry in transcript:
|
| 850 |
+
lines.append(f"{entry['role_label'].upper()}: {entry['content']}")
|
| 851 |
+
lines.append("")
|
| 852 |
+
return "\n".join(lines)
|
src/court/registrar.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Court Registrar Agent.
|
| 3 |
+
|
| 4 |
+
Procedural voice. Manages session flow. Keeps both sides accountable.
|
| 5 |
+
Only speaks twice per round:
|
| 6 |
+
1. Opening announcement for the round
|
| 7 |
+
2. Closing note if anything was left unaddressed
|
| 8 |
+
|
| 9 |
+
Uses only the transcript — no retrieval needed.
|
| 10 |
+
Runs on a fast, simple prompt — not the full LLM.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, List
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
REGISTRAR_SYSTEM_PROMPT = """You are the Court Registrar in an Indian Supreme Court moot court simulation.
|
| 19 |
+
|
| 20 |
+
YOUR ROLE:
|
| 21 |
+
You manage procedure. You are formal, neutral, bureaucratic.
|
| 22 |
+
You speak in short, precise announcements.
|
| 23 |
+
|
| 24 |
+
YOU SPEAK IN TWO SITUATIONS ONLY:
|
| 25 |
+
1. At the start of each round/phase — announce what is happening
|
| 26 |
+
2. When counsel has left something unaddressed — flag it
|
| 27 |
+
|
| 28 |
+
YOUR LANGUAGE:
|
| 29 |
+
- "The court is now in session."
|
| 30 |
+
- "Counsel for the petitioner may proceed with Round [N]."
|
| 31 |
+
- "The court notes that counsel has not addressed [specific observation from previous round]."
|
| 32 |
+
- "Cross-examination will now commence. Respondent's counsel will proceed."
|
| 33 |
+
- "The court stands adjourned pending deliberation."
|
| 34 |
+
|
| 35 |
+
NEVER give legal analysis. Never express an opinion. Never take sides.
|
| 36 |
+
Your job is procedure and record-keeping only.
|
| 37 |
+
|
| 38 |
+
Keep all announcements to 1-3 sentences."""
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_round_announcement(
|
| 42 |
+
session: Dict,
|
| 43 |
+
round_number: int,
|
| 44 |
+
phase: str,
|
| 45 |
+
) -> str:
|
| 46 |
+
"""
|
| 47 |
+
Generate registrar's round opening announcement.
|
| 48 |
+
This is deterministic — no LLM call needed for standard announcements.
|
| 49 |
+
"""
|
| 50 |
+
user_side = session.get("user_side", "petitioner")
|
| 51 |
+
user_label = "Petitioner's Counsel" if user_side == "petitioner" else "Respondent's Counsel"
|
| 52 |
+
max_rounds = session.get("max_rounds", 5)
|
| 53 |
+
|
| 54 |
+
announcements = {
|
| 55 |
+
"briefing": (
|
| 56 |
+
f"The court is now in session. "
|
| 57 |
+
f"Case: {session.get('case_title', 'Present Matter')}. "
|
| 58 |
+
f"The bench has perused the case brief. "
|
| 59 |
+
f"{user_label} may proceed with opening submissions."
|
| 60 |
+
),
|
| 61 |
+
"rounds": (
|
| 62 |
+
f"Round {round_number} of {max_rounds}. "
|
| 63 |
+
f"{user_label} may proceed."
|
| 64 |
+
),
|
| 65 |
+
"cross_examination": (
|
| 66 |
+
f"The court now moves to cross-examination. "
|
| 67 |
+
f"Opposing counsel will put three questions to {user_label}. "
|
| 68 |
+
f"Counsel is directed to answer specifically."
|
| 69 |
+
),
|
| 70 |
+
"closing": (
|
| 71 |
+
f"The court will now hear closing arguments. "
|
| 72 |
+
f"{user_label} may begin."
|
| 73 |
+
),
|
| 74 |
+
"completed": (
|
| 75 |
+
f"The court stands adjourned. "
|
| 76 |
+
f"A formal analysis of the proceedings will now be prepared."
|
| 77 |
+
),
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return announcements.get(phase, f"The court will now proceed with {phase}.")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def build_accountability_note(
|
| 84 |
+
session: Dict,
|
| 85 |
+
unaddressed_items: List[str],
|
| 86 |
+
) -> str:
|
| 87 |
+
"""
|
| 88 |
+
Generate a registrar note when counsel left something unaddressed.
|
| 89 |
+
Only called when there ARE unaddressed items.
|
| 90 |
+
"""
|
| 91 |
+
if not unaddressed_items:
|
| 92 |
+
return ""
|
| 93 |
+
|
| 94 |
+
if len(unaddressed_items) == 1:
|
| 95 |
+
return (
|
| 96 |
+
f"The court notes that counsel has not addressed "
|
| 97 |
+
f"the following observation from the previous round: "
|
| 98 |
+
f"{unaddressed_items[0]}"
|
| 99 |
+
)
|
| 100 |
+
else:
|
| 101 |
+
items_text = "; ".join(unaddressed_items[:2])
|
| 102 |
+
return (
|
| 103 |
+
f"The court notes that counsel has not addressed "
|
| 104 |
+
f"the following observations from the previous round: {items_text}."
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def get_objection_announcement(ruling: str, objection_type: str) -> str:
|
| 109 |
+
"""Format registrar's recording of an objection ruling."""
|
| 110 |
+
return f"Objection noted. The court has ruled: {ruling}"
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def get_document_announcement(doc_type: str, filed_by: str) -> str:
|
| 114 |
+
"""Announce when a document is produced."""
|
| 115 |
+
return (
|
| 116 |
+
f"The court notes that {filed_by} has produced "
|
| 117 |
+
f"{doc_type} which has been taken on record."
|
| 118 |
+
)
|
src/court/session.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Court Session Manager.
|
| 3 |
+
|
| 4 |
+
Single source of truth for everything that happens in a moot court session.
|
| 5 |
+
Every agent reads from and writes to the session object.
|
| 6 |
+
Sessions persist to HuggingFace Dataset for durability across container restarts.
|
| 7 |
+
|
| 8 |
+
Session lifecycle:
|
| 9 |
+
created → briefing → rounds → cross_examination → closing → completed
|
| 10 |
+
|
| 11 |
+
WHY store to HF Dataset?
|
| 12 |
+
HF Spaces containers are ephemeral. Without durable storage, all session
|
| 13 |
+
data is lost on restart. HF Dataset API gives us free durable storage
|
| 14 |
+
using the same HF_TOKEN already in the Space secrets.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import json
|
| 19 |
+
import uuid
|
| 20 |
+
import logging
|
| 21 |
+
from datetime import datetime, timezone
|
| 22 |
+
from typing import Optional, Dict, List, Any
|
| 23 |
+
from dataclasses import dataclass, field, asdict
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 28 |
+
SESSIONS_REPO = "CaffeinatedCoding/nyayasetu-court-sessions"
|
| 29 |
+
|
| 30 |
+
# ── In-memory session store ────────────────────────────────────
|
| 31 |
+
# Primary store during runtime. HF Dataset is the durable backup.
|
| 32 |
+
_sessions: Dict[str, Dict] = {}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ── Data structures ────────────────────────────────────────────
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class TranscriptEntry:
|
| 39 |
+
"""A single entry in the court transcript."""
|
| 40 |
+
speaker: str # JUDGE | OPPOSING_COUNSEL | REGISTRAR | PETITIONER | RESPONDENT
|
| 41 |
+
role_label: str # Display label e.g. "HON'BLE COURT", "RESPONDENT'S COUNSEL"
|
| 42 |
+
content: str # The actual text
|
| 43 |
+
round_number: int # Which round this belongs to
|
| 44 |
+
phase: str # briefing | argument | cross_examination | closing
|
| 45 |
+
timestamp: str # ISO timestamp
|
| 46 |
+
entry_type: str # argument | question | observation | objection | ruling | document | trap
|
| 47 |
+
metadata: Dict = field(default_factory=dict) # extra data e.g. trap_type, precedents_cited
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class Concession:
|
| 52 |
+
"""A concession made by the user during the session."""
|
| 53 |
+
round_number: int
|
| 54 |
+
exact_quote: str # The exact text where concession was made
|
| 55 |
+
legal_significance: str # What opposing counsel can do with this
|
| 56 |
+
exploited: bool = False # Has opposing counsel used this yet
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclass
|
| 60 |
+
class TrapEvent:
|
| 61 |
+
"""A trap set by opposing counsel."""
|
| 62 |
+
round_number: int
|
| 63 |
+
trap_type: str # admission_trap | precedent_trap | inconsistency_trap
|
| 64 |
+
trap_text: str # What opposing counsel said to set the trap
|
| 65 |
+
user_fell_in: bool # Whether user fell into the trap
|
| 66 |
+
user_response: str = "" # What user said in response
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclass
|
| 70 |
+
class CourtSession:
|
| 71 |
+
"""Complete court session state."""
|
| 72 |
+
|
| 73 |
+
# Identity
|
| 74 |
+
session_id: str
|
| 75 |
+
created_at: str
|
| 76 |
+
updated_at: str
|
| 77 |
+
|
| 78 |
+
# Case
|
| 79 |
+
case_title: str
|
| 80 |
+
user_side: str # petitioner | respondent
|
| 81 |
+
user_client: str
|
| 82 |
+
opposing_party: str
|
| 83 |
+
legal_issues: List[str]
|
| 84 |
+
brief_facts: str
|
| 85 |
+
jurisdiction: str # supreme_court | high_court | district_court
|
| 86 |
+
|
| 87 |
+
# Setup
|
| 88 |
+
bench_composition: str # single | division | constitutional
|
| 89 |
+
difficulty: str # moot | standard | adversarial
|
| 90 |
+
session_length: str # brief | standard | extended
|
| 91 |
+
show_trap_warnings: bool
|
| 92 |
+
|
| 93 |
+
# Derived from research session import
|
| 94 |
+
imported_from_session: Optional[str] # NyayaSetu research session ID
|
| 95 |
+
case_brief: str # Generated case brief text
|
| 96 |
+
retrieved_precedents: List[Dict] # Precedents from research session
|
| 97 |
+
|
| 98 |
+
# Session progress
|
| 99 |
+
phase: str # briefing | rounds | cross_examination | closing | completed
|
| 100 |
+
current_round: int
|
| 101 |
+
max_rounds: int # 3 | 5 | 8
|
| 102 |
+
|
| 103 |
+
# Transcript
|
| 104 |
+
transcript: List[Dict] # List of TranscriptEntry as dicts
|
| 105 |
+
|
| 106 |
+
# Tracking
|
| 107 |
+
concessions: List[Dict] # List of Concession as dicts
|
| 108 |
+
trap_events: List[Dict] # List of TrapEvent as dicts
|
| 109 |
+
cited_precedents: List[str] # Judgment IDs cited during session
|
| 110 |
+
documents_produced: List[Dict] # Documents generated during session
|
| 111 |
+
|
| 112 |
+
# Arguments tracking for inconsistency detection
|
| 113 |
+
user_arguments: List[Dict] # [{round, text, key_claims: []}]
|
| 114 |
+
|
| 115 |
+
# Analysis (populated at end)
|
| 116 |
+
analysis: Optional[Dict]
|
| 117 |
+
outcome_prediction: Optional[str]
|
| 118 |
+
performance_score: Optional[float]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def create_session(
|
| 122 |
+
case_title: str,
|
| 123 |
+
user_side: str,
|
| 124 |
+
user_client: str,
|
| 125 |
+
opposing_party: str,
|
| 126 |
+
legal_issues: List[str],
|
| 127 |
+
brief_facts: str,
|
| 128 |
+
jurisdiction: str,
|
| 129 |
+
bench_composition: str,
|
| 130 |
+
difficulty: str,
|
| 131 |
+
session_length: str,
|
| 132 |
+
show_trap_warnings: bool,
|
| 133 |
+
imported_from_session: Optional[str] = None,
|
| 134 |
+
case_brief: str = "",
|
| 135 |
+
retrieved_precedents: Optional[List[Dict]] = None,
|
| 136 |
+
) -> str:
|
| 137 |
+
"""
|
| 138 |
+
Create a new court session. Returns session_id.
|
| 139 |
+
"""
|
| 140 |
+
session_id = str(uuid.uuid4())
|
| 141 |
+
now = datetime.now(timezone.utc).isoformat()
|
| 142 |
+
|
| 143 |
+
max_rounds_map = {"brief": 3, "standard": 5, "extended": 8}
|
| 144 |
+
|
| 145 |
+
session = CourtSession(
|
| 146 |
+
session_id=session_id,
|
| 147 |
+
created_at=now,
|
| 148 |
+
updated_at=now,
|
| 149 |
+
case_title=case_title,
|
| 150 |
+
user_side=user_side,
|
| 151 |
+
user_client=user_client,
|
| 152 |
+
opposing_party=opposing_party,
|
| 153 |
+
legal_issues=legal_issues,
|
| 154 |
+
brief_facts=brief_facts,
|
| 155 |
+
jurisdiction=jurisdiction,
|
| 156 |
+
bench_composition=bench_composition,
|
| 157 |
+
difficulty=difficulty,
|
| 158 |
+
session_length=session_length,
|
| 159 |
+
show_trap_warnings=show_trap_warnings,
|
| 160 |
+
imported_from_session=imported_from_session,
|
| 161 |
+
case_brief=case_brief,
|
| 162 |
+
retrieved_precedents=retrieved_precedents or [],
|
| 163 |
+
phase="briefing",
|
| 164 |
+
current_round=0,
|
| 165 |
+
max_rounds=max_rounds_map.get(session_length, 5),
|
| 166 |
+
transcript=[],
|
| 167 |
+
concessions=[],
|
| 168 |
+
trap_events=[],
|
| 169 |
+
cited_precedents=[],
|
| 170 |
+
documents_produced=[],
|
| 171 |
+
user_arguments=[],
|
| 172 |
+
analysis=None,
|
| 173 |
+
outcome_prediction=None,
|
| 174 |
+
performance_score=None,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
_sessions[session_id] = asdict(session)
|
| 178 |
+
logger.info(f"Session created: {session_id} | {case_title}")
|
| 179 |
+
|
| 180 |
+
return session_id
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_session(session_id: str) -> Optional[Dict]:
|
| 184 |
+
"""Get session from memory. Returns None if not found."""
|
| 185 |
+
return _sessions.get(session_id)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def update_session(session_id: str, updates: Dict) -> bool:
|
| 189 |
+
"""Apply updates to session and persist to HF."""
|
| 190 |
+
if session_id not in _sessions:
|
| 191 |
+
logger.warning(f"Session not found: {session_id}")
|
| 192 |
+
return False
|
| 193 |
+
|
| 194 |
+
_sessions[session_id].update(updates)
|
| 195 |
+
_sessions[session_id]["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 196 |
+
|
| 197 |
+
# Async persist to HF Dataset
|
| 198 |
+
_persist_session(session_id)
|
| 199 |
+
|
| 200 |
+
return True
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def add_transcript_entry(
|
| 204 |
+
session_id: str,
|
| 205 |
+
speaker: str,
|
| 206 |
+
role_label: str,
|
| 207 |
+
content: str,
|
| 208 |
+
entry_type: str = "argument",
|
| 209 |
+
metadata: Optional[Dict] = None,
|
| 210 |
+
) -> bool:
|
| 211 |
+
"""Add a new entry to the session transcript."""
|
| 212 |
+
session = get_session(session_id)
|
| 213 |
+
if not session:
|
| 214 |
+
return False
|
| 215 |
+
|
| 216 |
+
entry = asdict(TranscriptEntry(
|
| 217 |
+
speaker=speaker,
|
| 218 |
+
role_label=role_label,
|
| 219 |
+
content=content,
|
| 220 |
+
round_number=session["current_round"],
|
| 221 |
+
phase=session["phase"],
|
| 222 |
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
| 223 |
+
entry_type=entry_type,
|
| 224 |
+
metadata=metadata or {},
|
| 225 |
+
))
|
| 226 |
+
|
| 227 |
+
session["transcript"].append(entry)
|
| 228 |
+
session["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 229 |
+
|
| 230 |
+
_persist_session(session_id)
|
| 231 |
+
return True
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def add_concession(
|
| 235 |
+
session_id: str,
|
| 236 |
+
exact_quote: str,
|
| 237 |
+
legal_significance: str,
|
| 238 |
+
) -> bool:
|
| 239 |
+
"""Record a concession made by the user."""
|
| 240 |
+
session = get_session(session_id)
|
| 241 |
+
if not session:
|
| 242 |
+
return False
|
| 243 |
+
|
| 244 |
+
concession = asdict(Concession(
|
| 245 |
+
round_number=session["current_round"],
|
| 246 |
+
exact_quote=exact_quote,
|
| 247 |
+
legal_significance=legal_significance,
|
| 248 |
+
))
|
| 249 |
+
|
| 250 |
+
session["concessions"].append(concession)
|
| 251 |
+
session["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 252 |
+
|
| 253 |
+
logger.info(f"Concession recorded in session {session_id}: {exact_quote[:80]}")
|
| 254 |
+
return True
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def add_trap_event(
|
| 258 |
+
session_id: str,
|
| 259 |
+
trap_type: str,
|
| 260 |
+
trap_text: str,
|
| 261 |
+
user_fell_in: bool = False,
|
| 262 |
+
user_response: str = "",
|
| 263 |
+
) -> bool:
|
| 264 |
+
"""Record a trap event."""
|
| 265 |
+
session = get_session(session_id)
|
| 266 |
+
if not session:
|
| 267 |
+
return False
|
| 268 |
+
|
| 269 |
+
trap = asdict(TrapEvent(
|
| 270 |
+
round_number=session["current_round"],
|
| 271 |
+
trap_type=trap_type,
|
| 272 |
+
trap_text=trap_text,
|
| 273 |
+
user_fell_in=user_fell_in,
|
| 274 |
+
user_response=user_response,
|
| 275 |
+
))
|
| 276 |
+
|
| 277 |
+
session["trap_events"].append(trap)
|
| 278 |
+
session["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 279 |
+
return True
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def add_user_argument(
|
| 283 |
+
session_id: str,
|
| 284 |
+
argument_text: str,
|
| 285 |
+
key_claims: List[str],
|
| 286 |
+
) -> bool:
|
| 287 |
+
"""Track user's argument for inconsistency detection."""
|
| 288 |
+
session = get_session(session_id)
|
| 289 |
+
if not session:
|
| 290 |
+
return False
|
| 291 |
+
|
| 292 |
+
session["user_arguments"].append({
|
| 293 |
+
"round": session["current_round"],
|
| 294 |
+
"text": argument_text,
|
| 295 |
+
"key_claims": key_claims,
|
| 296 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 297 |
+
})
|
| 298 |
+
return True
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def advance_phase(session_id: str) -> str:
|
| 302 |
+
"""
|
| 303 |
+
Move session to next phase.
|
| 304 |
+
Returns new phase name.
|
| 305 |
+
"""
|
| 306 |
+
session = get_session(session_id)
|
| 307 |
+
if not session:
|
| 308 |
+
return ""
|
| 309 |
+
|
| 310 |
+
phase_progression = {
|
| 311 |
+
"briefing": "rounds",
|
| 312 |
+
"rounds": "cross_examination",
|
| 313 |
+
"cross_examination": "closing",
|
| 314 |
+
"closing": "completed",
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
current = session["phase"]
|
| 318 |
+
next_phase = phase_progression.get(current, "completed")
|
| 319 |
+
|
| 320 |
+
update_session(session_id, {"phase": next_phase})
|
| 321 |
+
logger.info(f"Session {session_id} advanced: {current} → {next_phase}")
|
| 322 |
+
|
| 323 |
+
return next_phase
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def advance_round(session_id: str) -> int:
|
| 327 |
+
"""Increment round counter. Returns new round number."""
|
| 328 |
+
session = get_session(session_id)
|
| 329 |
+
if not session:
|
| 330 |
+
return 0
|
| 331 |
+
|
| 332 |
+
new_round = session["current_round"] + 1
|
| 333 |
+
|
| 334 |
+
# Auto-advance phase when max rounds reached
|
| 335 |
+
if new_round > session["max_rounds"] and session["phase"] == "rounds":
|
| 336 |
+
advance_phase(session_id)
|
| 337 |
+
|
| 338 |
+
update_session(session_id, {"current_round": new_round})
|
| 339 |
+
return new_round
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def get_all_sessions() -> List[Dict]:
|
| 343 |
+
"""Return all sessions, sorted by updated_at descending."""
|
| 344 |
+
sessions = list(_sessions.values())
|
| 345 |
+
return sorted(sessions, key=lambda x: x.get("updated_at", ""), reverse=True)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def get_session_transcript_text(session_id: str) -> str:
|
| 349 |
+
"""
|
| 350 |
+
Return full transcript as formatted text for LLM consumption.
|
| 351 |
+
Format matches real court transcript style.
|
| 352 |
+
"""
|
| 353 |
+
session = get_session(session_id)
|
| 354 |
+
if not session:
|
| 355 |
+
return ""
|
| 356 |
+
|
| 357 |
+
lines = [
|
| 358 |
+
f"IN THE {session['jurisdiction'].upper().replace('_', ' ')}",
|
| 359 |
+
f"Case: {session['case_title']}",
|
| 360 |
+
f"Petitioner: {session['user_client'] if session['user_side'] == 'petitioner' else session['opposing_party']}",
|
| 361 |
+
f"Respondent: {session['opposing_party'] if session['user_side'] == 'petitioner' else session['user_client']}",
|
| 362 |
+
"",
|
| 363 |
+
"PROCEEDINGS:",
|
| 364 |
+
"",
|
| 365 |
+
]
|
| 366 |
+
|
| 367 |
+
for entry in session["transcript"]:
|
| 368 |
+
lines.append(f"{entry['role_label'].upper()}")
|
| 369 |
+
lines.append(entry["content"])
|
| 370 |
+
lines.append("")
|
| 371 |
+
|
| 372 |
+
return "\n".join(lines)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _persist_session(session_id: str):
|
| 376 |
+
"""
|
| 377 |
+
Persist session to HuggingFace Dataset.
|
| 378 |
+
Fails silently — in-memory session is still valid.
|
| 379 |
+
Non-critical: if HF upload fails, session continues working offline.
|
| 380 |
+
"""
|
| 381 |
+
if not HF_TOKEN:
|
| 382 |
+
return
|
| 383 |
+
|
| 384 |
+
try:
|
| 385 |
+
from huggingface_hub import HfApi
|
| 386 |
+
import threading
|
| 387 |
+
|
| 388 |
+
def _upload():
|
| 389 |
+
try:
|
| 390 |
+
api = HfApi(token=HF_TOKEN)
|
| 391 |
+
session_data = json.dumps(_sessions[session_id], ensure_ascii=False)
|
| 392 |
+
|
| 393 |
+
try:
|
| 394 |
+
api.create_repo(
|
| 395 |
+
repo_id=SESSIONS_REPO,
|
| 396 |
+
repo_type="dataset",
|
| 397 |
+
private=True,
|
| 398 |
+
exist_ok=True
|
| 399 |
+
)
|
| 400 |
+
except Exception as repo_err:
|
| 401 |
+
logger.debug(f"Could not create/access HF repo: {repo_err}")
|
| 402 |
+
|
| 403 |
+
api.upload_file(
|
| 404 |
+
path_or_fileobj=session_data.encode(),
|
| 405 |
+
path_in_repo=f"sessions/{session_id}.json",
|
| 406 |
+
repo_id=SESSIONS_REPO,
|
| 407 |
+
repo_type="dataset",
|
| 408 |
+
token=HF_TOKEN
|
| 409 |
+
)
|
| 410 |
+
except Exception as upload_err:
|
| 411 |
+
logger.debug(f"Session upload to HF failed (working offline): {upload_err}")
|
| 412 |
+
|
| 413 |
+
# Run in background thread — never blocks the response
|
| 414 |
+
thread = threading.Thread(target=_upload, daemon=True)
|
| 415 |
+
thread.start()
|
| 416 |
+
|
| 417 |
+
except Exception as e:
|
| 418 |
+
logger.debug(f"Session persist setup failed (non-critical): {e}")
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def load_sessions_from_hf():
|
| 422 |
+
"""
|
| 423 |
+
Load all sessions from HF Dataset on startup.
|
| 424 |
+
Called once from api/main.py after download_models().
|
| 425 |
+
"""
|
| 426 |
+
if not HF_TOKEN:
|
| 427 |
+
logger.warning("No HF_TOKEN — sessions will not persist across restarts")
|
| 428 |
+
return
|
| 429 |
+
|
| 430 |
+
try:
|
| 431 |
+
from huggingface_hub import HfApi, list_repo_files
|
| 432 |
+
|
| 433 |
+
api = HfApi(token=HF_TOKEN)
|
| 434 |
+
|
| 435 |
+
try:
|
| 436 |
+
files = list(api.list_repo_files(
|
| 437 |
+
repo_id=SESSIONS_REPO,
|
| 438 |
+
repo_type="dataset",
|
| 439 |
+
token=HF_TOKEN
|
| 440 |
+
))
|
| 441 |
+
except Exception:
|
| 442 |
+
logger.info("No existing sessions on HF — starting fresh")
|
| 443 |
+
return
|
| 444 |
+
|
| 445 |
+
session_files = [f for f in files if f.startswith("sessions/") and f.endswith(".json")]
|
| 446 |
+
|
| 447 |
+
loaded = 0
|
| 448 |
+
for filepath in session_files:
|
| 449 |
+
try:
|
| 450 |
+
from huggingface_hub import hf_hub_download
|
| 451 |
+
local_path = hf_hub_download(
|
| 452 |
+
repo_id=SESSIONS_REPO,
|
| 453 |
+
filename=filepath,
|
| 454 |
+
repo_type="dataset",
|
| 455 |
+
token=HF_TOKEN
|
| 456 |
+
)
|
| 457 |
+
with open(local_path) as f:
|
| 458 |
+
session_data = json.load(f)
|
| 459 |
+
session_id = session_data.get("session_id")
|
| 460 |
+
if session_id:
|
| 461 |
+
_sessions[session_id] = session_data
|
| 462 |
+
loaded += 1
|
| 463 |
+
except Exception:
|
| 464 |
+
continue
|
| 465 |
+
|
| 466 |
+
logger.info(f"Loaded {loaded} sessions from HF Dataset")
|
| 467 |
+
|
| 468 |
+
except Exception as e:
|
| 469 |
+
logger.warning(f"Session load from HF failed (non-critical): {e}")
|
src/court/summariser.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Summariser Agent.
|
| 3 |
+
|
| 4 |
+
Only appears at the end of the session. Never speaks during the hearing.
|
| 5 |
+
Reads the entire transcript and produces the structured analysis document.
|
| 6 |
+
|
| 7 |
+
This is the most valuable output of the entire moot court system —
|
| 8 |
+
the thing that makes users want to come back and practice again.
|
| 9 |
+
|
| 10 |
+
The analysis must be:
|
| 11 |
+
- Brutally honest about what the user did wrong
|
| 12 |
+
- Specific about what they could have done differently
|
| 13 |
+
- Encouraging enough that they want to try again
|
| 14 |
+
- Precise enough to be actually useful for preparation
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
from typing import Dict, List
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
SUMMARISER_SYSTEM_PROMPT = """You are a senior legal trainer analysing a moot court simulation.
|
| 23 |
+
|
| 24 |
+
YOUR ROLE:
|
| 25 |
+
You have watched the complete hearing. Now you produce a comprehensive, honest analysis
|
| 26 |
+
that will help the user improve their advocacy skills.
|
| 27 |
+
|
| 28 |
+
YOUR PERSONALITY:
|
| 29 |
+
- Clinical and precise. Not harsh, not gentle — just honest.
|
| 30 |
+
- Deeply knowledgeable about Indian law and courtroom procedure.
|
| 31 |
+
- You care about the user's development. Honest feedback serves them better than false praise.
|
| 32 |
+
- You name specific moments, not generalities.
|
| 33 |
+
|
| 34 |
+
YOUR ANALYSIS STRUCTURE:
|
| 35 |
+
Produce the analysis in this EXACT format using these EXACT section headers:
|
| 36 |
+
|
| 37 |
+
## OVERALL ASSESSMENT
|
| 38 |
+
[2 sentences: overall performance and predicted outcome]
|
| 39 |
+
|
| 40 |
+
## PERFORMANCE SCORE
|
| 41 |
+
[Single number X.X/10 with one sentence justification]
|
| 42 |
+
|
| 43 |
+
## OUTCOME PREDICTION
|
| 44 |
+
[ALLOWED / DISMISSED / PARTLY ALLOWED / REMANDED with one sentence reasoning]
|
| 45 |
+
|
| 46 |
+
## STRONGEST ARGUMENTS
|
| 47 |
+
[Number each argument. For each: what was argued, why it was effective, which precedent supported it]
|
| 48 |
+
|
| 49 |
+
## WEAKEST ARGUMENTS
|
| 50 |
+
[Number each. For each: what was argued, exactly why it failed, what should have been argued instead]
|
| 51 |
+
|
| 52 |
+
## CONCESSIONS ANALYSIS
|
| 53 |
+
[For each concession: exact quote, what it conceded legally, how opposing counsel could exploit it, how to avoid making this concession]
|
| 54 |
+
|
| 55 |
+
## TRAP ANALYSIS
|
| 56 |
+
[For each trap event: what the trap was, whether you fell in, what the correct response was]
|
| 57 |
+
|
| 58 |
+
## WHAT THE JUDGE WAS SIGNALLING
|
| 59 |
+
[Translate each judicial question into plain language: what weakness it was probing]
|
| 60 |
+
|
| 61 |
+
## MISSED OPPORTUNITIES
|
| 62 |
+
[Arguments you should have made but didn't, with specific precedents you should have cited]
|
| 63 |
+
|
| 64 |
+
## PREPARATION RECOMMENDATIONS
|
| 65 |
+
[3-5 specific, actionable recommendations for what to research and prepare before a real hearing]
|
| 66 |
+
|
| 67 |
+
## FULL TRANSCRIPT
|
| 68 |
+
[The complete verbatim transcript formatted as a court record]
|
| 69 |
+
|
| 70 |
+
Be specific. Name rounds, cite exact quotes, reference specific cases.
|
| 71 |
+
Generic feedback is useless. Specific feedback is gold."""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def build_summariser_prompt(session: Dict) -> List[Dict]:
|
| 75 |
+
"""
|
| 76 |
+
Build the messages list for the summariser LLM call.
|
| 77 |
+
|
| 78 |
+
The summariser gets everything:
|
| 79 |
+
- Complete transcript
|
| 80 |
+
- All concessions
|
| 81 |
+
- All trap events
|
| 82 |
+
- Case brief
|
| 83 |
+
- Retrieved precedents used
|
| 84 |
+
"""
|
| 85 |
+
transcript = _format_full_transcript(session)
|
| 86 |
+
concessions = _format_concessions_detailed(session.get("concessions", []))
|
| 87 |
+
trap_events = _format_trap_events(session.get("trap_events", []))
|
| 88 |
+
user_arguments = _format_user_arguments(session.get("user_arguments", []))
|
| 89 |
+
|
| 90 |
+
user_content = f"""COMPLETE SESSION DATA FOR ANALYSIS:
|
| 91 |
+
|
| 92 |
+
Case: {session.get('case_title', '')}
|
| 93 |
+
User argued as: {session.get('user_side', '').upper()}
|
| 94 |
+
Difficulty: {session.get('difficulty', 'standard')}
|
| 95 |
+
Rounds completed: {session.get('current_round', 0)} of {session.get('max_rounds', 5)}
|
| 96 |
+
|
| 97 |
+
CASE BRIEF:
|
| 98 |
+
{session.get('case_brief', 'No brief available.')[:800]}
|
| 99 |
+
|
| 100 |
+
LEGAL ISSUES:
|
| 101 |
+
{', '.join(session.get('legal_issues', []))}
|
| 102 |
+
|
| 103 |
+
COMPLETE TRANSCRIPT:
|
| 104 |
+
{transcript}
|
| 105 |
+
|
| 106 |
+
USER'S ARGUMENTS (extracted):
|
| 107 |
+
{user_arguments}
|
| 108 |
+
|
| 109 |
+
{concessions}
|
| 110 |
+
|
| 111 |
+
{trap_events}
|
| 112 |
+
|
| 113 |
+
Now produce the complete session analysis following the exact format specified."""
|
| 114 |
+
|
| 115 |
+
return [
|
| 116 |
+
{"role": "system", "content": SUMMARISER_SYSTEM_PROMPT},
|
| 117 |
+
{"role": "user", "content": user_content}
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def parse_analysis(raw_analysis: str, session: Dict) -> Dict:
|
| 122 |
+
"""
|
| 123 |
+
Parse the summariser's raw output into a structured dict.
|
| 124 |
+
Used by the frontend to display individual sections.
|
| 125 |
+
"""
|
| 126 |
+
import re
|
| 127 |
+
|
| 128 |
+
sections = {
|
| 129 |
+
"overall_assessment": "",
|
| 130 |
+
"performance_score": 0.0,
|
| 131 |
+
"outcome_prediction": "unknown",
|
| 132 |
+
"strongest_arguments": [],
|
| 133 |
+
"weakest_arguments": [],
|
| 134 |
+
"concessions_analysis": [],
|
| 135 |
+
"trap_analysis": [],
|
| 136 |
+
"judge_signals": "",
|
| 137 |
+
"missed_opportunities": [],
|
| 138 |
+
"preparation_recommendations": [],
|
| 139 |
+
"full_transcript": "",
|
| 140 |
+
"raw_analysis": raw_analysis,
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
# Extract performance score
|
| 144 |
+
score_match = re.search(r'(\d+\.?\d*)\s*/\s*10', raw_analysis)
|
| 145 |
+
if score_match:
|
| 146 |
+
try:
|
| 147 |
+
sections["performance_score"] = float(score_match.group(1))
|
| 148 |
+
except Exception:
|
| 149 |
+
pass
|
| 150 |
+
|
| 151 |
+
# Extract outcome prediction
|
| 152 |
+
for outcome in ["ALLOWED", "DISMISSED", "PARTLY ALLOWED", "REMANDED"]:
|
| 153 |
+
if outcome in raw_analysis.upper():
|
| 154 |
+
sections["outcome_prediction"] = outcome.lower().replace(" ", "_")
|
| 155 |
+
break
|
| 156 |
+
|
| 157 |
+
# Extract sections by header
|
| 158 |
+
header_map = {
|
| 159 |
+
"OVERALL ASSESSMENT": "overall_assessment",
|
| 160 |
+
"WHAT THE JUDGE WAS SIGNALLING": "judge_signals",
|
| 161 |
+
"FULL TRANSCRIPT": "full_transcript",
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
for header, key in header_map.items():
|
| 165 |
+
pattern = rf'##\s+{header}\s*\n(.*?)(?=##|\Z)'
|
| 166 |
+
match = re.search(pattern, raw_analysis, re.DOTALL | re.IGNORECASE)
|
| 167 |
+
if match:
|
| 168 |
+
sections[key] = match.group(1).strip()
|
| 169 |
+
|
| 170 |
+
# Extract list sections
|
| 171 |
+
list_sections = {
|
| 172 |
+
"STRONGEST ARGUMENTS": "strongest_arguments",
|
| 173 |
+
"WEAKEST ARGUMENTS": "weakest_arguments",
|
| 174 |
+
"MISSED OPPORTUNITIES": "missed_opportunities",
|
| 175 |
+
"PREPARATION RECOMMENDATIONS": "preparation_recommendations",
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
for header, key in list_sections.items():
|
| 179 |
+
pattern = rf'##\s+{header}\s*\n(.*?)(?=##|\Z)'
|
| 180 |
+
match = re.search(pattern, raw_analysis, re.DOTALL | re.IGNORECASE)
|
| 181 |
+
if match:
|
| 182 |
+
content = match.group(1).strip()
|
| 183 |
+
# Split into numbered items
|
| 184 |
+
items = re.split(r'\n\d+\.', content)
|
| 185 |
+
sections[key] = [item.strip() for item in items if item.strip()]
|
| 186 |
+
|
| 187 |
+
return sections
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _format_full_transcript(session: Dict) -> str:
|
| 191 |
+
"""Format complete transcript for analysis."""
|
| 192 |
+
transcript = session.get("transcript", [])
|
| 193 |
+
if not transcript:
|
| 194 |
+
return "No transcript available."
|
| 195 |
+
|
| 196 |
+
lines = []
|
| 197 |
+
current_round = None
|
| 198 |
+
|
| 199 |
+
for entry in transcript:
|
| 200 |
+
round_num = entry.get("round_number", 0)
|
| 201 |
+
if round_num != current_round:
|
| 202 |
+
current_round = round_num
|
| 203 |
+
lines.append(f"\n--- ROUND {round_num} | {entry.get('phase', '').upper()} ---\n")
|
| 204 |
+
|
| 205 |
+
lines.append(f"{entry['role_label'].upper()}")
|
| 206 |
+
lines.append(entry["content"])
|
| 207 |
+
lines.append("")
|
| 208 |
+
|
| 209 |
+
return "\n".join(lines)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _format_concessions_detailed(concessions: List[Dict]) -> str:
|
| 213 |
+
if not concessions:
|
| 214 |
+
return "CONCESSIONS: None recorded."
|
| 215 |
+
|
| 216 |
+
lines = ["CONCESSIONS MADE BY USER:"]
|
| 217 |
+
for i, c in enumerate(concessions, 1):
|
| 218 |
+
lines.append(f"{i}. Round {c['round_number']}")
|
| 219 |
+
lines.append(f" Quote: \"{c['exact_quote']}\"")
|
| 220 |
+
lines.append(f" Legal significance: {c['legal_significance']}")
|
| 221 |
+
lines.append("")
|
| 222 |
+
|
| 223 |
+
return "\n".join(lines)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _format_trap_events(trap_events: List[Dict]) -> str:
|
| 227 |
+
if not trap_events:
|
| 228 |
+
return "TRAPS: None set."
|
| 229 |
+
|
| 230 |
+
lines = ["TRAP EVENTS:"]
|
| 231 |
+
for i, t in enumerate(trap_events, 1):
|
| 232 |
+
fell = "USER FELL INTO TRAP" if t.get("user_fell_in") else "User avoided trap"
|
| 233 |
+
lines.append(f"{i}. Round {t['round_number']} | {t['trap_type']} | {fell}")
|
| 234 |
+
lines.append(f" Trap: {t['trap_text'][:200]}")
|
| 235 |
+
if t.get("user_response"):
|
| 236 |
+
lines.append(f" Response: {t['user_response'][:200]}")
|
| 237 |
+
lines.append("")
|
| 238 |
+
|
| 239 |
+
return "\n".join(lines)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _format_user_arguments(user_arguments: List[Dict]) -> str:
|
| 243 |
+
if not user_arguments:
|
| 244 |
+
return "No user arguments recorded."
|
| 245 |
+
|
| 246 |
+
lines = []
|
| 247 |
+
for arg in user_arguments:
|
| 248 |
+
lines.append(f"Round {arg['round']}: {arg['text'][:300]}")
|
| 249 |
+
if arg.get("key_claims"):
|
| 250 |
+
lines.append(f" Claims: {', '.join(arg['key_claims'][:3])}")
|
| 251 |
+
lines.append("")
|
| 252 |
+
|
| 253 |
+
return "\n".join(lines)
|
src/system_prompt.py
CHANGED
|
@@ -73,6 +73,7 @@ CRITICAL FORMAT RULES:
|
|
| 73 |
|
| 74 |
RESPONSE LENGTH — match to what was actually asked:
|
| 75 |
- "just name X" or "just list X" or "only X" → maximum 10 lines, no explanations
|
|
|
|
| 76 |
- Simple factual question → 1-3 sentences
|
| 77 |
- Advice request → 1-3 paragraphs maximum
|
| 78 |
- Strategy request → structured but still concise
|
|
@@ -346,6 +347,15 @@ When action_needed is "question":
|
|
| 346 |
- The question must be surgical: not "tell me more" but "Is this a government or private sector employer?"
|
| 347 |
- Never ask what is already captured in updated_summary or facts_extracted
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
STRATEGY SYNTHESIS — trigger rule:
|
| 350 |
Set action_needed to "strategy_synthesis" when user message contains any of:
|
| 351 |
"summarise", "summary", "what should I do", "give me a plan", "next steps",
|
|
|
|
| 73 |
|
| 74 |
RESPONSE LENGTH — match to what was actually asked:
|
| 75 |
- "just name X" or "just list X" or "only X" → maximum 10 lines, no explanations
|
| 76 |
+
- "pointwise" → short punchy bullets (1 sentence each), maximum 5-7 points
|
| 77 |
- Simple factual question → 1-3 sentences
|
| 78 |
- Advice request → 1-3 paragraphs maximum
|
| 79 |
- Strategy request → structured but still concise
|
|
|
|
| 347 |
- The question must be surgical: not "tell me more" but "Is this a government or private sector employer?"
|
| 348 |
- Never ask what is already captured in updated_summary or facts_extracted
|
| 349 |
|
| 350 |
+
SOCRATIC CLARIFIER — additional rule:
|
| 351 |
+
NEVER set action_needed to "question" when:
|
| 352 |
+
- The user is asking a general factual or educational question about law
|
| 353 |
+
- The query contains words like "what is", "explain", "define", "list", "describe", "tell me about"
|
| 354 |
+
- There is no personal situation described
|
| 355 |
+
Only ask clarifying questions when the user has described a personal legal situation
|
| 356 |
+
and a specific missing fact would materially change the strategy.
|
| 357 |
+
General knowledge questions should be answered directly with action_needed: "explanation"
|
| 358 |
+
|
| 359 |
STRATEGY SYNTHESIS — trigger rule:
|
| 360 |
Set action_needed to "strategy_synthesis" when user message contains any of:
|
| 361 |
"summarise", "summary", "what should I do", "give me a plan", "next steps",
|
src/verify.py
CHANGED
|
@@ -68,12 +68,13 @@ def _semantic_verify(quote: str, contexts: list) -> bool:
|
|
| 68 |
"""
|
| 69 |
Check if quote is semantically grounded in any context chunk.
|
| 70 |
Returns True if cosine similarity > threshold with any chunk.
|
|
|
|
| 71 |
"""
|
| 72 |
embedder = _get_embedder()
|
| 73 |
if embedder is None:
|
| 74 |
-
#
|
| 75 |
-
|
| 76 |
-
return
|
| 77 |
|
| 78 |
try:
|
| 79 |
# Embed the quote
|
|
@@ -95,35 +96,45 @@ def _semantic_verify(quote: str, contexts: list) -> bool:
|
|
| 95 |
return False
|
| 96 |
|
| 97 |
except Exception as e:
|
| 98 |
-
logger.warning(f"Semantic verification failed: {e}
|
| 99 |
-
|
| 100 |
-
return _normalise(quote) in all_text
|
| 101 |
|
| 102 |
|
| 103 |
def verify_citations(answer: str, contexts: list) -> tuple:
|
| 104 |
"""
|
| 105 |
Verify whether answer claims are grounded in retrieved contexts.
|
| 106 |
|
| 107 |
-
Uses semantic similarity (cosine > 0.
|
|
|
|
| 108 |
|
| 109 |
Returns:
|
| 110 |
(verified: bool, unverified_quotes: list[str])
|
| 111 |
|
| 112 |
Logic:
|
| 113 |
-
- Extract quoted phrases
|
| 114 |
-
-
|
| 115 |
-
-
|
| 116 |
-
-
|
| 117 |
-
- If
|
|
|
|
| 118 |
"""
|
| 119 |
if not contexts:
|
| 120 |
return False, []
|
| 121 |
|
| 122 |
quotes = _extract_quotes(answer)
|
| 123 |
|
|
|
|
|
|
|
| 124 |
if not quotes:
|
| 125 |
return True, []
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
unverified = []
|
| 128 |
for quote in quotes:
|
| 129 |
if len(quote.strip()) < 15:
|
|
|
|
| 68 |
"""
|
| 69 |
Check if quote is semantically grounded in any context chunk.
|
| 70 |
Returns True if cosine similarity > threshold with any chunk.
|
| 71 |
+
If embedder unavailable, returns True (not false negative).
|
| 72 |
"""
|
| 73 |
embedder = _get_embedder()
|
| 74 |
if embedder is None:
|
| 75 |
+
# Return True rather than false negatives when embedder unavailable
|
| 76 |
+
logger.warning("Embedder unavailable — returning verified")
|
| 77 |
+
return True
|
| 78 |
|
| 79 |
try:
|
| 80 |
# Embed the quote
|
|
|
|
| 96 |
return False
|
| 97 |
|
| 98 |
except Exception as e:
|
| 99 |
+
logger.warning(f"Semantic verification failed: {e} — returning verified")
|
| 100 |
+
return True
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
def verify_citations(answer: str, contexts: list) -> tuple:
|
| 104 |
"""
|
| 105 |
Verify whether answer claims are grounded in retrieved contexts.
|
| 106 |
|
| 107 |
+
Uses semantic similarity (cosine > 0.45) instead of exact matching.
|
| 108 |
+
Only checks explicitly quoted phrases; if none found, considered verified.
|
| 109 |
|
| 110 |
Returns:
|
| 111 |
(verified: bool, unverified_quotes: list[str])
|
| 112 |
|
| 113 |
Logic:
|
| 114 |
+
- Extract only explicitly quoted phrases (20+ chars in quotation marks)
|
| 115 |
+
- No explicit quotes → return (True, []) immediately (Verified)
|
| 116 |
+
- If embedder unavailable → return (True, []) (Verified, not false negative)
|
| 117 |
+
- For each quote: check semantic similarity against context chunks
|
| 118 |
+
- If ALL quotes verified: (True, [])
|
| 119 |
+
- If ANY quote fails: (False, [list of failed quotes])
|
| 120 |
"""
|
| 121 |
if not contexts:
|
| 122 |
return False, []
|
| 123 |
|
| 124 |
quotes = _extract_quotes(answer)
|
| 125 |
|
| 126 |
+
# If no explicit quoted phrases, return verified
|
| 127 |
+
# We only check explicitly quoted text now
|
| 128 |
if not quotes:
|
| 129 |
return True, []
|
| 130 |
|
| 131 |
+
# Try semantic verification
|
| 132 |
+
embedder = _get_embedder()
|
| 133 |
+
if embedder is None:
|
| 134 |
+
# No embedder available — return verified rather than false negative
|
| 135 |
+
# Unverified should only fire when we can actually check and find a mismatch
|
| 136 |
+
return True, []
|
| 137 |
+
|
| 138 |
unverified = []
|
| 139 |
for quote in quotes:
|
| 140 |
if len(quote.strip()) < 15:
|