Kiy-K Codex commited on
Commit
cdc818d
·
1 Parent(s): d2ed801

style(ui): add parent-friendly theme selector

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (3) hide show
  1. AGENTS.md +5 -5
  2. HANDOFF.md +13 -12
  3. app.py +163 -20
AGENTS.md CHANGED
@@ -168,12 +168,12 @@ end-to-end testing.
168
  text and produces 48 kHz audio; Gemma 4 could (if enabled)
169
  read audio and produce text. Don't conflate them when
170
  debugging.
171
- - **Critical-path LLMs are kept warm.** Drafter and judge use
172
- `min_containers=1`, which removes most demo cold-start latency but
173
- bills continuously while deployed. TTS also uses `min_containers=1` on L4
174
- so **Read aloud** is responsive during demos.
175
  - **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
176
- `serve_tts` uses `gpu="L4"` plus `min_containers=1` instead of A10G.
177
  If L4 availability or latency is bad, switch back to A10G or try Modal
178
  GPU fallbacks.
179
  - **VoxCPM2 TTS is not vLLM.** `serve_tts` writes a generated FastAPI
 
168
  text and produces 48 kHz audio; Gemma 4 could (if enabled)
169
  read audio and produce text. Don't conflate them when
170
  debugging.
171
+ - **Critical-path LLMs scale to zero.** Drafter and judge use
172
+ `min_containers=0` with a 2-minute `scaledown_window`, so the first
173
+ generation after idle pays a Modal/vLLM cold start but the demo does not
174
+ bill continuously while nobody is using it. TTS follows the same policy on L4.
175
  - **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
176
+ `serve_tts` uses `gpu="L4"` plus `min_containers=0` instead of A10G.
177
  If L4 availability or latency is bad, switch back to A10G or try Modal
178
  GPU fallbacks.
179
  - **VoxCPM2 TTS is not vLLM.** `serve_tts` writes a generated FastAPI
HANDOFF.md CHANGED
@@ -60,12 +60,13 @@ with VoxCPM2.
60
  JSON plus one repair retry. Cross-field consistency (`ok` ⇔ `verdict`)
61
  is enforced in code, not in the prompt.
62
  - **Three separate Modal web_servers.** Drafter and judge run on A10G
63
- with `min_containers=1` while budget allows. TTS runs separately on
64
- L4 with `min_containers=1` so read-aloud is also warm during demos.
 
65
  - **TTS only runs on demand.** VoxCPM2 is a separate FastAPI wrapper,
66
  not vLLM. The HF Space `make_audio` API posts explanation text to
67
  `/synthesize`, receives `audio/wav`, and returns a base64 data URL to
68
- the browser. It is kept warm on L4 while budget allows.
69
  - **The judge has NO tool-calling flags on the server side.** Its
70
  prompt asks for raw JSON in `content`; the Pydantic parser does the
71
  rest. This dodges Nemotron-3-Nano's chat-template tool-dialect
@@ -183,12 +184,12 @@ The most recent session (pivot to Backyard AI) changed:
183
  text and produces 48 kHz audio; Gemma 4 could (if enabled)
184
  read audio and produce text. Don't conflate them when
185
  debugging.
186
- - **Critical-path LLMs are kept warm.** Drafter and judge use
187
- `min_containers=1`, which removes most demo cold-start latency but
188
- bills continuously while deployed. TTS also uses `min_containers=1` on L4
189
- so **Read aloud** is responsive during demos.
190
  - **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
191
- `serve_tts` uses `gpu="L4"` plus `min_containers=1` instead of A10G.
192
  If L4 availability or latency is bad, switch back to A10G or try Modal
193
  GPU fallbacks.
194
  - **VoxCPM2 TTS is not vLLM.** `serve_tts` writes a generated FastAPI
@@ -242,15 +243,15 @@ hf spaces restart build-small-hackathon/Fabella
242
 
243
  ## Cost
244
 
245
- - **Drafter**: 1× A10G, $0.80/hr, 10-min scaledown
246
- - **Judge**: 1× A10G, $0.80/hr, 10-min scaledown
247
- - **TTS**: 1× L4, `min_containers=1`, only used after **Read aloud**
248
  - **At idle**: $0/hr (scaledown)
