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"{_escape(name)} bust" - 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"
" - "Open docket book" - "Closed docket book" - "
" - ) - - -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"" - ) - 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"
" - "
" - "" - "" - "
" - "
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"
{_escape(JUDGE_NAME)}
" - "
Claimant Table
" - "
Respondent Table
" - "
" - "
Evidence Stand
" - "
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"{_escape(name)} bust" + 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"

    {_escape(label)}

    {fields}
    " + + +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 = ( + "

    Trial details

    " + "" + "
    " + ) + 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"
    " + "Open docket book" + "Closed docket book" + 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"" + ) + 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"" + "" + ) + 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, + 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)}" + "
    " + "" + "" + "
    " + "
    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"
    {_escape(JUDGE_NAME)}
    " + "
    Claimant Table
    " + "
    Respondent Table
    " + "
    " + "
    Evidence Stand
    " + "
    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(), + )