Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import os | |
| from collections.abc import Iterable | |
| import gradio as gr | |
| import httpx | |
| from sovereign_bench.engine import JUDGE_NAME, JUROR_PERSONAS, stream_trial | |
| from sovereign_bench.models import TrialEvent, TrialRequest | |
| def _load_env_file() -> None: | |
| path = ".env" | |
| if not os.path.exists(path): | |
| return | |
| with open(path, encoding="utf-8") as handle: | |
| for line in handle: | |
| stripped = line.strip() | |
| if not stripped or stripped.startswith("#") or "=" not in stripped: | |
| continue | |
| key, value = stripped.split("=", 1) | |
| os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) | |
| _load_env_file() | |
| CASE_OPTIONS = { | |
| "Trial of Socrates": "socrates", | |
| "The People v. Barnaby Buttons": "barnaby", | |
| "Live Search Tribunal": "live", | |
| } | |
| PHASE_GLYPHS = { | |
| "pretrial": "00", | |
| "intake": "01", | |
| "claims": "02", | |
| "opening": "03", | |
| "evidence": "04", | |
| "questions": "05", | |
| "deliberation": "06", | |
| "verdict": "07", | |
| "appeal": "08", | |
| } | |
| AUDIO_PATHS = { | |
| "score": "/gradio_api/file=assets/audio/courtroom.ogg", | |
| "judgement": "/gradio_api/file=assets/audio/Judgement.ogg", | |
| "crowd": "/gradio_api/file=assets/audio/crowd_shouting.ogg", | |
| "gavel": "/gradio_api/file=assets/audio/wood_hammer_01.ogg", | |
| "wood": "/gradio_api/file=assets/audio/wood_hit_03.ogg", | |
| "steps": "/gradio_api/file=assets/audio/steps_in_wood_floor.wav", | |
| "paper": "/gradio_api/file=assets/audio/paper_sound_1.mp3", | |
| "paper_long": "/gradio_api/file=assets/audio/paper_sound_4.mp3", | |
| "select": "/gradio_api/file=assets/audio/select_001.ogg", | |
| } | |
| CSS = """ | |
| :root { | |
| --ink: #23170e; | |
| --paper: #f3dfb7; | |
| --paper-dark: #c79455; | |
| --gold: #d9b060; | |
| --mahogany: #4b2119; | |
| --shadow: rgba(10, 5, 2, .6); | |
| --red: #8f2e2d; | |
| --green: #2f6f5e; | |
| --blue: #254f7a; | |
| } | |
| body, | |
| .gradio-container { | |
| margin: 0; | |
| background: #141413 !important; | |
| background-color: #141413 !important; | |
| color: var(--ink); | |
| font-family: Georgia, "Times New Roman", serif; | |
| } | |
| .gradio-container { | |
| max-width: none !important; | |
| padding: 0 !important; | |
| } | |
| .main, | |
| .contain { | |
| max-width: none !important; | |
| padding: 0 !important; | |
| background: transparent !important; | |
| } | |
| .gradio-container main, | |
| .gradio-container .wrap, | |
| .gradio-container .app, | |
| .gradio-container .html-container { | |
| background: transparent !important; | |
| padding-left: 0 !important; | |
| padding-right: 0 !important; | |
| } | |
| .docket-book-controls { | |
| position: fixed; | |
| left: 50%; | |
| top: clamp(172px, 21vh, 212px); | |
| z-index: 9999; | |
| width: min(620px, calc(100vw - 160px)); | |
| max-width: none; | |
| margin: 0; | |
| padding: 0; | |
| transform: translateX(-50%) rotate(-1deg); | |
| border: 0 !important; | |
| border-radius: 0 !important; | |
| background: transparent !important; | |
| box-shadow: none !important; | |
| color: #321d10; | |
| transition: opacity .32s ease, transform .65s ease; | |
| } | |
| body.trial-has-started .docket-book-controls { | |
| opacity: 0; | |
| pointer-events: none; | |
| transform: translateX(-50%) rotateX(56deg) rotate(-1deg) scale(.45); | |
| } | |
| .docket-book-controls::before { | |
| content: none; | |
| } | |
| .docket-book-controls, | |
| .docket-book-controls > *, | |
| .docket-book-controls .form, | |
| .docket-book-controls .block, | |
| .docket-book-controls .gap, | |
| .docket-book-controls .wrap { | |
| background: transparent !important; | |
| border: 0 !important; | |
| box-shadow: none !important; | |
| } | |
| .docket-book-controls .docket-book-controls { | |
| position: static !important; | |
| left: auto !important; | |
| top: auto !important; | |
| width: 100% !important; | |
| transform: none !important; | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| body.trial-has-started .docket-book-controls .docket-book-controls { | |
| pointer-events: none; | |
| } | |
| .book-control-heading { | |
| margin: 0 0 6px; | |
| color: #694019; | |
| font: 900 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| letter-spacing: .08em; | |
| text-transform: uppercase; | |
| } | |
| .docket-book-controls label, | |
| .docket-book-controls span, | |
| .docket-book-controls .prose { | |
| color: #321d10 !important; | |
| } | |
| .docket-book-controls label { | |
| font-size: 11px !important; | |
| font-weight: 800 !important; | |
| } | |
| .docket-book-controls input, | |
| .docket-book-controls textarea, | |
| .docket-book-controls [role="combobox"], | |
| .docket-book-controls .wrap-inner { | |
| border-color: rgba(90, 50, 20, .24) !important; | |
| border-radius: 4px !important; | |
| background: rgba(255, 243, 207, .58) !important; | |
| color: #241509 !important; | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,.24) !important; | |
| } | |
| .docket-book-controls textarea { | |
| min-height: 42px !important; | |
| } | |
| .docket-book-controls button.primary { | |
| min-height: 42px; | |
| border: 1px solid rgba(44, 21, 10, .42) !important; | |
| border-radius: 5px !important; | |
| background: #1c130d !important; | |
| color: #fff3d2 !important; | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 8px 18px rgba(40, 18, 9, .28); | |
| } | |
| .docket-book-controls .book-status p { | |
| margin: 0 !important; | |
| color: #5a3519 !important; | |
| font-size: 12px; | |
| line-height: 1.25; | |
| } | |
| .trial-options { | |
| max-width: 1120px; | |
| margin: 0 auto 14px; | |
| border: 1px solid rgba(255, 226, 154, .18); | |
| border-radius: 6px; | |
| background: rgba(18, 9, 5, .78); | |
| color: #f5dfb5; | |
| } | |
| .trial-options label, | |
| .trial-options span, | |
| .trial-options .prose { | |
| color: #f5dfb5 !important; | |
| } | |
| .court-episode-stage { | |
| --spot-x: 50%; | |
| --spot-y: 36%; | |
| position: relative; | |
| min-height: min(880px, calc(100vh - 112px)); | |
| height: min(880px, calc(100vh - 112px)); | |
| margin: 0; | |
| width: 100%; | |
| max-width: none; | |
| overflow: hidden; | |
| isolation: auto; | |
| color: #fff0d2; | |
| border: 0; | |
| border-radius: 0; | |
| background: transparent; | |
| box-shadow: none; | |
| } | |
| .court-episode-stage::before { | |
| content: ""; | |
| display: none; | |
| } | |
| .court-episode-stage::after { | |
| content: ""; | |
| display: none; | |
| } | |
| .court-episode-stage > * { | |
| position: relative; | |
| z-index: 4; | |
| } | |
| .episode-room { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 3; | |
| background: | |
| url('/gradio_api/file=assets/background/CourtRoom.png') center center / 100% 100% no-repeat, | |
| #26120b; | |
| filter: none; | |
| transform: none; | |
| } | |
| .trial-started .episode-room, | |
| .phase-intake .episode-room, | |
| .phase-claims .episode-room, | |
| .phase-opening .episode-room, | |
| .phase-evidence .episode-room, | |
| .phase-questions .episode-room, | |
| .phase-deliberation .episode-room, | |
| .phase-verdict .episode-room, | |
| .phase-appeal .episode-room { | |
| filter: none; | |
| transform: none; | |
| } | |
| .phase-intake, | |
| .phase-appeal { --spot-x: 50%; --spot-y: 30%; } | |
| .phase-claims, | |
| .phase-opening { --spot-x: 43%; --spot-y: 66%; } | |
| .phase-evidence { --spot-x: 70%; --spot-y: 56%; } | |
| .phase-questions, | |
| .phase-verdict { --spot-x: 50%; --spot-y: 34%; } | |
| .phase-deliberation { --spot-x: 79%; --spot-y: 60%; } | |
| .episode-title { | |
| position: absolute; | |
| left: 26px; | |
| top: 22px; | |
| z-index: 9; | |
| max-width: min(780px, calc(100% - 330px)); | |
| text-shadow: 0 3px 18px rgba(0, 0, 0, .75); | |
| } | |
| .episode-kicker, | |
| .prop-label, | |
| .caption-phase, | |
| .tooltip-meta, | |
| .drawer-kicker { | |
| color: #f4d58f; | |
| font: 800 11px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| letter-spacing: .06em; | |
| text-transform: uppercase; | |
| } | |
| .episode-title h1 { | |
| margin: 4px 0 6px; | |
| max-width: 780px; | |
| color: #fff4d7; | |
| font-size: clamp(28px, 4.2vw, 58px); | |
| line-height: .98; | |
| letter-spacing: 0; | |
| } | |
| .episode-title p { | |
| margin: 0; | |
| max-width: 720px; | |
| color: #f8dcaa; | |
| font-size: 15px; | |
| line-height: 1.38; | |
| } | |
| .audio-deck { | |
| display: none; | |
| } | |
| .sound-toggle { | |
| position: fixed; | |
| left: 18px; | |
| bottom: 18px; | |
| z-index: 80; | |
| width: 46px; | |
| height: 46px; | |
| border: 1px solid rgba(255, 226, 154, .48); | |
| border-radius: 50%; | |
| background: rgba(22, 11, 7, .82); | |
| box-shadow: 0 12px 28px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .12); | |
| cursor: pointer; | |
| } | |
| .sound-toggle:hover, | |
| .sound-toggle:focus-visible { | |
| outline: none; | |
| border-color: rgba(255, 226, 154, .82); | |
| background: rgba(41, 20, 12, .92); | |
| } | |
| .sound-toggle .sound-icon { | |
| position: absolute; | |
| left: 13px; | |
| top: 15px; | |
| width: 10px; | |
| height: 16px; | |
| border-radius: 2px 0 0 2px; | |
| background: #ffe5a6; | |
| } | |
| .sound-toggle .sound-icon::before { | |
| content: ""; | |
| position: absolute; | |
| left: 7px; | |
| top: -3px; | |
| width: 14px; | |
| height: 22px; | |
| border: 3px solid #ffe5a6; | |
| border-left: 0; | |
| border-radius: 0 18px 18px 0; | |
| } | |
| .sound-toggle .sound-icon::after { | |
| content: ""; | |
| position: absolute; | |
| left: 20px; | |
| top: -9px; | |
| width: 3px; | |
| height: 34px; | |
| border-radius: 4px; | |
| background: #d64d45; | |
| opacity: 0; | |
| transform: rotate(42deg); | |
| transform-origin: center; | |
| } | |
| .sound-toggle.muted .sound-icon::after { | |
| opacity: 1; | |
| } | |
| .episode-book { | |
| position: absolute; | |
| left: 50%; | |
| top: 12%; | |
| z-index: 12; | |
| width: min(760px, calc(100% - 32px)); | |
| aspect-ratio: 3 / 2; | |
| transform: translateX(-50%) rotateX(0) rotateZ(-1deg); | |
| transform-origin: center bottom; | |
| color: #2b1b10; | |
| filter: drop-shadow(0 34px 36px rgba(0, 0, 0, .48)); | |
| pointer-events: none; | |
| transition: top .85s ease, width .85s ease, transform .85s ease, filter .85s ease, opacity .85s ease; | |
| } | |
| .book-art { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| pointer-events: none; | |
| user-select: none; | |
| transition: opacity .36s ease; | |
| } | |
| .book-art.closed-art { | |
| opacity: 0; | |
| } | |
| .episode-book.closed { | |
| top: 36%; | |
| width: min(245px, 30vw); | |
| transform: translateX(-50%) rotateX(56deg) rotateZ(1deg); | |
| opacity: .92; | |
| filter: drop-shadow(0 18px 18px rgba(0, 0, 0, .45)); | |
| } | |
| .episode-book.closed .open-art { | |
| opacity: 0; | |
| } | |
| .episode-book.closed .closed-art { | |
| opacity: 1; | |
| } | |
| .episode-book.closed .book-open-content { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .book-open-content { | |
| position: absolute; | |
| inset: 17% 10% 13%; | |
| z-index: 2; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 72px; | |
| padding: 0 28px; | |
| transition: opacity .35s ease; | |
| } | |
| .book-open-content h2 { | |
| margin: 0 0 10px; | |
| color: #4c2a12; | |
| font-size: 30px; | |
| letter-spacing: 0; | |
| } | |
| .book-open-content p, | |
| .book-entry { | |
| color: #3c2615; | |
| font-size: 15px; | |
| line-height: 1.34; | |
| } | |
| .book-entry { | |
| margin: 11px 0; | |
| padding-left: 12px; | |
| border-left: 3px solid rgba(111, 61, 23, .36); | |
| } | |
| .judge-dais { | |
| position: absolute; | |
| left: 50%; | |
| top: 27%; | |
| z-index: 6; | |
| width: min(360px, 32vw); | |
| min-width: 230px; | |
| transform: translateX(-50%); | |
| text-align: center; | |
| } | |
| .bench-front { | |
| display: none; | |
| } | |
| .gavel { | |
| display: none; | |
| } | |
| .gavel::before { | |
| content: ""; | |
| display: none; | |
| } | |
| .phase-verdict .gavel { | |
| animation: gavel-hit .55s ease-out both; | |
| } | |
| .counsel-table { | |
| position: absolute; | |
| bottom: 19%; | |
| z-index: 6; | |
| width: min(255px, 22vw); | |
| height: 84px; | |
| border: 0; | |
| background: transparent; | |
| box-shadow: none; | |
| } | |
| .counsel-table.left { left: 17%; } | |
| .counsel-table.right { right: 17%; } | |
| .trial-floor-mark { | |
| display: none; | |
| } | |
| .witness-area { | |
| position: absolute; | |
| right: 8.5%; | |
| bottom: 28%; | |
| z-index: 6; | |
| width: min(190px, 18vw); | |
| height: 98px; | |
| border: 0; | |
| background: transparent; | |
| box-shadow: none; | |
| } | |
| .jury-benches { | |
| position: absolute; | |
| top: 43%; | |
| z-index: 7; | |
| width: min(220px, 16vw); | |
| min-width: 150px; | |
| display: grid; | |
| gap: 6px; | |
| } | |
| .jury-benches.left { | |
| left: 4.5%; | |
| } | |
| .jury-benches.right { | |
| right: 4.5%; | |
| } | |
| .jury-benches.left .jury-row { | |
| transform: rotate(-7deg) skewY(-3deg); | |
| } | |
| .jury-benches.right .jury-row { | |
| transform: rotate(7deg) skewY(3deg); | |
| } | |
| .jury-rail { | |
| display: none; | |
| } | |
| .jury-row { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| align-items: end; | |
| } | |
| .gallery-benches { | |
| display: none; | |
| } | |
| .gallery-benches div { | |
| display: none; | |
| } | |
| .prop-label { | |
| display: none; | |
| } | |
| .foreground-props { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 13; | |
| pointer-events: none; | |
| } | |
| .foreground-fence, | |
| .judge-table-foreground { | |
| position: absolute; | |
| display: block; | |
| max-width: none; | |
| height: auto; | |
| filter: none; | |
| opacity: 1; | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| .foreground-fence { | |
| bottom: -1.5%; | |
| width: 47%; | |
| } | |
| .foreground-fence.fence-left { | |
| left: 0; | |
| transform: translateX(-2%); | |
| } | |
| .foreground-fence.fence-right { | |
| right: 0; | |
| transform: translateX(2%); | |
| } | |
| .judge-table-foreground { | |
| left: 50%; | |
| top: 35%; | |
| z-index: 1; | |
| width: 46%; | |
| transform: translateX(-50%); | |
| } | |
| .puppet { | |
| --skin: #c99257; | |
| --robe: #282128; | |
| --accent: #8a2f2f; | |
| --portrait-width: 74px; | |
| --portrait-top: -14px; | |
| position: absolute; | |
| z-index: 8; | |
| width: 72px; | |
| height: 128px; | |
| transform: translate(-50%, -100%); | |
| transform-origin: center bottom; | |
| filter: none; | |
| color: inherit; | |
| text-decoration: none; | |
| } | |
| .puppet.small { | |
| width: 50px; | |
| height: 94px; | |
| --portrait-width: 54px; | |
| --portrait-top: -8px; | |
| } | |
| .puppet.active { | |
| animation: puppet-breathe 1.45s ease-in-out infinite; | |
| } | |
| .puppet.walking { | |
| animation: lawyer-walk 1.9s ease-in-out infinite; | |
| } | |
| .puppet.judge { | |
| left: 50%; | |
| top: 31%; | |
| --skin: #c38a55; | |
| --robe: #1b1b20; | |
| --accent: #79242a; | |
| --portrait-width: 96px; | |
| --portrait-top: -28px; | |
| } | |
| .puppet.clerk { | |
| left: 43%; | |
| top: 41%; | |
| --skin: #b77b52; | |
| --robe: #365548; | |
| --accent: #2f6f5e; | |
| } | |
| .puppet.auric { | |
| left: 24%; | |
| top: 62%; | |
| --skin: #c9975d; | |
| --robe: #5b2719; | |
| --accent: #a45c25; | |
| } | |
| .speaker-auric .puppet.auric { | |
| left: 43%; | |
| top: 66%; | |
| } | |
| .puppet.sable { | |
| left: 75%; | |
| top: 62%; | |
| --skin: #a86d4a; | |
| --robe: #1d3045; | |
| --accent: #254f7a; | |
| } | |
| .speaker-sable .puppet.sable { | |
| left: 57%; | |
| top: 66%; | |
| } | |
| .puppet.auditor { | |
| left: 71%; | |
| top: 55%; | |
| --skin: #c6a65b; | |
| --robe: #4b3d1b; | |
| --accent: #8d6b1f; | |
| } | |
| .puppet-portrait { | |
| position: absolute; | |
| left: 50%; | |
| top: var(--portrait-top); | |
| z-index: 3; | |
| width: var(--portrait-width); | |
| height: auto; | |
| max-height: 118px; | |
| transform: translateX(-50%); | |
| object-fit: contain; | |
| pointer-events: none; | |
| } | |
| .phase-evidence .puppet.auditor { | |
| animation: evidence-focus 1.35s ease-in-out infinite; | |
| } | |
| .puppet::before { | |
| content: ""; | |
| position: absolute; | |
| left: 50%; | |
| top: 0; | |
| width: 44px; | |
| height: 44px; | |
| transform: translateX(-50%); | |
| border: 2px solid rgba(255, 232, 174, .58); | |
| border-radius: 50%; | |
| background: | |
| radial-gradient(circle at 34% 32%, rgba(255,255,255,.38), transparent 22%), | |
| radial-gradient(circle at 36% 42%, #1b120c 0 2px, transparent 2.5px), | |
| radial-gradient(circle at 62% 42%, #1b120c 0 2px, transparent 2.5px), | |
| linear-gradient(180deg, var(--skin), #8b5638); | |
| } | |
| .puppet::after { | |
| content: ""; | |
| position: absolute; | |
| left: 50%; | |
| top: 48px; | |
| width: 58px; | |
| height: 70px; | |
| transform: translateX(-50%); | |
| border: 1px solid rgba(255, 232, 174, .22); | |
| border-radius: 24px 24px 8px 8px; | |
| background: | |
| linear-gradient(90deg, transparent 46%, rgba(255, 226, 154, .14) 49%, transparent 52%), | |
| linear-gradient(180deg, var(--accent), var(--robe) 52%, #130a07); | |
| } | |
| .puppet .mouth { | |
| position: absolute; | |
| left: 50%; | |
| top: 27px; | |
| z-index: 2; | |
| width: 15px; | |
| height: 7px; | |
| transform: translateX(-50%); | |
| border-bottom: 2px solid #28150c; | |
| border-radius: 0 0 18px 18px; | |
| } | |
| .puppet.active .mouth, | |
| .puppet.walking .mouth { | |
| animation: speak-mouth .5s ease-in-out infinite; | |
| } | |
| .speech-bubble { | |
| position: absolute; | |
| left: 50%; | |
| bottom: calc(100% + 12px); | |
| z-index: 18; | |
| width: 260px; | |
| max-width: min(320px, calc(100vw - 32px)); | |
| transform: translateX(-50%); | |
| padding: 10px 12px; | |
| border: 1px solid rgba(255, 226, 154, .48); | |
| border-radius: 6px; | |
| background: rgba(255, 244, 215, .94); | |
| color: #2d1b0d; | |
| box-shadow: 0 14px 30px rgba(0, 0, 0, .34); | |
| font-size: 12px; | |
| font-weight: 700; | |
| line-height: 1.3; | |
| pointer-events: none; | |
| } | |
| .speech-bubble::after { | |
| content: ""; | |
| position: absolute; | |
| left: 50%; | |
| bottom: -8px; | |
| width: 14px; | |
| height: 14px; | |
| transform: translateX(-50%) rotate(45deg); | |
| border-right: 1px solid rgba(255, 226, 154, .48); | |
| border-bottom: 1px solid rgba(255, 226, 154, .48); | |
| background: rgba(255, 244, 215, .94); | |
| } | |
| .tooltip { | |
| position: absolute; | |
| left: 50%; | |
| bottom: calc(100% + 10px); | |
| z-index: 20; | |
| width: 320px; | |
| max-width: min(360px, calc(100vw - 32px)); | |
| transform: translateX(-50%) translateY(6px); | |
| opacity: 0; | |
| pointer-events: none; | |
| padding: 8px 10px; | |
| border: 1px solid rgba(255, 226, 154, .34); | |
| border-radius: 5px; | |
| background: rgba(17, 9, 5, .88); | |
| color: #fff0d2; | |
| box-shadow: 0 12px 24px rgba(0,0,0,.36); | |
| transition: opacity .18s ease, transform .18s ease; | |
| } | |
| .puppet:hover .tooltip, | |
| .puppet:focus-within .tooltip, | |
| .juror:hover .tooltip, | |
| .juror:focus-within .tooltip { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .tooltip strong { | |
| display: block; | |
| color: #fff6df; | |
| font-size: 13px; | |
| } | |
| .tooltip p { | |
| margin: 6px 0 0; | |
| color: #f5dfb5; | |
| font-size: 11px; | |
| line-height: 1.28; | |
| white-space: normal; | |
| } | |
| .tooltip-meta { | |
| margin-top: 3px; | |
| color: #f4d58f; | |
| font-size: 10px; | |
| } | |
| .tooltip-io-label, | |
| .thread-label { | |
| display: block; | |
| margin-top: 7px; | |
| color: #f4d58f; | |
| font: 800 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| text-transform: uppercase; | |
| } | |
| .ai-thread-modal { | |
| display: none; | |
| position: fixed; | |
| inset: max(18px, 4vh) max(18px, 5vw); | |
| z-index: 120; | |
| overflow: auto; | |
| padding: 20px; | |
| border: 1px solid rgba(255, 226, 154, .42); | |
| border-radius: 8px; | |
| background: rgba(18, 9, 5, .96); | |
| color: #fff0d2; | |
| box-shadow: 0 24px 70px rgba(0, 0, 0, .58); | |
| } | |
| .ai-thread-modal:target { | |
| display: block; | |
| } | |
| .thread-close { | |
| position: sticky; | |
| top: 0; | |
| float: right; | |
| padding: 7px 10px; | |
| border: 1px solid rgba(255, 226, 154, .38); | |
| border-radius: 4px; | |
| background: rgba(255, 226, 154, .12); | |
| color: #fff0d2; | |
| text-decoration: none; | |
| font: 800 11px/1 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| } | |
| .thread-title { | |
| margin: 0 0 4px; | |
| color: #fff6df; | |
| font-size: 22px; | |
| } | |
| .thread-subtitle { | |
| margin: 0 0 16px; | |
| color: #f4d58f; | |
| font: 800 12px/1.3 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| text-transform: uppercase; | |
| } | |
| .thread-turn { | |
| margin: 0 0 18px; | |
| padding-bottom: 16px; | |
| border-bottom: 1px solid rgba(255, 226, 154, .16); | |
| } | |
| .thread-turn:last-child { | |
| border-bottom: 0; | |
| } | |
| .thread-block { | |
| margin: 7px 0 0; | |
| white-space: pre-wrap; | |
| color: #f8dfaa; | |
| font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| } | |
| .juror { | |
| --face: #c89259; | |
| --juror-image: none; | |
| position: relative; | |
| height: 72px; | |
| transform-origin: center bottom; | |
| filter: none; | |
| color: inherit; | |
| text-decoration: none; | |
| } | |
| .juror.active { | |
| animation: juror-react .82s ease-in-out infinite alternate; | |
| } | |
| .juror .speech-bubble { | |
| bottom: calc(100% + 6px); | |
| width: 230px; | |
| } | |
| .juror-face { | |
| position: absolute; | |
| left: 50%; | |
| top: 0; | |
| width: 30px; | |
| height: 30px; | |
| transform: translateX(-50%); | |
| border: 2px solid rgba(255, 232, 174, .5); | |
| border-radius: 50%; | |
| background: | |
| radial-gradient(circle at 35% 40%, #1d1109 0 2px, transparent 2.5px), | |
| radial-gradient(circle at 64% 40%, #1d1109 0 2px, transparent 2.5px), | |
| linear-gradient(180deg, var(--face), #835235); | |
| } | |
| .juror-face::after { | |
| content: ""; | |
| position: absolute; | |
| left: 10px; | |
| bottom: 9px; | |
| width: 14px; | |
| height: 7px; | |
| border-bottom: 2px solid #25140c; | |
| border-radius: 0 0 18px 18px; | |
| } | |
| .juror-portrait { | |
| position: absolute; | |
| left: 50%; | |
| top: -19px; | |
| z-index: 3; | |
| width: 58px; | |
| height: 74px; | |
| transform: translateX(-50%); | |
| object-fit: contain; | |
| pointer-events: none; | |
| } | |
| .juror-body { | |
| position: absolute; | |
| left: 50%; | |
| top: 32px; | |
| width: 36px; | |
| height: 36px; | |
| transform: translateX(-50%); | |
| border-radius: 20px 20px 7px 7px; | |
| border: 1px solid rgba(255, 232, 174, .18); | |
| background: linear-gradient(180deg, #5b496f, #211726); | |
| } | |
| .phase-deliberation .juror:nth-child(odd) { | |
| animation-delay: .18s; | |
| } | |
| .evidence-props { | |
| position: absolute; | |
| left: 55%; | |
| right: 11%; | |
| bottom: 36%; | |
| z-index: 9; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| justify-content: center; | |
| pointer-events: auto; | |
| } | |
| .evidence-sheet { | |
| width: 96px; | |
| min-height: 72px; | |
| padding: 8px; | |
| transform: rotate(var(--tilt)); | |
| border: 1px solid rgba(56, 32, 15, .22); | |
| border-radius: 3px; | |
| background: | |
| linear-gradient(135deg, transparent 0 84%, rgba(64, 38, 20, .18) 85%), | |
| #fff6df; | |
| color: #372212; | |
| box-shadow: 0 10px 20px rgba(0,0,0,.28); | |
| opacity: .18; | |
| transition: transform .25s ease, opacity .25s ease; | |
| } | |
| .phase-evidence .evidence-sheet, | |
| .phase-questions .evidence-sheet, | |
| .phase-deliberation .evidence-sheet, | |
| .phase-verdict .evidence-sheet, | |
| .phase-appeal .evidence-sheet { | |
| opacity: .96; | |
| animation: paper-land .55s ease-out both; | |
| } | |
| .evidence-sheet:hover { | |
| transform: rotate(0) translateY(-8px) scale(1.08); | |
| z-index: 15; | |
| } | |
| .evidence-sheet strong { | |
| display: block; | |
| margin-bottom: 4px; | |
| color: #254f7a; | |
| font: 800 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| } | |
| .evidence-sheet span { | |
| display: block; | |
| font-size: 11px; | |
| line-height: 1.2; | |
| } | |
| .trial-caption { | |
| position: absolute; | |
| left: 50%; | |
| bottom: 108px; | |
| z-index: 14; | |
| width: min(870px, calc(100% - 44px)); | |
| transform: translateX(-50%); | |
| padding: 12px 16px 13px; | |
| border: 1px solid rgba(255, 226, 154, .34); | |
| border-radius: 6px; | |
| background: rgba(13, 7, 4, .78); | |
| backdrop-filter: blur(12px); | |
| box-shadow: 0 18px 36px rgba(0,0,0,.38); | |
| } | |
| .caption-title { | |
| margin-top: 3px; | |
| color: #fff3d7; | |
| font-size: 20px; | |
| font-weight: 800; | |
| } | |
| .caption-body { | |
| margin-top: 5px; | |
| color: #f8dfaa; | |
| font-size: 14px; | |
| line-height: 1.36; | |
| white-space: pre-wrap; | |
| } | |
| .decree-ribbon { | |
| position: absolute; | |
| right: 26px; | |
| top: 22px; | |
| z-index: 10; | |
| max-width: 230px; | |
| padding: 9px 11px; | |
| border: 1px solid rgba(255, 226, 154, .26); | |
| border-radius: 5px; | |
| background: rgba(18, 9, 5, .68); | |
| color: #ffe6ae; | |
| font: 800 11px/1.35 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| text-transform: uppercase; | |
| } | |
| .phase-verdict .judge-dais, | |
| .phase-questions .judge-dais { | |
| animation: bench-lean .9s ease-in-out infinite alternate; | |
| } | |
| .phase-deliberation .jury-benches { | |
| animation: jury-murmur .7s ease-in-out infinite alternate; | |
| } | |
| .stage-prop-link { | |
| cursor: help; | |
| } | |
| .drawer-shell { | |
| max-width: 1500px; | |
| margin: 12px auto 0; | |
| } | |
| .drawer-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | |
| gap: 18px 28px; | |
| } | |
| .drawer-text-stack { | |
| color: var(--ink); | |
| line-height: 1.42; | |
| } | |
| .drawer-text-block { | |
| color: var(--ink); | |
| } | |
| .drawer-text-block h4 { | |
| margin: 5px 0 7px; | |
| } | |
| .drawer-text-block p, | |
| .drawer-empty { | |
| margin: 0 0 8px; | |
| line-height: 1.38; | |
| white-space: pre-wrap; | |
| } | |
| .vote-liable { color: var(--red); font-weight: 800; } | |
| .vote-not_liable { color: var(--green); font-weight: 800; } | |
| .vote-uncertain { color: var(--blue); font-weight: 800; } | |
| .mind-text { | |
| max-height: 340px; | |
| overflow: auto; | |
| color: var(--ink); | |
| font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace; | |
| white-space: pre-wrap; | |
| } | |
| @keyframes puppet-breathe { | |
| 0%, 100% { transform: translate(-50%, -100%) translateY(0); } | |
| 50% { transform: translate(-50%, -100%) translateY(-4px); } | |
| } | |
| @keyframes lawyer-walk { | |
| 0%, 100% { transform: translate(-50%, -100%) translateY(0) rotate(-1deg); } | |
| 25% { transform: translate(-50%, -100%) translateY(-7px) rotate(2deg); } | |
| 50% { transform: translate(-50%, -100%) translateY(0) rotate(1deg); } | |
| 75% { transform: translate(-50%, -100%) translateY(-5px) rotate(-2deg); } | |
| } | |
| @keyframes speak-mouth { | |
| 0%, 100% { height: 5px; border-radius: 0 0 18px 18px; } | |
| 50% { height: 10px; border-radius: 50%; border: 2px solid #28150c; } | |
| } | |
| @keyframes juror-react { | |
| from { transform: translateY(0) rotate(-1deg); } | |
| to { transform: translateY(-5px) rotate(2deg); } | |
| } | |
| @keyframes evidence-focus { | |
| 0%, 100% { transform: translate(-50%, -100%) translateY(0) scale(1); } | |
| 50% { transform: translate(-50%, -100%) translateY(-6px) scale(1.035); } | |
| } | |
| @keyframes paper-land { | |
| from { transform: rotate(var(--tilt)) translateY(-18px); opacity: 0; } | |
| to { transform: rotate(var(--tilt)) translateY(0); opacity: .96; } | |
| } | |
| @keyframes bench-lean { | |
| from { transform: translateX(-50%) translateY(0); } | |
| to { transform: translateX(-50%) translateY(-5px); } | |
| } | |
| @keyframes jury-murmur { | |
| from { transform: translateX(0); } | |
| to { transform: translateX(-3px); } | |
| } | |
| @keyframes gavel-hit { | |
| 0% { transform: rotate(-18deg) translateY(0); } | |
| 45% { transform: rotate(21deg) translateY(18px); } | |
| 100% { transform: rotate(-18deg) translateY(0); } | |
| } | |
| @media (max-width: 820px) { | |
| .docket-book-controls { | |
| position: fixed; | |
| top: 262px; | |
| width: calc(100vw - 52px); | |
| transform: translateX(-50%) rotate(-1deg); | |
| } | |
| .court-episode-stage { | |
| height: 1280px; | |
| min-height: 1280px; | |
| } | |
| .episode-room { | |
| background-position: center top; | |
| } | |
| .episode-title { | |
| left: 16px; | |
| right: 16px; | |
| max-width: none; | |
| } | |
| .decree-ribbon { | |
| top: 164px; | |
| left: 16px; | |
| right: auto; | |
| max-width: calc(100% - 32px); | |
| } | |
| .episode-book { | |
| top: 220px; | |
| width: min(680px, calc(100% - 20px)); | |
| } | |
| .episode-book.closed { | |
| top: 430px; | |
| width: 210px; | |
| } | |
| .book-open-content { | |
| grid-template-columns: 1fr; | |
| gap: 10px; | |
| inset: 17% 12% 14%; | |
| padding: 0 18px; | |
| } | |
| .book-open-content h2 { | |
| font-size: 22px; | |
| margin-bottom: 5px; | |
| } | |
| .book-open-content p, | |
| .book-entry { | |
| font-size: 12px; | |
| line-height: 1.22; | |
| } | |
| .book-entry { | |
| margin: 5px 0; | |
| } | |
| .judge-dais { | |
| top: 390px; | |
| width: 280px; | |
| } | |
| .counsel-table.left { | |
| left: 7%; | |
| bottom: 470px; | |
| } | |
| .counsel-table.right { | |
| right: 7%; | |
| bottom: 470px; | |
| } | |
| .counsel-table { | |
| width: 154px; | |
| } | |
| .puppet.auric { | |
| left: 20%; | |
| top: 650px; | |
| } | |
| .puppet.sable { | |
| left: 80%; | |
| top: 650px; | |
| } | |
| .speaker-auric .puppet.auric { | |
| left: 42%; | |
| top: 730px; | |
| } | |
| .speaker-sable .puppet.sable { | |
| left: 58%; | |
| top: 730px; | |
| } | |
| .puppet.clerk { | |
| left: 35%; | |
| top: 560px; | |
| } | |
| .puppet.auditor { | |
| left: 78%; | |
| top: 540px; | |
| } | |
| .witness-area { | |
| right: 5%; | |
| bottom: 580px; | |
| width: 138px; | |
| } | |
| .jury-benches { | |
| top: 520px; | |
| width: 126px; | |
| min-width: 126px; | |
| } | |
| .jury-benches.left { | |
| left: 5%; | |
| } | |
| .jury-benches.right { | |
| right: 5%; | |
| } | |
| .foreground-fence { | |
| bottom: -2px; | |
| width: 64%; | |
| } | |
| .foreground-fence.fence-left { | |
| left: -17%; | |
| } | |
| .foreground-fence.fence-right { | |
| right: -17%; | |
| } | |
| .judge-table-foreground { | |
| top: 405px; | |
| width: 760px; | |
| } | |
| .evidence-props { | |
| left: 8%; | |
| right: 8%; | |
| bottom: 410px; | |
| } | |
| .trial-caption { | |
| bottom: 105px; | |
| } | |
| .gallery-benches { | |
| bottom: 42px; | |
| grid-template-columns: repeat(3, 1fr); | |
| } | |
| } | |
| """ | |
| APP_JS = f""" | |
| () => {{ | |
| const paths = {json.dumps(AUDIO_PATHS)}; | |
| const SCORE_BASE_VOLUME = 0.16; | |
| const SCORE_QUIET_VOLUME = 0.035; | |
| const SCORE_BREATH_INTERVAL_MS = 20000; | |
| const SCORE_BREATH_DURATION_MS = 5000; | |
| const make = (name, volume = 1, loop = false) => {{ | |
| const audio = new Audio(paths[name]); | |
| audio.preload = 'auto'; | |
| audio.volume = volume; | |
| audio.loop = loop; | |
| return audio; | |
| }}; | |
| if (!window.SovereignCourtAudio) {{ | |
| const controller = {{ | |
| unlocked: false, | |
| lastPhase: null, | |
| muted: false, | |
| scoreVolume: SCORE_BASE_VOLUME, | |
| crowdVolume: 0.0, | |
| fadeFrame: null, | |
| breathTimer: null, | |
| score: make('score', SCORE_BASE_VOLUME, true), | |
| crowd: make('crowd', 0.0, true), | |
| begin() {{ | |
| this.unlocked = true; | |
| this.ensureLooping(); | |
| this.startBreathing(); | |
| this.play('select', 0.26); | |
| window.setTimeout(() => this.play('paper_long', 0.45), 120); | |
| window.setTimeout(() => this.play('gavel', 0.72), 520); | |
| this.observePhase(); | |
| this.updateToggle(); | |
| }}, | |
| ensureLooping() {{ | |
| if (!this.unlocked || this.muted) return; | |
| this.applyLoopVolumes(); | |
| this.score.play().catch(() => {{}}); | |
| this.crowd.play().catch(() => {{}}); | |
| }}, | |
| applyLoopVolumes() {{ | |
| this.score.volume = this.muted ? 0 : this.scoreVolume; | |
| this.crowd.volume = this.muted ? 0 : this.crowdVolume; | |
| }}, | |
| play(name, volume = 1) {{ | |
| if (!this.unlocked || this.muted) return; | |
| const cue = make(name, volume, false); | |
| cue.play().catch(() => {{}}); | |
| }}, | |
| setCrowd(volume) {{ | |
| this.crowdVolume = volume; | |
| this.applyLoopVolumes(); | |
| }}, | |
| fadeScore(toVolume, duration, onComplete) {{ | |
| if (this.fadeFrame) window.cancelAnimationFrame(this.fadeFrame); | |
| const fromVolume = this.scoreVolume; | |
| const started = window.performance.now(); | |
| const step = (now) => {{ | |
| const progress = Math.min(1, (now - started) / duration); | |
| this.scoreVolume = fromVolume + ((toVolume - fromVolume) * progress); | |
| this.applyLoopVolumes(); | |
| if (progress < 1) {{ | |
| this.fadeFrame = window.requestAnimationFrame(step); | |
| }} else {{ | |
| this.fadeFrame = null; | |
| if (onComplete) onComplete(); | |
| }} | |
| }}; | |
| this.fadeFrame = window.requestAnimationFrame(step); | |
| }}, | |
| breatheScore() {{ | |
| if (!this.unlocked) return; | |
| const halfDuration = SCORE_BREATH_DURATION_MS / 2; | |
| this.fadeScore(SCORE_QUIET_VOLUME, halfDuration, () => {{ | |
| this.fadeScore(SCORE_BASE_VOLUME, halfDuration); | |
| }}); | |
| }}, | |
| startBreathing() {{ | |
| if (this.breathTimer) return; | |
| this.breathTimer = window.setInterval(() => this.breatheScore(), SCORE_BREATH_INTERVAL_MS); | |
| }}, | |
| toggleMuted() {{ | |
| this.muted = !this.muted; | |
| if (this.muted) {{ | |
| this.applyLoopVolumes(); | |
| this.score.pause(); | |
| this.crowd.pause(); | |
| }} else {{ | |
| this.ensureLooping(); | |
| }} | |
| this.updateToggle(); | |
| }}, | |
| updateToggle() {{ | |
| document.querySelectorAll('.sound-toggle').forEach((button) => {{ | |
| button.classList.toggle('muted', this.muted); | |
| button.setAttribute('aria-pressed', String(this.muted)); | |
| button.setAttribute('title', this.muted ? 'Sound off' : 'Sound on'); | |
| }}); | |
| }}, | |
| cuePhase(phase) {{ | |
| if (!this.unlocked || !phase || phase === this.lastPhase) return; | |
| this.lastPhase = phase; | |
| if (phase === 'intake') {{ | |
| this.setCrowd(0.08); | |
| this.play('paper', 0.45); | |
| this.play('wood', 0.42); | |
| }} else if (phase === 'claims' || phase === 'opening') {{ | |
| this.setCrowd(0.045); | |
| this.play('steps', 0.33); | |
| }} else if (phase === 'evidence') {{ | |
| this.setCrowd(0.035); | |
| this.play('paper_long', 0.52); | |
| }} else if (phase === 'questions') {{ | |
| this.setCrowd(0.02); | |
| this.play('wood', 0.34); | |
| }} else if (phase === 'deliberation') {{ | |
| this.setCrowd(0.18); | |
| }} else if (phase === 'verdict') {{ | |
| this.setCrowd(0.0); | |
| this.play('judgement', 0.66); | |
| window.setTimeout(() => this.play('gavel', 0.9), 650); | |
| }} else if (phase === 'appeal') {{ | |
| this.setCrowd(0.035); | |
| this.play('paper_long', 0.5); | |
| }} | |
| }}, | |
| observePhase() {{ | |
| const stage = document.querySelector('.court-episode-stage'); | |
| if (stage) this.cuePhase(stage.dataset.phase); | |
| this.updateToggle(); | |
| }} | |
| }}; | |
| window.SovereignCourtAudio = controller; | |
| const observer = new MutationObserver(() => controller.observePhase()); | |
| observer.observe(document.body, {{ childList: true, subtree: true, attributes: true, attributeFilter: ['data-phase'] }}); | |
| document.addEventListener('click', (event) => {{ | |
| const toggle = event.target.closest('.sound-toggle'); | |
| if (toggle) {{ | |
| event.preventDefault(); | |
| controller.toggleMuted(); | |
| return; | |
| }} | |
| if (event.target.closest('.docket-book-controls')) {{ | |
| controller.play('select', 0.22); | |
| }} | |
| }}, true); | |
| }} | |
| }} | |
| """ | |
| APP_HEAD = f""" | |
| <script> | |
| (function() {{ | |
| const installCourtAudio = {APP_JS.strip()}; | |
| if (document.readyState === 'loading') {{ | |
| document.addEventListener('DOMContentLoaded', installCourtAudio, {{ once: true }}); | |
| }} else {{ | |
| installCourtAudio(); | |
| }} | |
| }})(); | |
| </script> | |
| """ | |
| START_JS = """ | |
| (case_label, search_query, hypothetical, speed, mind_layer) => { | |
| document.body.classList.add('trial-has-started'); | |
| if (window.SovereignCourtAudio) { | |
| window.SovereignCourtAudio.begin(); | |
| } | |
| return [case_label, search_query, hypothetical, speed, mind_layer]; | |
| } | |
| """ | |
| CHARACTERS = { | |
| JUDGE_NAME: { | |
| "class": "judge", | |
| "name": JUDGE_NAME, | |
| "role": "Stoic presiding judge", | |
| "model": "gpt-oss-20b", | |
| "image": "/gradio_api/file=assets/characters/marcus-aurelius.png", | |
| }, | |
| "Clerk Meridian": { | |
| "class": "clerk", | |
| "name": "Clerk Meridian", | |
| "role": "Court clerk", | |
| "model": "AgentCPM-Explore", | |
| }, | |
| "Advocate Auric": { | |
| "class": "auric", | |
| "name": "Advocate Auric", | |
| "role": "Claimant advocate", | |
| "model": "gpt-oss-20b", | |
| }, | |
| "Counsel Sable": { | |
| "class": "sable", | |
| "name": "Counsel Sable", | |
| "role": "Respondent advocate", | |
| "model": "gpt-oss-20b", | |
| }, | |
| "Auditor Prism": { | |
| "class": "auditor", | |
| "name": "Auditor Prism", | |
| "role": "Evidence auditor", | |
| "model": "Nemotron-Orchestrator-8B", | |
| }, | |
| "Nemotron Jury": { | |
| "class": "jury", | |
| "name": "Nemotron Jury", | |
| "role": "Jury panel", | |
| "model": "Nemotron-Orchestrator-8B", | |
| }, | |
| } | |
| JUROR_FACES = { | |
| "Karl Marx": "#d0b79c", | |
| "John Stuart Mill": "#c99b72", | |
| "Confucius": "#c49a64", | |
| "Cleopatra VII": "#b98755", | |
| "Niccolo Machiavelli": "#b88963", | |
| "Jensen Huang": "#b37758", | |
| } | |
| JUROR_IMAGES = { | |
| "Karl Marx": "/gradio_api/file=assets/characters/karl-marx.png", | |
| "John Stuart Mill": "/gradio_api/file=assets/characters/john-stuart-mill.png", | |
| "Confucius": "/gradio_api/file=assets/characters/confucius.png", | |
| "Cleopatra VII": "/gradio_api/file=assets/characters/cleopatra-vii.png", | |
| "Niccolo Machiavelli": "/gradio_api/file=assets/characters/niccolo-machiavelli.png", | |
| "Jensen Huang": "/gradio_api/file=assets/characters/jensen-huang.png", | |
| } | |
| PHASE_AGENTS = { | |
| "pretrial": ["Clerk Meridian"], | |
| } | |
| def _remote_events(request: TrialRequest) -> Iterable[TrialEvent] | None: | |
| endpoint = os.getenv("MODAL_TRIAL_URL", "").strip() | |
| if not endpoint: | |
| return None | |
| def iterator() -> Iterable[TrialEvent]: | |
| with httpx.stream("POST", endpoint, json=request.model_dump(), timeout=900.0) as response: | |
| response.raise_for_status() | |
| for line in response.iter_lines(): | |
| if line: | |
| yield TrialEvent.model_validate_json(line) | |
| return iterator() | |
| def get_events(request: TrialRequest) -> Iterable[TrialEvent]: | |
| remote = _remote_events(request) | |
| if remote is not None: | |
| yield from remote | |
| return | |
| delay = {"swift": 1.4, "measured": 2.4, "ceremonial": 3.4}[request.speed] | |
| yield from stream_trial(request, delay=delay) | |
| def _escape(value: str) -> str: | |
| return ( | |
| value.replace("&", "&") | |
| .replace("<", "<") | |
| .replace(">", ">") | |
| .replace('"', """) | |
| ) | |
| def _latest_packet_title(events: list[TrialEvent]) -> tuple[str, str]: | |
| if not events: | |
| return ( | |
| "Judge-GPT", | |
| "The gallery doors open on an AI-native courtroom. Choose a case from the docket book and begin the proceeding.", | |
| ) | |
| lines = events[0].body.splitlines() | |
| title = lines[0] if lines else "Judge-GPT" | |
| subtitle = lines[1] if len(lines) > 1 else events[0].title | |
| return title, subtitle | |
| def _active_agents_for(event: TrialEvent | None) -> set[str]: | |
| if event is None: | |
| return set(PHASE_AGENTS["pretrial"]) | |
| if not event.turns: | |
| return set() | |
| return {event.turns[0].agent} | |
| def _active_speaker_for(event: TrialEvent | None) -> str: | |
| if event is None: | |
| return "Clerk Meridian" | |
| if not event.turns: | |
| return "" | |
| return event.turns[0].agent | |
| def _speaker_class_for(speaker: str) -> str: | |
| if not speaker: | |
| return "" | |
| if speaker in CHARACTERS: | |
| return f" speaker-{CHARACTERS[speaker]['class']}" | |
| return " speaker-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in speaker).strip("-") | |
| def _latest_turn_text(event: TrialEvent | None, agent: str) -> str: | |
| if event is None: | |
| return "" | |
| turn = next((turn for turn in event.turns if turn.agent == agent), None) | |
| if turn is None: | |
| return "" | |
| return _short_text(turn.content, 210) | |
| def _thread_id(name: str) -> str: | |
| return "ai-thread-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in name).strip("-") | |
| def _turns_for_agent(events: list[TrialEvent], agent: str) -> list[dict[str, str]]: | |
| turns = [] | |
| for event in events: | |
| for turn in event.turns: | |
| if turn.agent == agent: | |
| turns.append( | |
| { | |
| "phase": event.phase, | |
| "title": event.title, | |
| "role": turn.role, | |
| "model": turn.model, | |
| "confidence": f"{turn.confidence:.2f}", | |
| "input": turn.input or "Prompt unavailable for this turn.", | |
| "output": turn.content or "No output captured yet.", | |
| } | |
| ) | |
| return turns | |
| def _thread_for_character(events: list[TrialEvent], agent: str) -> list[dict[str, str]]: | |
| if agent in JUROR_FACES: | |
| turns = _turns_for_agent(events, agent) | |
| vote = next((vote for event in reversed(events) for vote in event.votes if vote.juror == agent), None) | |
| if not turns: | |
| turns = _turns_for_agent(events, "Nemotron Jury") | |
| if vote and turns: | |
| turns = [ | |
| dict( | |
| turn, | |
| output=f"{turn['output']}\n\n{agent} persona: {vote.persona}\n{agent} vote: {vote.vote}\nReason: {vote.reason}", | |
| ) | |
| for turn in turns | |
| ] | |
| return turns | |
| return _turns_for_agent(events, agent) | |
| def _short_text(value: str, limit: int = 170) -> str: | |
| squashed = " ".join(value.split()) | |
| return squashed if len(squashed) <= limit else squashed[: limit - 1].rstrip() + "..." | |
| def _tooltip(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str: | |
| latest = turns[-1] if turns else None | |
| input_preview = _short_text(latest["input"] if latest else "Waiting for this model to receive its first prompt.") | |
| output_preview = _short_text(latest["output"] if latest else "No output has been emitted yet.") | |
| return ( | |
| "<span class='tooltip'>" | |
| f"<strong>{_escape(name)}</strong>" | |
| f"{_escape(role)}" | |
| f"<span class='tooltip-meta'>{_escape(model)}</span>" | |
| "<span class='tooltip-io-label'>Input</span>" | |
| f"<p>{_escape(input_preview)}</p>" | |
| "<span class='tooltip-io-label'>Output</span>" | |
| f"<p>{_escape(output_preview)}</p>" | |
| "<span class='tooltip-meta'>Click to open full thread</span>" | |
| "</span>" | |
| ) | |
| def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str: | |
| body = ( | |
| "".join( | |
| "<section class='thread-turn'>" | |
| f"<div class='thread-subtitle'>{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}</div>" | |
| "<span class='thread-label'>Input</span>" | |
| f"<div class='thread-block'>{_escape(turn['input'])}</div>" | |
| "<span class='thread-label'>Output</span>" | |
| f"<div class='thread-block'>{_escape(turn['output'])}</div>" | |
| "</section>" | |
| for turn in turns | |
| ) | |
| or "<section class='thread-turn'><div class='thread-block'>Waiting for this model thread to appear.</div></section>" | |
| ) | |
| return ( | |
| f"<aside id='{_escape(_thread_id(name))}' class='ai-thread-modal'>" | |
| "<a class='thread-close' href='#court-stage'>Close</a>" | |
| f"<h2 class='thread-title'>{_escape(name)}</h2>" | |
| f"<div class='thread-subtitle'>{_escape(role)} / {_escape(model)}</div>" | |
| f"{body}</aside>" | |
| ) | |
| def _puppet(agent: str, active_agents: set[str], phase: str, events: list[TrialEvent], latest: TrialEvent | None) -> str: | |
| meta = CHARACTERS[agent] | |
| active = " active" if agent in active_agents else "" | |
| walking = " walking" if agent in {"Advocate Auric", "Counsel Sable"} and agent in active_agents else "" | |
| small = " small" if agent in {"Clerk Meridian", "Auditor Prism"} else "" | |
| turns = _thread_for_character(events, agent) | |
| bubble = "" | |
| if agent in active_agents: | |
| speech = _latest_turn_text(latest, agent) | |
| if speech: | |
| bubble = f"<span class='speech-bubble'>{_escape(speech)}</span>" | |
| portrait = "" | |
| if meta.get("image"): | |
| portrait = ( | |
| f"<img class='puppet-portrait' src='{_escape(meta['image'])}' " | |
| f"alt='{_escape(meta['name'])} bust' onerror=\"this.style.display='none'\">" | |
| ) | |
| return ( | |
| f"<a class='puppet {meta['class']}{active}{walking}{small}' href='#{_escape(_thread_id(agent))}' aria-label='Open {_escape(agent)} model thread'>" | |
| f"{portrait}" | |
| "<span class='mouth'></span>" | |
| f"{bubble}" | |
| f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}" | |
| "</a>" | |
| ) | |
| def _juror(name: str, active: bool, events: list[TrialEvent] | None = None, latest: TrialEvent | None = None) -> str: | |
| face = JUROR_FACES.get(name, "#c89259") | |
| image = JUROR_IMAGES.get(name, "") | |
| active_cls = " active" if active else "" | |
| turns = _thread_for_character(events or [], name) | |
| bubble = "" | |
| if active: | |
| vote = next((vote for vote in (latest.votes if latest else []) if vote.juror == name), None) | |
| speech = _latest_turn_text(latest, name) | |
| if vote: | |
| speech = f"{vote.vote.replace('_', ' ').title()}. {vote.reason}" | |
| if speech: | |
| bubble = f"<span class='speech-bubble'>{_escape(_short_text(speech, 190))}</span>" | |
| portrait = ( | |
| f"<img class='juror-portrait' src='{_escape(image)}' alt='{_escape(name)} bust' " | |
| "onerror=\"this.style.display='none'\">" | |
| if image | |
| else "" | |
| ) | |
| return ( | |
| f"<a class='juror{active_cls}' href='#{_escape(_thread_id(name))}' style='--face: {face}' aria-label='Open {_escape(name)} model thread'>" | |
| f"{portrait}" | |
| "<span class='juror-face'></span><span class='juror-body'></span>" | |
| f"{bubble}" | |
| f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}" | |
| "</a>" | |
| ) | |
| def _book(open_book: bool) -> str: | |
| closed = "" if open_book else " closed" | |
| return ( | |
| f"<div class='episode-book{closed}'>" | |
| "<img class='book-art open-art' src='/gradio_api/file=assets/book/docket-book-open.png' alt='Open docket book'>" | |
| "<img class='book-art closed-art' src='/gradio_api/file=assets/book/docket-book-closed.png' alt='Closed docket book'>" | |
| "</div>" | |
| ) | |
| def _caption(event: TrialEvent | None, phase: str) -> tuple[str, str, str]: | |
| if event is None: | |
| return ( | |
| "PRETRIAL", | |
| "The Courtroom Is Ready", | |
| "The docket is open, the room is dimmed, and the clerk is waiting for the first call.", | |
| ) | |
| body = event.body.splitlines()[0] if event.body.splitlines() else event.body | |
| return (f"{PHASE_GLYPHS[phase]} / {phase.upper()}", event.title, body) | |
| def _evidence_props(events: list[TrialEvent]) -> str: | |
| evidence = next((event.evidence for event in reversed(events) if event.evidence), []) | |
| if not evidence: | |
| return "" | |
| tilts = ["-5deg", "3deg", "-2deg", "5deg"] | |
| sheets = [] | |
| for idx, item in enumerate(evidence[:4]): | |
| sheets.append( | |
| f"<div class='evidence-sheet stage-prop-link' style='--tilt: {tilts[idx % len(tilts)]}' title='{_escape(item.note)}'>" | |
| f"<strong>{_escape(item.id)}</strong>" | |
| f"<span>{_escape(item.title)}</span></div>" | |
| ) | |
| return f"<div class='evidence-props'>{''.join(sheets)}</div>" | |
| def _foreground_props() -> str: | |
| fence = "/gradio_api/file=assets/foreground/foregroundFence.png" | |
| judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png" | |
| return ( | |
| "<div class='foreground-props' aria-hidden='true'>" | |
| f"<img class='foreground-fence fence-left' src='{fence}' alt=''>" | |
| f"<img class='foreground-fence fence-right' src='{fence}' alt=''>" | |
| f"<img class='judge-table-foreground' src='{judge_table}' alt=''>" | |
| "</div>" | |
| ) | |
| def _courtroom_juror_names(votes: list) -> list[str]: | |
| names = list(JUROR_FACES) | |
| names.extend(vote.juror for vote in votes if vote.juror not in names) | |
| return names[:6] | |
| def _latest_votes(events: list[TrialEvent]) -> list: | |
| by_juror = {} | |
| for event in events: | |
| for vote in event.votes: | |
| by_juror[vote.juror] = vote | |
| ordered = [by_juror[name] for name in JUROR_FACES if name in by_juror] | |
| ordered.extend(vote for juror, vote in by_juror.items() if juror not in JUROR_FACES) | |
| return ordered | |
| def render_court(events: list[TrialEvent], started: bool = False) -> str: | |
| latest = events[-1] if events else None | |
| phase = latest.phase if latest else "pretrial" | |
| title, subtitle = _latest_packet_title(events) | |
| active_agents = _active_agents_for(latest) | |
| active_speaker = _active_speaker_for(latest) | |
| speaker_cls = _speaker_class_for(active_speaker) | |
| caption_phase, caption_title, caption_body = _caption(latest, phase) | |
| latest_votes = _latest_votes(events) | |
| juror_names = _courtroom_juror_names(latest_votes) | |
| started_cls = " trial-started" if started or events else "" | |
| book_open = not started and not events | |
| puppets = "".join( | |
| _puppet(agent, active_agents, phase, events, latest) | |
| for agent in [JUDGE_NAME, "Clerk Meridian", "Advocate Auric", "Counsel Sable", "Auditor Prism"] | |
| ) | |
| left_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[:3]) | |
| right_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[3:6]) | |
| evidence_props = _evidence_props(events) | |
| thread_modals = "".join( | |
| _thread_modal(meta["name"], meta["role"], meta["model"], _thread_for_character(events, agent)) | |
| for agent, meta in CHARACTERS.items() | |
| ) + "".join( | |
| _thread_modal(name, "HF-style juror", "Nemotron panel", _thread_for_character(events, name)) | |
| for name in juror_names | |
| ) | |
| return ( | |
| f"<section id='court-stage' class='court-episode-stage phase-{_escape(phase)}{_escape(speaker_cls)}{started_cls}' data-phase='{_escape(phase)}'>" | |
| "<div class='episode-room'></div>" | |
| "<div class='audio-deck' aria-hidden='true'>" | |
| + "".join(f"<audio preload='auto' src='{_escape(src)}'></audio>" for src in AUDIO_PATHS.values()) | |
| + "</div>" | |
| "<button class='sound-toggle' type='button' aria-label='Toggle sound' aria-pressed='false' title='Sound on'>" | |
| "<span class='sound-icon' aria-hidden='true'></span></button>" | |
| "<div class='episode-title'>" | |
| "<div class='episode-kicker'>Judge-GPT Virtual Courtroom</div>" | |
| f"<h1>{_escape(title)}</h1>" | |
| f"<p>{_escape(subtitle)}</p></div>" | |
| f"<div class='decree-ribbon'>Step {len(events) if events else 0}: {caption_title}<br>Hover characters for agent and model details</div>" | |
| f"{_book(book_open)}" | |
| f"<div class='judge-dais'><div class='prop-label'>{_escape(JUDGE_NAME)}</div><div class='bench-front'></div><span class='gavel'></span></div>" | |
| "<div class='counsel-table left'><div class='prop-label'>Claimant Table</div></div>" | |
| "<div class='counsel-table right'><div class='prop-label'>Respondent Table</div></div>" | |
| "<div class='trial-floor-mark'></div>" | |
| "<div class='witness-area'><div class='prop-label'>Evidence Stand</div></div>" | |
| "<div class='jury-benches left'><div class='prop-label'>Jury Box</div><div class='jury-row'>" | |
| f"{left_jurors}</div><div class='jury-rail'></div></div>" | |
| "<div class='jury-benches right'><div class='prop-label'>Jury Box</div><div class='jury-row'>" | |
| f"{right_jurors}</div><div class='jury-rail'></div></div>" | |
| f"{puppets}" | |
| f"{evidence_props}" | |
| f"{_foreground_props()}" | |
| "<div class='gallery-benches'><div></div><div></div><div></div><div></div><div></div><div></div></div>" | |
| "<div class='trial-caption'>" | |
| f"<div class='caption-phase'>Live Trial Feed / {_escape(caption_phase)}</div>" | |
| f"<div class='caption-title'>{_escape(caption_title)}</div>" | |
| f"<div class='caption-body'>{_escape(caption_body)}</div>" | |
| "</div>" | |
| f"{thread_modals}</section>" | |
| ) | |
| def render_evidence(events: list[TrialEvent]) -> str: | |
| evidence = next((event.evidence for event in reversed(events) if event.evidence), []) | |
| if not evidence: | |
| return "<div class='drawer-empty'>The exhibit drawer is closed until the clerk opens the docket.</div>" | |
| return ( | |
| "<div class='drawer-grid drawer-text-stack'>" | |
| + "".join( | |
| "<section class='drawer-text-block'>" | |
| f"<div class='drawer-kicker'>{_escape(item.id)} / {item.reliability:.2f}</div>" | |
| f"<h4>{_escape(item.title)}</h4>" | |
| f"<p>{_escape(item.excerpt)}</p>" | |
| f"<p><strong>Direction:</strong> {_escape(item.supports)}</p>" | |
| f"<p>{_escape(item.note)}</p></section>" | |
| for item in evidence | |
| ) | |
| + "</div>" | |
| ) | |
| def render_jurors(events: list[TrialEvent]) -> str: | |
| votes = _latest_votes(events) | |
| if not votes: | |
| sleepers = "".join(_juror(name, False) for name in JUROR_FACES) | |
| return ( | |
| "<section class='drawer-text-block drawer-text-stack'><div class='drawer-kicker'>Jury Box</div>" | |
| f"<div class='jury-row'>{sleepers}</div>" | |
| "<p>The jurors are seated and silent.</p></section>" | |
| ) | |
| return ( | |
| "<div class='drawer-grid drawer-text-stack'>" | |
| + "".join( | |
| "<section class='drawer-text-block'>" | |
| f"<div class='drawer-kicker'>{_escape(vote.juror)}</div>" | |
| f"<p><strong>Persona:</strong> {_escape(vote.persona)}</p>" | |
| f"<p class='vote-{vote.vote}'>{_escape(vote.vote.replace('_', ' '))}</p>" | |
| f"<p>{_escape(vote.reason)}</p>" | |
| f"<p><strong>Evidence:</strong> {_escape(', '.join(vote.evidence_ids))}</p></section>" | |
| for vote in votes | |
| ) | |
| + "</div>" | |
| ) | |
| def render_mind(events: list[TrialEvent], enabled: bool) -> str: | |
| if not enabled: | |
| return "<div class='mind-text'>Mind Layer hidden.</div>" | |
| if not events: | |
| return "<div class='mind-text'>Awaiting trace.</div>" | |
| compact = [ | |
| { | |
| "phase": event.phase, | |
| "title": event.title, | |
| "turns": [turn.model_dump() for turn in event.turns], | |
| "trace": event.trace, | |
| } | |
| for event in events | |
| ] | |
| return f"<pre class='mind-text'>{_escape(json.dumps(compact, indent=2))}</pre>" | |
| def run_ui(case_label: str, search_query: str, hypothetical: str, speed: str, mind_layer: bool): | |
| request = TrialRequest( | |
| case_id=CASE_OPTIONS.get(case_label, "socrates"), | |
| search_query=search_query or "", | |
| hypothetical=hypothetical or "", | |
| speed=speed or "swift", | |
| mind_layer=bool(mind_layer), | |
| ) | |
| events: list[TrialEvent] = [] | |
| yield ( | |
| render_court(events, started=True), | |
| render_evidence(events), | |
| render_jurors(events), | |
| render_mind(events, mind_layer), | |
| "The docket closes and the bailiff calls the room to order.", | |
| ) | |
| try: | |
| for event in get_events(request): | |
| events.append(event) | |
| status = f"Step {len(events)}: {event.title}" | |
| yield ( | |
| render_court(events, started=True), | |
| render_evidence(events), | |
| render_jurors(events), | |
| render_mind(events, mind_layer), | |
| status, | |
| ) | |
| except Exception as exc: | |
| yield ( | |
| render_court(events, started=True), | |
| render_evidence(events), | |
| render_jurors(events), | |
| render_mind(events, mind_layer), | |
| f"Model response required. Trial stopped: {exc}", | |
| ) | |
| return | |
| yield ( | |
| render_court(events, started=True), | |
| render_evidence(events), | |
| render_jurors(events), | |
| render_mind(events, mind_layer), | |
| "Verdict sealed.", | |
| ) | |
| def build_app() -> gr.Blocks: | |
| with gr.Blocks(title="Judge-GPT") as demo: | |
| with gr.Group(elem_classes=["docket-book-controls"]): | |
| gr.HTML("<div class='book-control-heading'>DATA TRIAL:</div>") | |
| with gr.Row(): | |
| case = gr.Dropdown( | |
| label="Case entry", | |
| choices=list(CASE_OPTIONS.keys()), | |
| value="Trial of Socrates", | |
| scale=2, | |
| ) | |
| start = gr.Button("Begin Trial", variant="primary", scale=1) | |
| status = gr.Markdown("Ready.", elem_classes=["book-status"]) | |
| courtroom = gr.HTML(render_court([]), label="Live courtroom") | |
| search = gr.State("") | |
| speed = gr.State("swift") | |
| mind = gr.State(True) | |
| with gr.Accordion("Advanced trial options", open=False, elem_classes=["trial-options"]): | |
| with gr.Row(): | |
| hypo = gr.Textbox(label="Hypothetical sidebar", lines=1) | |
| with gr.Row(elem_classes=["drawer-shell"]): | |
| with gr.Column(scale=1): | |
| with gr.Tab("Evidence Drawer"): | |
| evidence = gr.HTML(render_evidence([])) | |
| with gr.Tab("Juror Panel"): | |
| jurors = gr.HTML(render_jurors([])) | |
| mind_html = gr.HTML(render_mind([], True), visible=False) | |
| start.click( | |
| run_ui, | |
| inputs=[case, search, hypo, speed, mind], | |
| outputs=[courtroom, evidence, jurors, mind_html, status], | |
| js=START_JS, | |
| ) | |
| return demo | |
| demo = build_app() | |
| if __name__ == "__main__": | |
| demo.queue().launch( | |
| show_error=True, | |
| allowed_paths=["assets"], | |
| css=CSS, | |
| head=APP_HEAD, | |
| theme=gr.themes.Soft(), | |
| ) | |