Changeset 0.22.9 (#151)

This commit is contained in:
2026-03-04 17:39:22 +00:00
parent 7e26a2a261
commit 6ed76fb2d3
27 changed files with 3392 additions and 3224 deletions

View File

@@ -117,6 +117,11 @@ async function startApp() {
}
} catch (_) {}
// If no chat was restored, show the welcome/empty state
if (!App.currentChatId) {
UI.showEmptyState();
}
UI.updateUser();
UI.showAdminButton(API.isAdmin);
initListeners();
@@ -125,6 +130,12 @@ async function startApp() {
try {
Events.connect((window.__BASE__ || '') + '/ws');
// Once WS connects, re-fetch tools so browser extension tools appear
// (ToolsToggle.init runs before WS is up → server omits browser tools)
Events.on('ws.connected', () => {
if (typeof ToolsToggle !== 'undefined') ToolsToggle.refresh();
});
// Admin-only: persistent fallback alert banner (v0.17.0)
Events.on('role.fallback', (payload) => {
if (App.user?.role !== 'admin') return;
@@ -161,80 +172,6 @@ function showFallbackBanner(data) {
// ── Modal Helpers ───────────────────────────
function openModal(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.add('active');
// Defer overflow check — browser needs a frame to lay out the modal
requestAnimationFrame(() => {
el.querySelectorAll('.modal-tabs').forEach(checkTabsOverflow);
});
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('active');
}
// Toggle .is-scrollable on tab bars that overflow horizontally
// and inject arrow navigation buttons when needed.
function checkTabsOverflow(tabs) {
if (!tabs) return;
const scrollable = tabs.scrollWidth > tabs.clientWidth + 1;
if (!scrollable) {
// Remove wrapper if overflow went away (e.g. zoom decreased)
const wrap = tabs.parentElement;
if (wrap && wrap.classList.contains('modal-tabs-wrap')) {
wrap.parentElement.insertBefore(tabs, wrap);
wrap.remove();
}
return;
}
// Wrap if not already wrapped
if (!tabs.parentElement.classList.contains('modal-tabs-wrap')) {
const wrap = document.createElement('div');
wrap.className = 'modal-tabs-wrap';
tabs.parentElement.insertBefore(wrap, tabs);
wrap.appendChild(tabs);
// Inject arrows
const left = document.createElement('button');
left.className = 'tab-arrow tab-arrow-left';
left.innerHTML = '‹';
left.addEventListener('click', () => { tabs.scrollLeft -= 120; });
const right = document.createElement('button');
right.className = 'tab-arrow tab-arrow-right';
right.innerHTML = '›';
right.addEventListener('click', () => { tabs.scrollLeft += 120; });
wrap.appendChild(left);
wrap.appendChild(right);
// Update arrow visibility on scroll
tabs.addEventListener('scroll', () => updateTabArrows(tabs), { passive: true });
}
updateTabArrows(tabs);
}
function updateTabArrows(tabs) {
const wrap = tabs.parentElement;
if (!wrap || !wrap.classList.contains('modal-tabs-wrap')) return;
const left = wrap.querySelector('.tab-arrow-left');
const right = wrap.querySelector('.tab-arrow-right');
if (!left || !right) return;
const atStart = tabs.scrollLeft <= 2;
const atEnd = tabs.scrollLeft + tabs.clientWidth >= tabs.scrollWidth - 2;
left.classList.toggle('visible', !atStart);
right.classList.toggle('visible', !atEnd);
}
// ── Auth Flow ────────────────────────────────
function showSplash(health) {

View File

@@ -14,6 +14,9 @@ const ToolsToggle = (() => {
knowledge: { label: 'Knowledge Bases', icon: '📚' },
notes: { label: 'Notes', icon: '📝' },
utilities: { label: 'Utilities', icon: '🧮' },
workspace: { label: 'Workspace', icon: '🗂️' },
context: { label: 'Context', icon: '📎' },
memory: { label: 'Memory', icon: '🧠' },
browser: { label: 'Extensions', icon: '🧩' },
};
@@ -157,14 +160,15 @@ const ToolsToggle = (() => {
const state = _categoryState(cat);
const tools = _categories[cat];
const expanded = _expanded.has(cat);
const stateClass = state === 'all' ? 'enabled' : state === 'partial' ? 'partial' : '';
const switchClass = state === 'all' ? 'on' : state === 'partial' ? 'on partial' : '';
const chevron = tools.length > 1
? `<span class="tools-popup-expand ${expanded ? 'open' : ''}" data-expand="${cat}"></span>`
: '';
const labelExpand = tools.length > 1 ? ` data-expand="${cat}"` : '';
html += `<div class="tools-popup-category ${stateClass}" data-category="${cat}">
<span class="tools-popup-check"></span>
<span class="tools-popup-label">${meta.icon} ${meta.label}</span>
html += `<div class="tools-popup-category" data-category="${cat}">
<span class="tools-popup-switch ${switchClass}" data-switch="${cat}"><span class="tools-popup-switch-knob"></span></span>
<span class="tools-popup-label"${labelExpand}>${meta.icon} ${meta.label}</span>
${chevron}
</div>`;
@@ -172,8 +176,8 @@ const ToolsToggle = (() => {
if (expanded && tools.length > 1) {
for (const t of tools) {
const on = !_disabled.has(t.name);
html += `<div class="tools-popup-tool ${on ? 'enabled' : ''}" data-tool="${t.name}">
<span class="tools-popup-check"></span>
html += `<div class="tools-popup-tool" data-tool="${t.name}">
<span class="tools-popup-switch ${on ? 'on' : ''}" data-tool-switch="${t.name}"><span class="tools-popup-switch-knob"></span></span>
<span class="tools-popup-label">${t.displayName}</span>
</div>`;
}
@@ -196,23 +200,40 @@ const ToolsToggle = (() => {
popup.addEventListener('click', (e) => {
e.stopPropagation();
// Expand/collapse chevron
const expander = e.target.closest('.tools-popup-expand');
// Toggle switch on category row
const catSwitch = e.target.closest('[data-switch]');
if (catSwitch) {
_toggleCategory(catSwitch.dataset.switch);
return;
}
// Toggle switch on individual tool row
const toolSwitch = e.target.closest('[data-tool-switch]');
if (toolSwitch) {
_toggleTool(toolSwitch.dataset.toolSwitch);
return;
}
// Expand/collapse: chevron or label click on multi-tool categories
const expander = e.target.closest('[data-expand]');
if (expander) {
_toggleExpand(expander.dataset.expand);
return;
}
// Individual tool toggle
// Individual tool label (no expand, just toggle)
const toolRow = e.target.closest('.tools-popup-tool');
if (toolRow) {
_toggleTool(toolRow.dataset.tool);
return;
}
// Category toggle
// Single-tool category label click (no chevron) — toggle
const catRow = e.target.closest('.tools-popup-category');
if (catRow) _toggleCategory(catRow.dataset.category);
if (catRow) {
_toggleCategory(catRow.dataset.category);
return;
}
});
// Close on outside click

View File

@@ -539,9 +539,10 @@ const UI = {
},
showEmptyState() {
var base = window.__BASE__ || '';
document.getElementById('chatMessages').innerHTML = `
<div class="empty-state">
<div class="empty-logo"><img src="favicon-256.png" class="empty-logo-img" alt=""></div>
<div class="empty-logo"><img src="${base}/favicon-256.png" class="empty-logo-img" alt=""></div>
<h2>Chat Switchboard</h2>
<p>Select a model and start chatting</p>
</div>`;
@@ -995,6 +996,8 @@ const UI = {
showAdminButton(show) {
document.getElementById('menuAdmin').style.display = show ? '' : 'none';
var dbg = document.getElementById('menuDebug');
if (dbg) dbg.style.display = show ? '' : 'none';
},
// ── Generating State ─────────────────────

View File

@@ -348,13 +348,18 @@ function popOutExtBlock(blockId) {
const lang = block.getAttribute('data-ext-lang') || '';
// Read current theme colors so the iframe respects light/dark mode
const cs = getComputedStyle(document.documentElement);
const bgColor = cs.getPropertyValue('--bg-surface').trim() || '#fff';
const textColor = cs.getPropertyValue('--text').trim() || '#1a1a2e';
// Wrap in a minimal HTML document for the iframe
const html = `<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<style>
body { margin: 0; padding: 16px; display: flex; justify-content: center;
align-items: flex-start; min-height: 100vh; background: #fff;
align-items: flex-start; min-height: 100vh; background: ${bgColor}; color: ${textColor};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body > * { max-width: 100%; }
svg { max-width: 100%; height: auto; }

View File

@@ -791,3 +791,77 @@ function showConfirm(message, opts = {}) {
overlay.querySelector('[data-action="cancel"]').focus();
});
}
// ── Modal open/close ────────────────────────
// Generic modal show/hide used by settings, admin, debug, team-admin modals.
// Moved here from app.js so all surfaces (not just chat) can use modals.
function openModal(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.add('active');
// Defer overflow check — browser needs a frame to lay out the modal
requestAnimationFrame(() => {
el.querySelectorAll('.modal-tabs').forEach(checkTabsOverflow);
});
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('active');
}
// Toggle .is-scrollable on tab bars that overflow horizontally
// and inject arrow navigation buttons when needed.
function checkTabsOverflow(tabs) {
if (!tabs) return;
const scrollable = tabs.scrollWidth > tabs.clientWidth + 1;
if (!scrollable) {
const wrap = tabs.parentElement;
if (wrap && wrap.classList.contains('modal-tabs-wrap')) {
wrap.parentElement.insertBefore(tabs, wrap);
wrap.remove();
}
return;
}
if (!tabs.parentElement.classList.contains('modal-tabs-wrap')) {
const wrap = document.createElement('div');
wrap.className = 'modal-tabs-wrap';
tabs.parentElement.insertBefore(wrap, tabs);
wrap.appendChild(tabs);
const left = document.createElement('button');
left.className = 'tab-arrow tab-arrow-left';
left.innerHTML = '&#x2039;';
left.addEventListener('click', () => { tabs.scrollLeft -= 120; });
const right = document.createElement('button');
right.className = 'tab-arrow tab-arrow-right';
right.innerHTML = '&#x203A;';
right.addEventListener('click', () => { tabs.scrollLeft += 120; });
wrap.appendChild(left);
wrap.appendChild(right);
tabs.addEventListener('scroll', () => updateTabArrows(tabs), { passive: true });
}
updateTabArrows(tabs);
}
function updateTabArrows(tabs) {
const wrap = tabs.parentElement;
if (!wrap || !wrap.classList.contains('modal-tabs-wrap')) return;
const left = wrap.querySelector('.tab-arrow-left');
const right = wrap.querySelector('.tab-arrow-right');
if (!left || !right) return;
const atStart = tabs.scrollLeft <= 2;
const atEnd = tabs.scrollLeft + tabs.clientWidth >= tabs.scrollWidth - 2;
left.classList.toggle('visible', !atStart);
right.classList.toggle('visible', !atEnd);
}