From ef17783789ab8e5973e647d8c8224911ea154e41 Mon Sep 17 00:00:00 2001 From: Jon Tidd Date: Mon, 18 May 2026 23:26:12 -0400 Subject: [PATCH] Friction-reduction redesign: saved characters, custom length, print fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story Studio overhaul: - Saved characters with descriptions ("Mason, 7, long brown hair, loves dinosaurs"). Tap chips to select, multi-select for multi-kid stories, pencil to edit. Descriptions auto-wove into every story prompt. - Custom story length: 3 preset chips (Quick 3 / Standard 6 / Long 10) plus a Custom minute input (1-20). Backed by a 130-wpm bedtime read-aloud constant so the prompt instructs the model with a real word-count target and acceptable range. Post-generation, the actual word count + minutes are surfaced so users see when the model misses. - Age input becomes direct-edit (type=text, inputmode=numeric, native spinners suppressed). No more up/down arrows. - Post-generation polish: "Another like this" regenerate button, last-8 stories saved to localStorage and reopenable via collapsed library. Routing: - Returning visitors always land on the marketing page now, never the dashboard. (StoryForge has saved-character chips at the top so they're still one tap from generating.) Print: - Virtue label badge is navy text on solid pale yellow with dark gold border — visible in print instead of barely-there translucent gold. - Discussion box and family-activity panels get page-break-inside:avoid so they don't get cut across pages. - print-color-adjust:exact forces printers to respect backgrounds. API: - Bumped max_tokens from 3000 → 8000 so longer custom stories don't get truncated. Sonnet bills on actual output so the cap is free headroom. Snapshot tag pre-friction-redesign-2026-05-19 marks the prior state for easy revert. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/generate/route.ts | 4 +- components/VirtueForgeApp.tsx | 25 +- components/app/ChildManager.tsx | 35 +- components/app/StoryForge.tsx | 1033 ++++++++++++++++++++++++------- lib/data.ts | 23 + 5 files changed, 877 insertions(+), 243 deletions(-) diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index 29b7af8..b67c19b 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -143,7 +143,9 @@ export async function POST(req: NextRequest) { }, body: JSON.stringify({ model: "claude-sonnet-4-20250514", - max_tokens: 3000, + // Headroom for ~2500-word custom stories (long bedtime sagas). + // Sonnet bills on actual output, not the cap, so raising this is free. + max_tokens: 8000, system: SYSTEM_PROMPT, messages: [{ role: "user", content: cleanPrompt }], }), diff --git a/components/VirtueForgeApp.tsx b/components/VirtueForgeApp.tsx index a8d7864..e90a47c 100644 --- a/components/VirtueForgeApp.tsx +++ b/components/VirtueForgeApp.tsx @@ -35,10 +35,8 @@ export default function VirtueForgeApp() { setAppData(data); setPremiumState(isPremium()); setLoaded(true); - // If returning user with setup done, go to dashboard - if (data.setupComplete && data.children.length > 0) { - setPage("dashboard"); - } + // Always start at the landing page — even returning visitors should see + // the marketing surface (and the "Continue with [child]" CTA there). }, []); useEffect(() => { @@ -83,6 +81,12 @@ export default function VirtueForgeApp() { upd({ children: [...appData.children, child], setupComplete: true }); }; + const updateChild = (i: number, child: ChildProfile) => { + const next = [...appData.children]; + next[i] = child; + upd({ children: next }); + }; + const resetAll = () => { trackEvent("data_reset"); setAppData({ children: [], familyVirtues: [], setupComplete: false }); @@ -91,13 +95,10 @@ export default function VirtueForgeApp() { }; const startJourney = () => { - // Go straight to Story Studio — the fastest path to value. - // StoryForge has inline setup, so no funnel is required. - if (appData.setupComplete && appData.children.length > 0) { - setPage("dashboard"); - } else { - setPage("stories"); - } + // Always land in Story Studio — the fastest path to value. StoryForge + // shows saved characters at the top if any exist, so returning users + // can jump straight to picking one and generating. + setPage("stories"); }; const handleDemo = (scenario: DemoScenario) => { @@ -248,6 +249,8 @@ export default function VirtueForgeApp() { })); setSelChild(appData.children.length); }} + onUpdateChild={updateChild} + onRemoveChild={removeChild} /> )} diff --git a/components/app/ChildManager.tsx b/components/app/ChildManager.tsx index fdf53b5..ceeb065 100644 --- a/components/app/ChildManager.tsx +++ b/components/app/ChildManager.tsx @@ -19,6 +19,7 @@ export default function ChildManager({ children, onAdd, onRemove, premium, onNex const [sex, setSex] = useState("boy"); const [readingLevel, setReadingLevel] = useState(""); const [struggles, setStruggles] = useState([]); + const [description, setDescription] = useState(""); const atLimit = !premium && children.length >= PLANS.free.children; @@ -28,8 +29,9 @@ export default function ChildManager({ children, onAdd, onRemove, premium, onNex name: name.trim(), age, sex, readingLevel: readingLevel || getDefaultReadingLevel(age), struggles, readBooks: [], virtueProgress: {}, + description: description.trim() || undefined, }); - setName(""); setAge(5); setSex("boy"); setReadingLevel(""); setStruggles([]); + setName(""); setAge(5); setSex("boy"); setReadingLevel(""); setStruggles([]); setDescription(""); setShowForm(false); }; @@ -132,8 +134,17 @@ export default function ChildManager({ children, onAdd, onRemove, premium, onNex display: "block", fontFamily: T.fontSans, fontSize: 13, fontWeight: 600, color: T.navy, marginBottom: 6, }}>Age - setAge(parseInt(e.target.value) || 0)} - min={1} max={16} style={inputStyle} /> + { + const v = e.target.value.replace(/[^0-9]/g, ""); + setAge(v ? parseInt(v, 10) : 0); + }} + style={{ ...inputStyle, appearance: "textfield" as React.CSSProperties["appearance"], MozAppearance: "textfield" }} + />
+
+ +