focus-buddy / static /js /bridge.js
pocanman's picture
migrate to new repo
3006400
Raw
History Blame Contribute Delete
6.02 kB
// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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 `<div class="todo-item" id="todo-${t.id}">` +
`<input type="checkbox" ${checked} class="todo-check" data-action="complete_todo" data-id="${t.id}">` +
`<span class="todo-task${doneCls}">${_escTodo(t.task)}</span>` +
`<button class="todo-del" data-action="remove_todo" data-id="${t.id}">βœ•</button>` +
`</div>`;
}).join('');
return `<div class="todo-panel" id="todo-panel">` +
`<h3 class="panel-title">πŸ“‹ Quests</h3>` +
`<div class="todo-list">${items}</div>` +
`</div>`;
}
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);