// 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 `