Spaces:
Running on Zero
Running on Zero
agharsallah commited on
Commit Β·
d2180f0
1
Parent(s): f5ed461
feat: Enhance color handling and styling across fishbowl components
Browse files- src/ui/fishbowl/adapter.py +19 -0
- src/ui/fishbowl/assets/styles.css +23 -18
- src/ui/fishbowl/lab.py +1 -3
- src/ui/fishbowl/render/avatar.py +5 -10
- src/ui/fishbowl/render/feed.py +8 -1
- src/ui/fishbowl/render/mindcard.py +2 -2
- src/ui/fishbowl/render/stage.py +9 -1
- src/ui/fishbowl/render/winner.py +6 -3
- src/ui/fishbowl/view_model.py +6 -0
- tests/test_fishbowl_mindcard.py +1 -1
src/ui/fishbowl/adapter.py
CHANGED
|
@@ -64,6 +64,25 @@ def agent_hue(manifest) -> int:
|
|
| 64 |
return int(digest[:4], 16) % 360
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def agent_archetype(manifest) -> str:
|
| 68 |
"""The manifest's ``archetype``, or a fallback derived from its role."""
|
| 69 |
return getattr(manifest, "archetype", None) or f"the {manifest.role}"
|
|
|
|
| 64 |
return int(digest[:4], 16) % 360
|
| 65 |
|
| 66 |
|
| 67 |
+
# ββ agent colour (one phosphor per hue; every cast member is mapped the same way) β
|
| 68 |
+
# Lives here, with the other design-vocabulary mappings (hue/tier/mood colour), so every
|
| 69 |
+
# surface β avatar, MindCard, feed line, split row β derives the *same* colour from the
|
| 70 |
+
# *same* hue. That single source is what lets the eye map a name to a face to a transcript
|
| 71 |
+
# line at a glance. Lightness/chroma are shared across the cast; only the hue varies.
|
| 72 |
+
_AC_LIGHTNESS = 0.82 # high enough to stay legible as name text on the dark tank
|
| 73 |
+
_AC_CHROMA = 0.17 # vivid enough that adjacent hues read as distinct identities
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def agent_color(hue: int, lightness: float = _AC_LIGHTNESS, chroma: float = _AC_CHROMA) -> str:
|
| 77 |
+
"""The agent's phosphor colour β all cast share L/C, only the hue varies."""
|
| 78 |
+
return f"oklch({lightness} {chroma} {hue})"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def agent_color_dim(hue: int) -> str:
|
| 82 |
+
"""The dimmed companion colour (used for sealed / inactive surfaces)."""
|
| 83 |
+
return f"oklch(0.5 0.1 {hue})"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
def agent_archetype(manifest) -> str:
|
| 87 |
"""The manifest's ``archetype``, or a fallback derived from its role."""
|
| 88 |
return getattr(manifest, "archetype", None) or f"the {manifest.role}"
|
src/ui/fishbowl/assets/styles.css
CHANGED
|
@@ -651,7 +651,10 @@ footer { display: none !important; }
|
|
| 651 |
.fishbowl .stage { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); padding: 22px; gap: 10px; overflow: auto; min-height: 0; place-items: center; }
|
| 652 |
.fishbowl .core { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; pointer-events: none; z-index: 1; }
|
| 653 |
.fishbowl .core-glyph { font-size: 64px; color: var(--cyan); text-shadow: var(--glow-hot); line-height: 1; opacity: 0.85; animation: coreFloat 6s ease-in-out infinite; }
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
| 655 |
.fishbowl .core-title { font-size: 16px; letter-spacing: 0.1em; color: var(--ink); }
|
| 656 |
.fishbowl .core-round { font-size: 9px; color: var(--ink-dim); }
|
| 657 |
/* Audience-only secret peek (Twenty Sprouts) β a dashed "director's view" chip the cast
|
|
@@ -685,7 +688,7 @@ footer { display: none !important; }
|
|
| 685 |
/* head: avatar Β· identity Β· the live mood pill (the one datum that changes turn to turn) */
|
| 686 |
.fishbowl .mind-head { display: grid; grid-template-columns: auto 1fr auto; gap: 11px; align-items: center; padding-bottom: 10px; border-bottom: 1px solid var(--line-soft); }
|
| 687 |
.fishbowl .mind-id { min-width: 0; }
|
| 688 |
-
.fishbowl .mind-name { font-size: 14.5px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink); line-height: 1.15; display: flex; align-items: center; gap: 7px; }
|
| 689 |
.fishbowl .mic { color: var(--coral); font-size: 9px; animation: livePulse 0.9s ease-in-out infinite; }
|
| 690 |
.fishbowl .mind-arch { margin-top: 3px; font-size: 10.5px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 691 |
|
|
@@ -728,17 +731,21 @@ footer { display: none !important; }
|
|
| 728 |
/* keep the narrator rail feed compact β minimal height, scroll stays as-is */
|
| 729 |
.fishbowl .rail .feed { flex: none; max-height: 220px; }
|
| 730 |
.fishbowl .feed.dense { gap: 9px; }
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
@keyframes feIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
| 733 |
.fishbowl .fe.narr { border-left: 2px solid var(--violet); padding-left: 12px; }
|
| 734 |
.fishbowl .narr-voice { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--violet); display: block; margin-bottom: 4px; }
|
| 735 |
.fishbowl .fe.narr p { margin: 0; font-size: 13px; line-height: 1.6; color: var(--ink); opacity: 0.92; font-style: italic; }
|
| 736 |
-
.fishbowl .fe.say { background: rgba(3,12,18,0.6); border-left: 2px solid var(--ac); border-radius: 0 var(--r) var(--r) 0; padding: 8px 11px; }
|
| 737 |
.fishbowl .say-line { font-size: 12.5px; line-height: 1.45; }
|
| 738 |
-
.fishbowl .say-line b { color: var(--ac); margin-right: 7px; letter-spacing: 0.04em; }
|
| 739 |
.fishbowl .say-line span { color: var(--ink); }
|
| 740 |
-
.fishbowl .say-think { margin-top: 5px; font-size: 11px; color: var(--ac); opacity: 0.85; }
|
| 741 |
-
.fishbowl .say-think i { color: color-mix(in oklab, var(--ac) 75%, var(--ink)); }
|
| 742 |
.fishbowl .fe.poke, .fishbowl .fe.verdict-fe { border: 1px solid var(--amber); border-radius: var(--r); padding: 9px 11px; background: rgba(255,207,107,0.07); }
|
| 743 |
.fishbowl .poke-tag { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); border: 1px solid rgba(255,207,107,0.4); border-radius: 999px; padding: 2px 8px; margin-bottom: 6px; }
|
| 744 |
.fishbowl .fe.poke p, .fishbowl .fe.verdict-fe p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink); }
|
|
@@ -757,7 +764,7 @@ footer { display: none !important; }
|
|
| 757 |
.fishbowl .roster { border-right: 1px solid var(--line); padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: rgba(6,20,27,0.4); overflow: auto; }
|
| 758 |
.fishbowl .roster-chip { display: flex; align-items: center; gap: 11px; padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r); transition: all 0.2s ease; }
|
| 759 |
.fishbowl .roster-chip.on { border-color: var(--ac); box-shadow: 0 0 14px color-mix(in oklab, var(--ac) 35%, transparent); background: color-mix(in oklab, var(--ac) 6%, transparent); }
|
| 760 |
-
.fishbowl .rc-name { font-size: 12.5px; letter-spacing: 0.05em; color: var(--ink); }
|
| 761 |
.fishbowl .rc-mood { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.1em; text-transform: uppercase; }
|
| 762 |
.fishbowl .feedview-main { min-height: 0; display: flex; }
|
| 763 |
.fishbowl .feedview-main .feed { max-width: 720px; margin: 0 auto; width: 100%; }
|
|
@@ -770,7 +777,7 @@ footer { display: none !important; }
|
|
| 770 |
.fishbowl .split-row { display: grid; grid-template-columns: 230px 1fr 1fr; gap: 16px; align-items: stretch; padding: 14px; border: 1px solid var(--line); border-radius: var(--r-lg); margin-bottom: 12px; background: var(--panel); transition: all 0.2s ease; }
|
| 771 |
.fishbowl .split-row.on { border-color: var(--ac); box-shadow: 0 0 18px color-mix(in oklab, var(--ac) 30%, transparent); }
|
| 772 |
.fishbowl .split-id { display: flex; gap: 12px; align-items: center; }
|
| 773 |
-
.fishbowl .split-name { font-size: 14px; letter-spacing: 0.06em; color: var(--ink); }
|
| 774 |
.fishbowl .split-arch { font-size: 10.5px; color: var(--ink-dim); }
|
| 775 |
.fishbowl .split-model { display: flex; align-items: center; gap: 5px; font-size: 9.5px; color: var(--ink-mid); margin-top: 3px; }
|
| 776 |
.fishbowl .split-said, .fishbowl .split-think { border-radius: var(--r); padding: 12px 14px; display: flex; align-items: center; }
|
|
@@ -845,7 +852,7 @@ footer { display: none !important; }
|
|
| 845 |
.fishbowl .wf-sub { display: flex; align-items: center; justify-content: center; gap: 9px; flex-wrap: wrap; margin-top: -2px; }
|
| 846 |
.fishbowl .wf-arch { font-style: italic; font-size: 12px; color: var(--ink-dim); letter-spacing: 0.02em; }
|
| 847 |
.fishbowl .wf-roster { display: flex; align-items: center; justify-content: center; gap: 7px; flex-wrap: wrap; }
|
| 848 |
-
.fishbowl .wf-chip { font-family: var(--font-display); font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--win-hue)
|
| 849 |
.fishbowl .wf-cheer { font-size: 12.5px; color: var(--ink); opacity: 0.92; margin-top: 4px; }
|
| 850 |
.fishbowl .wf-loser { font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint); }
|
| 851 |
|
|
@@ -998,15 +1005,13 @@ footer { display: none !important; }
|
|
| 998 |
.fishbowl.fb-constellation .core-round { color: var(--ink-faint); }
|
| 999 |
|
| 1000 |
/* ---- MindCard depth + life: entrance, hover lift, richer speaking glow ---- */
|
| 1001 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
@media (prefers-reduced-motion: no-preference) {
|
| 1003 |
-
.fishbowl.fb-constellation .
|
| 1004 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(1) { animation-delay: 0.04s; }
|
| 1005 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(2) { animation-delay: 0.10s; }
|
| 1006 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(3) { animation-delay: 0.16s; }
|
| 1007 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(4) { animation-delay: 0.22s; }
|
| 1008 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(5) { animation-delay: 0.28s; }
|
| 1009 |
-
.fishbowl.fb-constellation .ring-slot:nth-child(6) { animation-delay: 0.34s; }
|
| 1010 |
}
|
| 1011 |
.fishbowl .mind-front {
|
| 1012 |
transition: border-color 0.22s ease, box-shadow 0.22s ease, transform 0.22s ease;
|
|
|
|
| 651 |
.fishbowl .stage { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); padding: 22px; gap: 10px; overflow: auto; min-height: 0; place-items: center; }
|
| 652 |
.fishbowl .core { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; pointer-events: none; z-index: 1; }
|
| 653 |
.fishbowl .core-glyph { font-size: 64px; color: var(--cyan); text-shadow: var(--glow-hot); line-height: 1; opacity: 0.85; animation: coreFloat 6s ease-in-out infinite; }
|
| 654 |
+
/* Gentle amplitude: the stage HTML is replaced every tick, restarting this float mid-cycle,
|
| 655 |
+
so a large drift would snap the central glyph each time. A small rise keeps the drift
|
| 656 |
+
alive in static/scrub views without a visible jump during live autoplay. */
|
| 657 |
+
@keyframes coreFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
| 658 |
.fishbowl .core-title { font-size: 16px; letter-spacing: 0.1em; color: var(--ink); }
|
| 659 |
.fishbowl .core-round { font-size: 9px; color: var(--ink-dim); }
|
| 660 |
/* Audience-only secret peek (Twenty Sprouts) β a dashed "director's view" chip the cast
|
|
|
|
| 688 |
/* head: avatar Β· identity Β· the live mood pill (the one datum that changes turn to turn) */
|
| 689 |
.fishbowl .mind-head { display: grid; grid-template-columns: auto 1fr auto; gap: 11px; align-items: center; padding-bottom: 10px; border-bottom: 1px solid var(--line-soft); }
|
| 690 |
.fishbowl .mind-id { min-width: 0; }
|
| 691 |
+
.fishbowl .mind-name { font-size: 14.5px; font-weight: 700; letter-spacing: 0.04em; color: var(--ac, var(--ink)); text-shadow: 0 0 9px color-mix(in oklab, var(--ac, var(--ink)) 35%, transparent); line-height: 1.15; display: flex; align-items: center; gap: 7px; }
|
| 692 |
.fishbowl .mic { color: var(--coral); font-size: 9px; animation: livePulse 0.9s ease-in-out infinite; }
|
| 693 |
.fishbowl .mind-arch { margin-top: 3px; font-size: 10.5px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 694 |
|
|
|
|
| 731 |
/* keep the narrator rail feed compact β minimal height, scroll stays as-is */
|
| 732 |
.fishbowl .rail .feed { flex: none; max-height: 220px; }
|
| 733 |
.fishbowl .feed.dense { gap: 9px; }
|
| 734 |
+
/* Only the newest line slides in. The Show re-renders the whole feed HTML every tick
|
| 735 |
+
(Gradio replaces innerHTML wholesale), so animating every .fe re-played the entrance on
|
| 736 |
+
the entire transcript each time β reading as a full-screen reload. Scoping the entrance
|
| 737 |
+
to :last-child keeps the "new message" cue while the rest of the log stays still. */
|
| 738 |
+
.fishbowl .fe:last-child { animation: feIn 0.3s ease; }
|
| 739 |
@keyframes feIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
| 740 |
.fishbowl .fe.narr { border-left: 2px solid var(--violet); padding-left: 12px; }
|
| 741 |
.fishbowl .narr-voice { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--violet); display: block; margin-bottom: 4px; }
|
| 742 |
.fishbowl .fe.narr p { margin: 0; font-size: 13px; line-height: 1.6; color: var(--ink); opacity: 0.92; font-style: italic; }
|
| 743 |
+
.fishbowl .fe.say { background: rgba(3,12,18,0.6); border-left: 2px solid var(--ac, var(--cyan)); border-radius: 0 var(--r) var(--r) 0; padding: 8px 11px; }
|
| 744 |
.fishbowl .say-line { font-size: 12.5px; line-height: 1.45; }
|
| 745 |
+
.fishbowl .say-line b { color: var(--ac, var(--cyan)); text-shadow: 0 0 8px color-mix(in oklab, var(--ac, var(--cyan)) 40%, transparent); margin-right: 7px; font-weight: 700; letter-spacing: 0.04em; }
|
| 746 |
.fishbowl .say-line span { color: var(--ink); }
|
| 747 |
+
.fishbowl .say-think { margin-top: 5px; font-size: 11px; color: var(--ac, var(--cyan)); opacity: 0.85; }
|
| 748 |
+
.fishbowl .say-think i { color: color-mix(in oklab, var(--ac, var(--cyan)) 75%, var(--ink)); }
|
| 749 |
.fishbowl .fe.poke, .fishbowl .fe.verdict-fe { border: 1px solid var(--amber); border-radius: var(--r); padding: 9px 11px; background: rgba(255,207,107,0.07); }
|
| 750 |
.fishbowl .poke-tag { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); border: 1px solid rgba(255,207,107,0.4); border-radius: 999px; padding: 2px 8px; margin-bottom: 6px; }
|
| 751 |
.fishbowl .fe.poke p, .fishbowl .fe.verdict-fe p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink); }
|
|
|
|
| 764 |
.fishbowl .roster { border-right: 1px solid var(--line); padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: rgba(6,20,27,0.4); overflow: auto; }
|
| 765 |
.fishbowl .roster-chip { display: flex; align-items: center; gap: 11px; padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r); transition: all 0.2s ease; }
|
| 766 |
.fishbowl .roster-chip.on { border-color: var(--ac); box-shadow: 0 0 14px color-mix(in oklab, var(--ac) 35%, transparent); background: color-mix(in oklab, var(--ac) 6%, transparent); }
|
| 767 |
+
.fishbowl .rc-name { font-size: 12.5px; letter-spacing: 0.05em; color: var(--ac, var(--ink)); }
|
| 768 |
.fishbowl .rc-mood { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.1em; text-transform: uppercase; }
|
| 769 |
.fishbowl .feedview-main { min-height: 0; display: flex; }
|
| 770 |
.fishbowl .feedview-main .feed { max-width: 720px; margin: 0 auto; width: 100%; }
|
|
|
|
| 777 |
.fishbowl .split-row { display: grid; grid-template-columns: 230px 1fr 1fr; gap: 16px; align-items: stretch; padding: 14px; border: 1px solid var(--line); border-radius: var(--r-lg); margin-bottom: 12px; background: var(--panel); transition: all 0.2s ease; }
|
| 778 |
.fishbowl .split-row.on { border-color: var(--ac); box-shadow: 0 0 18px color-mix(in oklab, var(--ac) 30%, transparent); }
|
| 779 |
.fishbowl .split-id { display: flex; gap: 12px; align-items: center; }
|
| 780 |
+
.fishbowl .split-name { font-size: 14px; letter-spacing: 0.06em; font-weight: 700; color: var(--ac, var(--ink)); text-shadow: 0 0 8px color-mix(in oklab, var(--ac, var(--ink)) 32%, transparent); }
|
| 781 |
.fishbowl .split-arch { font-size: 10.5px; color: var(--ink-dim); }
|
| 782 |
.fishbowl .split-model { display: flex; align-items: center; gap: 5px; font-size: 9.5px; color: var(--ink-mid); margin-top: 3px; }
|
| 783 |
.fishbowl .split-said, .fishbowl .split-think { border-radius: var(--r); padding: 12px 14px; display: flex; align-items: center; }
|
|
|
|
| 852 |
.fishbowl .wf-sub { display: flex; align-items: center; justify-content: center; gap: 9px; flex-wrap: wrap; margin-top: -2px; }
|
| 853 |
.fishbowl .wf-arch { font-style: italic; font-size: 12px; color: var(--ink-dim); letter-spacing: 0.02em; }
|
| 854 |
.fishbowl .wf-roster { display: flex; align-items: center; justify-content: center; gap: 7px; flex-wrap: wrap; }
|
| 855 |
+
.fishbowl .wf-chip { font-family: var(--font-display); font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--ch, var(--win-hue)) 80% 80%); border: 1px solid hsl(var(--ch, var(--win-hue)) 75% 62% / 0.45); background: hsl(var(--ch, var(--win-hue)) 75% 50% / 0.12); border-radius: 999px; padding: 3px 10px; white-space: nowrap; }
|
| 856 |
.fishbowl .wf-cheer { font-size: 12.5px; color: var(--ink); opacity: 0.92; margin-top: 4px; }
|
| 857 |
.fishbowl .wf-loser { font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint); }
|
| 858 |
|
|
|
|
| 1005 |
.fishbowl.fb-constellation .core-round { color: var(--ink-faint); }
|
| 1006 |
|
| 1007 |
/* ---- MindCard depth + life: entrance, hover lift, richer speaking glow ---- */
|
| 1008 |
+
/* The stage HTML is rebuilt every tick (wholesale innerHTML swap), so a per-card entrance
|
| 1009 |
+
replayed the full staggered cascade on the whole cast each time β the dominant "screen
|
| 1010 |
+
reload" flicker. Instead, only the card that just acted gives a small settle, drawing the
|
| 1011 |
+
eye to the active speaker while the rest of the cast holds perfectly still. */
|
| 1012 |
+
@keyframes mindSpeakIn { from { opacity: 0.6; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
| 1013 |
@media (prefers-reduced-motion: no-preference) {
|
| 1014 |
+
.fishbowl.fb-constellation .mind.speaking { animation: mindSpeakIn 0.3s cubic-bezier(0.2,0.7,0.2,1) both; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1015 |
}
|
| 1016 |
.fishbowl .mind-front {
|
| 1017 |
transition: border-color 0.22s ease, box-shadow 0.22s ease, transform 0.22s ease;
|
src/ui/fishbowl/lab.py
CHANGED
|
@@ -414,9 +414,7 @@ def build_lab() -> dict[str, gr.components.Component]:
|
|
| 414 |
)
|
| 415 |
|
| 416 |
if workers and not choices:
|
| 417 |
-
gr.Markdown(
|
| 418 |
-
f"_No {backend_label} models in the catalogue β the cast runs the deterministic stub._"
|
| 419 |
-
)
|
| 420 |
|
| 421 |
# The Judge. Static handles (the app shell reads them on Summon and the picker
|
| 422 |
# offers the catalogue), wrapped in a Group whose visibility tracks the effective
|
|
|
|
| 414 |
)
|
| 415 |
|
| 416 |
if workers and not choices:
|
| 417 |
+
gr.Markdown(f"_No {backend_label} models in the catalogue β the cast runs the deterministic stub._")
|
|
|
|
|
|
|
| 418 |
|
| 419 |
# The Judge. Static handles (the app shell reads them on Summon and the picker
|
| 420 |
# offers the catalogue), wrapped in a Group whose visibility tracks the effective
|
src/ui/fishbowl/render/avatar.py
CHANGED
|
@@ -9,17 +9,12 @@ Unit 1's CSS animates the emitted ``av-*`` classes.
|
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
"""The agent's phosphor colour β all cast share L/C, only the hue varies."""
|
| 17 |
-
return f"oklch({lightness} {chroma} {hue})"
|
| 18 |
|
| 19 |
-
|
| 20 |
-
def agent_color_dim(hue: int) -> str:
|
| 21 |
-
"""The dimmed companion colour (used for sealed / inactive surfaces)."""
|
| 22 |
-
return f"oklch(0.5 0.09 {hue})"
|
| 23 |
|
| 24 |
|
| 25 |
# mouth path per mood (panic is drawn as an open 'o' instead of a path)
|
|
|
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
+
# Colour helpers live in the design-vocabulary layer (``adapter``) so the avatar, MindCard,
|
| 13 |
+
# feed line, and split row all derive one phosphor from one hue. Re-exported here for the
|
| 14 |
+
# renderers that have always reached for them via this module.
|
| 15 |
+
from src.ui.fishbowl.adapter import agent_color, agent_color_dim
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
__all__ = ["render_avatar", "agent_color", "agent_color_dim"]
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
# mouth path per mood (panic is drawn as an open 'o' instead of a path)
|
src/ui/fishbowl/render/feed.py
CHANGED
|
@@ -22,6 +22,8 @@ from __future__ import annotations
|
|
| 22 |
|
| 23 |
import html
|
| 24 |
|
|
|
|
|
|
|
| 25 |
_BOLT = "β‘" # poke lines carry a lightning bolt
|
| 26 |
_SCALES = "β" # the verdict line carries the scales of judgement
|
| 27 |
|
|
@@ -49,7 +51,12 @@ def _say_line(item: dict, *, mind_reader: bool) -> str:
|
|
| 49 |
if mind_reader and thought:
|
| 50 |
thought_html = html.escape(thought)
|
| 51 |
line += f'<div class="say-think">β³ <i class="thought">{thought_html}</i></div>'
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def _poke_line(item: dict) -> str:
|
|
|
|
| 22 |
|
| 23 |
import html
|
| 24 |
|
| 25 |
+
from src.ui.fishbowl.adapter import agent_color, agent_color_dim
|
| 26 |
+
|
| 27 |
_BOLT = "β‘" # poke lines carry a lightning bolt
|
| 28 |
_SCALES = "β" # the verdict line carries the scales of judgement
|
| 29 |
|
|
|
|
| 51 |
if mind_reader and thought:
|
| 52 |
thought_html = html.escape(thought)
|
| 53 |
line += f'<div class="say-think">β³ <i class="thought">{thought_html}</i></div>'
|
| 54 |
+
# The line wears the speaker's own phosphor (name, accent border, thought tint) so the
|
| 55 |
+
# transcript is colour-coded to the cast β the same hue as their MindCard and avatar.
|
| 56 |
+
# A hue-less line (e.g. an un-cast actor) falls back to the CSS default accent.
|
| 57 |
+
hue = item.get("hue")
|
| 58 |
+
style = f' style="--ac:{agent_color(int(hue))};--acd:{agent_color_dim(int(hue))}"' if hue is not None else ""
|
| 59 |
+
return f'<div class="fe say"{style}>{line}</div>'
|
| 60 |
|
| 61 |
|
| 62 |
def _poke_line(item: dict) -> str:
|
src/ui/fishbowl/render/mindcard.py
CHANGED
|
@@ -12,8 +12,8 @@ from __future__ import annotations
|
|
| 12 |
|
| 13 |
import html
|
| 14 |
|
| 15 |
-
from src.ui.fishbowl.adapter import TIER_COLOR, mood_color
|
| 16 |
-
from src.ui.fishbowl.render.avatar import
|
| 17 |
|
| 18 |
__all__ = ["render_mindcard"]
|
| 19 |
|
|
|
|
| 12 |
|
| 13 |
import html
|
| 14 |
|
| 15 |
+
from src.ui.fishbowl.adapter import TIER_COLOR, agent_color, agent_color_dim, mood_color
|
| 16 |
+
from src.ui.fishbowl.render.avatar import render_avatar
|
| 17 |
|
| 18 |
__all__ = ["render_mindcard"]
|
| 19 |
|
src/ui/fishbowl/render/stage.py
CHANGED
|
@@ -18,6 +18,8 @@ from __future__ import annotations
|
|
| 18 |
|
| 19 |
import html
|
| 20 |
|
|
|
|
|
|
|
| 21 |
# Fixed, evocative core glyph for the stage centre. The prototype uses the scenario's
|
| 22 |
# own glyph; the view-model carries no glyph field, so we fall back to this (and honour
|
| 23 |
# a ``glyph`` key if a future view-model provides one).
|
|
@@ -117,8 +119,14 @@ def render_split(vm: dict) -> str:
|
|
| 117 |
leak = " leak" if member.get("mood") == "panic" else ""
|
| 118 |
said_cell = _split_cell(member.get("said"), placeholder="β hasn't spoken β", think=False)
|
| 119 |
think_cell = _split_cell(member.get("thought"), placeholder="β quiet β", think=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
rows.append(
|
| 121 |
-
f'<div class="split-row{speaking}">'
|
| 122 |
'<div class="split-id">'
|
| 123 |
f'<div class="disp split-name">{name}</div>'
|
| 124 |
f'<div class="split-arch">{archetype}</div>'
|
|
|
|
| 18 |
|
| 19 |
import html
|
| 20 |
|
| 21 |
+
from src.ui.fishbowl.adapter import agent_color, agent_color_dim
|
| 22 |
+
|
| 23 |
# Fixed, evocative core glyph for the stage centre. The prototype uses the scenario's
|
| 24 |
# own glyph; the view-model carries no glyph field, so we fall back to this (and honour
|
| 25 |
# a ``glyph`` key if a future view-model provides one).
|
|
|
|
| 119 |
leak = " leak" if member.get("mood") == "panic" else ""
|
| 120 |
said_cell = _split_cell(member.get("said"), placeholder="β hasn't spoken β", think=False)
|
| 121 |
think_cell = _split_cell(member.get("thought"), placeholder="β quiet β", think=True)
|
| 122 |
+
# Each row carries its mind's own phosphor so the omniscient table is colour-keyed
|
| 123 |
+
# to the same hue the cast wears on stage and in the transcript.
|
| 124 |
+
hue = member.get("hue")
|
| 125 |
+
row_style = (
|
| 126 |
+
f' style="--ac:{agent_color(int(hue))};--acd:{agent_color_dim(int(hue))}"' if hue is not None else ""
|
| 127 |
+
)
|
| 128 |
rows.append(
|
| 129 |
+
f'<div class="split-row{speaking}"{row_style}>'
|
| 130 |
'<div class="split-id">'
|
| 131 |
f'<div class="disp split-name">{name}</div>'
|
| 132 |
f'<div class="split-arch">{archetype}</div>'
|
src/ui/fishbowl/render/winner.py
CHANGED
|
@@ -55,8 +55,11 @@ def _confetti() -> str:
|
|
| 55 |
return f'<div class="wf-confetti" aria-hidden="true">{"".join(bits)}</div>'
|
| 56 |
|
| 57 |
|
| 58 |
-
def _chip(text: str) -> str:
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def render_winner(vm: dict) -> str:
|
|
@@ -90,7 +93,7 @@ def render_winner(vm: dict) -> str:
|
|
| 90 |
members = [by_id[m] for m in (teams.get(winner) or []) if m in by_id]
|
| 91 |
if members:
|
| 92 |
hue = int(members[0].get("hue", hue))
|
| 93 |
-
chips = "".join(_chip(str(m.get("name", ""))) for m in members)
|
| 94 |
roster = f'<div class="wf-roster">{chips}</div>'
|
| 95 |
else: # an individual mind took it
|
| 96 |
card = by_id.get(winner)
|
|
|
|
| 55 |
return f'<div class="wf-confetti" aria-hidden="true">{"".join(bits)}</div>'
|
| 56 |
|
| 57 |
|
| 58 |
+
def _chip(text: str, hue: int | None = None) -> str:
|
| 59 |
+
# A roster chip wears its own member's hue so a teammate's name matches the colour
|
| 60 |
+
# they wore on stage; hue-less chips (e.g. the model chip) keep the winner's tint.
|
| 61 |
+
style = f' style="--ch:{int(hue)}"' if hue is not None else ""
|
| 62 |
+
return f'<span class="wf-chip"{style}>{html.escape(text)}</span>'
|
| 63 |
|
| 64 |
|
| 65 |
def render_winner(vm: dict) -> str:
|
|
|
|
| 93 |
members = [by_id[m] for m in (teams.get(winner) or []) if m in by_id]
|
| 94 |
if members:
|
| 95 |
hue = int(members[0].get("hue", hue))
|
| 96 |
+
chips = "".join(_chip(str(m.get("name", "")), m.get("hue")) for m in members)
|
| 97 |
roster = f'<div class="wf-roster">{chips}</div>'
|
| 98 |
else: # an individual mind took it
|
| 99 |
card = by_id.get(winner)
|
src/ui/fishbowl/view_model.py
CHANGED
|
@@ -116,11 +116,17 @@ def view_model_at(
|
|
| 116 |
}
|
| 117 |
)
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
feed = []
|
| 120 |
for e in prefix:
|
| 121 |
item = event_to_feed_item(e, names)
|
| 122 |
if item is not None:
|
| 123 |
item["turn"] = e.turn
|
|
|
|
|
|
|
| 124 |
feed.append(item)
|
| 125 |
|
| 126 |
# The arena contract for this run (ADR-0029), stamped on run.started β lets the
|
|
|
|
| 116 |
}
|
| 117 |
)
|
| 118 |
|
| 119 |
+
# The hue a name maps to, so a feed/transcript line can wear the same phosphor as the
|
| 120 |
+
# speaker's MindCard and avatar β the colour is what lets the eye tie line to face.
|
| 121 |
+
hue_by_name = {m.name: agent_hue(m) for m in cast}
|
| 122 |
+
|
| 123 |
feed = []
|
| 124 |
for e in prefix:
|
| 125 |
item = event_to_feed_item(e, names)
|
| 126 |
if item is not None:
|
| 127 |
item["turn"] = e.turn
|
| 128 |
+
if item.get("agent") in hue_by_name:
|
| 129 |
+
item["hue"] = hue_by_name[item["agent"]]
|
| 130 |
feed.append(item)
|
| 131 |
|
| 132 |
# The arena contract for this run (ADR-0029), stamped on run.started β lets the
|
tests/test_fishbowl_mindcard.py
CHANGED
|
@@ -40,7 +40,7 @@ def test_avatar_is_svg_with_hue_colour():
|
|
| 40 |
out = render_avatar(120, "smug")
|
| 41 |
assert out.startswith("<div")
|
| 42 |
assert "<svg" in out
|
| 43 |
-
assert "oklch(0.82 0.
|
| 44 |
assert "av-smug" in out
|
| 45 |
|
| 46 |
|
|
|
|
| 40 |
out = render_avatar(120, "smug")
|
| 41 |
assert out.startswith("<div")
|
| 42 |
assert "<svg" in out
|
| 43 |
+
assert "oklch(0.82 0.17 120)" in out
|
| 44 |
assert "av-smug" in out
|
| 45 |
|
| 46 |
|