(function () {
let busy = false;
let dojoMode = 'splat';
let lastScene = null;
let dojoScope = 'mine';
let dojoSearch = ''; // library search query (matches dojo name)
let improvedByAya = false; // current prompt came from tiny-aya → button reads "reroll"
let improving = false; // a tiny-aya suggestion call is in flight
let _communityCache = null; // canonical + contributed dojos fetched from the dataset
let _lastDojo = null; // { scene, cfg } of the just-generated dojo (for "Save to dataset")
// Community starters — tapping one opens the create view prefilled, so generating
// produces a real flux + splat scene (which then lands under "Mine").
const COMMUNITY_SEED = [
{ name: 'Classic dojo', mode: 'splat', settings: { room_size: 6.5, room_height: 3.2 },
prompt: 'A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.' },
{ name: 'Mountain dojo', mode: 'splat', settings: { room_size: 7.5, room_height: 4 },
prompt: 'An outdoor mountain dojo courtyard at sunrise, stone tiles, wooden gates, misty trees, full environment visible, high detail, no people, no text.' },
{ name: 'Neon dojo', mode: 'splat', settings: { room_size: 6.5, room_height: 3.2 },
prompt: 'A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.' }
];
function dojoEls() {
return {
library: document.getElementById('kimodo-dojo-library'),
create: document.getElementById('kimodo-dojo-create'),
scope: document.getElementById('kimodo-dojo-scope'),
add: document.getElementById('kimodo-dojo-add'),
search: document.getElementById('kimodo-dojo-search'),
back: document.getElementById('kimodo-dojo-back'),
grid: document.getElementById('kimodo-dojo-grid'),
emptyMsg: document.getElementById('kimodo-dojo-empty'),
improve: document.getElementById('kimodo-dojo-improve'),
save: document.getElementById('kimodo-dojo-save'),
erase: document.getElementById('kimodo-dojo-erase'),
prompt: document.getElementById('kimodo-dojo-prompt'),
mode: document.getElementById('kimodo-dojo-mode'),
roomSize: document.getElementById('kimodo-dojo-room-size'),
roomSizeValue: document.getElementById('kimodo-dojo-room-size-value'),
roomHeight: document.getElementById('kimodo-dojo-room-height'),
roomHeightValue: document.getElementById('kimodo-dojo-room-height-value'),
skybox: document.getElementById('kimodo-dojo-skybox'),
seed: document.getElementById('kimodo-dojo-seed'),
steps: document.getElementById('kimodo-dojo-steps'),
generate: document.getElementById('kimodo-dojo-generate'),
clear: document.getElementById('kimodo-dojo-clear'),
status: document.getElementById('kimodo-dojo-status'),
stage: document.getElementById('kimodo-dojo-stage'),
iso: document.getElementById('kimodo-dojo-iso'),
isoImg: document.getElementById('kimodo-dojo-iso-img')
};
}
function setStatus(text) {
const els = dojoEls();
if (els.status) els.status.textContent = text;
}
function formatSeconds(value) {
const seconds = Math.max(0, Math.round(Number(value) || 0));
const minutes = Math.floor(seconds / 60);
const rest = seconds % 60;
if (minutes <= 0) return rest + 's';
return minutes + 'm ' + String(rest).padStart(2, '0') + 's';
}
function stageLabel(stage) {
if (/^flux/.test(stage || '')) return 'Flux';
if (/^splat/.test(stage || '')) return 'TripoSplat';
if (/^panorama/.test(stage || '')) return 'Panorama';
if (/^viewer/.test(stage || '')) return 'Viewer';
return 'Dojo';
}
function bestProgressText(stage) {
if (!stage) return '';
const progress = Array.isArray(stage.progress) ? stage.progress : [];
for (let i = progress.length - 1; i >= 0; i -= 1) {
if (!progress[i]) continue;
const item = progress[i];
const bits = [];
if (item.desc) bits.push(item.desc);
if (Number.isFinite(Number(item.progress))) bits.push(Math.round(Number(item.progress) * 100) + '%');
if (item.index != null && item.length != null) bits.push(item.index + '/' + item.length + (item.unit ? ' ' + item.unit : ''));
if (bits.length) return bits.join(' - ');
}
if (stage.progress_desc) return stage.progress_desc;
return '';
}
function progressFraction(stage) {
const progress = Array.isArray(stage && stage.progress) ? stage.progress : [];
for (let i = progress.length - 1; i >= 0; i -= 1) {
const item = progress[i] || {};
const direct = Number(item.progress);
if (Number.isFinite(direct) && direct > 0 && direct <= 1) return direct;
const index = Number(item.index);
const length = Number(item.length);
if (Number.isFinite(index) && Number.isFinite(length) && index > 0 && length > 0) return Math.min(1, index / length);
}
return null;
}
function setTiming(stage) {
const els = dojoEls();
if (!els.time || !stage) return;
const parts = [];
const fraction = progressFraction(stage);
const estimatedRemaining = stage.eta == null && fraction && stage.elapsed ? (Number(stage.elapsed) * (1 - fraction)) / fraction : null;
if (stage.elapsed != null) parts.push('elapsed ' + formatSeconds(stage.elapsed));
if (stage.eta != null) parts.push('remaining ' + formatSeconds(stage.eta));
else if (estimatedRemaining != null) parts.push('remaining ~' + formatSeconds(estimatedRemaining));
if (stage.rank != null && stage.rank > 0) parts.push('queue #' + stage.rank);
if (stage.queue_size != null && stage.queue_size > 0) parts.push(stage.queue_size + ' waiting');
if (stage.job_status) parts.push(String(stage.job_status).toLowerCase());
els.time.textContent = parts.length ? stageLabel(stage.stage) + ': ' + parts.join(' - ') : '';
if (els.detail) els.detail.textContent = bestProgressText(stage);
}
function hiddenInput(id) { return inputInside(id); }
function writeHidden(id, value) {
const input = hiddenInput(id);
if (!input) return false;
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
function readHidden(id) {
const input = hiddenInput(id);
return input ? input.value || '' : '';
}
function roomSettings() {
const els = dojoEls();
return {
room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 6.5),
room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2),
use_panorama_skybox: !!(els.skybox && els.skybox.checked)
};
}
function sendScene(scene) {
const frame = previewFrame();
if (!scene) return;
const payload = { ...scene, ...roomSettings(), render_mode: dojoMode };
payload.use_panorama_skybox = !!(dojoEls().skybox && dojoEls().skybox.checked);
if (!payload.image && payload.panorama_image) payload.image = payload.panorama_image;
if (payload.render_mode === 'panorama' && !payload.image && payload.source_image) payload.image = payload.source_image;
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*');
}
var CHAR_HEIGHT_M = 1.7; // character ≈ SMPLX_HEIGHT — room size is shown relative to it
function updateRoomLabels() {
const els = dojoEls();
const settings = roomSettings();
if (els.roomSizeValue) els.roomSizeValue.textContent = settings.room_size.toFixed(1);
if (els.roomHeightValue) els.roomHeightValue.textContent = settings.room_height.toFixed(1);
const ratio = document.getElementById('kimodo-dojo-room-ratio');
if (ratio) ratio.textContent = (settings.room_size / CHAR_HEIGHT_M).toFixed(1);
}
function sendRoomSettings() {
const frame = previewFrame();
const settings = roomSettings();
if (lastScene) Object.assign(lastScene, settings);
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-room-settings', settings }, '*');
}
// ---- Generation stage: iso image loads in, then a 3D-conversion scan ----
function stageReset() {
const els = dojoEls();
if (els.stage) els.stage.hidden = true;
if (els.iso) els.iso.classList.remove('generating', 'has-img', 'converting');
if (els.isoImg) els.isoImg.removeAttribute('src');
if (els.save) els.save.hidden = true;
_lastDojo = null;
}
function stageGenerating() { // iso not ready yet → shimmer placeholder
const els = dojoEls();
if (els.stage) els.stage.hidden = false;
if (els.iso) { els.iso.classList.add('generating'); els.iso.classList.remove('has-img', 'converting'); }
}
function stageIsoReady(src) { // iso image arrived → fade/deblur it in
const els = dojoEls();
if (!src || !els.iso || !els.isoImg) return;
if (els.stage) els.stage.hidden = false;
els.iso.classList.remove('generating');
if (els.isoImg.getAttribute('src') !== src) {
els.isoImg.onload = () => els.iso.classList.add('has-img');
els.isoImg.src = src;
}
if (els.isoImg.complete && els.isoImg.naturalWidth) els.iso.classList.add('has-img');
}
function stageConverting(on) { // 3D-conversion scan overlay
const els = dojoEls();
if (els.iso) els.iso.classList.toggle('converting', !!on);
}
function setBusy(next) {
busy = !!next;
const els = dojoEls();
if (els.generate) {
els.generate.disabled = busy;
els.generate.innerHTML = busy ? '⟳ Generating…' : 'Generate Scene';
}
if (els.improve) {
els.improve.disabled = busy || improving;
if (busy) { els.improve.classList.add('loading'); els.improve.innerHTML = '⟳ generating…'; }
else { els.improve.classList.remove('loading'); updateImproveBtn(); }
}
}
function waitForResult(previousValue, onStage) {
return new Promise((resolve, reject) => {
const started = Date.now();
let lastValue = previousValue || '';
const timer = window.setInterval(() => {
const value = readHidden('dojo-result');
if (value && value !== lastValue) {
lastValue = value;
try {
const msg = JSON.parse(value);
if (onStage) onStage(msg);
if (msg.done || msg.source === 'dit360' || msg.source === 'default' || msg.source === 'none' || msg.error) {
window.clearInterval(timer);
resolve(value);
}
} catch (e) {
window.clearInterval(timer);
reject(e);
}
}
if (Date.now() - started > 600000) {
window.clearInterval(timer);
reject(new Error('Scene generation timed out.'));
}
}, 300);
});
}
function currentConfig() {
const els = dojoEls();
return {
prompt: (els.prompt && els.prompt.value || '').trim(),
seed: Number(els.seed && els.seed.value),
steps: Number(els.steps && els.steps.value),
mode: dojoMode,
...roomSettings()
};
}
// ---------- Saved dojos (localStorage) ----------
function loadMyDojos() { try { return JSON.parse(localStorage.getItem('kimodoMyDojos') || '[]'); } catch (e) { return []; } }
function saveMyDojos(list) {
let arr = (list || []).slice(0, 16);
// Scene payloads carry data-URL splats (can be MBs); drop the oldest until it fits.
while (arr.length) {
try { localStorage.setItem('kimodoMyDojos', JSON.stringify(arr)); return true; }
catch (e) { arr = arr.slice(0, arr.length - 1); }
}
try { localStorage.setItem('kimodoMyDojos', '[]'); } catch (e) {}
return false;
}
function dojoName(prompt) {
const head = (prompt || '').replace(/[.,].*$/, '').trim().replace(/^(a|an|the)\s+/i, '');
const name = head.split(/\s+/).slice(0, 4).join(' ');
return (name || 'Dojo').slice(0, 30);
}
// After a dojo is saved to the dataset, add a LIGHT (URL-backed) entry to My dojos —
// no heavy data-URL splat in localStorage, durable across reloads.
function addMineFromSaved(entry) {
const iso = communityUrl(entry.iso);
const mineEntry = {
id: entry.id, name: entry.name, prompt: entry.prompt, mode: 'splat',
settings: entry.settings, thumb: iso, contributor: entry.contributor, saved: true, ts: Date.now(),
scene: {
ok: true, source: 'klein-triposplat', render_mode: 'splat',
image: iso, isometric_image: iso, source_image: iso, splat: communityUrl(entry.splat),
room_size: entry.settings && entry.settings.room_size, room_height: entry.settings && entry.settings.room_height
}
};
const arr = loadMyDojos().filter((x) => x.id !== entry.id);
arr.unshift(mineEntry);
saveMyDojos(arr);
}
function dojoAnonId() { const a = inputInside('anonymous-client-id'); return a ? (a.value || '') : ''; }
// Publish the just-generated dojo to the dataset (tagged as the contributor) and
// add it to My dojos.
function saveDojoToDataset() {
if (!_lastDojo || !_lastDojo.scene) { setStatus('Generate a dojo first.'); return; }
const sc = _lastDojo.scene, cfg = _lastDojo.cfg || {};
const iso = sc.isometric_image || sc.source_image, splat = sc.splat;
if (!iso || !splat) { setStatus('Nothing to save (no splat).'); return; }
const els = dojoEls();
const trigger = document.querySelector('#dojo-save-btn button, #dojo-save-btn');
if (!trigger || !hiddenInput('dojo-save-payload')) { setStatus('Save backend missing.'); return; }
if (els.save) { els.save.disabled = true; els.save.innerHTML = '⟳ saving…'; }
setStatus('Saving to the dataset…');
writeHidden('dojo-save-result', '');
writeHidden('dojo-save-payload', JSON.stringify({
name: dojoName(cfg.prompt || (els.prompt && els.prompt.value) || 'Dojo'),
prompt: cfg.prompt || '', settings: { room_size: cfg.room_size, room_height: cfg.room_height },
iso: iso, splat: splat, anon_id: dojoAnonId()
}));
const started = Date.now();
const timer = window.setInterval(() => {
const val = readHidden('dojo-save-result');
if (val) { window.clearInterval(timer); finish(val); }
else if (Date.now() - started > 150000) { window.clearInterval(timer); finish('{"ok":false,"error":"timed out"}'); }
}, 400);
window.setTimeout(() => trigger.click(), 40);
function finish(val) {
if (els.save) { els.save.disabled = false; els.save.innerHTML = '☁ Save to my dojos'; }
try {
const r = JSON.parse(val);
if (r.ok && r.entry) {
addMineFromSaved(r.entry);
_communityCache = null; // contributed dojo now shows in Community
if (els.save) els.save.hidden = true;
setStatus('Saved to My dojos ✓ — shared to Community as ' + (r.entry.contributor || 'you') + '.');
} else {
setStatus('Save failed: ' + (r.error || 'unknown'));
}
} catch (e) { setStatus('Save failed.'); }
}
}
// ---------- View + library ----------
function showView(view) {
const els = dojoEls();
if (els.library) els.library.hidden = (view !== 'library');
if (els.create) els.create.hidden = (view !== 'create');
if (view === 'library') renderLibrary();
}
function syncModeButtons() {
const els = dojoEls();
if (!els.mode) return;
els.mode.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.mode === dojoMode));
}
function applySettings(settings) {
const els = dojoEls();
if (!settings) return;
if (els.roomSize && settings.room_size) els.roomSize.value = settings.room_size;
if (els.roomHeight && settings.room_height) els.roomHeight.value = settings.room_height;
if (els.skybox) els.skybox.checked = !!settings.use_panorama_skybox;
if (els.seed && settings.seed != null) els.seed.value = settings.seed;
if (els.steps && settings.steps != null) els.steps.value = settings.steps;
updateRoomLabels();
}
function openCreate(seed) {
showView('create');
stageReset();
const els = dojoEls();
improvedByAya = false;
if (seed) {
if (els.prompt) { els.prompt.value = seed.prompt || ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
dojoMode = seed.mode || 'splat';
syncModeButtons();
applySettings(seed.settings);
setStatus('Loaded "' + (seed.name || 'starter') + '". Tweak and Generate.');
} else {
if (els.prompt) { els.prompt.value = ''; } // blank by default — placeholder "a random dojo"
setStatus('Describe a dojo, or let tiny-aya decide 🎲');
}
updateImproveBtn();
}
function mineCard(entry) {
const card = document.createElement('div');
card.className = 'kimodo-dojo-card';
card.title = entry.name || 'dojo';
if (entry.thumb) {
const img = document.createElement('img');
img.className = 'thumb'; img.src = entry.thumb; img.alt = entry.name || 'dojo';
card.appendChild(img);
} else {
const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
}
const del = document.createElement('button'); del.className = 'del'; del.type = 'button'; del.textContent = '✕'; del.title = 'Delete';
del.onclick = (e) => { e.stopPropagation(); if (!window.confirm('Delete this dojo?')) return; saveMyDojos(loadMyDojos().filter((x) => x.id !== entry.id)); renderLibrary(); };
card.appendChild(del);
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = entry.name || 'Dojo'; card.appendChild(nm);
card.onclick = () => applySavedDojo(entry);
return card;
}
function communityCard(seed) {
const card = document.createElement('div');
card.className = 'kimodo-dojo-card';
card.title = seed.name;
const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = 'STARTER'; card.appendChild(tag);
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = seed.name; card.appendChild(nm);
card.onclick = () => openCreate(seed);
return card;
}
// Canonical community dojos are hosted in the dataset (viewer/dojos/community.json),
// each with a real flux iso thumbnail + a splat that applies instantly.
function communityUrl(path) { return (typeof DATASET_BASE !== 'undefined' && DATASET_BASE ? DATASET_BASE : '') + '/' + path; }
function loadCommunity() {
if (_communityCache) return Promise.resolve(_communityCache);
const grab = (f) => fetch(communityUrl(f), { cache: 'no-store' }).then((r) => (r.ok ? r.json() : null)).catch(() => null);
return Promise.all([grab('viewer/dojos/community.json'), grab('viewer/dojos/contributed.json')]).then(([canon, contrib]) => {
const list = [].concat(Array.isArray(canon) ? canon : [], Array.isArray(contrib) ? contrib : []);
if (list.length) { _communityCache = list; return list; }
return null;
});
}
function communityRealCard(d) {
const card = document.createElement('div');
card.className = 'kimodo-dojo-card';
card.title = d.name + (d.contributor ? ' · by ' + d.contributor : '');
const img = document.createElement('img'); img.className = 'thumb'; img.src = communityUrl(d.iso); img.alt = d.name; card.appendChild(img);
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = d.name; card.appendChild(nm);
if (d.contributor) { const by = document.createElement('div'); by.className = 'by'; by.textContent = 'by ' + d.contributor; card.appendChild(by); }
card.onclick = () => applyCommunityDojo(d);
return card;
}
function applyCommunityDojo(d) {
const iso = communityUrl(d.iso);
const scene = {
ok: true, source: 'klein-triposplat', render_mode: 'splat',
image: iso, isometric_image: iso, source_image: iso,
splat: communityUrl(d.splat),
room_size: d.settings && d.settings.room_size, room_height: d.settings && d.settings.room_height
};
dojoMode = 'splat'; syncModeButtons(); applySettings(d.settings);
lastScene = scene; sendScene(scene);
setStatus('Applying "' + d.name + '"…');
}
// Clear the applied dojo scene → back to the empty grid floor.
function clearDojo() {
const frame = previewFrame();
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*');
lastScene = null;
setStatus('Dojo cleared — back to the grid floor.');
}
function clearCard() {
const card = document.createElement('div');
card.className = 'kimodo-dojo-card kimodo-dojo-clearcard';
card.title = 'Clear the dojo — back to the grid floor';
const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '▦'; card.appendChild(ph);
const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = 'No dojo'; card.appendChild(nm);
card.onclick = clearDojo;
return card;
}
// Anchored header (mirrors the kata library): Add button is Mine-only, search is
// Community-only. Match a dojo by name against the search query.
function dojoMatches(name) {
const q = (dojoSearch || '').trim().toLowerCase();
return !q || String(name || '').toLowerCase().includes(q);
}
function renderLibrary() {
const els = dojoEls();
if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope));
if (els.add) els.add.style.display = (dojoScope === 'mine') ? '' : 'none';
if (els.search) els.search.style.display = (dojoScope === 'community') ? '' : 'none';
if (!els.grid) return;
els.grid.innerHTML = '';
if (dojoScope === 'mine') {
els.grid.appendChild(clearCard()); // first card: clear + back to the grid floor
const arr = loadMyDojos().filter((e) => dojoMatches(e.name));
arr.forEach((e) => els.grid.appendChild(mineCard(e)));
if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.';
return;
}
// Community: canonical hosted dojos (instant apply), else the tap-to-generate starters.
if (els.emptyMsg) els.emptyMsg.textContent = 'Loading community dojos…';
loadCommunity().then((list) => {
if (dojoScope !== 'community' || !els.grid) return;
els.grid.innerHTML = '';
els.grid.appendChild(clearCard());
const real = (list || []).filter((d) => dojoMatches(d.name));
if (list && list.length) real.forEach((d) => els.grid.appendChild(communityRealCard(d)));
else COMMUNITY_SEED.filter((s) => dojoMatches(s.name)).forEach((s) => els.grid.appendChild(communityCard(s)));
if (els.emptyMsg) els.emptyMsg.textContent = (list && list.length && !real.length) ? 'No dojos match.' : '';
});
}
function applySavedDojo(entry) {
const scene = entry.scene;
if (!scene) { openCreate(entry); return; } // recipe-only fallback → re-generate
dojoMode = scene.render_mode || (scene.splat || scene.splat_url ? 'splat' : 'panorama');
syncModeButtons();
applySettings(entry.settings);
lastScene = scene;
sendScene(scene);
setStatus('Applied "' + (entry.name || 'dojo') + '".');
}
// ---------- tiny-aya prompt suggestion ----------
// Improve-button label tracks state: blank prompt → "let tiny-aya decide",
// tiny-aya already wrote one → "reroll", otherwise → "improve".
function updateImproveBtn() {
const els = dojoEls();
if (!els.improve || improving) return;
const has = !!(els.prompt && els.prompt.value.trim());
els.improve.textContent = improvedByAya ? 'Reroll with tiny-aya 🎲'
: (has ? '✦ Improve with tiny-aya' : 'let tiny-aya decide 🎲');
}
function improvePrompt() {
const els = dojoEls();
const payloadInput = hiddenInput('dojo-suggest-payload');
const trigger = document.querySelector('#dojo-suggest-btn button, #dojo-suggest-btn');
if (!payloadInput || !trigger) { setStatus('Suggest backend missing.'); return; }
const words = (els.prompt && els.prompt.value || '').trim();
improving = true;
if (els.improve) { els.improve.disabled = true; els.improve.textContent = '🎲 thinking…'; }
setStatus(words ? 'Rerolling your prompt with tiny-aya…' : 'Letting tiny-aya invent a dojo…');
writeHidden('dojo-suggest-result', '');
writeHidden('dojo-suggest-payload', JSON.stringify({ words: words }));
const started = Date.now();
const timer = window.setInterval(() => {
const val = readHidden('dojo-suggest-result');
if (val) { window.clearInterval(timer); finishImprove(val); }
else if (Date.now() - started > 150000) { window.clearInterval(timer); finishImprove('{"ok":false,"error":"timed out"}'); }
}, 300);
window.setTimeout(() => trigger.click(), 40);
function finishImprove(val) {
improving = false;
if (els.improve) els.improve.disabled = busy;
try {
const r = JSON.parse(val);
if (r.ok && r.prompt) {
if (els.prompt) { els.prompt.value = r.prompt; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
improvedByAya = true; // set AFTER the input event (which resets it) so the button reads "reroll"
setStatus('Prompt ready ✦ — tweak it or tap Generate.');
} else {
setStatus('tiny-aya failed: ' + (r.error || 'unknown'));
}
} catch (e) { setStatus('tiny-aya failed.'); }
updateImproveBtn();
}
}
// ---------- Generate ----------
async function runGenerate() {
if (busy) return;
const cfg = currentConfig();
if (!cfg.prompt) { setStatus('Describe a dojo first, or tap “let tiny-aya decide 🎲”.'); return; }
const payloadInput = hiddenInput('dojo-payload');
const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; }
setBusy(true);
stageReset();
stageGenerating();
setStatus('Generating the dojo image…');
writeHidden('dojo-result', '');
writeHidden('dojo-payload', JSON.stringify(cfg));
try {
trigger.click();
const value = await waitForResult('', (stage) => {
const st = stage.stage || '';
const iso = stage.isometric_image || stage.source_image;
if (st === 'flux-iso-start' || st === 'flux-iso-progress') {
stageGenerating();
setStatus(stage.status || 'Generating the dojo image…');
} else if (st === 'flux-iso-done') {
stageIsoReady(iso);
setStatus(stage.status || 'Image ready — converting to 3D…');
} else if (st === 'splat-start' || st === 'splat-progress') {
if (iso) stageIsoReady(iso);
stageConverting(true);
setStatus(bestProgressText(stage) || stage.status || 'Converting to 3D…');
} else if (st === 'splat-done') {
stageConverting(false);
setStatus(stage.status || 'Loading the scene into the viewer…');
} else if (st === 'fallback') {
stageConverting(false);
setStatus(stage.warning || stage.error || 'Fallback scene generated.');
}
});
const scene = JSON.parse(value);
if (scene && (scene.image || scene.splat || scene.splat_url)) {
Object.assign(scene, roomSettings());
lastScene = scene;
sendScene(scene);
// A real splat scene can be saved to the dataset + My dojos (tap the Save button).
if (scene.source === 'klein-triposplat' && scene.splat) {
_lastDojo = { scene: scene, cfg: cfg };
const saveBtn = dojoEls().save; if (saveBtn) saveBtn.hidden = false;
setStatus('Generated ✓ — tap “☁ Save to my dojos” to keep it.');
} else if (scene.source === 'klein-triposplat' || scene.source === 'dit360') {
setStatus(scene.info || 'Scene applied.');
} else {
setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.');
}
} else {
setStatus((scene && scene.error) || 'Scene generation failed.');
}
} catch (e) {
console.error('dojo scene generation failed', e);
setStatus('Scene generation failed: ' + e.message);
} finally {
setBusy(false);
}
}
function initDojo() {
const drawer = document.getElementById('kimodo-dojo-drawer');
const els = dojoEls();
if (!drawer || drawer.dataset.ready === '1') { renderLibrary(); return; }
drawer.dataset.ready = '1';
updateRoomLabels();
showView('library');
if (els.scope) els.scope.addEventListener('click', (e) => {
const b = e.target.closest('.kimodo-cz-segbtn[data-scope]');
if (!b) return;
dojoScope = b.dataset.scope || 'mine';
renderLibrary();
});
if (els.add) els.add.addEventListener('click', () => openCreate(null));
if (els.search) els.search.addEventListener('input', () => { dojoSearch = els.search.value || ''; renderLibrary(); });
if (els.back) els.back.addEventListener('click', () => showView('library'));
if (els.improve) els.improve.addEventListener('click', () => { if (!busy && !improving) improvePrompt(); });
if (els.save) els.save.addEventListener('click', () => { if (!els.save.disabled) saveDojoToDataset(); });
if (els.erase) els.erase.addEventListener('click', () => {
if (els.prompt) { els.prompt.value = ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); els.prompt.focus(); }
setStatus('Prompt cleared — let tiny-aya decide 🎲');
});
// Manual edits drop the "reroll" state (a fresh prompt → "improve"/"decide").
if (els.prompt) els.prompt.addEventListener('input', () => { if (improving) return; improvedByAya = false; updateImproveBtn(); });
if (els.mode) els.mode.addEventListener('click', (e) => {
const button = e.target.closest('.kimodo-cz-segbtn[data-mode]');
if (!button || busy) return;
dojoMode = button.dataset.mode || 'splat';
syncModeButtons();
setStatus(dojoMode === 'panorama' ? 'Panorama mode selected.' : 'Splat mode selected.');
if (lastScene) sendScene(lastScene);
});
['roomSize', 'roomHeight', 'skybox'].forEach((key) => {
const input = els[key];
if (!input) return;
input.addEventListener('input', () => { updateRoomLabels(); sendRoomSettings(); });
input.addEventListener('change', () => { updateRoomLabels(); if (lastScene) sendScene(lastScene); });
});
if (els.generate) els.generate.addEventListener('click', runGenerate);
}
window.addEventListener('kimodo-drawer-open', (e) => {
if (e.detail === 'kimodo-dojo-drawer') initDojo();
});
window.addEventListener('message', (event) => {
const msg = event.data || {};
if (msg.kind === 'dojo-status' && msg.text) {
if (/loaded in viewer/i.test(msg.text)) stageConverting(false);
setStatus(msg.text);
}
});
})();