Changeset 0.13.1 (#65)
This commit is contained in:
222
src/js/tools-toggle.js
Normal file
222
src/js/tools-toggle.js
Normal file
@@ -0,0 +1,222 @@
|
||||
// ── tools-toggle.js ─────────────────────────
|
||||
// Tools toggle popup: category-based enable/disable of server-side tools.
|
||||
// Categories expand to show individual tool toggles.
|
||||
// State persisted in localStorage, sent as disabled_tools[] in completion requests.
|
||||
|
||||
'use strict';
|
||||
|
||||
const ToolsToggle = (() => {
|
||||
const STORAGE_KEY = 'cs-disabled-tools';
|
||||
|
||||
// Category display config
|
||||
const CATEGORY_META = {
|
||||
search: { label: 'Web Search', icon: '🔍' },
|
||||
notes: { label: 'Notes', icon: '📝' },
|
||||
utilities: { label: 'Utilities', icon: '🧮' },
|
||||
browser: { label: 'Extensions', icon: '🧩' },
|
||||
};
|
||||
|
||||
// Cached tool manifest from GET /api/v1/tools
|
||||
let _tools = []; // [{name, display_name, description, category}]
|
||||
let _categories = {}; // {category: [{name, displayName}]}
|
||||
let _disabled = new Set();
|
||||
let _expanded = new Set(); // which categories are expanded
|
||||
|
||||
// ── Public API ──────────────────────────
|
||||
|
||||
/** Initialize: load disabled set from storage, fetch tool manifest. */
|
||||
async function init() {
|
||||
_loadDisabled();
|
||||
try {
|
||||
const resp = await API.getTools();
|
||||
_tools = resp.data || [];
|
||||
_buildCategories();
|
||||
_render();
|
||||
_show();
|
||||
} catch (e) {
|
||||
console.warn('Tools toggle: failed to load tools', e);
|
||||
}
|
||||
_wireEvents();
|
||||
}
|
||||
|
||||
/** Returns array of disabled tool names for the completion request. */
|
||||
function getDisabledTools() {
|
||||
return [..._disabled];
|
||||
}
|
||||
|
||||
/** Refresh tool list (e.g. after browser extension connects). */
|
||||
async function refresh() {
|
||||
try {
|
||||
const resp = await API.getTools();
|
||||
_tools = resp.data || [];
|
||||
_buildCategories();
|
||||
_render();
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
// ── Internal ────────────────────────────
|
||||
|
||||
function _loadDisabled() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
_disabled = raw ? new Set(JSON.parse(raw)) : new Set();
|
||||
} catch { _disabled = new Set(); }
|
||||
}
|
||||
|
||||
function _saveDisabled() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([..._disabled]));
|
||||
_updateBtnState();
|
||||
}
|
||||
|
||||
function _buildCategories() {
|
||||
_categories = {};
|
||||
for (const t of _tools) {
|
||||
const cat = t.category || 'other';
|
||||
if (!_categories[cat]) _categories[cat] = [];
|
||||
_categories[cat].push({
|
||||
name: t.name,
|
||||
displayName: t.display_name || t.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns 'all' | 'none' | 'partial' for a category's enabled state. */
|
||||
function _categoryState(cat) {
|
||||
const tools = _categories[cat] || [];
|
||||
if (tools.length === 0) return 'none';
|
||||
const enabledCount = tools.filter(t => !_disabled.has(t.name)).length;
|
||||
if (enabledCount === tools.length) return 'all';
|
||||
if (enabledCount === 0) return 'none';
|
||||
return 'partial';
|
||||
}
|
||||
|
||||
function _toggleCategory(cat) {
|
||||
const tools = _categories[cat] || [];
|
||||
const state = _categoryState(cat);
|
||||
if (state === 'all') {
|
||||
// All on → all off
|
||||
tools.forEach(t => _disabled.add(t.name));
|
||||
} else {
|
||||
// Partial or none → all on
|
||||
tools.forEach(t => _disabled.delete(t.name));
|
||||
}
|
||||
_saveDisabled();
|
||||
_render();
|
||||
}
|
||||
|
||||
function _toggleTool(name) {
|
||||
if (_disabled.has(name)) {
|
||||
_disabled.delete(name);
|
||||
} else {
|
||||
_disabled.add(name);
|
||||
}
|
||||
_saveDisabled();
|
||||
_render();
|
||||
}
|
||||
|
||||
function _toggleExpand(cat) {
|
||||
if (_expanded.has(cat)) {
|
||||
_expanded.delete(cat);
|
||||
} else {
|
||||
_expanded.add(cat);
|
||||
}
|
||||
_render();
|
||||
}
|
||||
|
||||
function _show() {
|
||||
const wrap = document.querySelector('.tools-toggle-wrap');
|
||||
if (wrap && _tools.length > 0) {
|
||||
wrap.style.display = '';
|
||||
_updateBtnState();
|
||||
}
|
||||
}
|
||||
|
||||
function _updateBtnState() {
|
||||
const btn = document.getElementById('toolsToggleBtn');
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('has-disabled', _disabled.size > 0);
|
||||
const count = _disabled.size;
|
||||
btn.title = count > 0 ? `Tools (${count} disabled)` : 'Tools';
|
||||
}
|
||||
|
||||
function _render() {
|
||||
const popup = document.getElementById('toolsPopup');
|
||||
if (!popup) return;
|
||||
|
||||
const cats = Object.keys(_categories);
|
||||
const order = Object.keys(CATEGORY_META);
|
||||
cats.sort((a, b) => {
|
||||
const ai = order.indexOf(a), bi = order.indexOf(b);
|
||||
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
for (const cat of cats) {
|
||||
const meta = CATEGORY_META[cat] || { label: cat, icon: '🔧' };
|
||||
const state = _categoryState(cat);
|
||||
const tools = _categories[cat];
|
||||
const expanded = _expanded.has(cat);
|
||||
const stateClass = state === 'all' ? 'enabled' : state === 'partial' ? 'partial' : '';
|
||||
const chevron = tools.length > 1
|
||||
? `<span class="tools-popup-expand ${expanded ? 'open' : ''}" data-expand="${cat}">›</span>`
|
||||
: '';
|
||||
|
||||
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>
|
||||
${chevron}
|
||||
</div>`;
|
||||
|
||||
// Expanded children
|
||||
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>
|
||||
<span class="tools-popup-label">${t.displayName}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup.innerHTML = html;
|
||||
}
|
||||
|
||||
function _wireEvents() {
|
||||
const btn = document.getElementById('toolsToggleBtn');
|
||||
const popup = document.getElementById('toolsPopup');
|
||||
if (!btn || !popup) return;
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
popup.classList.toggle('open');
|
||||
});
|
||||
|
||||
popup.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Expand/collapse chevron
|
||||
const expander = e.target.closest('.tools-popup-expand');
|
||||
if (expander) {
|
||||
_toggleExpand(expander.dataset.expand);
|
||||
return;
|
||||
}
|
||||
|
||||
// Individual tool toggle
|
||||
const toolRow = e.target.closest('.tools-popup-tool');
|
||||
if (toolRow) {
|
||||
_toggleTool(toolRow.dataset.tool);
|
||||
return;
|
||||
}
|
||||
|
||||
// Category toggle
|
||||
const catRow = e.target.closest('.tools-popup-category');
|
||||
if (catRow) _toggleCategory(catRow.dataset.category);
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', () => popup.classList.remove('open'));
|
||||
}
|
||||
|
||||
return { init, getDisabledTools, refresh };
|
||||
})();
|
||||
Reference in New Issue
Block a user