249
  - **Typical demo session**: a few minutes warm = ~$0.03-0.05
250
 
251
  ## Known Issues / Open Questions
252
 
253
- 1. **Cold start latency** — First request after 10 min idle triggers
254
  vLLM cold start (~2 min per container for model load + torch.compile
255
  + CUDA graph capture). Both containers cold-start in sequence on
256
  the first request of a new session. Could add `min_containers=1` to
 
60
  JSON plus one repair retry. Cross-field consistency (`ok` ⇔ `verdict`)
61
  is enforced in code, not in the prompt.
62
  - **Three separate Modal web_servers.** Drafter and judge run on A10G
63
+ with `min_containers=0` and a 2-minute `scaledown_window` so they scale
64
+ to zero when idle. TTS runs separately on L4 with the same scale-to-zero
65
+ policy.
66
  - **TTS only runs on demand.** VoxCPM2 is a separate FastAPI wrapper,
67
  not vLLM. The HF Space `make_audio` API posts explanation text to
68
  `/synthesize`, receives `audio/wav`, and returns a base64 data URL to
69
+ the browser. It cold-starts on demand after an idle period.
70
  - **The judge has NO tool-calling flags on the server side.** Its
71
  prompt asks for raw JSON in `content`; the Pydantic parser does the
72
  rest. This dodges Nemotron-3-Nano's chat-template tool-dialect
 
184
  text and produces 48 kHz audio; Gemma 4 could (if enabled)
185
  read audio and produce text. Don't conflate them when
186
  debugging.
187
+ - **Critical-path LLMs scale to zero.** Drafter and judge use
188
+ `min_containers=0` with a 2-minute `scaledown_window`, so the first
189
+ generation after idle pays a Modal/vLLM cold start but the demo does not
190
+ bill continuously while nobody is using it. TTS follows the same policy on L4.
191
  - **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
192
+ `serve_tts` uses `gpu="L4"` plus `min_containers=0` instead of A10G.
193
  If L4 availability or latency is bad, switch back to A10G or try Modal
194
  GPU fallbacks.
195
  - **VoxCPM2 TTS is not vLLM.** `serve_tts` writes a generated FastAPI
 
243
 
244
  ## Cost
245
 
246
+ - **Drafter**: 1× A10G while active, $0.80/hr, 2-minute scaledown
247
+ - **Judge**: 1× A10G while active, $0.80/hr, 2-minute scaledown
248
+ - **TTS**: 1× L4 while active, `min_containers=0`, only used after **Read aloud**
249
  - **At idle**: $0/hr (scaledown)
250
  - **Typical demo session**: a few minutes warm = ~$0.03-0.05
251
 
252
  ## Known Issues / Open Questions
253
 
254
+ 1. **Cold start latency** — First request after 2 min idle triggers
255
  vLLM cold start (~2 min per container for model load + torch.compile
256
  + CUDA graph capture). Both containers cold-start in sequence on
257
  the first request of a new session. Could add `min_containers=1` to
app.py CHANGED
@@ -762,8 +762,21 @@ INDEX_HTML = r"""<!doctype html>
762
  <link rel="preconnect" href="https://fonts.googleapis.com">
763
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
764
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
 
 
 
 
 
 
 
 
 
 
 
 
765
  <style>
766
- :root {
 
767
  --bg: #f7f4ec;
768
  --bg-2: #efeae0;
769
  --surface: #ffffff;
@@ -788,26 +801,117 @@ INDEX_HTML = r"""<!doctype html>
788
  --ease: cubic-bezier(.2,.7,.2,1);
789
  --font-sans: "Outfit", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
790
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
 
791
  }
792
- @media (prefers-color-scheme: dark) {
793
- :root {
794
- --bg: #14181d;
795
- --bg-2: #1a1f25;
796
- --surface: #1d242b;
797
- --surface-2: #232a32;
798
- --line: #2d343d;
799
- --line-soft: #262c34;
800
- --text: #ece7d8;
801
- --text-soft: #cfd2cc;
802
- --text-muted: #8d9089;
803
- --accent: #8fbf83;
804
- --accent-strong: #b5d3a9;
805
- --accent-soft: #2a3a30;
806
- --bubble-parent: #2e5a36;
807
- --bubble-parent-text: #f1f1ec;
808
- --bubble-fabella: #232a32;
809
- --bubble-fabella-text: #ece7d8;
810
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  }
812
  * { box-sizing: border-box; }
813
  html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; }
