diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,2102 +1,2800 @@
-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"""
-
-"""
-
-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 (
- ""
- f"{_escape(name)} "
- f"{_escape(role)}"
- f"{_escape(model)} "
- "Input "
- f"{_escape(input_preview)}
"
- "Output "
- f"{_escape(output_preview)}
"
- "Click to open full thread "
- " "
- )
-
-
-def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
- body = (
- "".join(
- ""
- f"{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}
"
- "Input "
- f"{_escape(turn['input'])}
"
- "Output "
- f"{_escape(turn['output'])}
"
- " "
- for turn in turns
- )
- or "Waiting for this model thread to appear.
"
- )
- return (
- f""
- )
-
-
-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"{_escape(speech)} "
- portrait = ""
- if meta.get("image"):
- portrait = (
- f" "
- )
- return (
- f""
- f"{portrait}"
- " "
- f"{bubble}"
- f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}"
- " "
- )
-
-
-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"{_escape(_short_text(speech, 190))} "
- portrait = (
- f" "
- if image
- else ""
- )
- return (
- f""
- f"{portrait}"
- " "
- f"{bubble}"
- f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}"
- " "
- )
-
-
-def _book(open_book: bool) -> str:
- closed = "" if open_book else " closed"
- return (
- f"
"
- "
"
- "
"
- "
"
- )
-
-
-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""
- f"{_escape(item.id)} "
- f"{_escape(item.title)}
"
- )
- return f"{''.join(sheets)}
"
-
-
-def _foreground_props() -> str:
- fence = "/gradio_api/file=assets/foreground/foregroundFence.png"
- judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png"
- return (
- ""
- )
-
-
-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""
- "
"
- ""
- + "".join(f"
" for src in AUDIO_PATHS.values())
- + "
"
- ""
- " "
- ""
- "
Judge-GPT Virtual Courtroom
"
- f"
{_escape(title)} "
- f"
{_escape(subtitle)}
"
- f"Step {len(events) if events else 0}: {caption_title} Hover characters for agent and model details
"
- f"{_book(book_open)}"
- f""
- ""
- ""
- "
"
- ""
- "Jury Box
"
- f"{left_jurors}
"
- "Jury Box
"
- f"{right_jurors}
"
- f"{puppets}"
- f"{evidence_props}"
- f"{_foreground_props()}"
- ""
- ""
- f"
Live Trial Feed / {_escape(caption_phase)}
"
- f"
{_escape(caption_title)}
"
- f"
{_escape(caption_body)}
"
- "
"
- f"{thread_modals} "
- )
-
-
-def render_evidence(events: list[TrialEvent]) -> str:
- evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
- if not evidence:
- return "The exhibit drawer is closed until the clerk opens the docket.
"
- return (
- ""
- + "".join(
- "
"
- f"{_escape(item.id)} / {item.reliability:.2f}
"
- f"{_escape(item.title)} "
- f"{_escape(item.excerpt)}
"
- f"Direction: {_escape(item.supports)}
"
- f"{_escape(item.note)}
"
- for item in evidence
- )
- + "
"
- )
-
-
-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 (
- "Jury Box
"
- f"{sleepers}
"
- "The jurors are seated and silent.
"
- )
- return (
- ""
- + "".join(
- "
"
- f"{_escape(vote.juror)}
"
- f"Persona: {_escape(vote.persona)}
"
- f"{_escape(vote.vote.replace('_', ' '))}
"
- f"{_escape(vote.reason)}
"
- f"Evidence: {_escape(', '.join(vote.evidence_ids))}
"
- for vote in votes
- )
- + "
"
- )
-
-
-def render_mind(events: list[TrialEvent], enabled: bool) -> str:
- if not enabled:
- return "Mind Layer hidden.
"
- if not events:
- return "Awaiting trace.
"
- 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"{_escape(json.dumps(compact, indent=2))} "
-
-
-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("DATA TRIAL:
")
- 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(),
- )
+from __future__ import annotations
+
+import json
+import os
+import queue
+import threading
+import time
+from collections.abc import Iterable
+from dataclasses import dataclass
+
+import gradio as gr
+import httpx
+
+from sovereign_bench.engine import JUDGE_NAME, JUROR_PERSONAS, stream_trial
+from sovereign_bench.cases import CASES, get_case
+from sovereign_bench.models import CasePacket, EvidenceItem, 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",
+ "Greg Heffley vs Mom": "greg",
+ "Custom": "custom",
+}
+
+DEFAULT_MODAL_TRIAL_URL = "https://ali-j-iqbal24--trial-stream.modal.run"
+MIN_READ_SECONDS = 2.2
+WORDS_PER_SECOND = 3.2
+READ_BUFFER_SECONDS = 0.8
+MAX_READ_SECONDS = 8.5
+
+PHASE_GLYPHS = {
+ "pretrial": "00",
+ "intake": "01",
+ "claims": "02",
+ "opening": "03",
+ "evidence": "04",
+ "questions": "05",
+ "deliberation": "06",
+ "verdict": "07",
+ "appeal": "08",
+}
+
+TRIAL_PROGRESS_STAGES = (
+ ("pretrial", "Pretrial"),
+ ("intake", "Intake"),
+ ("claims", "Claims"),
+ ("opening", "Opening"),
+ ("evidence", "Evidence"),
+ ("questions", "Questions"),
+ ("deliberation", "Deliberation"),
+ ("verdict", "Verdict"),
+)
+
+VERDICT_LABELS = {
+ "liable": "Guilty",
+ "not_liable": "Not Guilty",
+ "mixed": "Mixed",
+ "uncertain": "Uncertain",
+}
+
+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: 72px;
+ z-index: 9999;
+ width: min(760px, 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;
+}
+
+.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;
+}
+
+.trial-progress {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 70;
+ display: grid;
+ grid-template-columns: repeat(8, minmax(0, 1fr));
+ gap: 1px;
+ padding: 3px clamp(10px, 2vw, 24px) 4px;
+ border-bottom: 1px solid rgba(217, 176, 96, .2);
+ background: rgba(23, 13, 8, .58);
+ backdrop-filter: blur(8px);
+ box-shadow: 0 8px 18px rgba(8, 4, 2, .22);
+ pointer-events: none;
+}
+
+.trial-progress-segment {
+ position: relative;
+ min-width: 0;
+ padding-top: 5px;
+ overflow: hidden;
+ color: rgba(244, 213, 143, .38);
+ font: 800 10px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
+ letter-spacing: .04em;
+ text-align: center;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.trial-progress-segment::before {
+ content: "";
+ position: absolute;
+ left: 3px;
+ right: 3px;
+ top: 0;
+ height: 2px;
+ border-radius: 999px;
+ background: rgba(217, 176, 96, .18);
+}
+
+.trial-progress-segment.complete {
+ color: rgba(217, 176, 96, .68);
+}
+
+.trial-progress-segment.complete::before {
+ background: rgba(217, 176, 96, .48);
+}
+
+.trial-progress-segment.current {
+ color: #ffe6a6;
+ text-shadow: 0 0 10px rgba(255, 211, 116, .52);
+}
+
+.trial-progress-segment.current::before {
+ height: 3px;
+ background: #ffd675;
+ box-shadow: 0 0 12px rgba(255, 214, 117, .68);
+}
+
+.trial-progress-abbrev {
+ display: none;
+}
+
+.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: 122px;
+ z-index: 14;
+ width: min(980px, 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;
+}
+
+.episode-book.custom-book {
+ pointer-events: auto;
+}
+
+.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: 50%;
+ width: min(163px, 20vw);
+ 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: 15% 9% 12%;
+ z-index: 2;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 82px;
+ padding: 0 20px;
+ transition: opacity .35s ease;
+}
+
+.book-open-content h2 {
+ margin: 0 0 8px;
+ color: #4c2a12;
+ font-size: 28px;
+ letter-spacing: 0;
+}
+
+.book-open-content p,
+.book-entry {
+ color: #3c2615;
+ font-size: 14px;
+ line-height: 1.28;
+}
+
+.book-entry {
+ margin: 8px 0;
+ padding-left: 12px;
+ border-left: 3px solid rgba(111, 61, 23, .36);
+}
+
+.book-context {
+ margin-top: 8px;
+}
+
+.book-case-title {
+ margin: 0 0 6px;
+ color: #4c2a12;
+ font-weight: 800;
+}
+
+.book-evidence-columns {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.book-evidence-column h3 {
+ margin: 0 0 6px;
+ color: #4c2a12;
+ font-size: 15px;
+ line-height: 1.12;
+}
+
+.book-evidence-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.book-evidence-list li {
+ margin: 0 0 6px;
+ padding-left: 9px;
+ border-left: 2px solid rgba(111, 61, 23, .32);
+ color: #3c2615;
+ font-size: 12px;
+ line-height: 1.2;
+}
+
+.book-field {
+ width: 100%;
+ min-height: 42px;
+ resize: none;
+ border: 1px solid rgba(90, 50, 20, .34);
+ border-radius: 4px;
+ background: rgba(255, 247, 224, .7);
+ color: #2b1b10;
+ font: 12px/1.22 Georgia, "Times New Roman", serif;
+ box-shadow: inset 0 1px 2px rgba(59, 29, 10, .16);
+ pointer-events: auto;
+}
+
+.book-context-field {
+ min-height: 138px;
+ font-size: 13px;
+}
+
+.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: 1%;
+}
+
+.jury-benches.right {
+ right: 1%;
+}
+
+.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: -6.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: 20%;
+ z-index: 1;
+ width: 39.1%;
+ 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: calc(40% + 156px);
+ --skin: #c38a55;
+ --robe: #1b1b20;
+ --accent: #79242a;
+ --portrait-width: 96px;
+ --portrait-top: -28px;
+}
+
+.puppet.clerk {
+ left: 43%;
+ top: 66%;
+ z-index: 14;
+ --skin: #b77b52;
+ --robe: #365548;
+ --accent: #2f6f5e;
+}
+
+.puppet.auric {
+ left: 24%;
+ top: 87%;
+ --skin: #c9975d;
+ --robe: #5b2719;
+ --accent: #a45c25;
+}
+
+.speaker-auric .puppet.auric {
+ left: 43%;
+ top: 87%;
+}
+
+.puppet.sable {
+ left: 75%;
+ top: 87%;
+ --skin: #a86d4a;
+ --robe: #1d3045;
+ --accent: #254f7a;
+}
+
+.speaker-sable .puppet.sable {
+ left: 75%;
+ top: 87%;
+}
+
+.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;
+}
+
+.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.judge::before,
+.puppet.judge::after {
+ display: none;
+}
+
+.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.judge .mouth {
+ display: none;
+}
+
+.puppet.active .mouth,
+.puppet.walking .mouth {
+ animation: speak-mouth .5s ease-in-out infinite;
+}
+
+.speech-bubble.active-dialogue {
+ position: absolute;
+ left: 50%;
+ top: 43%;
+ bottom: auto;
+ z-index: 30;
+ width: min(500px, calc(100vw - 44px));
+ max-height: 34vh;
+ overflow: visible;
+ transform: translate(-50%, -100%);
+ padding: 10px 13px 11px;
+ border: 2px solid #141413;
+ border-radius: 20px;
+ background: rgba(255, 253, 247, .97);
+ color: #141413 !important;
+ box-shadow: 0 12px 24px rgba(0, 0, 0, .32);
+ font-size: 12px;
+ font-weight: 650;
+ line-height: 1.32;
+ pointer-events: none;
+}
+
+.speech-bubble.active-dialogue,
+.speech-bubble.active-dialogue * {
+ color: #141413 !important;
+}
+
+.speech-bubble.active-dialogue::before,
+.speech-bubble.active-dialogue::after {
+ content: "";
+ position: absolute;
+ left: var(--bubble-tail-x, 50%);
+ display: block;
+ transform: translateX(-50%) rotate(45deg);
+}
+
+.speech-bubble.active-dialogue::before {
+ bottom: -13px;
+ width: 22px;
+ height: 22px;
+ background: #141413;
+ border-radius: 0 0 5px 0;
+}
+
+.speech-bubble.active-dialogue::after {
+ bottom: -9px;
+ width: 16px;
+ height: 16px;
+ transform: translateX(-50%) rotate(45deg);
+ background: rgba(255, 253, 247, .97);
+ border-radius: 0 0 3px 0;
+}
+
+.speech-bubble.active-dialogue.pending {
+ opacity: .82;
+}
+
+.dialogue-meta {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ margin-bottom: 5px;
+ font: 800 9px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
+ text-transform: uppercase;
+}
+
+.dialogue-meta strong {
+ font-size: 10px;
+}
+
+.dialogue-text {
+ max-height: calc(34vh - 42px);
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
+.speech-bubble.active-dialogue.speaker-clerk { left: 43%; top: 62%; }
+.speech-bubble.active-dialogue.speaker-judge { left: 50%; top: 43%; }
+.speech-bubble.active-dialogue.speaker-auric { left: 43%; top: 78%; }
+.speech-bubble.active-dialogue.speaker-sable { left: 75%; top: 78%; }
+.speech-bubble.active-dialogue.juror-dialogue { left: 50%; top: 57%; }
+.speech-bubble.active-dialogue.juror-dialogue {
+ top: 42%;
+ width: min(340px, calc(50vw - 24px));
+}
+
+.speech-bubble.active-dialogue.speaker-karl-marx,
+.speech-bubble.active-dialogue.speaker-john-stuart-mill,
+.speech-bubble.active-dialogue.speaker-confucius {
+ left: 1.5%;
+ transform: translateY(-100%);
+}
+
+.speech-bubble.active-dialogue.speaker-cleopatra-vii,
+.speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
+.speech-bubble.active-dialogue.speaker-jensen-huang {
+ right: 1.5%;
+ left: auto;
+ transform: translateY(-100%);
+}
+
+.speech-bubble.active-dialogue.speaker-karl-marx,
+.speech-bubble.active-dialogue.speaker-cleopatra-vii {
+ --bubble-tail-x: 19%;
+}
+
+.speech-bubble.active-dialogue.speaker-john-stuart-mill,
+.speech-bubble.active-dialogue.speaker-niccolo-machiavelli {
+ --bubble-tail-x: 50%;
+}
+
+.speech-bubble.active-dialogue.speaker-confucius,
+.speech-bubble.active-dialogue.speaker-jensen-huang {
+ --bubble-tail-x: 81%;
+}
+
+.verdict-popup {
+ position: absolute;
+ left: 50%;
+ top: 54%;
+ z-index: 42;
+ width: min(460px, calc(100vw - 44px));
+ transform: translate(-50%, -50%);
+ padding: 18px 22px 20px;
+ border: 2px solid rgba(255, 235, 178, .94);
+ border-radius: 8px;
+ background: rgba(20, 12, 7, .95);
+ color: #fff4d6;
+ text-align: center;
+ box-shadow: 0 28px 58px rgba(0, 0, 0, .5);
+ animation: verdict-pop .34s ease-out both;
+}
+
+.verdict-popup-kicker {
+ display: block;
+ margin-bottom: 7px;
+ color: #d9b060;
+ font: 800 11px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+.verdict-popup-finding {
+ display: block;
+ color: #fff8e6;
+ font: 900 clamp(28px, 5vw, 48px)/1.02 Georgia, serif;
+}
+
+.verdict-popup-decree {
+ margin: 10px auto 0;
+ max-width: 38ch;
+ color: rgba(255, 244, 214, .86);
+ font-size: 13px;
+ line-height: 1.35;
+}
+
+.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-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); }
+}
+
+@keyframes verdict-pop {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -46%) scale(.94);
+ }
+ 100% {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
+@media (max-width: 820px) {
+ .docket-book-controls {
+ position: fixed;
+ top: 130px;
+ width: calc(100vw - 52px);
+ transform: translateX(-50%) rotate(-1deg);
+ }
+
+ .trial-progress {
+ grid-template-columns: repeat(8, minmax(24px, 1fr));
+ padding: 2px 5px 3px;
+ }
+
+ .trial-progress-segment {
+ font-size: 9px;
+ letter-spacing: 0;
+ }
+
+ .trial-progress-label {
+ display: none;
+ }
+
+ .trial-progress-abbrev {
+ display: inline;
+ }
+
+ .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);
+ }
+
+ .speech-bubble.active-dialogue,
+ .speech-bubble.active-dialogue.speaker-clerk,
+ .speech-bubble.active-dialogue.speaker-judge,
+ .speech-bubble.active-dialogue.speaker-auric,
+ .speech-bubble.active-dialogue.speaker-sable,
+ .speech-bubble.active-dialogue.juror-dialogue {
+ left: 50%;
+ top: 218px;
+ width: calc(100% - 28px);
+ max-height: 260px;
+ transform: translateX(-50%);
+ }
+
+ .speech-bubble.active-dialogue::after {
+ display: none;
+ }
+
+ .speech-bubble.active-dialogue.juror-dialogue,
+ .speech-bubble.active-dialogue.speaker-karl-marx,
+ .speech-bubble.active-dialogue.speaker-john-stuart-mill,
+ .speech-bubble.active-dialogue.speaker-confucius,
+ .speech-bubble.active-dialogue.speaker-cleopatra-vii,
+ .speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
+ .speech-bubble.active-dialogue.speaker-jensen-huang {
+ top: 500px;
+ width: min(320px, calc(100vw - 28px));
+ transform: translateY(-100%);
+ }
+
+ .speech-bubble.active-dialogue.speaker-karl-marx,
+ .speech-bubble.active-dialogue.speaker-john-stuart-mill,
+ .speech-bubble.active-dialogue.speaker-confucius {
+ left: 14px;
+ right: auto;
+ }
+
+ .speech-bubble.active-dialogue.speaker-cleopatra-vii,
+ .speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
+ .speech-bubble.active-dialogue.speaker-jensen-huang {
+ right: 14px;
+ left: auto;
+ }
+
+ .episode-book {
+ top: 218px;
+ width: min(680px, calc(100% - 20px));
+ }
+
+ .episode-book.closed {
+ top: 640px;
+ width: 140px;
+ }
+
+ .book-open-content {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ inset: 15% 11% 13%;
+ padding: 0 16px;
+ }
+
+ .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;
+ }
+
+ .book-evidence-columns {
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ }
+
+ .book-evidence-list li {
+ font-size: 10px;
+ line-height: 1.12;
+ }
+
+ .book-field {
+ min-height: 34px;
+ font-size: 10px;
+ }
+
+ .book-context-field {
+ min-height: 84px;
+ }
+
+ .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: 970px;
+ }
+
+ .puppet.sable {
+ left: 80%;
+ top: 970px;
+ }
+
+ .speaker-auric .puppet.auric {
+ left: 42%;
+ top: 970px;
+ }
+
+ .speaker-sable .puppet.sable {
+ left: 80%;
+ top: 970px;
+ }
+
+ .puppet.clerk {
+ left: 35%;
+ top: 880px;
+ }
+
+ .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: -66px;
+ width: 64%;
+ }
+
+ .foreground-fence.fence-left {
+ left: -17%;
+ }
+
+ .foreground-fence.fence-right {
+ right: -17%;
+ }
+
+ .judge-table-foreground {
+ top: 213px;
+ width: 646px;
+ }
+
+ .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"""
+
+"""
+
+START_JS = """
+(case_label, search_query, hypothetical, custom_payload, speed, mind_layer) => {
+ const book = document.querySelector('.episode-book.custom-book');
+ const collect = (selector) => Array.from(document.querySelectorAll(selector)).map((node) => node.value || '');
+ const payload = book ? JSON.stringify({
+ context: document.querySelector('.book-context-field')?.value || '',
+ claimant_evidence: collect('.book-claimant-field'),
+ respondent_evidence: collect('.book-respondent-field')
+ }) : (custom_payload || '');
+ if (book) {
+ const data = JSON.parse(payload);
+ const hasContext = data.context.trim().length > 0;
+ const hasClaimant = data.claimant_evidence.some((value) => value.trim().length > 0);
+ const hasRespondent = data.respondent_evidence.some((value) => value.trim().length > 0);
+ if (!hasContext || !hasClaimant || !hasRespondent) {
+ return [case_label, search_query, hypothetical, payload, speed, mind_layer];
+ }
+ }
+ document.body.classList.add('trial-has-started');
+ if (window.SovereignCourtAudio) {
+ window.SovereignCourtAudio.begin();
+ }
+ return [case_label, search_query, hypothetical, payload, 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",
+ },
+ "Mike OSS": {
+ "class": "auric",
+ "name": "Mike OSS",
+ "role": "Claimant advocate",
+ "model": "gpt-oss-20b",
+ },
+ "Harvey Vector": {
+ "class": "sable",
+ "name": "Harvey Vector",
+ "role": "Respondent advocate",
+ "model": "gpt-oss-20b",
+ },
+ "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",
+}
+
+TRIAL_TURN_ORDER = (
+ "Clerk Meridian",
+ JUDGE_NAME,
+ "Mike OSS",
+ "Harvey Vector",
+ JUDGE_NAME,
+ "Mike OSS",
+ "Harvey Vector",
+ "Nemotron Jury",
+ *JUROR_PERSONAS.keys(),
+ JUDGE_NAME,
+)
+
+PHASE_AGENTS = {
+ "pretrial": ["Clerk Meridian"],
+}
+
+
+@dataclass(frozen=True)
+class SpeakerCue:
+ name: str
+ role: str
+ text: str
+ pending: bool = False
+
+
+_EVENT_STREAM_DONE = object()
+
+
+def _remote_events(request: TrialRequest) -> Iterable[TrialEvent] | None:
+ endpoint = os.getenv("MODAL_TRIAL_URL", DEFAULT_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, delay: float | None = None) -> Iterable[TrialEvent]:
+ remote = _remote_events(request)
+ if remote is not None:
+ yield from remote
+ return
+ stream_delay = {"swift": 1.4, "measured": 2.4, "ceremonial": 3.4}[request.speed] if delay is None else delay
+ yield from stream_trial(request, delay=stream_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 _role_for_speaker(name: str, event: TrialEvent | None = None) -> str:
+ if event is not None:
+ turn = next((turn for turn in event.turns if turn.agent == name), None)
+ if turn is not None:
+ return turn.role
+ if name in CHARACTERS:
+ return CHARACTERS[name]["role"]
+ if name in JUROR_FACES:
+ return "juror"
+ return "speaker"
+
+
+def _expected_next_speaker(events: list[TrialEvent]) -> SpeakerCue | None:
+ if len(events) >= len(TRIAL_TURN_ORDER):
+ return None
+ name = TRIAL_TURN_ORDER[len(events)]
+ role = _role_for_speaker(name)
+ return SpeakerCue(name=name, role=role, text=f"{name} is preparing a response.", pending=True)
+
+
+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 _active_speaker_cue(event: TrialEvent | None, pending_speaker: SpeakerCue | None = None) -> SpeakerCue | None:
+ if pending_speaker is not None:
+ return pending_speaker
+ if event is None or not event.turns:
+ return None
+ turn = event.turns[0]
+ text = turn.content.strip()
+ if not text:
+ return None
+ return SpeakerCue(name=turn.agent, role=turn.role, text=text)
+
+
+def _reading_duration(text: str) -> float:
+ word_count = len(text.split())
+ return min(MAX_READ_SECONDS, max(MIN_READ_SECONDS, (word_count / WORDS_PER_SECOND) + READ_BUFFER_SECONDS))
+
+
+def _event_dialogue_text(event: TrialEvent) -> str:
+ if event.turns:
+ return event.turns[0].content
+ return event.body
+
+
+def _event_status(event: TrialEvent, step: int) -> str:
+ if event.turns:
+ return f"Step {step}: {event.turns[0].agent} - {event.title}"
+ return f"Step {step}: {event.title}"
+
+
+def _pending_status(cue: SpeakerCue | None) -> str:
+ if cue is None:
+ return "The court is preparing the next turn."
+ return f"{cue.name} is preparing their response."
+
+
+def _start_event_producer(request: TrialRequest) -> queue.Queue[object]:
+ events: queue.Queue[object] = queue.Queue()
+
+ def produce() -> None:
+ try:
+ try:
+ stream = get_events(request, delay=0.0)
+ except TypeError:
+ stream = get_events(request)
+ for event in stream:
+ events.put(event)
+ except Exception as exc:
+ events.put(exc)
+ finally:
+ events.put(_EVENT_STREAM_DONE)
+
+ threading.Thread(target=produce, name="trial-event-producer", daemon=True).start()
+ return events
+
+
+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 (
+ ""
+ f"{_escape(name)} "
+ f"{_escape(role)}"
+ f"{_escape(model)} "
+ "Input "
+ f"{_escape(input_preview)}
"
+ "Output "
+ f"{_escape(output_preview)}
"
+ "Click to open full thread "
+ " "
+ )
+
+
+def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
+ body = (
+ "".join(
+ ""
+ f"{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}
"
+ "Input "
+ f"{_escape(turn['input'])}
"
+ "Output "
+ f"{_escape(turn['output'])}
"
+ " "
+ for turn in turns
+ )
+ or "Waiting for this model thread to appear.
"
+ )
+ return (
+ f""
+ )
+
+
+def _active_dialogue(cue: SpeakerCue | None) -> str:
+ if cue is None:
+ return ""
+ speaker_cls = _speaker_class_for(cue.name).strip()
+ classes = ["speech-bubble", "active-dialogue"]
+ if speaker_cls:
+ classes.append(speaker_cls)
+ if cue.name in JUROR_FACES:
+ classes.append("juror-dialogue")
+ if cue.pending:
+ classes.append("pending")
+ pending_attr = " data-pending='true'" if cue.pending else ""
+ return (
+ f""
+ "
"
+ f"{_escape(cue.name)} "
+ f"{_escape(cue.role)} "
+ "
"
+ f"
{_escape(cue.text)}
"
+ "
"
+ )
+
+
+def _verdict_popup(events: list[TrialEvent], show: bool) -> str:
+ if not show:
+ return ""
+ verdict = next((event.verdict for event in reversed(events) if event.verdict is not None), None)
+ if verdict is None:
+ return ""
+ finding = VERDICT_LABELS.get(verdict.finding, verdict.finding.replace("_", " ").title())
+ return (
+ f""
+ )
+
+
+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 {"Mike OSS", "Harvey Vector"} and agent in active_agents else ""
+ small = " small" if agent == "Clerk Meridian" else ""
+ turns = _thread_for_character(events, agent)
+ portrait = ""
+ if meta.get("image"):
+ portrait = (
+ f" "
+ )
+ return (
+ f""
+ f"{portrait}"
+ " "
+ f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}"
+ " "
+ )
+
+
+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)
+ portrait = (
+ f" "
+ if image
+ else ""
+ )
+ fallback_art = "" if image else " "
+ return (
+ f""
+ f"{portrait}"
+ f"{fallback_art}"
+ f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}"
+ " "
+ )
+
+
+def _packet_for_label(case_label: str) -> CasePacket:
+ return get_case(CASE_OPTIONS.get(case_label, "socrates"))
+
+
+def _split_evidence(packet: CasePacket) -> tuple[list[EvidenceItem], list[EvidenceItem]]:
+ claimant = [item for item in packet.evidence if item.supports == "claimant"]
+ respondent = [item for item in packet.evidence if item.supports == "respondent"]
+ if len(claimant) < 3:
+ claimant.extend(item for item in packet.evidence if item.supports in {"mixed", "context"} and item not in claimant)
+ if len(respondent) < 3:
+ respondent.extend(item for item in packet.evidence if item.supports in {"mixed", "context"} and item not in respondent)
+ return claimant[:3], respondent[:3]
+
+
+def _book_evidence_column(title: str, items: list[EvidenceItem]) -> str:
+ entries = "".join(
+ ""
+ f"{_escape(item.title)} "
+ f"{_escape(item.note)}"
+ " "
+ for item in items
+ )
+ return (
+ ""
+ f"{_escape(title)} "
+ f""
+ " "
+ )
+
+
+def _custom_evidence_fields(class_name: str, label: str) -> str:
+ fields = "".join(
+ f""
+ for index in range(1, 4)
+ )
+ return f""
+
+
+def _book(open_book: bool, packet: CasePacket | None = None, custom_mode: bool = False) -> str:
+ closed = "" if open_book else " closed"
+ custom_class = " custom-book" if custom_mode and open_book else ""
+ hidden_attr = "" if custom_mode and open_book else " aria-hidden='true'"
+ packet = packet or get_case("socrates")
+ if custom_mode and open_book:
+ left_page = (
+ ""
+ )
+ right_page = (
+ "Evidence "
+ f"{_custom_evidence_fields('book-claimant-field', 'Evidence for Claimant')}"
+ f"{_custom_evidence_fields('book-respondent-field', 'Evidence against Claimant')}"
+ "
"
+ )
+ else:
+ claimant_evidence, respondent_evidence = _split_evidence(packet)
+ left_page = (
+ "Trial details "
+ f"{_escape(packet.title)}
"
+ f"{_escape(packet.context or packet.setting)}
"
+ f"{_escape(packet.claimant)} {_escape(packet.claimant_claim)}
"
+ f"{_escape(packet.respondent)} {_escape(packet.respondent_claim)}
"
+ " "
+ )
+ right_page = (
+ "Evidence "
+ f"{_book_evidence_column(f'Evidence for {packet.claimant}', claimant_evidence)}"
+ f"{_book_evidence_column(f'Evidence for {packet.respondent}', respondent_evidence)}"
+ "
"
+ )
+ return (
+ f""
+ "
"
+ "
"
+ f"
"
+ f"{left_page}"
+ f"{right_page}"
+ "
"
+ "
"
+ )
+
+
+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""
+ f"{_escape(item.id)} "
+ f"{_escape(item.title)}
"
+ )
+ return f"{''.join(sheets)}
"
+
+
+def _foreground_props() -> str:
+ fence = "/gradio_api/file=assets/foreground/foregroundFence.png"
+ judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png"
+ return (
+ ""
+ )
+
+
+def _trial_progress(events: list[TrialEvent]) -> str:
+ latest = events[-1] if events else None
+ current_phase = latest.phase if latest else "pretrial"
+ stage_keys = [key for key, _label in TRIAL_PROGRESS_STAGES]
+ current_index = stage_keys.index(current_phase) if current_phase in stage_keys else None
+ segments = []
+ for index, (key, label) in enumerate(TRIAL_PROGRESS_STAGES):
+ classes = ["trial-progress-segment"]
+ attrs = [f"data-phase='{_escape(key)}'"]
+ if current_index is not None and index < current_index:
+ classes.append("complete")
+ if current_index == index:
+ classes.append("current")
+ attrs.append("aria-current='step'")
+ if key == "verdict":
+ classes.append("complete")
+ abbrev = label[:3]
+ segments.append(
+ f""
+ f"{_escape(label)} "
+ f"{_escape(abbrev)} "
+ " "
+ )
+ return (
+ ""
+ + "".join(segments)
+ + " "
+ )
+
+
+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,
+ pending_speaker: SpeakerCue | None = None,
+ show_verdict_popup: bool = False,
+ pretrial_case: CasePacket | None = None,
+ custom_mode: bool = False,
+) -> str:
+ latest = events[-1] if events else None
+ phase = latest.phase if latest else "pretrial"
+ title, subtitle = _latest_packet_title(events)
+ active_cue = _active_speaker_cue(latest, pending_speaker)
+ active_speaker = active_cue.name if active_cue is not None else _active_speaker_for(latest)
+ active_agents = {active_speaker} if active_speaker else _active_agents_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", "Mike OSS", "Harvey Vector"]
+ )
+ 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""
+ f"{_trial_progress(events)}"
+ "
"
+ ""
+ + "".join(f"
" for src in AUDIO_PATHS.values())
+ + "
"
+ ""
+ " "
+ ""
+ "
Judge-GPT Virtual Courtroom
"
+ f"
{_escape(title)} "
+ f"
{_escape(subtitle)}
"
+ f"Step {len(events) if events else 0}: {caption_title} Hover characters for agent and model details
"
+ f"{_book(book_open, pretrial_case, custom_mode)}"
+ f""
+ ""
+ ""
+ "
"
+ ""
+ "Jury Box
"
+ f"{left_jurors}
"
+ "Jury Box
"
+ f"{right_jurors}
"
+ f"{puppets}"
+ f"{evidence_props}"
+ f"{_foreground_props()}"
+ f"{_active_dialogue(active_cue)}"
+ f"{_verdict_popup(events, show_verdict_popup)}"
+ ""
+ ""
+ f"
Live Trial Feed / {_escape(caption_phase)}
"
+ f"
{_escape(caption_title)}
"
+ f"
{_escape(caption_body)}
"
+ "
"
+ f"{thread_modals} "
+ )
+
+
+def render_evidence(events: list[TrialEvent]) -> str:
+ evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
+ if not evidence:
+ return "The exhibit drawer is closed until the clerk opens the docket.
"
+ return (
+ ""
+ + "".join(
+ "
"
+ f"{_escape(item.id)} / {item.reliability:.2f}
"
+ f"{_escape(item.title)} "
+ f"{_escape(item.excerpt)}
"
+ f"Direction: {_escape(item.supports)}
"
+ f"{_escape(item.note)}
"
+ for item in evidence
+ )
+ + "
"
+ )
+
+
+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 (
+ "Jury Box
"
+ f"{sleepers}
"
+ "The jurors are seated and silent.
"
+ )
+ return (
+ ""
+ + "".join(
+ "
"
+ f"{_escape(vote.juror)}
"
+ f"Persona: {_escape(vote.persona)}
"
+ f"{_escape(vote.vote.replace('_', ' '))}
"
+ f"{_escape(vote.reason)}
"
+ f"Evidence: {_escape(', '.join(vote.evidence_ids))}
"
+ for vote in votes
+ )
+ + "
"
+ )
+
+
+def render_mind(events: list[TrialEvent], enabled: bool) -> str:
+ if not enabled:
+ return "Mind Layer hidden.
"
+ if not events:
+ return "Awaiting trace.
"
+ 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"{_escape(json.dumps(compact, indent=2))} "
+
+
+def _clean_custom_items(values: list[str]) -> list[str]:
+ return [" ".join(value.split()) for value in values if " ".join(value.split())]
+
+
+def _custom_case_from_payload(payload: str) -> CasePacket:
+ try:
+ data = json.loads(payload or "{}")
+ except json.JSONDecodeError as exc:
+ raise ValueError("Custom case details could not be read from the docket book.") from exc
+ context = " ".join(str(data.get("context", "")).split())
+ claimant_items = _clean_custom_items([str(value) for value in data.get("claimant_evidence", [])])
+ respondent_items = _clean_custom_items([str(value) for value in data.get("respondent_evidence", [])])
+ if not context:
+ raise ValueError("Custom requires a trial details paragraph.")
+ if not claimant_items or not respondent_items:
+ raise ValueError("Custom requires at least one evidence item for each side.")
+ evidence = [
+ EvidenceItem(
+ id=f"CUS-F{index}",
+ title=f"Claimant Evidence {index}",
+ source="Custom docket entry",
+ excerpt=item,
+ supports="claimant",
+ reliability=0.65,
+ note=item,
+ )
+ for index, item in enumerate(claimant_items[:3], start=1)
+ ]
+ evidence.extend(
+ EvidenceItem(
+ id=f"CUS-A{index}",
+ title=f"Respondent Evidence {index}",
+ source="Custom docket entry",
+ excerpt=item,
+ supports="respondent",
+ reliability=0.65,
+ note=item,
+ )
+ for index, item in enumerate(respondent_items[:3], start=1)
+ )
+ return CasePacket(
+ id="custom",
+ title="Custom Trial",
+ subtitle="A custom docket assembled in the opening book.",
+ claimant="Claimant",
+ respondent="Respondent",
+ charge="Whether the custom record supports the claimant or the respondent.",
+ setting="A custom courtroom packet entered by the user.",
+ context=context,
+ claimant_claim="The claimant says the custom context and supporting evidence justify a favorable finding.",
+ respondent_claim="The respondent says the custom context is incomplete, overread, or answered by contrary evidence.",
+ source_note="Custom user-entered case packet from the docket book.",
+ evidence=evidence,
+ )
+
+
+def render_case_preview(case_label: str) -> str:
+ case_id = CASE_OPTIONS.get(case_label, "socrates")
+ return render_court(
+ [],
+ pretrial_case=get_case(case_id) if case_id != "custom" else None,
+ custom_mode=case_id == "custom",
+ )
+
+
+def run_ui(
+ case_label: str,
+ search_query: str,
+ hypothetical: str,
+ custom_payload: str,
+ speed: str,
+ mind_layer: bool,
+):
+ case_id = CASE_OPTIONS.get(case_label, "socrates")
+ try:
+ custom_case = _custom_case_from_payload(custom_payload) if case_id == "custom" else None
+ except ValueError as exc:
+ yield (
+ render_court([], pretrial_case=None, custom_mode=True),
+ render_evidence([]),
+ render_jurors([]),
+ render_mind([], mind_layer),
+ str(exc),
+ )
+ return
+ request = TrialRequest(
+ case_id=case_id,
+ search_query=search_query or "",
+ hypothetical=hypothetical or "",
+ custom_case=custom_case,
+ speed=speed or "swift",
+ mind_layer=bool(mind_layer),
+ )
+ events: list[TrialEvent] = []
+ produced_events = _start_event_producer(request)
+ pending_speaker = _expected_next_speaker(events)
+ yield (
+ render_court(events, started=True, pending_speaker=pending_speaker),
+ render_evidence(events),
+ render_jurors(events),
+ render_mind(events, mind_layer),
+ _pending_status(pending_speaker),
+ )
+ try:
+ while True:
+ item = produced_events.get()
+ if item is _EVENT_STREAM_DONE:
+ break
+ if isinstance(item, Exception):
+ raise item
+ event = item
+ events.append(event)
+ yield (
+ render_court(events, started=True),
+ render_evidence(events),
+ render_jurors(events),
+ render_mind(events, mind_layer),
+ _event_status(event, len(events)),
+ )
+ duration = _reading_duration(_event_dialogue_text(event))
+ if duration > 0:
+ time.sleep(duration)
+ pending_speaker = _expected_next_speaker(events)
+ if pending_speaker is not None and produced_events.empty():
+ yield (
+ render_court(events, started=True, pending_speaker=pending_speaker),
+ render_evidence(events),
+ render_jurors(events),
+ render_mind(events, mind_layer),
+ _pending_status(pending_speaker),
+ )
+ 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, show_verdict_popup=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("DATA TRIAL:
")
+ 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_case_preview("Trial of Socrates"), label="Live courtroom")
+ search = gr.State("")
+ hypo = gr.State("")
+ custom_payload = gr.State("")
+ speed = gr.State("swift")
+ mind = gr.State(True)
+ 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)
+ case.change(
+ render_case_preview,
+ inputs=[case],
+ outputs=[courtroom],
+ )
+ start.click(
+ run_ui,
+ inputs=[case, search, hypo, custom_payload, 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(),
+ )