curieous's picture
Hotfix Epic Errands V2 seeded UI
028613e verified
Raw
History Blame Contribute Delete
64.9 kB
(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: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6h-4v6H5a1 1 0 0 1-1-1v-9.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>',
wand: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m14 4 6 6M3 21 17 7l-3-3L3 15v6Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M5 3v3M3.5 4.5h3M20 16v3M18.5 17.5h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
kid: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM5.5 20c.7-3.8 2.9-5.8 6.5-5.8s5.8 2 6.5 5.8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
settings: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4Z" stroke="currentColor" stroke-width="2"/><path d="M19 12a7.1 7.1 0 0 0-.1-1l2-1.5-2-3.5-2.4 1a7 7 0 0 0-1.7-1L14.5 3h-5l-.3 3a7 7 0 0 0-1.7 1L5.1 6l-2 3.5 2 1.5a7.1 7.1 0 0 0 0 2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 1.7 1l.3 3h5l.3-3a7 7 0 0 0 1.7-1l2.4 1 2-3.5-2-1.5c.1-.3.1-.7.1-1Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>',
lab: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 3h6M10 3v5l-5 9a3 3 0 0 0 2.6 4.5h8.8A3 3 0 0 0 19 17l-5-9V3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.5 16h9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
check: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m5 12.5 4.2 4L19 7" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>',
x: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>',
sound: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 10v4h4l5 4V6l-5 4H4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M16 9.5c.8.7 1.2 1.5 1.2 2.5s-.4 1.8-1.2 2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
play: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5.5v13l11-6.5-11-6.5Z" fill="currentColor"/></svg>',
pause: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M7 5h3v14H7V5Zm7 0h3v14h-3V5Z" fill="currentColor"/></svg>',
plus: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>',
};
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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 = '<svg viewBox="0 0 60 60" fill="none"><path d="M8 52c16-3 19-10 20-22 1 12 5 19 24 22M8 8c16 3 19 10 20 22 1-12 5-19 24-22" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
return `
<span class="corner-flourish tl">${flourish}</span>
<span class="corner-flourish tr">${flourish}</span>
<span class="corner-flourish bl">${flourish}</span>
<span class="corner-flourish br">${flourish}</span>
`;
}
function shellHeader() {
return `
<div class="app-head">
<div class="wordmark" aria-label="Epic Errands">
<span class="display">Epic Errands</span>
<span class="sub">${escapeHtml(theme().parentTitle)} / ${escapeHtml(theme().kidTitle)}</span>
</div>
</div>
`;
}
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 `
<nav class="mobile-tabs" data-design-id="mobile-tab-nav" aria-label="Primary">
${tabs
.map(([id, label, icon, badge]) => `
<button class="tab-btn" type="button" data-action="select-tab" data-tab="${id}" aria-pressed="${state.active_tab === id}">
${icon}<span>${escapeHtml(label)}</span>${badge ? `<b>${escapeHtml(badge)}</b>` : ""}
</button>
`)
.join("")}
</nav>
`;
}
function themePickerHtml() {
return `
<section class="panel" data-design-id="app-theme-picker">
<div class="panel-head">
<h2>App Styling</h2>
<span>${escapeHtml(theme().label)}</span>
</div>
<div class="theme-image-grid">
${Object.entries(themes)
.map(([id, item]) => `
<button class="theme-image-btn" type="button" data-action="select-theme" data-theme="${id}" aria-pressed="${state.active_theme_id === id}">
<img src="${escapeHtml(asset(addElementsBaseThemeImages[id] || assets["project-outline"][id]?.image || assets["project-outline"].questbook.image))}" alt="">
<span>${escapeHtml(item.label)}</span>
</button>
`)
.join("")}
</div>
</section>
`;
}
function audioControlHtml(src, label, id) {
return `
<div class="audio-shell" data-design-id="audio-play-hook">
<button class="audio-toggle" type="button" data-action="toggle-audio" aria-pressed="false" aria-label="Play ${escapeHtml(label)}">
<span class="audio-toggle__icon" data-audio-icon>${icons.play}</span>
</button>
<span class="audio-label">${escapeHtml(label)}</span>
<audio class="visually-hidden-audio" preload="metadata" src="${escapeHtml(asset(src))}" data-audio-id="${escapeHtml(id || src)}"></audio>
</div>
`;
}
function uploadPanel(title, designId, kind, refs, accept) {
const items = refs.length
? refs
.map((ref, index) => `
<div class="upload-chip${kind === "parent_reference_audio" ? " upload-chip--stack" : ""}">
<div class="upload-chip__row">
<span class="upload-chip__main">
${ref.preview_ref && kind !== "parent_reference_audio" ? `<img class="upload-thumb" src="${escapeHtml(asset(ref.preview_ref))}" alt="">` : ""}
<span>${escapeHtml(ref.asset_ref || `${title} ${index + 1}`)}</span>
</span>
<button class="icon-mini" type="button" data-action="${removeActions[kind]}" data-kind="${kind}" data-id="${ref.id}" aria-label="Remove ${escapeHtml(title)}">${icons.x}</button>
</div>
${ref.preview_ref && kind === "parent_reference_audio" ? audioControlHtml(ref.preview_ref, ref.asset_ref || title, ref.id) : ""}
</div>
`)
.join("")
: `<p class="empty-line">Session only</p>`;
return `
<section class="panel" data-design-id="${designId}">
<div class="panel-head">
<h2>${escapeHtml(title)}</h2>
<label class="btn-ghost compact upload-trigger" data-action="${uploadActions[kind]}">
${icons.plus}<span>Add</span>
<input class="visually-hidden-file" type="file" data-upload-kind="${kind}" accept="${escapeHtml(accept)}">
</label>
</div>
<div class="upload-list">${items}</div>
</section>
`;
}
function photoUploadPanel(title, designId, kind, refs, accept) {
const items = refs.length
? refs
.map((ref, index) => `
<div class="photo-token">
<div class="photo-token__frame">
${ref.preview_ref ? `<img src="${escapeHtml(asset(ref.preview_ref))}" alt="${escapeHtml(ref.asset_ref || `${title} ${index + 1}`)}">` : `<span>${escapeHtml(uploadLabel(kind).slice(0, 1))}</span>`}
<button class="icon-mini photo-token__remove" type="button" data-action="${removeActions[kind]}" data-kind="${kind}" data-id="${ref.id}" aria-label="Remove ${escapeHtml(ref.asset_ref || title)}">${icons.x}</button>
</div>
<span>${escapeHtml(shortRefLabel(ref))}</span>
</div>
`)
.join("")
: `<p class="empty-line">Session only</p>`;
return `
<section class="panel" data-design-id="${designId}">
<div class="panel-head">
<h2>${escapeHtml(title)}</h2>
<label class="btn-ghost compact upload-trigger" data-action="${uploadActions[kind]}">
${icons.plus}<span>Add</span>
<input class="visually-hidden-file" type="file" data-upload-kind="${kind}" accept="${escapeHtml(accept)}">
</label>
</div>
<div class="photo-token-grid">${items}</div>
</section>
`;
}
function generationReferencesHtml() {
const refs = [
...state.uploads.parent_photo_refs,
...state.uploads.child_photo_refs,
...state.uploads.custom_image_reference_refs,
];
return `
<section class="panel" data-design-id="generation-reference-picker">
<div class="panel-head">
<h2>Generation References</h2>
<span>${refs.length ? `${refs.length} available` : "Optional"}</span>
</div>
<div class="reference-avatar-grid">
${refs.length
? refs
.map((ref) => `
<button class="reference-avatar" type="button" data-action="select-generation-reference" data-ref-id="${ref.id}" aria-pressed="${state.goal_draft.selected_generation_reference_ids.includes(ref.id)}">
${ref.preview_ref ? `<img src="${escapeHtml(asset(ref.preview_ref))}" alt="${escapeHtml(ref.asset_ref || uploadLabel(ref.kind))}">` : `<span class="reference-avatar__initial">${escapeHtml(uploadLabel(ref.kind).slice(0, 1))}</span>`}
<span>${escapeHtml(shortRefLabel(ref))}</span>
<small>${state.goal_draft.selected_generation_reference_ids.includes(ref.id) ? "Selected" : "Tap"}</small>
</button>
`)
.join("")
: `<p class="empty-line">Parent photo, child photo, or custom image can be added above.</p>`}
</div>
</section>
`;
}
function generationModeHtml() {
return `
<section class="panel" data-design-id="generation-mode-control">
<div class="panel-head">
<h2>Generation Mode</h2>
<span>Quality selected</span>
</div>
<div class="mode-row">
<button class="mode-btn" type="button" data-mode="quality" aria-pressed="true">
<b>Quality</b>
<span>1024 x 1024</span>
</button>
<button class="mode-btn" type="button" data-mode="speed" disabled aria-disabled="true">
<b>Speed</b>
<span>720 x 720 planned</span>
</button>
</div>
</section>
`;
}
function generationStatusHtml() {
const status = state.generation_status;
if (!status || status.state === "ready") return "";
const label = status.state === "fallback" ? "Fallback" : "Backend";
return `
<div class="statusbar generation-status" data-generation-status="${escapeHtml(status.state)}">
<span class="statusbar__dot"></span>
<div class="statusbar__info">
<b>${escapeHtml(label)}</b>
${escapeHtml(status.message)}
${status.latency_ms ? `<small>${escapeHtml(status.latency_ms)}ms</small>` : ""}
</div>
</div>
`;
}
function homeHtml() {
const c = counts();
const current = selectedGoal();
return `
<section class="lead compact-lead">
<h1>${escapeHtml(theme().goalNoun)} Board</h1>
<p>${escapeHtml(theme().kidTitle)} has ${c.activeGoals} active ${c.activeGoals === 1 ? "card" : "cards"}.</p>
</section>
<section class="summary-grid" data-design-id="home-status-summary">
<div class="summary-tile"><b>${c.pendingReview}</b><span>Review</span></div>
<div class="summary-tile"><b>${c.awaiting}</b><span>Waiting</span></div>
<div class="summary-tile"><b>${c.approved}</b><span>Rewards</span></div>
</section>
<section class="panel current-goal" data-design-id="current-kid-goal-shortcut">
<div class="panel-head">
<h2>Current Kid Card</h2>
<button class="btn-ghost compact" type="button" data-action="open-kid-goal">${icons.kid}<span>Open</span></button>
</div>
${current ? goalMiniHtml(current) : `<p class="empty-line">No active kid card yet.</p>`}
</section>
`;
}
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()}
<div class="statusbar">
<span class="statusbar__dot"></span>
<div class="statusbar__info"><b>Audio enabled.</b> Parent reference audio is optional.</div>
</div>
`;
}
function parentGoalsHtml() {
const isGenerating = state.generation_status?.state === "generating";
return `
<section class="panel" data-design-id="create-goal-form">
<div class="panel-head">
<h2>Create ${escapeHtml(theme().goalNoun)}</h2>
<span>Quality</span>
</div>
<label class="field">
<span class="lab">Ordinary goal</span>
<textarea class="field__input goal-textarea" data-field="goal-draft">${escapeHtml(state.goal_draft.ordinary_goal)}</textarea>
</label>
${generationReferencesHtml()}
<button class="btn-primary" type="button" data-action="generate-goal" ${isGenerating ? "disabled" : ""}>
${icons.wand}<span>${isGenerating ? "Generating..." : "Generate Goal"}</span>
</button>
${generationStatusHtml()}
</section>
${state.pending_review_goal ? reviewGoalHtml(state.pending_review_goal) : ""}
${approvalQueueHtml()}
`;
}
function reviewGoalHtml(goal) {
return `
<section class="review-panel" data-design-id="review-goal-panel">
<div class="panel-head">
<h2>Review Goal</h2>
<span>${escapeHtml(goal.media.image_output_size_px)} x ${escapeHtml(goal.media.image_output_size_px)}</span>
</div>
<div class="media-square">
<img src="${asset(goal.media.image_asset_ref)}" alt="${escapeHtml(goal.generated_title)} generated image">
<div class="goal-overlay editable" data-design-id="editable-goal-overlay" contenteditable="true" role="textbox" aria-label="Editable goal overlay">${escapeHtml(goal.overlay_text.text)}</div>
</div>
<div class="copy-block">
<h3>${escapeHtml(goal.generated_title)}</h3>
<p>${escapeHtml(goal.generated_narration)}</p>
<b>${escapeHtml(goal.generated_reward_label)}</b>
</div>
${audioControlHtml(goal.media.audio_asset_ref, "Generated narration", `review-${goal.id}`)}
<details class="provenance">
<summary>Provenance</summary>
<p>${escapeHtml(goal.provenance.text_model_id)} / ${escapeHtml(goal.provenance.image_model_id)} / ${escapeHtml(goal.provenance.audio_model_id)}</p>
<p>${escapeHtml(goal.provenance.image_fallback_source || "generated_v2_fixture")} / ${escapeHtml(goal.provenance.add_elements_contract || "not_applied")}</p>
<p>fallback_used=${escapeHtml(String(goal.provenance.fallback_used))}</p>
</details>
<div class="review-actions">
<button class="btn-ghost" type="button" data-action="cancel-goal">${icons.x}<span>Cancel</span></button>
<button class="btn-primary" type="button" data-action="accept-goal">${icons.check}<span>Accept</span></button>
</div>
</section>
`;
}
function approvalQueueHtml() {
const rows = state.accepted_goals;
return `
<section class="panel" data-design-id="parent-approval-queue">
<div class="panel-head">
<h2>Reward Queue</h2>
<span>${counts().awaiting} waiting</span>
</div>
<div class="parent-list">
${rows
.map((goal) => {
const waiting = goal.parent_reward_state === "waiting_for_approval";
const approved = goal.parent_reward_state === "approved_reward_given";
return `
<article class="parent-row">
<div>
<div class="parent-row__title">${escapeHtml(goal.generated_title)}</div>
<div class="parent-row__meta">${escapeHtml(goal.ordinary_goal)} - ${escapeHtml(goal.generated_reward_label)}</div>
</div>
<div class="parent-actions">
${waiting ? `<span class="parent-row__status">Waiting</span>` : ""}
<button class="btn-ghost" type="button" data-action="approve-goal" data-goal-id="${goal.id}" ${waiting ? "" : "disabled"}>
${icons.check}<span>${approved ? "Approved" : "Approve"}</span>
</button>
</div>
</article>
`;
})
.join("")}
</div>
</section>
`;
}
function goalMiniHtml(goal) {
const isSelected = selectedGoal()?.id === goal.id;
return `
<button class="mini-goal" type="button" data-action="open-goal-card" data-goal-id="${goal.id}" aria-pressed="${isSelected}">
<img src="${asset(goal.media.image_asset_ref)}" alt="${escapeHtml(goal.generated_title)} thumbnail">
<span>
<b>${escapeHtml(goal.generated_title)}</b>
<small>${escapeHtml(goal.ordinary_goal)}</small>
</span>
</button>
`;
}
function kidGoalsHtml() {
const modalGoal = kidModalGoal();
return `
<section class="panel" data-design-id="kid-goal-thumbnail-grid">
<div class="panel-head">
<h2>${escapeHtml(theme().kidTitle)}</h2>
<span>${state.accepted_goals.length} cards</span>
</div>
<div class="thumb-grid">
${state.accepted_goals.length ? state.accepted_goals.map(goalMiniHtml).join("") : `<p class="empty-line">No accepted goals yet.</p>`}
</div>
</section>
${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 `
<div class="kid-modal" data-design-id="kid-goal-card-modal" role="presentation">
<section class="kid-card kid-card-modal no-generated-steps" data-design-id="kid-goal-card" role="dialog" aria-modal="true" aria-labelledby="kid-goal-title">
<button class="icon-mini kid-modal__close" type="button" data-action="close-kid-goal" aria-label="Close goal card">${icons.x}</button>
<div class="media-square">
<img src="${asset(goal.media.image_asset_ref)}" alt="${escapeHtml(goal.generated_title)} generated image">
<div class="goal-overlay readonly" data-design-id="read-only-goal-overlay">${escapeHtml(goal.overlay_text.text)}</div>
</div>
<div class="copy-block">
<h3 id="kid-goal-title">${escapeHtml(goal.generated_title)}</h3>
<p>${escapeHtml(goal.generated_narration)}</p>
<b>${escapeHtml(goal.generated_reward_label)}</b>
</div>
${audioControlHtml(goal.media.audio_asset_ref, "Goal narration", `kid-${goal.id}`)}
<button class="btn-primary" type="button" data-action="complete-goal" data-goal-id="${goal.id}" ${waiting || approved ? "disabled" : ""}>
${icons.check}<span>${approved ? "Reward Given" : waiting ? "Waiting" : "I Did It"}</span>
</button>
${waiting ? `<p class="state-note"><strong>${escapeHtml(themes[goal.theme_id_at_creation].parentTitle)}:</strong> ${escapeHtml(themes[goal.theme_id_at_creation].waitingCopy)}</p>` : ""}
</section>
</div>
`;
}
function diyRedirectHtml() {
return `
<section class="panel diy-frame-panel" data-design-id="diy-workflow-embed">
<div class="panel-head">
<h2>DIY Lab</h2>
<span>Workflow canvas</span>
</div>
<div class="diy-frame-wrap">
<iframe class="diy-workflow-frame" src="/diy-workflow/" title="Epic Errands DIY workflow canvas" loading="lazy"></iframe>
</div>
</section>
`;
}
function stepHtml(label, step) {
return `
<div class="diy-step">
<b>${escapeHtml(label)}</b>
<span>${escapeHtml(step.model || step.result_asset_ref || step.overlay_text || "Ready")}</span>
<small>${escapeHtml(step.runtime || step.output_size_px || step.fallback_used || "")}</small>
</div>
`;
}
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 `
<div class="diy-topline">
<button class="btn-ghost compact" type="button" data-action="back-main-app">${icons.home}<span>Main App</span></button>
<span class="empty-line">Isolated surface</span>
</div>
<section class="panel diy-native-panel" data-design-id="diy-workflow-native-link">
<div class="panel-head">
<h1>DIY Lab</h1>
<span>Embedded Space mode</span>
</div>
<p class="empty-line diy-native-note">
The workflow mirror opens inside this Space so the private hosted app does not navigate into a recursive Gradio frame.
</p>
</section>
<section class="panel" data-design-id="diy-workflow-mirror">
<div class="panel-head">
<h2>V2 Preview Inputs</h2>
<span>${escapeHtml(themes[draft.selected_theme_id]?.label || themes.questbook.label)}</span>
</div>
<label class="field">
<span class="lab">Workflow draft</span>
<textarea class="field__input goal-textarea" data-field="diy-goal">${escapeHtml(draft.ordinary_goal)}</textarea>
</label>
<div class="segmented small">
${Object.entries(themes)
.map(([id, item]) => `
<button class="seg-btn" type="button" data-action="diy-theme" data-theme="${id}" aria-pressed="${draft.selected_theme_id === id}">
<span>${escapeHtml(item.label)}</span>
</button>
`)
.join("")}
</div>
<div class="diy-ref-list" aria-label="Selected generation references">
${draft.selected_generation_reference_ids.map((ref) => `<span>${escapeHtml(ref)}</span>`).join("")}
</div>
<button class="btn-ghost" type="button" data-action="update-diy-preview">${icons.wand}<span>Update Preview</span></button>
</section>
<section class="panel">
<div class="panel-head">
<h2>Pipeline</h2>
<span>Quality</span>
</div>
<div class="diy-step-grid">
${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 || {})}
</div>
</section>
${preview ? `
<section class="kid-card diy-preview">
<div class="media-square">
<img src="${asset(preview.media.image_asset_ref)}" alt="${escapeHtml(preview.generated_title)} DIY preview">
<div class="goal-overlay readonly">${escapeHtml(preview.overlay_text.text)}</div>
</div>
<div class="copy-block">
<h3>${escapeHtml(preview.generated_title)}</h3>
<p>${escapeHtml(preview.generated_narration)}</p>
<b>${escapeHtml(preview.generated_reward_label)}</b>
</div>
${audioControlHtml(preview.media.audio_asset_ref, "Preview narration", "diy-preview")}
</section>
` : ""}
<section class="panel" data-design-id="diy-save-back-notice">
<div class="panel-head">
<h2>Trace</h2>
<button class="btn-ghost compact" type="button" data-action="save-to-app" disabled>Coming soon</button>
</div>
<details class="provenance" open>
<summary>Model details</summary>
<p>${escapeHtml(JSON.stringify(trace.quality_mode || trace.quality_model_contract || {}, null, 2))}</p>
</details>
<pre class="trace-json">${escapeHtml(JSON.stringify(trace, null, 2))}</pre>
</section>
`;
}
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 = `
<article class="card ornate v2-shell">
${flourishCorners()}
<div class="card-inner app-shell">
<div data-shell-region="header"></div>
<div data-shell-region="nav"></div>
<main class="app-screen" data-shell-region="screen" data-active-tab="${escapeHtml(state.active_tab)}"></main>
</div>
</article>
`;
}
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();
})();