(function () { if (window.__EPIC_ERRANDS_V2_APP_STARTED__) return; window.__EPIC_ERRANDS_V2_APP_STARTED__ = true; const ASSET_BASE = window.EPIC_ASSET_BASE || "assets/"; const ASSET_MANIFEST = window.EPIC_ASSET_MANIFEST || {}; const API_BASE = window.EPIC_API_BASE || ""; const EMBEDDED_SPACE_MODE = Boolean(window.EPIC_EMBEDDED_SPACE_MODE); const GRADIO_API_PREFIX = window.EPIC_GRADIO_API_PREFIX || "/gradio_api"; const root = document.getElementById("app"); const themeToToken = { classroom: "storybook", questbook: "atelier", comic: "comic", }; const themes = { classroom: { label: "Classroom", token: "storybook", parentTitle: "Classroom Captain", kidTitle: "Star Student", goalNoun: "Quest", rewardNoun: "Badge", waitingCopy: "Waiting for Classroom Captain approval.", }, questbook: { label: "Questbook", token: "atelier", parentTitle: "Quest Keeper", kidTitle: "Young Adventurer", goalNoun: "Quest", rewardNoun: "Crest", waitingCopy: "Waiting for Quest Keeper approval.", }, comic: { label: "Comic", token: "comic", parentTitle: "Comic Editor", kidTitle: "Panel Hero", goalNoun: "Mission", rewardNoun: "Starburst", waitingCopy: "Waiting for Comic Editor approval.", }, }; const assets = { "clean-room": { questbook: { image: "generated-v2/clean-room-questbook-d35e6ee10c.png", audio: "generated-v2/clean-room-questbook-01493f395c.wav", }, classroom: { image: "generated-v2/clean-room-classroom-c5cb506427.png", audio: "generated-v2/clean-room-classroom-cf6f43d6ce.wav", }, comic: { image: "generated-v2/clean-room-comic-9bce2b2b21.png", audio: "generated-v2/clean-room-comic-88b770a8dc.wav", }, }, "project-outline": { questbook: { image: "generated-v2/project-outline-questbook-e1d5ced2e8.png", audio: "generated-v2/project-outline-questbook-bddf03af1a.wav", }, classroom: { image: "generated-v2/project-outline-classroom-3783f10bae.png", audio: "generated-v2/project-outline-classroom-70a886be5a.wav", }, comic: { image: "generated-v2/project-outline-comic-4f9eaa443a.png", audio: "generated-v2/project-outline-comic-ef3f180846.wav", }, }, "read-20": { questbook: { image: "generated-v2/read-20-questbook-39aac8ba25.png", audio: "generated-v2/read-20-questbook-5805518930.wav", }, classroom: { image: "generated-v2/read-20-classroom-7c13f43782.png", audio: "generated-v2/read-20-classroom-cca67b8288.wav", }, comic: { image: "generated-v2/read-20-comic-b5ae2d561a.png", audio: "generated-v2/read-20-comic-7ec8c91bf8.wav", }, }, }; const addElementsBaseThemeImages = { classroom: "base-theme-images/classroom-base-theme-1024.png", questbook: "base-theme-images/questbook-base-theme-1024.png", comic: "base-theme-images/comic-base-theme-1024.png", }; const addElementsPhase1Variants = { "clean-room": { classroom: { image: "add-elements/phase1-classroom.png", source_goal: "Clean up my room before dinner", }, }, "project-outline": { questbook: { image: "add-elements/phase1-questbook.png", source_goal: "Finish my class project outline", }, }, "read-20": { comic: { image: "add-elements/phase1-comic.png", source_goal: "Read for 20 minutes", }, }, }; const addElementsPhase2Variants = { "clean-room": { classroom: { image: "add-elements/phase2-classroom.png", source_goal: "Clean up my room before dinner", parent_reference_id: "parent-mom-demo", child_reference_id: "child-boy-demo", }, }, "project-outline": { questbook: { image: "add-elements/phase2-questbook.png", source_goal: "Finish my class project outline", parent_reference_id: "parent-dad-demo", child_reference_id: "child-girl-demo", }, }, "read-20": { comic: { image: "add-elements/phase2-comic.png", source_goal: "Read for 20 minutes", parent_reference_id: "parent-dad-demo", child_reference_id: "child-boy-demo", }, }, }; const seededUploads = { parent_photo_refs: [ { id: "parent-dad-demo", kind: "parent_photo", asset_ref: "Dad reference portrait", preview_ref: "reference-seeds/parent-reference-photo.png", privacy_scope: "session_only", created_at: "local-demo", }, { id: "parent-mom-demo", kind: "parent_photo", asset_ref: "Mom reference portrait", preview_ref: "reference-seeds/placeholder-female-parent-720.png", privacy_scope: "session_only", created_at: "local-demo", }, ], child_photo_refs: [ { id: "child-boy-demo", kind: "child_photo", asset_ref: "Boy reference portrait", preview_ref: "reference-seeds/kid-reference-photo.png", privacy_scope: "session_only", created_at: "local-demo", }, { id: "child-girl-demo", kind: "child_photo", asset_ref: "Girl reference portrait", preview_ref: "reference-seeds/placeholder-girl-720.png", privacy_scope: "session_only", created_at: "local-demo", }, ], custom_image_reference_refs: [], parent_reference_audio_ref: { id: "parent-audio-demo", kind: "parent_reference_audio", asset_ref: "Parent reference audio sample", preview_ref: "reference-seeds/parent-reference-audio.m4a", privacy_scope: "session_only", created_at: "local-demo", }, }; const defaultGenerationReferenceIds = ["parent-dad-demo", "child-girl-demo"]; const seedGoalSpecs = [ { id: "goal-seed-clean-room", ordinary_goal: "Clean up my room before dinner", theme_id_at_creation: "questbook", selected_generation_reference_ids: defaultGenerationReferenceIds, audio_used_parent_reference: true, kid_completion_state: "completed", parent_reward_state: "waiting_for_approval", }, { id: "goal-seed-project-outline", ordinary_goal: "Finish my class project outline", theme_id_at_creation: "questbook", selected_generation_reference_ids: defaultGenerationReferenceIds, audio_used_parent_reference: true, kid_completion_state: "not_started", parent_reward_state: "not_reviewed", }, { id: "goal-seed-read-20", ordinary_goal: "Read for 20 minutes", theme_id_at_creation: "questbook", selected_generation_reference_ids: defaultGenerationReferenceIds, audio_used_parent_reference: true, kid_completion_state: "not_started", parent_reward_state: "not_reviewed", }, ]; const qualityMode = { id: "quality", label: "Quality", status: "enabled", text_model_id: "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF", text_runtime: "llama.cpp", text_format: "GGUF", text_quantization: "Q4_K_M", image_model_id: "black-forest-labs/FLUX.2-klein-9B", image_runtime: "Diffusers", image_format: "safetensors", image_precision: "bf16", image_output_size_px: 1024, audio_model_id: "openbmb/VoxCPM2", audio_runtime: "voxcpm", audio_format: "safetensors", audio_precision: "bf16", }; const speedMode = { id: "speed", label: "Speed", status: "disabled_planned", image_output_size_px: 720, }; const icons = { home: '', wand: '', kid: '', settings: '', lab: '', check: '', x: '', sound: '', play: '', pause: '', plus: '', }; let state = makeFallbackState(); function makeFallbackState() { const seedGoals = seedGoalSpecs.map((spec) => { const goal = createGoal( spec.ordinary_goal, spec.theme_id_at_creation, spec.id, spec.selected_generation_reference_ids, spec.audio_used_parent_reference ); goal.review_state = "accepted"; goal.kid_completion_state = spec.kid_completion_state; goal.parent_reward_state = spec.parent_reward_state; return goal; }); const fallback = { active_tab: "home", active_theme_id: "questbook", generation_mode: "quality", available_generation_modes: [qualityMode, speedMode], parent_profile: { display_name: "Parent", photo_refs: seededUploads.parent_photo_refs, reference_audio_ref: seededUploads.parent_reference_audio_ref, reference_audio_optional: true, }, children: [{ id: "child-1", display_name: "Kid", photo_refs: seededUploads.child_photo_refs }], custom_image_reference_refs: [], uploads: { parent_photo_refs: seededUploads.parent_photo_refs, child_photo_refs: seededUploads.child_photo_refs, custom_image_reference_refs: [], parent_reference_audio_ref: seededUploads.parent_reference_audio_ref, }, generation_references: [], goal_draft: { ordinary_goal: "Finish my class project outline", selected_generation_reference_ids: defaultGenerationReferenceIds, generation_mode: "quality", }, pending_review_goal: null, kid_modal_goal_id: null, goals: seedGoals, accepted_goals: seedGoals, selected_goal_id: seedGoals[0].id, diy: buildLocalDiyState("questbook", seedGoals[0].ordinary_goal, defaultGenerationReferenceIds), generation_status: { state: "ready", message: "Hosted deterministic generation is ready.", backend_transport: "browser_fallback", fallback_used: true, }, }; syncGenerationReferences(fallback); return fallback; } function asset(path) { if (/^(data:|https?:|blob:)/.test(String(path || ""))) return path; if (ASSET_MANIFEST[path]) return ASSET_MANIFEST[path]; return `${ASSET_BASE}${path}`; } function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function theme() { return themes[state.active_theme_id] || themes.questbook; } function assetKey(goalText) { const text = String(goalText || "").toLowerCase(); if (text.includes("read")) return "read-20"; if (text.includes("project") || text.includes("outline")) return "project-outline"; return "clean-room"; } function referenceKind(refId) { const lowered = String(refId || "").toLowerCase(); if (["parent", "dad", "mom"].some((token) => lowered.includes(token))) return "parent_photo"; if (["child", "kid", "boy", "girl"].some((token) => lowered.includes(token))) return "child_photo"; return "custom_image_reference"; } function hasParentChildRefs(referenceIds) { const kinds = new Set((referenceIds || []).map(referenceKind)); return kinds.has("parent_photo") && kinds.has("child_photo"); } function mediaAssetsFor(key, themeId, referenceIds) { const variant = { ...(assets[key][themeId] || assets[key].questbook) }; const imageProvenance = { image_fallback_source: "generated_v2_fixture", add_elements_cache_hit: false, add_elements_contract: "not_applied", base_theme_image_ref: addElementsBaseThemeImages[themeId], }; if (hasParentChildRefs(referenceIds)) { const addElements = addElementsPhase2Variants[key]?.[themeId]; if (addElements) { variant.image = addElements.image; Object.assign(imageProvenance, { image_fallback_source: "cached_flux2_add_elements_phase2", add_elements_cache_hit: true, add_elements_phase: "phase2", add_elements_contract: "base_theme_plus_parent_child_goal", add_elements_source_goal: addElements.source_goal, add_elements_reference_ids: [addElements.parent_reference_id, addElements.child_reference_id], }); } else { Object.assign(imageProvenance, { add_elements_contract: "base_theme_plus_parent_child_goal", add_elements_cache_miss_reason: "no_exact_cached_phase2_theme_goal_match", }); } return { variant, imageProvenance }; } if (!(referenceIds || []).length) { const addElements = addElementsPhase1Variants[key]?.[themeId]; if (addElements) { variant.image = addElements.image; Object.assign(imageProvenance, { image_fallback_source: "cached_flux2_add_elements_phase1", add_elements_cache_hit: true, add_elements_phase: "phase1", add_elements_contract: "base_theme_plus_goal_no_people", add_elements_source_goal: addElements.source_goal, }); } } return { variant, imageProvenance }; } function titleFor(goalText, themeId) { if (themeId === "comic") return "Mission: Everyday Hero"; if (themeId === "classroom") return "Quest: Ready to Shine"; return "Quest: Small Brave Step"; } function rewardFor(themeId) { if (themeId === "comic") return "Starburst"; if (themeId === "classroom") return "Badge"; return "Crest"; } function narrationFor(goalText, themeId) { if (themeId === "comic") return `Zoom in on the next win: ${goalText}. Finish it, then call in your victory.`; if (themeId === "classroom") return `Take one clear school-day step and finish: ${goalText}.`; return `The questbook opens to today's errand: ${goalText}.`; } function createGoal(ordinaryGoal, themeId, id, referenceIds, audioUsedParentReference) { const normalizedTheme = themes[themeId] ? themeId : "questbook"; const key = assetKey(ordinaryGoal); const selectedRefs = referenceIds || []; const { variant, imageProvenance } = mediaAssetsFor(key, normalizedTheme, selectedRefs); const referenceSignature = (referenceIds || []).join(","); return { id: id || `goal-${Date.now()}`, ordinary_goal: ordinaryGoal, generation_mode: "quality", theme_id_at_creation: normalizedTheme, selected_generation_reference_ids: selectedRefs, generated_title: titleFor(ordinaryGoal, normalizedTheme), generated_narration: narrationFor(ordinaryGoal, normalizedTheme), generated_reward_label: rewardFor(normalizedTheme), overlay_text: { text: ordinaryGoal, theme_style_id: normalizedTheme, position: "bottom", max_lines: 3, is_app_owned: true, mutates_image_pixels: false, mutates_audio: false, }, media: { image_asset_ref: variant.image, image_output_size_px: 1024, image_mutable_from_review: false, audio_asset_ref: variant.audio, audio_enabled: true, audio_used_parent_reference: Boolean(audioUsedParentReference), audio_mutable_from_review: false, }, provenance: { text_model_id: qualityMode.text_model_id, text_runtime: qualityMode.text_runtime, text_format: qualityMode.text_format, text_quantization: qualityMode.text_quantization, image_model_id: qualityMode.image_model_id, image_runtime: qualityMode.image_runtime, image_format: qualityMode.image_format, image_precision: qualityMode.image_precision, audio_model_id: qualityMode.audio_model_id, audio_runtime: qualityMode.audio_runtime, audio_format: qualityMode.audio_format, audio_precision: qualityMode.audio_precision, fallback_used: true, trace_id: `local-v2-${key}-${normalizedTheme}-quality-media-${referenceSignature || "no-refs"}`, ...imageProvenance, }, review_state: "pending", kid_completion_state: "not_started", parent_reward_state: "not_reviewed", }; } function buildTrace(goal) { return { pipeline: ["plain_goal", "text_step", "image_step", "audio_step", "composed_card"], plain_goal: goal.ordinary_goal, selected_theme_id: goal.theme_id_at_creation, selected_generation_reference_ids: goal.selected_generation_reference_ids, generation_mode: "quality", text_step: { model: qualityMode.text_model_id, runtime: qualityMode.text_runtime, fallback_used: true, }, image_step: { model: qualityMode.image_model_id, runtime: qualityMode.image_runtime, output_size_px: qualityMode.image_output_size_px, result_asset_ref: goal.media.image_asset_ref, image_fallback_source: goal.provenance.image_fallback_source, add_elements_cache_hit: goal.provenance.add_elements_cache_hit, add_elements_contract: goal.provenance.add_elements_contract, base_theme_image_ref: goal.provenance.base_theme_image_ref, }, audio_step: { model: qualityMode.audio_model_id, runtime: qualityMode.audio_runtime, result_asset_ref: goal.media.audio_asset_ref, }, composed_card_step: { overlay_text: goal.overlay_text.text, mutates_image_pixels: false, }, quality_model_contract: qualityMode, image_output_size_px: 1024, fallback_used: true, save_to_app_status: "coming_soon", }; } function buildLocalDiyState(themeId, ordinaryGoal, referenceIds) { const selectedRefs = referenceIds && referenceIds.length ? referenceIds : defaultGenerationReferenceIds; const preview = createGoal(ordinaryGoal, themeId, "diy-preview", selectedRefs); return { isolated_surface: true, workflow_draft: { ordinary_goal: ordinaryGoal, selected_theme_id: themes[themeId] ? themeId : "questbook", selected_generation_reference_ids: selectedRefs, text_step: { model: qualityMode.text_model_id, runtime: qualityMode.text_runtime }, image_step: { model: qualityMode.image_model_id, output_size_px: qualityMode.image_output_size_px }, audio_step: { model: qualityMode.audio_model_id, runtime: qualityMode.audio_runtime }, composed_card_step: { overlay_text: preview.overlay_text.text }, }, preview, trace_json: buildTrace(preview), save_to_app_status: "coming_soon", }; } function normalizeGoal(goal) { return { ...goal, selected_generation_reference_ids: goal.selected_generation_reference_ids || [], overlay_text: { text: goal.overlay_text?.text || goal.ordinary_goal || "", theme_style_id: goal.overlay_text?.theme_style_id || goal.theme_id_at_creation || "questbook", position: goal.overlay_text?.position || "bottom", max_lines: goal.overlay_text?.max_lines || 3, is_app_owned: true, mutates_image_pixels: false, mutates_audio: false, }, media: { image_asset_ref: goal.media?.image_asset_ref || assets["clean-room"].questbook.image, image_output_size_px: goal.media?.image_output_size_px || 1024, image_mutable_from_review: false, audio_asset_ref: goal.media?.audio_asset_ref || assets["clean-room"].questbook.audio, audio_enabled: true, audio_used_parent_reference: Boolean(goal.media?.audio_used_parent_reference), audio_mutable_from_review: false, }, provenance: goal.provenance || { ...qualityMode, fallback_used: true }, review_state: goal.review_state || "pending", kid_completion_state: goal.kid_completion_state || "not_started", parent_reward_state: goal.parent_reward_state || "not_reviewed", }; } function syncStateAliases(targetState = state) { const parent = targetState.parent_profile || {}; const child = (targetState.children || [])[0] || { photo_refs: [] }; const existingUploads = targetState.uploads || {}; const hasAudioUpload = Object.prototype.hasOwnProperty.call(existingUploads, "parent_reference_audio_ref"); targetState.uploads = { parent_photo_refs: existingUploads.parent_photo_refs?.length ? existingUploads.parent_photo_refs : parent.photo_refs || [], child_photo_refs: existingUploads.child_photo_refs?.length ? existingUploads.child_photo_refs : child.photo_refs || [], custom_image_reference_refs: existingUploads.custom_image_reference_refs || targetState.custom_image_reference_refs || [], parent_reference_audio_ref: hasAudioUpload ? existingUploads.parent_reference_audio_ref : parent.reference_audio_ref || parent.parent_reference_audio_ref || null, }; targetState.parent_profile = { display_name: parent.display_name || "Parent", photo_refs: targetState.uploads.parent_photo_refs, reference_audio_ref: targetState.uploads.parent_reference_audio_ref, reference_audio_optional: true, }; targetState.children = [ { id: child.id || "child-1", display_name: child.display_name || "Kid", photo_refs: targetState.uploads.child_photo_refs, }, ]; targetState.custom_image_reference_refs = targetState.uploads.custom_image_reference_refs; targetState.accepted_goals = (targetState.accepted_goals || targetState.goals || []).map(normalizeGoal); targetState.goals = targetState.accepted_goals; if (!targetState.selected_goal_id && targetState.accepted_goals[0]) targetState.selected_goal_id = targetState.accepted_goals[0].id; syncGenerationReferences(targetState); } function normalizeBootstrap(payload) { const fallback = makeFallbackState(); const nextState = { ...fallback, ...payload, active_tab: "home", active_theme_id: themes[payload.active_theme_id] ? payload.active_theme_id : fallback.active_theme_id, goal_draft: { ...fallback.goal_draft, ...(payload.goal_draft || {}), }, diy: payload.diy || fallback.diy, generation_status: payload.generation_status || fallback.generation_status, }; syncStateAliases(nextState); return nextState; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function gradioEndpointFor(path) { if (path === "/api/bootstrap") return "epic_bootstrap"; if (path === "/api/generate-goal") return "epic_generate_goal"; if (path === "/api/diy-preview") return "epic_diy_preview"; return ""; } function parseSseData(text) { const lines = String(text || "").split(/\r?\n/); const eventLine = lines.find((line) => line.startsWith("event: error")); const dataLine = lines.find((line) => line.startsWith("data: ")); if (!dataLine) throw new Error("Gradio response did not include data."); const parsed = JSON.parse(dataLine.slice(6)); if (eventLine) throw new Error(Array.isArray(parsed) ? parsed.join(" ") : String(parsed)); const value = Array.isArray(parsed) ? parsed[0] : parsed; return typeof value === "string" ? JSON.parse(value) : value; } async function gradioApiJson(apiName, payload = {}) { const callResponse = await fetch(`${GRADIO_API_PREFIX}/call/${apiName}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ data: [JSON.stringify(payload || {})] }), }); if (!callResponse.ok) throw new Error(`Gradio call failed: ${callResponse.status}`); const callBody = await callResponse.json(); if (!callBody.event_id) throw new Error("Gradio call did not return an event id."); const eventResponse = await fetch(`${GRADIO_API_PREFIX}/call/${apiName}/${callBody.event_id}`); if (!eventResponse.ok) throw new Error(`Gradio event failed: ${eventResponse.status}`); return parseSseData(await eventResponse.text()); } async function apiJson(path, options = {}) { const gradioEndpoint = EMBEDDED_SPACE_MODE ? gradioEndpointFor(path) : ""; if (gradioEndpoint) { const payload = options.body ? JSON.parse(options.body) : {}; return gradioApiJson(gradioEndpoint, payload); } const response = await fetch(`${API_BASE}${path}`, options); if (!response.ok) throw new Error(`Request failed: ${response.status}`); return response.json(); } function counts() { const pendingReview = state.pending_review_goal ? 1 : 0; const activeGoals = state.accepted_goals.filter((goal) => goal.parent_reward_state !== "approved_reward_given").length; const awaiting = state.accepted_goals.filter((goal) => goal.parent_reward_state === "waiting_for_approval").length; const approved = state.accepted_goals.filter((goal) => goal.parent_reward_state === "approved_reward_given").length; return { pendingReview, activeGoals, awaiting, approved }; } function selectedGoal() { return state.accepted_goals.find((goal) => goal.id === state.selected_goal_id) || state.accepted_goals[0] || null; } function kidModalGoal() { return state.accepted_goals.find((goal) => goal.id === state.kid_modal_goal_id) || null; } function setTheme(themeId) { if (!themes[themeId]) return; state.active_theme_id = themeId; document.documentElement.dataset.theme = themeToToken[themeId]; render(); } function toggleThemeChoice(themeId) { if (!themes[themeId]) return; setTheme(state.active_theme_id === themeId && themeId !== "questbook" ? "questbook" : themeId); } function shortRefLabel(ref) { if (!ref) return "Reference"; return String(ref.asset_ref || uploadLabel(ref.kind)).replace(/\s+reference\s+portrait/i, "").replace(/\s+sample/i, ""); } function selectTab(tabId) { if (tabId === "diy") { openDiySurface(); return; } state.active_tab = tabId; render(); } function openDiySurface() { state.active_tab = "diy"; render(); } async function updateDiyPreview() { const draft = state.diy?.workflow_draft || { ordinary_goal: state.goal_draft.ordinary_goal || "Clean up my room before dinner", selected_theme_id: state.active_theme_id, selected_generation_reference_ids: state.goal_draft.selected_generation_reference_ids, }; try { state.diy = await apiJson("/api/diy-preview", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ordinary_goal: draft.ordinary_goal, selected_theme_id: draft.selected_theme_id, selected_generation_reference_ids: draft.selected_generation_reference_ids, }), }); } catch (error) { state.diy = buildLocalDiyState( draft.selected_theme_id, draft.ordinary_goal, draft.selected_generation_reference_ids ); state.diy.trace_json.local_preview_api = "unavailable_using_embedded_fallback"; } render(); } function setDiyTheme(themeId) { if (!themes[themeId]) return; state.diy = state.diy || buildLocalDiyState(state.active_theme_id, state.goal_draft.ordinary_goal, state.goal_draft.selected_generation_reference_ids); state.diy.workflow_draft.selected_theme_id = themeId; updateDiyPreview(); } const uploadActions = { parent_photo: "upload-parent-photo", child_photo: "upload-child-photo", parent_reference_audio: "upload-parent-audio", custom_image_reference: "upload-custom-image-reference", }; const removeActions = { parent_photo: "remove-parent-photo", child_photo: "remove-child-photo", parent_reference_audio: "remove-parent-audio", custom_image_reference: "remove-custom-image-reference", }; function uploadLabel(kind) { if (kind === "parent_photo") return "Parent photo"; if (kind === "child_photo") return "Child photo"; if (kind === "parent_reference_audio") return "Parent audio"; return "Custom reference"; } function fileToUploadRef(kind, file) { const ref = { id: `${kind}-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`, kind, asset_ref: file?.name || `${kind}-session-demo`, preview_ref: file ? URL.createObjectURL(file) : null, privacy_scope: "session_only", created_at: new Date().toISOString(), }; return ref; } function addUpload(kind, file) { const ref = fileToUploadRef(kind, file); if (kind === "parent_photo") state.uploads.parent_photo_refs.push(ref); if (kind === "child_photo") state.uploads.child_photo_refs.push(ref); if (kind === "custom_image_reference") state.uploads.custom_image_reference_refs.push(ref); if (kind === "parent_reference_audio") state.uploads.parent_reference_audio_ref = ref; syncStateAliases(); render(); } function removeUpload(kind, id) { const refs = [ ...state.uploads.parent_photo_refs, ...state.uploads.child_photo_refs, ...state.uploads.custom_image_reference_refs, state.uploads.parent_reference_audio_ref, ].filter(Boolean); const removed = refs.find((ref) => ref.kind === kind && (!id || ref.id === id)); if (removed?.preview_ref?.startsWith("blob:")) URL.revokeObjectURL(removed.preview_ref); if (kind === "parent_photo") state.uploads.parent_photo_refs = state.uploads.parent_photo_refs.filter((ref) => ref.id !== id); if (kind === "child_photo") state.uploads.child_photo_refs = state.uploads.child_photo_refs.filter((ref) => ref.id !== id); if (kind === "custom_image_reference") state.uploads.custom_image_reference_refs = state.uploads.custom_image_reference_refs.filter((ref) => ref.id !== id); if (kind === "parent_reference_audio") state.uploads.parent_reference_audio_ref = null; syncStateAliases(); render(); } function syncGenerationReferences(targetState = state) { const refs = [ ...targetState.uploads.parent_photo_refs, ...targetState.uploads.child_photo_refs, ...targetState.uploads.custom_image_reference_refs, ]; targetState.generation_references = refs.map((ref) => ({ id: `gen-${ref.id}`, source: ref.kind, upload_ref_id: ref.id, selected: targetState.goal_draft.selected_generation_reference_ids.includes(ref.id), })); } function toggleGenerationReference(refId) { const selected = state.goal_draft.selected_generation_reference_ids; state.goal_draft.selected_generation_reference_ids = selected.includes(refId) ? selected.filter((item) => item !== refId) : [...selected, refId]; syncGenerationReferences(); render(); } async function generateGoal() { const goalText = state.goal_draft.ordinary_goal.trim(); if (!goalText) return; const started = performance.now(); state.generation_status = { state: "generating", message: EMBEDDED_SPACE_MODE ? "Generating through the hosted Gradio backend..." : "Generating through the local app backend...", backend_transport: EMBEDDED_SPACE_MODE ? "gradio_blocks_named_api" : "fastapi_route", fallback_used: null, }; render(); try { const [generated] = await Promise.all([ apiJson("/api/generate-goal", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ordinary_goal: goalText, ui_theme_id: state.active_theme_id, selected_generation_reference_ids: state.goal_draft.selected_generation_reference_ids, audio_used_parent_reference: Boolean(state.uploads.parent_reference_audio_ref), }), }), sleep(500), ]); state.pending_review_goal = normalizeGoal(generated); state.generation_status = { state: "success", message: `Generated through ${state.pending_review_goal.provenance.backend_transport || "app backend"}; fallback_used=${String(Boolean(state.pending_review_goal.provenance.fallback_used))}.`, backend_transport: state.pending_review_goal.provenance.backend_transport || "app_backend", fallback_used: Boolean(state.pending_review_goal.provenance.fallback_used), latency_ms: Math.round(performance.now() - started), }; } catch (error) { state.pending_review_goal = createGoal( goalText, state.active_theme_id, `goal-review-${Date.now()}`, state.goal_draft.selected_generation_reference_ids, Boolean(state.uploads.parent_reference_audio_ref) ); state.pending_review_goal.provenance.backend_transport = "browser_fallback"; state.pending_review_goal.provenance.fallback_reason = String(error?.message || error || "backend unavailable"); state.generation_status = { state: "fallback", message: "Hosted backend was unavailable, so this preview used browser fallback.", backend_transport: "browser_fallback", fallback_used: true, latency_ms: Math.round(performance.now() - started), }; } state.active_tab = "parent_goals"; render(); } function acceptGoal() { if (!state.pending_review_goal) return; const accepted = { ...state.pending_review_goal, review_state: "accepted", kid_completion_state: "not_started", parent_reward_state: "not_reviewed", }; state.accepted_goals.unshift(accepted); state.goals = state.accepted_goals; state.selected_goal_id = accepted.id; state.pending_review_goal = null; state.active_tab = "kid_goals"; render(); } function cancelGoal() { state.pending_review_goal = null; state.active_tab = "parent_goals"; render(); } function updateOverlay(text) { if (!state.pending_review_goal) return; state.pending_review_goal.overlay_text.text = text; } function completeGoal(goalId) { const goal = state.accepted_goals.find((item) => item.id === goalId); if (!goal) return; goal.kid_completion_state = "completed"; goal.parent_reward_state = "waiting_for_approval"; render(); } function approveGoal(goalId) { const goal = state.accepted_goals.find((item) => item.id === goalId); if (!goal) return; goal.kid_completion_state = "completed"; goal.parent_reward_state = "approved_reward_given"; render(); } function openKidGoalCard(goalId) { const goal = state.accepted_goals.find((item) => item.id === goalId); if (!goal) return; state.selected_goal_id = goal.id; state.kid_modal_goal_id = goal.id; state.active_tab = "kid_goals"; render(); } function closeKidGoalCard() { state.kid_modal_goal_id = null; render(); } function setAudioButtonState(button, isPlaying) { if (!button) return; const icon = button.querySelector("[data-audio-icon]"); button.setAttribute("aria-pressed", String(isPlaying)); button.setAttribute("aria-label", isPlaying ? "Pause audio" : "Play audio"); if (icon) icon.innerHTML = isPlaying ? icons.pause : icons.play; } function toggleAudio(button) { const shell = button.closest(".audio-shell"); const audio = shell?.querySelector("audio"); if (!audio) return; document.querySelectorAll(".audio-shell audio").forEach((item) => { if (item !== audio) { item.pause(); setAudioButtonState(item.closest(".audio-shell")?.querySelector(".audio-toggle"), false); } }); if (audio.paused) { audio.play() .then(() => setAudioButtonState(button, true)) .catch(() => setAudioButtonState(button, false)); } else { audio.pause(); setAudioButtonState(button, false); } } function flourishCorners() { const flourish = ''; return ` ${flourish} ${flourish} ${flourish} ${flourish} `; } function shellHeader() { return `
Session only
`; return `Session only
`; return `Parent photo, child photo, or custom image can be added above.
`}${escapeHtml(theme().kidTitle)} has ${c.activeGoals} active ${c.activeGoals === 1 ? "card" : "cards"}.
No active kid card yet.
`}${escapeHtml(goal.generated_narration)}
${escapeHtml(goal.generated_reward_label)}${escapeHtml(goal.provenance.text_model_id)} / ${escapeHtml(goal.provenance.image_model_id)} / ${escapeHtml(goal.provenance.audio_model_id)}
${escapeHtml(goal.provenance.image_fallback_source || "generated_v2_fixture")} / ${escapeHtml(goal.provenance.add_elements_contract || "not_applied")}
fallback_used=${escapeHtml(String(goal.provenance.fallback_used))}
No accepted goals yet.
`}${escapeHtml(goal.generated_narration)}
${escapeHtml(goal.generated_reward_label)}${escapeHtml(themes[goal.theme_id_at_creation].parentTitle)}: ${escapeHtml(themes[goal.theme_id_at_creation].waitingCopy)}
` : ""}The workflow mirror opens inside this Space so the private hosted app does not navigate into a recursive Gradio frame.
${escapeHtml(preview.generated_narration)}
${escapeHtml(preview.generated_reward_label)}${escapeHtml(JSON.stringify(trace.quality_mode || trace.quality_model_contract || {}, null, 2))}
${escapeHtml(JSON.stringify(trace, null, 2))}