// Writes a JSON payload into the hidden Gradio textbox so Python sees it via .change() function bridgeSend(eventType, payload) { const el = document.querySelector('#bridge_input textarea'); if (!el) return; const data = JSON.stringify({ type: eventType, ts: Date.now(), ...(payload || {}) }); // React-compatible value setter so Gradio's synthetic event fires const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; setter.call(el, data); el.dispatchEvent(new Event('input', { bubbles: true })); } // ── Todo event delegation (onclick stripped by Gradio HTML sanitizer) ───────── document.addEventListener('click', function (e) { const el = e.target.closest('[data-action]'); if (!el) return; const action = el.dataset.action; const id = parseInt(el.dataset.id, 10); if (!action || isNaN(id)) return; bridgeSend(action, { id }); if (action === 'remove_todo') _removeTodoFromStorage(id); }); document.addEventListener('change', function (e) { const el = e.target.closest('[data-action]'); if (!el) return; const action = el.dataset.action; const id = parseInt(el.dataset.id, 10); if (!action || isNaN(id)) return; bridgeSend(action, { id }); if (action === 'complete_todo') { _toggleTodoDoneInStorage(id); if (el.checked && window.avatarPlay) window.avatarPlay('celebrate'); } }); // ── Python → JS command channel ─────────────────────────────────────────────── // Called by Gradio's .then(fn=None, js=...) after each chat response. // cmd is a JS expression string returned by Python (e.g. "timerStart(25,'pomodoro')"). function runPyCmd(cmd) { if (!cmd) return; try { eval(cmd); } catch (e) { console.error('[py_cmd]', e); } } // ── Todo localStorage sync ──────────────────────────────────────────────────── const _TODOS_LS_KEY = 'focus_buddy_todos'; function _escTodo(s) { return s.replace(/&/g, '&').replace(//g, '>'); } function _parseTodosFromDOM() { const todos = []; document.querySelectorAll('.todo-item[id^="todo-"]').forEach(el => { const id = parseInt(el.id.replace('todo-', ''), 10); const checkbox = el.querySelector('.todo-check'); const span = el.querySelector('.todo-task'); if (span && !isNaN(id)) { todos.push({ id, task: span.textContent.trim(), done: checkbox?.checked || false }); } }); return todos; } function _saveTodosToStorage() { try { localStorage.setItem(_TODOS_LS_KEY, JSON.stringify(_parseTodosFromDOM())); } catch (_) {} } function _removeTodoFromStorage(id) { try { const saved = JSON.parse(localStorage.getItem(_TODOS_LS_KEY) || '[]'); localStorage.setItem(_TODOS_LS_KEY, JSON.stringify(saved.filter(t => t.id !== id))); } catch (_) {} } function _toggleTodoDoneInStorage(id) { try { const saved = JSON.parse(localStorage.getItem(_TODOS_LS_KEY) || '[]'); localStorage.setItem(_TODOS_LS_KEY, JSON.stringify( saved.map(t => t.id === id ? { ...t, done: !t.done } : t) )); } catch (_) {} } function _buildTodoHTML(todos) { const sorted = todos; const items = sorted.map(t => { const checked = t.done ? 'checked' : ''; const doneCls = t.done ? ' todo-done' : ''; return `
` + `` + `${_escTodo(t.task)}` + `` + `
`; }).join(''); return `
` + `

📋 Quests

` + `
${items}
` + `
`; } function _loadTodosFromStorage() { try { const saved = JSON.parse(localStorage.getItem(_TODOS_LS_KEY) || 'null'); if (!saved || saved.length === 0) return; // Only inject if the server rendered no todos (avoids stomping server state) const existing = document.querySelectorAll('.todo-item[id^="todo-"]'); if (existing.length > 0) return; const panel = document.getElementById('todo-panel'); if (!panel) return; panel.outerHTML = _buildTodoHTML(saved); } catch (_) {} } function _initTodosStorage() { const panel = document.getElementById('todo-panel'); if (!panel) { setTimeout(_initTodosStorage, 300); return; } // Restore from localStorage if server rendered an empty list _loadTodosFromStorage(); // Watch for Python re-renders and keep localStorage in sync const watchTarget = panel.closest('[data-testid]') || panel.parentElement; new MutationObserver(_saveTodosToStorage).observe(watchTarget, { childList: true, subtree: true }); } setTimeout(_initTodosStorage, 500); // ── Inventory / Chat panel toggle ───────────────────────────────────────────── function showPanel(which) { const isInv = which === 'inv'; const invWrap = document.getElementById('inventory-panel-wrap'); const chatWrap = document.getElementById('chatbot-wrap'); const inputWrap = document.getElementById('chat-input-wrap'); if (invWrap) invWrap.style.display = isInv ? '' : 'none'; if (chatWrap) chatWrap.style.display = isInv ? 'none' : ''; if (inputWrap) inputWrap.style.display = isInv ? 'none' : ''; document.querySelectorAll('.panel-tab').forEach(btn => { btn.classList.toggle('active', btn.dataset.panel === which); }); } function _initPanelToggle() { const invWrap = document.getElementById('inventory-panel-wrap'); if (!invWrap) { setTimeout(_initPanelToggle, 300); return; } invWrap.style.display = 'none'; } setTimeout(_initPanelToggle, 400);