Spaces:
Running
Running
style(ui): add parent-friendly theme selector
Browse filesCo-authored-by: Codex <codex@openai.com>
- AGENTS.md +5 -5
- HANDOFF.md +13 -12
- 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
|
| 172 |
-
`min_containers=
|
| 173 |
-
|
| 174 |
-
|
| 175 |
- **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
|
| 176 |
-
`serve_tts` uses `gpu="L4"` plus `min_containers=
|
| 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=
|
| 64 |
-
|
|
|
|
| 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
|
| 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
|
| 187 |
-
`min_containers=
|
| 188 |
-
|
| 189 |
-
|
| 190 |
- **TTS runs on L4.** VoxCPM2 is ~2B and fits smaller GPUs, so
|
| 191 |
-
`serve_tts` uses `gpu="L4"` plus `min_containers=
|
| 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,
|
| 246 |
-
- **Judge**: 1× A10G, $0.80/hr,
|
| 247 |
-
- **TTS**: 1× L4, `min_containers=
|
| 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
|
| 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 |
-
|
| 793 |
-
:
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 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 {
|