This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/chat/tools-popup.js
gobha b7746c3004 Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-23 16:47:48 +00:00

167 lines
6.6 KiB
JavaScript

// ==========================================
// Chat Surface — ToolsPopup Component
// ==========================================
// Scrollable popup anchored to a toolbar icon.
// Category-based tool toggles with persistence to localStorage.
const html = window.html;
const { useState, useEffect, useRef, useCallback, useMemo } = window.hooks;
const STORAGE_KEY = 'sw-disabled-tools';
/**
* Load tools list from API and organize by category.
* @returns {{ categories: Array, disabled: Set, toggle: Function, getDisabled: Function }}
*/
export function useTools() {
const [tools, setTools] = useState([]);
const [disabled, setDisabled] = useState(_loadDisabled);
useEffect(() => {
// Load available tools from API
if (window.sw?.api?.tools?.list) {
window.sw.api.tools.list().then(resp => {
setTools(resp || []);
}).catch(() => {});
}
}, []);
const categories = useMemo(() => {
const map = {};
for (const t of tools) {
const cat = t.category || 'Other';
if (!map[cat]) map[cat] = [];
map[cat].push(t);
}
return Object.entries(map).map(([name, items]) => ({ name, items }));
}, [tools]);
const toggle = useCallback((toolName) => {
setDisabled(prev => {
const next = new Set(prev);
if (next.has(toolName)) next.delete(toolName);
else next.add(toolName);
_saveDisabled(next);
return next;
});
}, []);
const toggleCategory = useCallback((catName) => {
setDisabled(prev => {
const cat = categories.find(c => c.name === catName);
if (!cat) return prev;
const next = new Set(prev);
const allDisabled = cat.items.every(t => next.has(t.name));
for (const t of cat.items) {
if (allDisabled) next.delete(t.name);
else next.add(t.name);
}
_saveDisabled(next);
return next;
});
}, [categories]);
const getDisabled = useCallback(() => [...disabled], [disabled]);
return { categories, disabled, toggle, toggleCategory, getDisabled };
}
/**
* @param {{
* open: boolean,
* onClose: () => void,
* categories: Array<{name: string, items: Array<{name: string, description?: string}>}>,
* disabled: Set<string>,
* onToggle: (name: string) => void,
* onToggleCategory: (catName: string) => void,
* }} props
*/
export function ToolsPopup({ open, onClose, categories, disabled, onToggle, onToggleCategory }) {
const panelRef = useRef(null);
const [expandedCats, setExpandedCats] = useState({});
// Close on Escape or outside click
useEffect(() => {
if (!open) return;
function _onKey(e) {
if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
}
function _onClick(e) {
if (panelRef.current && !panelRef.current.contains(e.target)) onClose();
}
document.addEventListener('keydown', _onKey, true);
setTimeout(() => document.addEventListener('click', _onClick), 0);
return () => {
document.removeEventListener('keydown', _onKey, true);
document.removeEventListener('click', _onClick);
};
}, [open, onClose]);
const _toggleCat = useCallback((name) => {
setExpandedCats(prev => ({ ...prev, [name]: !prev[name] }));
}, []);
if (!open) return null;
const disabledCount = disabled.size;
return html`
<div class="sw-tools-popup" ref=${panelRef} role="dialog" aria-label="Tools toggle">
<div class="sw-tools-popup__header">
<span class="sw-tools-popup__title">Tools</span>
${disabledCount > 0 && html`
<span class="sw-tools-popup__badge">${disabledCount} disabled</span>`}
</div>
<div class="sw-tools-popup__body">
${categories.length === 0 && html`
<div class="sw-tools-popup__empty">No tools available</div>`}
${categories.map(cat => {
const allDisabled = cat.items.every(t => disabled.has(t.name));
const someDisabled = cat.items.some(t => disabled.has(t.name));
const isExpanded = expandedCats[cat.name] !== false; // default open
return html`
<div key=${cat.name} class="sw-tools-popup__category">
<div class="sw-tools-popup__cat-header">
<button class="sw-tools-popup__cat-toggle"
onClick=${() => _toggleCat(cat.name)}
aria-expanded=${isExpanded}>
<span class=${'sw-tools-popup__chevron' + (isExpanded ? ' sw-tools-popup__chevron--open' : '')}>
\u25B6
</span>
<span>${cat.name}</span>
<span class="sw-tools-popup__cat-count">${cat.items.length}</span>
</button>
<label class="sw-tools-popup__cat-switch">
<input type="checkbox"
checked=${!allDisabled}
indeterminate=${someDisabled && !allDisabled}
onChange=${() => onToggleCategory(cat.name)} />
</label>
</div>
${isExpanded && cat.items.map(tool => html`
<label key=${tool.name} class="sw-tools-popup__tool">
<input type="checkbox"
checked=${!disabled.has(tool.name)}
onChange=${() => onToggle(tool.name)} />
<span class="sw-tools-popup__tool-name">${tool.name}</span>
${tool.description && html`
<span class="sw-tools-popup__tool-desc">${tool.description}</span>`}
</label>`)}
</div>`;
})}
</div>
</div>`;
}
function _loadDisabled() {
try {
const arr = JSON.parse(localStorage.getItem(STORAGE_KEY));
return new Set(arr || []);
} catch (_) { return new Set(); }
}
function _saveDisabled(set) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify([...set])); } catch (_) {}
}