Changeset 0.13.1 (#65)

This commit is contained in:
2026-02-25 23:56:27 +00:00
parent 8292a6efa8
commit 113b98ace4
24 changed files with 1565 additions and 24 deletions

View File

@@ -168,11 +168,12 @@ const API = {
return this._post(`/api/v1/channels/${channelId}/messages/${messageId}/edit`, { content });
},
async streamRegenerate(channelId, messageId, signal, model, presetId, apiConfigId) {
async streamRegenerate(channelId, messageId, signal, model, presetId, apiConfigId, disabledTools) {
const body = {};
if (model) body.model = model;
if (presetId) body.preset_id = presetId;
if (apiConfigId) body.provider_config_id = apiConfigId;
if (disabledTools?.length) body.disabled_tools = disabledTools;
let resp = await fetch(BASE + `/api/v1/channels/${channelId}/messages/${messageId}/regenerate`, {
method: 'POST',
@@ -226,7 +227,7 @@ const API = {
// ── Completions ──────────────────────────
async streamCompletion(channelId, content, model, signal, apiConfigId, presetId, attachmentIds) {
async streamCompletion(channelId, content, model, signal, apiConfigId, presetId, attachmentIds, disabledTools) {
const body = { channel_id: channelId, content, stream: true };
if (presetId) {
body.preset_id = presetId;
@@ -239,6 +240,7 @@ const API = {
// Only send max_tokens if user explicitly set it (non-zero = override)
if (App.settings.maxTokens > 0) body.max_tokens = App.settings.maxTokens;
if (attachmentIds?.length) body.attachment_ids = attachmentIds;
if (disabledTools?.length) body.disabled_tools = disabledTools;
let resp = await fetch(BASE + '/api/v1/chat/completions', {
method: 'POST',
@@ -278,6 +280,10 @@ const API = {
return resp;
},
// ── Tools ────────────────────────────────
getTools() { return this._get('/api/v1/tools'); },
// ── Models ───────────────────────────────
listEnabledModels() { return this._get('/api/v1/models/enabled'); },

View File

@@ -187,6 +187,7 @@ async function startApp() {
await initBanners();
initAttachments();
if (typeof ToolsToggle !== 'undefined') ToolsToggle.init();
UI.renderChatList();
UI.updateModelSelector();
UI.updateUser();

View File

@@ -294,7 +294,8 @@ async function sendMessage() {
UI.setGenerating(true);
try {
const resp = await API.streamCompletion(App.currentChatId, text, model, App.abortController.signal, modelInfo?.configId, presetId, attachmentIds);
const resp = await API.streamCompletion(App.currentChatId, text, model, App.abortController.signal, modelInfo?.configId, presetId, attachmentIds,
typeof ToolsToggle !== 'undefined' ? ToolsToggle.getDisabledTools() : []);
await UI.streamResponse(resp, chat.messages, modelInfo?.name);
// Reload active path from server to get proper IDs and sibling metadata
@@ -391,7 +392,8 @@ async function regenerateMessage(messageId) {
try {
const resp = await API.streamRegenerate(
App.currentChatId, messageId, App.abortController.signal,
model, presetId, modelInfo?.configId
model, presetId, modelInfo?.configId,
typeof ToolsToggle !== 'undefined' ? ToolsToggle.getDisabledTools() : []
);
await UI.streamResponse(resp, chat?.messages || [], modelInfo?.name);
@@ -475,7 +477,8 @@ async function submitEdit(messageId, newContent) {
// message that streamCompletion would create.
const resp = await API.streamRegenerate(
App.currentChatId, editResp.id, App.abortController.signal,
model, presetId, modelInfo?.configId
model, presetId, modelInfo?.configId,
typeof ToolsToggle !== 'undefined' ? ToolsToggle.getDisabledTools() : []
);
await UI.streamResponse(resp, chat?.messages || [], modelInfo?.name);

View File

@@ -195,6 +195,15 @@ async function handleSaveAdminSettings() {
};
await API.adminUpdateSetting('banner', { value: banner });
// Web Search config → global_settings (JSON value)
const searchConfig = {
provider: document.getElementById('adminSearchProvider').value,
endpoint: document.getElementById('adminSearchEndpoint').value.trim(),
api_key: document.getElementById('adminSearchApiKey').value.trim(),
max_results: parseInt(document.getElementById('adminSearchMaxResults').value) || 5,
};
await API.adminUpdateSetting('search_config', { value: searchConfig });
UI.toast('Settings saved', 'success');
// Live-apply: refresh policies and dependent UI
await initBanners();
@@ -582,6 +591,11 @@ function _initSettingsListeners() {
document.getElementById('adminBannerBgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerBg').value = e.target.value; UI.updateBannerPreview(); }});
document.getElementById('adminBannerFgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerFg').value = e.target.value; UI.updateBannerPreview(); }});
// Admin — search provider controls
document.getElementById('adminSearchProvider')?.addEventListener('change', (e) => {
document.getElementById('searxngConfigFields').style.display = e.target.value === 'searxng' ? '' : 'none';
});
// Admin — audit log
document.getElementById('auditRefreshBtn')?.addEventListener('click', () => UI.loadAuditLog(1));
document.getElementById('auditFilterAction')?.addEventListener('change', () => UI.loadAuditLog(1));

222
src/js/tools-toggle.js Normal file
View 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 };
})();

View File

@@ -780,6 +780,16 @@ Object.assign(UI, {
// Vault / Encryption status
const vaultEl = document.getElementById('adminVaultStatus');
// Web Search config (global_settings)
const searchCfg = getSetting('search_config', {}) || {};
document.getElementById('adminSearchProvider').value = searchCfg.provider || 'duckduckgo';
document.getElementById('adminSearchEndpoint').value = searchCfg.endpoint || '';
document.getElementById('adminSearchApiKey').value = searchCfg.api_key || '';
document.getElementById('adminSearchMaxResults').value = String(searchCfg.max_results || 5);
document.getElementById('searxngConfigFields').style.display =
(searchCfg.provider === 'searxng') ? '' : 'none';
if (vaultEl) {
try {
const vault = await API.adminGetVaultStatus();