@@ -1100,6 +1204,15 @@ a { color: var(--accent-strong); }
1100
  <input id="child-name" type="text" maxlength="30" placeholder="leave empty to address the parent" style="width:100%; padding: 10px 12px; border:1px solid var(--line); border-radius: 10px; background: var(--bg); color: var(--text); font: 400 14px var(--font-sans);">
1101
  <label style="display:block; font: 500 12px var(--font-mono); color: var(--text-muted); margin: 16px 0 6px; letter-spacing: 0.06em; text-transform: uppercase;">Tone</label>
1102
  <div id="tone-row" style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px;"></div>
 
 
 
 
 
 
 
 
 
1103
  <div style="display:flex; gap: 8px; justify-content: flex-end; margin-top: 18px;">
1104
  <button type="button" class="btn-ghost" id="settings-cancel">Close</button>
1105
  <button type="submit" class="btn-send" style="padding: 8px 14px;">Save</button>
@@ -1133,6 +1246,25 @@ a { color: var(--accent-strong); }
1133
  var EXAMPLES = __EXAMPLES__;
1134
  var SECTION_SEP = "\x1f";
1135
  var SESSION_KEY = "fabella_session_id";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
 
1137
  var sessionId = (function () {
1138
  try {
@@ -1166,6 +1298,8 @@ a { color: var(--accent-strong); }
1166
  var toneRow = document.getElementById("tone-row");
1167
  var settingsCancel = document.getElementById("settings-cancel");
1168
  var openSettings = document.getElementById("open-settings");
 
 
1169
 
1170
  var currentAge = 7;
1171
  var currentTone = "gentle";
@@ -1646,6 +1780,15 @@ a { color: var(--accent-strong); }
1646
  openSettings.addEventListener("click", function () { dlg.showModal(); });
1647
  settingsCancel.addEventListener("click", function () { dlg.close(); });
1648
 
 
 
 
 
 
 
 
 
 
1649
  clearBtn.addEventListener("click", async function () {
1650
  if (!confirm("Clear this conversation and long-term memory? This cannot be undone.")) return;
1651
  try {
 
762
  <link rel="preconnect" href="https://fonts.googleapis.com">
763
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
764
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
765
+ <script>
766
+ (function () {
767
+ try {
768
+ var allowed = ["storybook","bedtime","kitchen-table","garden","notebook","playroom"];
769
+ var saved = localStorage.getItem("fabella-theme");
770
+ var theme = allowed.indexOf(saved) >= 0 ? saved : "storybook";
771
+ document.documentElement.dataset.theme = theme;
772
+ } catch (_) {
773
+ document.documentElement.dataset.theme = "storybook";
774
+ }
775
+ })();
776
+ </script>
777
  <style>
778
+ :root,
779
+ :root[data-theme="storybook"] {
780
  --bg: #f7f4ec;
781
  --bg-2: #efeae0;
782
  --surface: #ffffff;
 
801
  --ease: cubic-bezier(.2,.7,.2,1);
802
  --font-sans: "Outfit", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
803
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
804
+ color-scheme: light;
805
  }
806
+ :root[data-theme="bedtime"] {
807
+ --bg: #0f1320;
808
+ --bg-2: #141a2b;
809
+ --surface: #1a2138;
810
+ --surface-2: #202a45;
811
+ --line: #2a3354;
812
+ --line-soft: #232b46;
813
+ --text: #e6e8f2;
814
+ --text-soft: #c2c7d8;
815
+ --text-muted: #8b91aa;
816
+ --accent: #9aa9e8;
817
+ --accent-strong: #c0cbf5;
818
+ --accent-soft: #2a335a;
819
+ --bubble-parent: #3a4a8a;
820
+ --bubble-parent-text: #f3f4ff;
821
+ --bubble-fabella: #1c2440;
822
+ --bubble-fabella-text: #e6e8f2;
823
+ --danger: #d18a8a;
824
+ --shadow: 0 1px 0 rgba(0,0,0,0.3), 0 12px 30px -22px rgba(0,0,0,0.5);
825
+ color-scheme: dark;
826
+ }
827
+ :root[data-theme="kitchen-table"] {
828
+ --bg: #faf3e3;
829
+ --bg-2: #f3e9c9;
830
+ --surface: #fff8e6;
831
+ --surface-2: #f7ecce;
832
+ --line: #d9c79a;
833
+ --line-soft: #e6d8b0;
834
+ --text: #3a2a18;
835
+ --text-soft: #574231;
836
+ --text-muted: #8a7556;
837
+ --accent: #b07a2a;
838
+ --accent-strong: #8a5a18;
839
+ --accent-soft: #f0d9a5;
840
+ --bubble-parent: #a86a1c;
841
+ --bubble-parent-text: #fff8e6;
842
+ --bubble-fabella: #fff8e6;
843
+ --bubble-fabella-text: #3a2a18;
844
+ --danger: #a14a2a;
845
+ --shadow: 0 1px 0 rgba(0,0,0,0.02), 0 12px 30px -22px rgba(120,80,20,0.22);
846
+ color-scheme: light;
847
+ }
848
+ :root[data-theme="garden"] {
849
+ --bg: #f1f5e8;
850
+ --bg-2: #e6efd4;
851
+ --surface: #fbfaf0;
852
+ --surface-2: #eef3dc;
853
+ --line: #cfdcb5;
854
+ --line-soft: #dde8c4;
855
+ --text: #1f2a14;
856
+ --text-soft: #3a4a26;
857
+ --text-muted: #6e7d4d;
858
+ --accent: #6b8a3a;
859
+ --accent-strong: #4a6a1c;
860
+ --accent-soft: #dde8c4;
861
+ --bubble-parent: #6b8a3a;
862
+ --bubble-parent-text: #fbfaf0;
863
+ --bubble-fabella: #fbfaf0;
864
+ --bubble-fabella-text: #1f2a14;
865
+ --danger: #a14a2a;
866
+ --shadow: 0 1px 0 rgba(0,0,0,0.02), 0 12px 30px -22px rgba(60,80,20,0.18);
867
+ color-scheme: light;
868
+ }
869
+ :root[data-theme="notebook"] {
870
+ --bg: #f6f4ee;
871
+ --bg-2: #ece8dd;
872
+ --surface: #fdfcf7;
873
+ --surface-2: #efece2;
874
+ --line: #d4cebd;
875
+ --line-soft: #e1ddce;
876
+ --text: #20242e;
877
+ --text-soft: #3b414c;
878
+ --text-muted: #6a6f7a;
879
+ --accent: #2a3a52;
880
+ --accent-strong: #1a2a42;
881
+ --accent-soft: #d8deeb;
882
+ --bubble-parent: #2a3a52;
883
+ --bubble-parent-text: #fdfcf7;
884
+ --bubble-fabella: #fdfcf7;
885
+ --bubble-fabella-text: #20242e;
886
+ --danger: #a23a3a;
887
+ --shadow: 0 1px 0 rgba(0,0,0,0.03), 0 12px 30px -22px rgba(20,30,50,0.18);
888
+ color-scheme: light;
889
+ }
890
+ :root[data-theme="playroom"] {
891
+ --bg: #fdf2f4;
892
+ --bg-2: #fbe5ea;
893
+ --surface: #ffffff;
894
+ --surface-2: #fdeef2;
895
+ --line: #f1d4dc;
896
+ --line-soft: #f9e3e9;
897
+ --text: #3a2030;
898
+ --text-soft: #5a3a4a;
899
+ --text-muted: #8a6a78;
900
+ --accent: #e07aa0;
901
+ --accent-strong: #b8507a;
902
+ --accent-soft: #fbd6e3;
903
+ --bubble-parent: #e07aa0;
904
+ --bubble-parent-text: #ffffff;
905
+ --bubble-fabella: #ffffff;
906
+ --bubble-fabella-text: #3a2030;
907
+ --danger: #c44a4a;
908
+ --shadow: 0 1px 0 rgba(0,0,0,0.02), 0 12px 30px -22px rgba(180,80,120,0.18);
909
+ color-scheme: light;
910
+ }
911
+ *:focus-visible {
912
+ outline: 2px solid var(--accent);
913
+ outline-offset: 2px;
914
+ border-radius: 6px;
915
  }
916
  * { box-sizing: border-box; }
917
  html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; }
 
1204
  <input id="child-name" type="text" maxlength="30" placeholder="leave empty to address the parent" style="width:100%; padding: 10px 12px; border:1px solid var(--line); border-radius: 10px; background: var(--bg); color: var(--text); font: 400 14px var(--font-sans);">
1205
  <label style="display:block; font: 500 12px var(--font-mono); color: var(--text-muted); margin: 16px 0 6px; letter-spacing: 0.06em; text-transform: uppercase;">Tone</label>
1206
  <div id="tone-row" style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px;"></div>
1207
+ <label style="display:block; font: 500 12px var(--font-mono); color: var(--text-muted); margin: 16px 0 6px; letter-spacing: 0.06em; text-transform: uppercase;">Theme</label>
1208
+ <select id="theme-select" style="width:100%; padding: 10px 12px; border:1px solid var(--line); border-radius: 10px; background: var(--bg); color: var(--text); font: 500 14px var(--font-sans);">
1209
+ <option value="storybook">Storybook (default)</option>
1210
+ <option value="bedtime">Bedtime (soft dark)</option>
1211
+ <option value="kitchen-table">Kitchen Table (warm cream)</option>
1212
+ <option value="garden">Garden (gentle greens)</option>
1213
+ <option value="notebook">Notebook (paper and ink)</option>
1214
+ <option value="playroom">Playroom (soft pastels)</option>
1215
+ </select>
1216
  <div style="display:flex; gap: 8px; justify-content: flex-end; margin-top: 18px;">
1217
  <button type="button" class="btn-ghost" id="settings-cancel">Close</button>
1218
  <button type="submit" class="btn-send" style="padding: 8px 14px;">Save</button>
 
1246
  var EXAMPLES = __EXAMPLES__;
1247
  var SECTION_SEP = "\x1f";
1248
  var SESSION_KEY = "fabella_session_id";
1249
+ var THEME_KEY = "fabella-theme";
1250
+ var THEME_ALLOW = ["storybook","bedtime","kitchen-table","garden","notebook","playroom"];
1251
+
1252
+ function readTheme() {
1253
+ try {
1254
+ var saved = localStorage.getItem(THEME_KEY);
1255
+ return THEME_ALLOW.indexOf(saved) >= 0 ? saved : "storybook";
1256
+ } catch (_) {
1257
+ return "storybook";
1258
+ }
1259
+ }
1260
+ function applyTheme(name) {
1261
+ var theme = THEME_ALLOW.indexOf(name) >= 0 ? name : "storybook";
1262
+ document.documentElement.dataset.theme = theme;
1263
+ var sel = document.getElementById("theme-select");
1264
+ if (sel) sel.value = theme;
1265
+ }
1266
+ var currentTheme = readTheme();
1267
+ applyTheme(currentTheme);
1268
 
1269
  var sessionId = (function () {
1270
  try {
 
1298
  var toneRow = document.getElementById("tone-row");
1299
  var settingsCancel = document.getElementById("settings-cancel");
1300
  var openSettings = document.getElementById("open-settings");
1301
+ var themeSelect = document.getElementById("theme-select");
1302
+ if (themeSelect) themeSelect.value = currentTheme;
1303
 
1304
  var currentAge = 7;
1305
  var currentTone = "gentle";
 
1780
  openSettings.addEventListener("click", function () { dlg.showModal(); });
1781
  settingsCancel.addEventListener("click", function () { dlg.close(); });
1782
 
1783
+ if (themeSelect) {
1784
+ themeSelect.addEventListener("change", function () {
1785
+ var next = THEME_ALLOW.indexOf(themeSelect.value) >= 0 ? themeSelect.value : "storybook";
1786
+ currentTheme = next;
1787
+ try { localStorage.setItem(THEME_KEY, next); } catch (_) {}
1788
+ applyTheme(next);
1789
+ });
1790
+ }
1791
+
1792
  clearBtn.addEventListener("click", async function () {
1793
  if (!confirm("Clear this conversation and long-term memory? This cannot be undone.")) return;
1794
  try {