(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 `
Epic Errands ${escapeHtml(theme().parentTitle)} / ${escapeHtml(theme().kidTitle)}
`; } function navHtml() { const c = counts(); const tabs = [ ["home", "Home", icons.home, ""], ["parent_goals", "Parent", icons.wand, c.pendingReview + c.awaiting ? c.pendingReview + c.awaiting : ""], ["kid_goals", "Kid", icons.kid, c.activeGoals || ""], ["settings", "Settings", icons.settings, ""], ["diy", "DIY", icons.lab, ""], ]; return ` `; } function themePickerHtml() { return `

App Styling

${escapeHtml(theme().label)}
${Object.entries(themes) .map(([id, item]) => ` `) .join("")}
`; } function audioControlHtml(src, label, id) { return `
${escapeHtml(label)}
`; } function uploadPanel(title, designId, kind, refs, accept) { const items = refs.length ? refs .map((ref, index) => `
${ref.preview_ref && kind !== "parent_reference_audio" ? `` : ""} ${escapeHtml(ref.asset_ref || `${title} ${index + 1}`)}
${ref.preview_ref && kind === "parent_reference_audio" ? audioControlHtml(ref.preview_ref, ref.asset_ref || title, ref.id) : ""}
`) .join("") : `

Session only

`; return `

${escapeHtml(title)}

${items}
`; } function photoUploadPanel(title, designId, kind, refs, accept) { const items = refs.length ? refs .map((ref, index) => `
${ref.preview_ref ? `${escapeHtml(ref.asset_ref || `${title} ${index + 1}`)}` : `${escapeHtml(uploadLabel(kind).slice(0, 1))}`}
${escapeHtml(shortRefLabel(ref))}
`) .join("") : `

Session only

`; return `

${escapeHtml(title)}

${items}
`; } function generationReferencesHtml() { const refs = [ ...state.uploads.parent_photo_refs, ...state.uploads.child_photo_refs, ...state.uploads.custom_image_reference_refs, ]; return `

Generation References

${refs.length ? `${refs.length} available` : "Optional"}
${refs.length ? refs .map((ref) => ` `) .join("") : `

Parent photo, child photo, or custom image can be added above.

`}
`; } function generationModeHtml() { return `

Generation Mode

Quality selected
`; } function generationStatusHtml() { const status = state.generation_status; if (!status || status.state === "ready") return ""; const label = status.state === "fallback" ? "Fallback" : "Backend"; return `
${escapeHtml(label)} ${escapeHtml(status.message)} ${status.latency_ms ? `${escapeHtml(status.latency_ms)}ms` : ""}
`; } function homeHtml() { const c = counts(); const current = selectedGoal(); return `

${escapeHtml(theme().goalNoun)} Board

${escapeHtml(theme().kidTitle)} has ${c.activeGoals} active ${c.activeGoals === 1 ? "card" : "cards"}.

${c.pendingReview}Review
${c.awaiting}Waiting
${c.approved}Rewards

Current Kid Card

${current ? goalMiniHtml(current) : `

No active kid card yet.

`}
`; } function settingsHtml() { const audioRefs = state.uploads.parent_reference_audio_ref ? [state.uploads.parent_reference_audio_ref] : []; return ` ${themePickerHtml()} ${photoUploadPanel("Parent Photos", "parent-details-panel", "parent_photo", state.uploads.parent_photo_refs, "image/*")} ${uploadPanel("Parent Reference Audio", "parent-details-panel", "parent_reference_audio", audioRefs, "audio/*")} ${photoUploadPanel("Child Photos", "children-details-panel", "child_photo", state.uploads.child_photo_refs, "image/*")} ${photoUploadPanel("Custom Image Reference", "custom-image-reference-panel", "custom_image_reference", state.uploads.custom_image_reference_refs, "image/*")} ${generationModeHtml()}
Audio enabled. Parent reference audio is optional.
`; } function parentGoalsHtml() { const isGenerating = state.generation_status?.state === "generating"; return `

Create ${escapeHtml(theme().goalNoun)}

Quality
${generationReferencesHtml()} ${generationStatusHtml()}
${state.pending_review_goal ? reviewGoalHtml(state.pending_review_goal) : ""} ${approvalQueueHtml()} `; } function reviewGoalHtml(goal) { return `

Review Goal

${escapeHtml(goal.media.image_output_size_px)} x ${escapeHtml(goal.media.image_output_size_px)}
${escapeHtml(goal.generated_title)} generated image
${escapeHtml(goal.overlay_text.text)}

${escapeHtml(goal.generated_title)}

${escapeHtml(goal.generated_narration)}

${escapeHtml(goal.generated_reward_label)}
${audioControlHtml(goal.media.audio_asset_ref, "Generated narration", `review-${goal.id}`)}
Provenance

${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))}

`; } function approvalQueueHtml() { const rows = state.accepted_goals; return `

Reward Queue

${counts().awaiting} waiting
${rows .map((goal) => { const waiting = goal.parent_reward_state === "waiting_for_approval"; const approved = goal.parent_reward_state === "approved_reward_given"; return `
${escapeHtml(goal.generated_title)}
${escapeHtml(goal.ordinary_goal)} - ${escapeHtml(goal.generated_reward_label)}
${waiting ? `Waiting` : ""}
`; }) .join("")}
`; } function goalMiniHtml(goal) { const isSelected = selectedGoal()?.id === goal.id; return ` `; } function kidGoalsHtml() { const modalGoal = kidModalGoal(); return `

${escapeHtml(theme().kidTitle)}

${state.accepted_goals.length} cards
${state.accepted_goals.length ? state.accepted_goals.map(goalMiniHtml).join("") : `

No accepted goals yet.

`}
${modalGoal ? kidGoalModalHtml(modalGoal) : ""} `; } function kidGoalModalHtml(goal) { const waiting = goal.parent_reward_state === "waiting_for_approval"; const approved = goal.parent_reward_state === "approved_reward_given"; return ` `; } function diyRedirectHtml() { return `

DIY Lab

Workflow canvas
`; } function stepHtml(label, step) { return `
${escapeHtml(label)} ${escapeHtml(step.model || step.result_asset_ref || step.overlay_text || "Ready")} ${escapeHtml(step.runtime || step.output_size_px || step.fallback_used || "")}
`; } function diyInlineHtml() { state.diy = state.diy || buildLocalDiyState(state.active_theme_id, state.goal_draft.ordinary_goal, state.goal_draft.selected_generation_reference_ids); const draft = state.diy.workflow_draft; const preview = state.diy.preview; const trace = state.diy.trace_json || {}; return `
Isolated surface

DIY Lab

Embedded Space mode

The workflow mirror opens inside this Space so the private hosted app does not navigate into a recursive Gradio frame.

V2 Preview Inputs

${escapeHtml(themes[draft.selected_theme_id]?.label || themes.questbook.label)}
${Object.entries(themes) .map(([id, item]) => ` `) .join("")}
${draft.selected_generation_reference_ids.map((ref) => `${escapeHtml(ref)}`).join("")}

Pipeline

Quality
${stepHtml("Text / Quest JSON", trace.text_step || {})} ${stepHtml("Image Request / Result", trace.image_step || {})} ${stepHtml("Audio Request / Result", trace.audio_step || {})} ${stepHtml("Composed Card", trace.composed_card_step || {})}
${preview ? `
${escapeHtml(preview.generated_title)} DIY preview
${escapeHtml(preview.overlay_text.text)}

${escapeHtml(preview.generated_title)}

${escapeHtml(preview.generated_narration)}

${escapeHtml(preview.generated_reward_label)}
${audioControlHtml(preview.media.audio_asset_ref, "Preview narration", "diy-preview")}
` : ""}

Trace

Model details

${escapeHtml(JSON.stringify(trace.quality_mode || trace.quality_model_contract || {}, null, 2))}

${escapeHtml(JSON.stringify(trace, null, 2))}
`; } function currentScreenHtml() { if (state.active_tab === "settings") return settingsHtml(); if (state.active_tab === "parent_goals") return parentGoalsHtml(); if (state.active_tab === "kid_goals") return kidGoalsHtml(); if (state.active_tab === "diy") return diyRedirectHtml(); return homeHtml(); } function renderShell() { root.innerHTML = `
${flourishCorners()}
`; } function render() { if (!root) return; document.documentElement.dataset.theme = themeToToken[state.active_theme_id]; if (!root.querySelector('[data-shell-region="screen"]')) renderShell(); root.querySelector('[data-shell-region="header"]').innerHTML = shellHeader(); root.querySelector('[data-shell-region="nav"]').innerHTML = navHtml(); const screen = root.querySelector('[data-shell-region="screen"]'); screen.dataset.activeTab = state.active_tab; screen.innerHTML = currentScreenHtml(); } document.addEventListener("click", (event) => { const button = event.target.closest("[data-action]"); if (!button || button.disabled) return; const action = button.dataset.action; if (action === "select-tab") selectTab(button.dataset.tab); if (action === "select-theme") toggleThemeChoice(button.dataset.theme); if (Object.values(removeActions).includes(action)) removeUpload(button.dataset.kind, button.dataset.id); if (action === "select-generation-reference") toggleGenerationReference(button.dataset.refId); if (action === "generate-goal") generateGoal(); if (action === "accept-goal") acceptGoal(); if (action === "cancel-goal") cancelGoal(); if (action === "toggle-audio") toggleAudio(button); if (action === "open-diy-surface") openDiySurface(); if (action === "back-main-app") selectTab("home"); if (action === "diy-theme") setDiyTheme(button.dataset.theme); if (action === "update-diy-preview") updateDiyPreview(); if (action === "open-kid-goal") selectTab("kid_goals"); if (action === "open-goal-card") openKidGoalCard(button.dataset.goalId); if (action === "close-kid-goal") closeKidGoalCard(); if (action === "complete-goal") completeGoal(button.dataset.goalId); if (action === "approve-goal") approveGoal(button.dataset.goalId); }); document.addEventListener("change", (event) => { const input = event.target.closest("[data-upload-kind]"); if (!input || !input.files || !input.files[0]) return; addUpload(input.dataset.uploadKind, input.files[0]); input.value = ""; }); document.addEventListener("input", (event) => { const field = event.target.closest("[data-field]"); if (!field) return; if (field.dataset.field === "goal-draft") state.goal_draft.ordinary_goal = field.value; if (field.dataset.field === "diy-goal") { 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.ordinary_goal = field.value; } }); document.addEventListener("ended", (event) => { const audio = event.target.closest(".audio-shell audio"); if (!audio) return; setAudioButtonState(audio.closest(".audio-shell")?.querySelector(".audio-toggle"), false); }, true); document.addEventListener("blur", (event) => { const overlay = event.target.closest('[data-design-id="editable-goal-overlay"]'); if (overlay) updateOverlay(overlay.textContent.trim()); }, true); async function init() { try { state = normalizeBootstrap(await apiJson("/api/bootstrap")); } catch (error) { state = makeFallbackState(); } setTheme(state.active_theme_id); } init(); })();