| 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(), |
| ) |
|
|