Changeset 0.37.10 (#222)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
171
src/js/sw/surfaces/chat/tools-popup.js
Normal file
171
src/js/sw/surfaces/chat/tools-popup.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// ==========================================
|
||||
// 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?.admin?.tools?.list) {
|
||||
window.sw.api.admin.tools.list().then(resp => {
|
||||
setTools(resp?.data || resp || []);
|
||||
}).catch(() => {});
|
||||
} else if (window.sw?.api?.channels?.tools) {
|
||||
// Fallback — try the channels tools endpoint
|
||||
window.sw.api.channels.tools?.().then(resp => {
|
||||
setTools(resp?.data || 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 (_) {}
|
||||
}
|
||||
Reference in New Issue
Block a user