mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 09:45:24 -04:00
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and small visual nudges across the app. Highlights: Settings restructure - Add Models: split into separate Local + API cards (no more in-card tabs); each fuses Type/Provider with the URL input. - Added Models: new dedicated sidebar tab, with Probe + Clear-offline pulled into its header; Local/API sub-section icons accent-tinted. - Search: Web Search and a new Deep Research card (Model + tuning), with a cross-link to AI Defaults. Provider hints use real clickable anchors; Web Search Test button shows a whirlpool spinner. - AI Defaults: Image Generation card returns; Research Model card carries only Endpoint+Model with a cross-link to Search; Vision / Default / Utility fallbacks unified under one numbered-row design matching Search's chain. - API Permissions (was 'API Tokens'): per-row rename, inline Permissions toggle that expands the scope-edit panel, in-field copy icons (icon→check on success). Empty state accent-tinted. - Integrations: + Add Integration drops a type-picker menu directly under the button (drop-up on tight viewports); each integration form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses the same accent-outlined Save/Test/Cancel buttons right-aligned. - Danger Zone: Wipe→Delete with trash icons; new 'Delete everything' row at the bottom that loops every category. AI Synthesis (Reminders) - Persona dropdown sourced from PROMPT_TEMPLATES + custom preset. - src/reminder_personas.py mirrors the five built-ins for the server-side synthesis path. - dispatch_reminder() reads reminder_llm_persona and uses the persona's system prompt; empty/unknown falls back to warm-neutral. Esc handling - Kebab menus and the provider picker intercept Esc in capture phase so dismissing a popup no longer closes the whole Settings modal. Accent tinting - Scoped CSS rule across data-settings-panel=ai/services/added-models/ search/integrations/reminders for card h2 icons + the Added Models sub-section icons. Codex/Claude integration form - No more auto-creation on form open — explicit Create token button. - New tokens start with every scope granted; existing tokens move out of the integration form into the API Permissions card. - Setup reveal: copy buttons inline inside the token + setup code blocks; shorter subtitle wording. Misc visual polish - Save/Test/Cancel uniformly accent-outlined and right-aligned on every integration form. - Provider logos render inline next to the search fallback selects and the Deep Research Search dropdown. - Trash icons in fallback rows bumped to 20x20 so they fill the 32px button. - Image generation default flipped to off.
This commit is contained in:
+3
-14
@@ -1221,7 +1221,7 @@ function initializeEventListeners() {
|
||||
sortDropdown.querySelectorAll('.sort-option').forEach(o => {
|
||||
const check = o.querySelector('.sort-check') || document.createElement('span');
|
||||
check.className = 'sort-check';
|
||||
check.style.cssText = 'float:right;font-size:20px;line-height:1;position:relative;top:3px;color:var(--accent, var(--red));opacity:' + (o.dataset.sort === current ? '1' : '0');
|
||||
check.style.cssText = 'float:right;font-size:20px;line-height:1;position:relative;top:1px;color:var(--accent, var(--red));opacity:' + (o.dataset.sort === current ? '1' : '0');
|
||||
check.textContent = '\u2022';
|
||||
if (!o.querySelector('.sort-check')) o.appendChild(check);
|
||||
});
|
||||
@@ -1265,9 +1265,9 @@ function initializeEventListeners() {
|
||||
let msg;
|
||||
if (data.updated > 0) {
|
||||
msg = `Sorted ${data.updated} into ${data.folders.length} folder${data.folders.length === 1 ? '' : 's'}`;
|
||||
if (remaining > 0) msg += ` — ${remaining} unfiled left, hit Tidy again`;
|
||||
if (remaining > 0) msg += ` — ${remaining} unfiled left, hit Group again`;
|
||||
} else if (remaining > 0) {
|
||||
msg = `${remaining} unfiled chats — hit Tidy again`;
|
||||
msg = `${remaining} unfiled chats — hit Group again`;
|
||||
} else {
|
||||
msg = 'All sorted';
|
||||
}
|
||||
@@ -1288,17 +1288,6 @@ function initializeEventListeners() {
|
||||
|
||||
const autoSortBtn = el('auto-sort-sessions-btn');
|
||||
if (autoSortBtn) autoSortBtn.addEventListener('click', () => _runTidy(false));
|
||||
|
||||
// Chevron next to the Tidy row toggles the no-AI sub-item.
|
||||
const autoSortMoreBtn = el('auto-sort-sessions-more');
|
||||
const autoSortNoaiBtn = el('auto-sort-sessions-noai-btn');
|
||||
if (autoSortMoreBtn && autoSortNoaiBtn) {
|
||||
autoSortMoreBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
autoSortNoaiBtn.style.display = autoSortNoaiBtn.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
autoSortNoaiBtn.addEventListener('click', () => _runTidy(true));
|
||||
}
|
||||
}
|
||||
|
||||
// Model sort dropdown
|
||||
|
||||
+204
-157
@@ -704,12 +704,13 @@
|
||||
<div class="section-header-flex">
|
||||
<span class="section-title" id="chats-section-title"><svg class="section-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span id="chats-section-label" class="section-title-label">Chats</span><span id="chats-notif-dot" class="sidebar-notif-dot" style="display:none"></span></span>
|
||||
<div style="position:relative; display:inline-block; display:flex; gap:4px; align-items:center;">
|
||||
<button type="button" class="section-header-btn chats-manage-btn" id="chats-library-btn" title="Manage Chats (Library)">
|
||||
<button type="button" class="section-header-btn list-item-plus-btn chats-manage-btn" id="chats-library-btn" title="Manage Chats (Library)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
<path d="M9 7h6M9 11h4"/>
|
||||
</svg>
|
||||
<span class="list-item-plus-label">manage</span>
|
||||
</button>
|
||||
<button type="button" class="section-header-btn" id="session-sort-btn" title="Sort sessions">
|
||||
<svg class="sort-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -726,14 +727,11 @@
|
||||
<div class="dropdown-item sort-option sort-dropdown-item" data-sort="newest">Newest First</div>
|
||||
<div class="dropdown-item sort-option sort-dropdown-item" data-sort="group">By Folder</div>
|
||||
<div class="dropdown-item sort-dropdown-item sort-dropdown-sep" id="auto-sort-sessions-row" style="display:flex;align-items:center;padding:0;">
|
||||
<span id="auto-sort-sessions-btn" style="flex:1;padding:5px 10px;cursor:pointer;display:inline-flex;align-items:center;gap:4px;">
|
||||
<span class="auto-sort-icon"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy</span>
|
||||
<span id="auto-sort-sessions-btn" style="flex:1;padding:5px 10px 5px 10px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;">
|
||||
<span class="auto-sort-icon"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg></span>
|
||||
<span>Group</span>
|
||||
<span class="auto-sort-spinner" style="display:none;">Sorting...</span>
|
||||
</span>
|
||||
<button type="button" id="auto-sort-sessions-more" title="Tidy options" aria-label="Tidy options" style="background:none;border:none;border-left:1px solid var(--border);color:inherit;cursor:pointer;padding:5px 8px;font-size:9px;opacity:0.7;"><svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>
|
||||
</div>
|
||||
<div class="dropdown-item sort-dropdown-item" id="auto-sort-sessions-noai-btn" style="display:none;padding-left:24px;">
|
||||
Tidy <span class="auto-sort-noai-spinner" style="display:none;font-size:9px;opacity:0.6;margin-left:4px;">Cleaning...</span>
|
||||
</div>
|
||||
<div class="dropdown-item rearrange-toggle sort-dropdown-item sort-dropdown-sep" id="session-rearrange-toggle">
|
||||
↑↓ Rearrange <span class="rearrange-check" style="float:right; opacity:0;">•</span>
|
||||
@@ -1317,7 +1315,6 @@
|
||||
</button>
|
||||
<button class="close-btn" aria-label="Close settings">✖</button>
|
||||
</div>
|
||||
<div class="admin-toggle-sub" style="padding:0 12px 8px;opacity:0.6;font-size:11px;">Toggle on/off visibility of tools and modules across the interface.</div>
|
||||
<div class="settings-layout">
|
||||
<div class="settings-sidebar">
|
||||
<!-- Section 1: AI plumbing (Add Models → AI Defaults → Search) -->
|
||||
@@ -1399,10 +1396,16 @@
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Model</label>
|
||||
<span class="adm-model-logo" id="set-defaultModelSelect-logo" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;opacity:0.9;color:var(--fg);"></span>
|
||||
<select id="set-defaultModelSelect" class="settings-select"></select>
|
||||
</div>
|
||||
<div id="set-defaultFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-defaultAddFallback" title="Add a model to try if the one above fails">+ Add fallback</button>
|
||||
<div class="settings-row" style="align-items:flex-start;">
|
||||
<label class="settings-label" style="margin-top:6px;">Fallbacks</label>
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:6px;">
|
||||
<div id="set-defaultFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-defaultAddFallback" title="Add a model to try if the one above fails">+ Add fallback</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-defaultChatMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1416,10 +1419,16 @@
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Model</label>
|
||||
<span class="adm-model-logo" id="set-utilityModelSelect-logo" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;opacity:0.9;color:var(--fg);"></span>
|
||||
<select id="set-utilityModelSelect" class="settings-select"><option value="">—</option></select>
|
||||
</div>
|
||||
<div id="set-utilityFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-utilityAddFallback" title="Add a model to try if the utility model fails">+ Add fallback</button>
|
||||
<div class="settings-row" style="align-items:flex-start;">
|
||||
<label class="settings-label" style="margin-top:6px;">Fallbacks</label>
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:6px;">
|
||||
<div id="set-utilityFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-utilityAddFallback" title="Add a model to try if the utility model fails">+ Add fallback</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-utilityChatMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1429,16 +1438,22 @@
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<label class="settings-label">Model</label>
|
||||
<span class="adm-model-logo" id="set-vlModelSelect-logo" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;opacity:0.9;color:var(--fg);"></span>
|
||||
<select id="set-vlModelSelect" class="settings-select"><option value="">Auto-detect</option></select>
|
||||
</div>
|
||||
<div id="set-visionFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-visionAddFallback" title="Add a vision model to try if the one above fails">+ Add fallback</button>
|
||||
<div class="settings-row" style="align-items:flex-start;">
|
||||
<label class="settings-label" style="margin-top:6px;">Fallbacks</label>
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:6px;">
|
||||
<div id="set-visionFallbacks" class="settings-fallbacks"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-visionAddFallback" title="Add a vision model to try if the one above fails">+ Add fallback</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-visionSettingsMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>Research Model</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Model used for Deep Research. Falls back to the default chat model if not set.</div>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Model used for Deep Research, more settings under <a href="#" data-go-settings-tab="search" style="color:var(--accent, var(--red));text-decoration:underline;font-weight:600;">Search →</a></div>
|
||||
<div class="settings-col">
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Endpoint</label>
|
||||
@@ -1448,48 +1463,17 @@
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Model</label>
|
||||
<span class="adm-model-logo" id="set-researchModel-logo" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;opacity:0.9;color:var(--fg);"></span>
|
||||
<select id="set-researchModel" class="settings-select">
|
||||
<option value="">Same as chat</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Search</label>
|
||||
<select id="set-researchSearch" class="settings-select">
|
||||
<option value="">Same as web search</option>
|
||||
<option value="searxng">SearXNG</option>
|
||||
<option value="duckduckgo">DuckDuckGo</option>
|
||||
<option value="tavily">Tavily</option>
|
||||
<option value="brave">Brave</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="serper">Serper</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Max Tokens</label>
|
||||
<input id="set-researchMaxTokens" type="text" inputmode="numeric" placeholder="8192 (default)" class="settings-select" style="width:120px;">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Extract Timeout</label>
|
||||
<input id="set-researchExtractTimeout" type="text" inputmode="numeric" placeholder="90 sec" class="settings-select" style="width:120px;">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Extract Parallel</label>
|
||||
<input id="set-researchExtractConcurrency" type="text" inputmode="numeric" placeholder="3" class="settings-select" style="width:120px;">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Max Time</label>
|
||||
<input id="set-researchRunTimeout" type="text" inputmode="numeric" placeholder="1800 sec (0 = no limit)" class="settings-select" style="width:120px;">
|
||||
</div>
|
||||
<div id="set-researchMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||
<div id="set-researchMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);margin-top:2px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Agent card moved to the Agent Tools tab. -->
|
||||
<!-- Image Generation removed — only inpaint remains in this build,
|
||||
and inpaint is configured via the gallery editor not this card.
|
||||
Keeping the DOM (hidden) so JS wiring against the inputs
|
||||
doesn't throw. -->
|
||||
<div class="admin-card" hidden style="display:none">
|
||||
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>Image Generation<span style="flex:1"></span><label class="admin-switch"><input type="checkbox" id="set-imgEnabledToggle" checked><span class="admin-slider"></span></label></h2>
|
||||
<div class="admin-card">
|
||||
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>Image Generation<span style="flex:1"></span><label class="admin-switch"><input type="checkbox" id="set-imgEnabledToggle"><span class="admin-slider"></span></label></h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Configure which model to use for image generation.</div>
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
@@ -1596,10 +1580,12 @@
|
||||
<option value="serper" data-search-logo="serper">Serper.dev</option>
|
||||
<option value="disabled" data-search-logo="disabled">Disabled</option>
|
||||
</select>
|
||||
<button type="button" class="admin-btn-sm" id="set-searchTestBtn" title="Run a test query against the configured provider" style="margin-left:6px;flex-shrink:0;position:relative;top:2px;">Test</button>
|
||||
<button type="button" class="admin-btn-sm" id="set-searchTestBtn" title="Run a test query against the configured provider" style="margin-left:2px;flex-shrink:0;position:relative;top:2px;display:inline-flex;align-items:center;gap:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Results</label>
|
||||
<label class="settings-label" title="How many web search results to fetch per query">Results per query</label>
|
||||
<div style="display:flex;gap:8px;flex:1;">
|
||||
<select id="set-searchResultCount" class="settings-select" style="flex:1;">
|
||||
<option value="3">3</option>
|
||||
@@ -1613,30 +1599,78 @@
|
||||
</div>
|
||||
<div id="set-searchUrlRow" class="settings-row">
|
||||
<label class="settings-label">URL</label>
|
||||
<input id="set-searchUrl" type="text" placeholder="http://localhost:8080" class="settings-select">
|
||||
<input id="set-searchUrl" type="text" placeholder="http://localhost:8080 (optional)" class="settings-select">
|
||||
</div>
|
||||
<div id="set-searchKeyRow" class="settings-row" style="display:none;">
|
||||
<label class="settings-label">API Key</label>
|
||||
<input id="set-searchApiKey" type="password" placeholder="API key" class="settings-select">
|
||||
<div style="position:relative;flex:1;display:flex;align-items:center;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;left:9px;top:50%;transform:translateY(-50%);opacity:0.55;pointer-events:none;"><path d="M21 2l-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/><path d="M15.5 7.5l3 3"/></svg>
|
||||
<input id="set-searchApiKey" type="password" placeholder="API key" class="settings-select" style="flex:1;padding-left:28px;">
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-searchCxRow" class="settings-row" style="display:none;">
|
||||
<label class="settings-label">CX ID</label>
|
||||
<input id="set-searchCx" type="text" placeholder="Google PSE engine ID" class="settings-select">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label" title="Providers tried in order when the primary fails or hits a rate limit">Fallbacks</label>
|
||||
<div class="search-fallback-chain" id="set-searchFallbackChain"></div>
|
||||
<div class="settings-row" style="align-items:flex-start;">
|
||||
<label class="settings-label" style="margin-top:6px;" title="Providers tried in order when the primary fails or hits a rate limit">Fallbacks</label>
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:6px;">
|
||||
<div class="settings-fallbacks" id="set-searchFallbackChain"></div>
|
||||
<button type="button" class="settings-fallback-add" id="set-searchAddFallback" title="Add a search provider to try if the primary fails">+ Add fallback</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-searchHint" class="admin-toggle-sub"></div>
|
||||
<div id="set-searchMsg" style="font-size:11px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>Deep Research</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Deep Research runtime settings. Default Model is picked in <a href="#" data-go-settings-tab="ai" style="color:var(--accent, var(--red));text-decoration:underline;font-weight:600;">AI Defaults →</a></div>
|
||||
<div class="settings-col">
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Search</label>
|
||||
<span style="margin-left:auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;color:var(--fg);" id="set-researchSearch-logo"></span>
|
||||
<select id="set-researchSearch" class="settings-select" style="width:358.5px;flex:0 0 auto;max-width:calc(100% - 24px);">
|
||||
<option value="" data-search-logo="">Same as web search</option>
|
||||
<option value="searxng" data-search-logo="searxng">SearXNG</option>
|
||||
<option value="duckduckgo" data-search-logo="duckduckgo">DuckDuckGo</option>
|
||||
<option value="tavily" data-search-logo="tavily">Tavily</option>
|
||||
<option value="brave" data-search-logo="brave">Brave</option>
|
||||
<option value="google" data-search-logo="google_pse">Google</option>
|
||||
<option value="serper" data-search-logo="serper">Serper</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Max Tokens</label>
|
||||
<input id="set-researchMaxTokens" type="text" inputmode="numeric" placeholder="8192 (default)" class="settings-select" style="width:382.5px;flex:0 0 auto;margin-left:auto;">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Extract Timeout</label>
|
||||
<div style="position:relative;width:382.5px;flex:0 0 auto;margin-left:auto;">
|
||||
<input id="set-researchExtractTimeout" type="text" inputmode="numeric" placeholder="90 sec" class="settings-select" style="width:100%;padding-right:30px;">
|
||||
<span title="How long the researcher waits for a single URL to fetch and extract before giving up on it. Slow sites get skipped. Default 90 seconds." style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:50%;border:1px solid var(--border);display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;opacity:0.55;cursor:help;user-select:none;">?</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Extract Parallel</label>
|
||||
<div style="position:relative;width:382.5px;flex:0 0 auto;margin-left:auto;">
|
||||
<input id="set-researchExtractConcurrency" type="text" inputmode="numeric" placeholder="3" class="settings-select" style="width:100%;padding-right:30px;">
|
||||
<span title="How many URLs the researcher fetches and extracts in parallel. Higher is faster but uses more memory/CPU. Default 3." style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:50%;border:1px solid var(--border);display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;opacity:0.55;cursor:help;user-select:none;">?</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label class="settings-label">Timeout</label>
|
||||
<input id="set-researchRunTimeout" type="text" inputmode="numeric" placeholder="1800 sec (0 = no limit)" class="settings-select" style="width:382.5px;flex:0 0 auto;margin-left:auto;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ APPEARANCE TAB ═══ -->
|
||||
<div data-settings-panel="appearance" class="settings-appearance-panel hidden">
|
||||
<div class="admin-card" style="padding-bottom:6px;">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>Sidebar</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>Sidebar<span style="flex:1"></span><button type="button" class="vis-reset-btn" data-vis-reset title="Reset this section to defaults" aria-label="Reset Sidebar to defaults" style="background:none;border:none;padding:2px 4px;cursor:pointer;color:inherit;opacity:0.55;display:inline-flex;align-items:center;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button></h2>
|
||||
<div class="vis-toggles">
|
||||
<label class="vis-row">
|
||||
<span class="vis-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M8 12l2.5 2.5L16 9"/></svg></span>
|
||||
@@ -1736,7 +1770,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card" style="padding-bottom:6px;">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>Chat Area</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>Chat Area<span style="flex:1"></span><button type="button" class="vis-reset-btn" data-vis-reset title="Reset this section to defaults" aria-label="Reset Chat Area to defaults" style="background:none;border:none;padding:2px 4px;cursor:pointer;color:inherit;opacity:0.55;display:inline-flex;align-items:center;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button></h2>
|
||||
<div class="vis-toggles">
|
||||
<label class="vis-row">
|
||||
<span class="vis-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16"/><path d="M4 10h8"/></svg></span>
|
||||
@@ -1771,7 +1805,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card" style="padding-bottom:6px;">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>Chat Bar</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>Chat Bar<span style="flex:1"></span><button type="button" class="vis-reset-btn" data-vis-reset title="Reset this section to defaults" aria-label="Reset Chat Bar to defaults" style="background:none;border:none;padding:2px 4px;cursor:pointer;color:inherit;opacity:0.55;display:inline-flex;align-items:center;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button></h2>
|
||||
<div class="vis-toggles">
|
||||
<label class="vis-row">
|
||||
<span class="vis-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
||||
@@ -1815,9 +1849,6 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;padding:0 4px;">
|
||||
<button type="button" class="admin-btn-sm" id="set-uiVisResetBtn" style="opacity:0.5;">Reset All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ THEME TAB ═══ -->
|
||||
@@ -1830,7 +1861,7 @@
|
||||
<h2 style="margin:0;font-size:13px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/></svg>Keyboard Shortcuts</h2>
|
||||
<p style="font-size:10px;opacity:0.4;margin:2px 0 0;">Click a shortcut to rebind. Press Escape to cancel.</p>
|
||||
</div>
|
||||
<button type="button" class="shortcut-action-btn is-reset" id="shortcuts-reset-btn" title="Reset Shortcuts" style="width:28px;height:28px;font-size:15px;">↩</button>
|
||||
<button type="button" class="vis-reset-btn" id="shortcuts-reset-btn" title="Reset shortcuts to defaults" aria-label="Reset shortcuts to defaults" style="background:none;border:none;padding:2px 4px;cursor:pointer;color:inherit;opacity:0.55;display:inline-flex;align-items:center;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<div id="shortcuts-list"></div>
|
||||
@@ -1842,7 +1873,7 @@
|
||||
<div data-settings-panel="account" class="hidden">
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>Account</h2>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin:4px 0 12px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin:12px 0 12px;">
|
||||
<div class="user-bar-avatar" id="settings-account-avatar" style="width:32px;height:32px;font-size:14px;"></div>
|
||||
<div style="flex:1;">
|
||||
<div id="settings-account-username" style="font-size:13px;font-weight:600;"></div>
|
||||
@@ -1880,7 +1911,7 @@
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>Email Accounts</h2>
|
||||
<div class="settings-row" style="align-items:center;">
|
||||
<div class="admin-toggle-sub" style="margin:0;flex:1;">Add, edit, delete, and test accounts in Integrations.</div>
|
||||
<button class="admin-btn-add" id="set-email-open-integrations" style="display:inline-flex;align-items:center;gap:6px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="opacity:0.7"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Manage in Integrations</button>
|
||||
<button class="admin-btn-add" id="set-email-open-integrations" style="display:inline-flex;align-items:center;gap:6px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="opacity:0.7"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Open Integrations</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1896,10 +1927,10 @@
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Writing Style</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">AI-extracted from your sent emails. Used when AI drafts replies.</div>
|
||||
<div class="settings-col">
|
||||
<textarea id="set-email-style" rows="4" class="settings-select" style="font-family:inherit;resize:vertical" placeholder="e.g. I write emails in this style. I don't use exclamation marks. I sign emails with: ..."></textarea>
|
||||
<textarea id="set-email-style" rows="6" class="settings-select" style="font-family:inherit;resize:none" placeholder="e.g. I write emails in this style. I don't use exclamation marks. I sign emails with: ..."></textarea>
|
||||
<div class="settings-row" style="margin-top:4px">
|
||||
<span id="set-email-style-msg" style="font-size:11px;"></span>
|
||||
<button class="admin-btn-add" id="set-email-style-extract" style="margin-left:auto;">Extract from Sent (15 emails)</button>
|
||||
<button class="admin-btn-add" id="set-email-style-extract" style="margin-left:auto;display:inline-flex;align-items:center;gap:5px;"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg>Extract from Sent (15 emails)</button>
|
||||
<button class="admin-btn-add" id="set-email-style-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1909,7 +1940,7 @@
|
||||
<!-- ═══ REMINDERS TAB ═══ -->
|
||||
<div data-settings-panel="reminders" class="hidden">
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>How you're reminded</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>How you're reminded<span style="flex:1"></span><span id="set-reminder-test-msg" style="font-size:11px;font-weight:normal;"></span><button class="admin-btn-sm" id="set-reminder-test-btn" style="font-size:11px;font-weight:normal;display:inline-flex;align-items:center;gap:4px;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test</button></h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Controls how fired note reminders are delivered.</div>
|
||||
<div class="settings-col">
|
||||
<div class="settings-row">
|
||||
@@ -1947,7 +1978,19 @@
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg>AI Synthesis<span style="flex:1"></span><label class="admin-switch" title="Use the utility model to write reminder messages"><input type="checkbox" id="set-reminder-llm-toggle"><span class="admin-slider"></span></label></h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">When on, the utility model writes a short, warm one-line reminder for browser, email, ntfy, AND webhook reminders instead of just the raw note content.</div>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">When on, the utility model writes a short, warm one-line reminder for browser, email, ntfy, and webhook reminders instead of just the raw note content.</div>
|
||||
<div class="settings-col">
|
||||
<div class="settings-row">
|
||||
<label class="settings-label" title="Optional — write the reminder in the voice of a saved character">Persona</label>
|
||||
<select id="set-reminder-llm-persona" class="settings-select" style="flex:1;">
|
||||
<option value="">Default (warm, neutral)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="font-size:11px;opacity:0.7;margin-top:2px;">
|
||||
<a href="#" data-open-prompt-modal style="color:var(--accent, var(--red));text-decoration:underline;font-weight:600;">Edit persona settings here →</a>
|
||||
</div>
|
||||
<div id="set-reminder-llm-persona-msg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 55%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Public App URL</h2>
|
||||
@@ -1960,14 +2003,6 @@
|
||||
<div id="set-app-public-url-msg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 55%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>Test</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Fire a test reminder using your current settings to verify everything works.</div>
|
||||
<div class="settings-row">
|
||||
<span id="set-reminder-test-msg" style="font-size:11px;"></span>
|
||||
<button class="admin-btn-add" id="set-reminder-test-btn" style="margin-left:auto;">Send Test Reminder</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ ADMIN: USERS TAB ═══ -->
|
||||
@@ -2005,13 +2040,32 @@
|
||||
|
||||
<!-- ── Local card ─────────────────────────────────────────── -->
|
||||
<div class="admin-card">
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>Local Models</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>Add Local Models <span style="opacity:0.45;font-weight:normal;font-size:0.82em">(Endpoint)</span>
|
||||
<span style="flex:1"></span>
|
||||
<button class="admin-btn-sm" id="adm-epLocalTestBtn" style="font-size:11px;font-weight:normal;display:inline-flex;align-items:center;gap:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test
|
||||
</button>
|
||||
<div style="position:relative;display:inline-block;">
|
||||
<button class="admin-btn-sm" id="adm-epLocalMoreBtn" title="More options" aria-haspopup="true" aria-expanded="false" style="font-size:11px;font-weight:normal;padding:4px 8px;line-height:1;">
|
||||
<svg width="14" height="4" viewBox="0 0 14 4" fill="currentColor"><circle cx="2" cy="2" r="1.4"/><circle cx="7" cy="2" r="1.4"/><circle cx="12" cy="2" r="1.4"/></svg>
|
||||
</button>
|
||||
<div id="adm-epLocalMoreMenu" style="display:none;position:absolute;top:calc(100% + 4px);right:0;z-index:50;min-width:170px;padding:4px;background:var(--panel,var(--bg));border:1px solid var(--border);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.22);flex-direction:column;gap:1px;">
|
||||
<button class="admin-btn-sm adm-more-item" id="adm-epDiscoverBtn" title="Scan your network for running model servers" style="background:none;border:0;border-radius:5px;padding:7px 9px;display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;font-weight:normal;color:var(--fg);cursor:pointer;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Scan network
|
||||
</button>
|
||||
<button class="admin-btn-sm adm-more-item" id="adm-epOllamaBtn" title="Fill the default Ollama endpoint" style="background:none;border:0;border-radius:5px;padding:7px 9px;display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;font-weight:normal;color:var(--fg);cursor:pointer;"><span class="adm-ollama-logo" style="display:inline-flex;width:13px;height:13px;"></span>Add Ollama</button>
|
||||
<button class="admin-btn-sm adm-more-item" id="adm-epLocalKeyBtn" title="Show / hide the API key field" aria-expanded="false" aria-controls="adm-epLocalApiKey-row" style="background:none;border:0;border-radius:5px;padding:7px 9px;display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;font-weight:normal;color:var(--fg);cursor:pointer;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/><path d="M15.5 7.5l3 3"/></svg>API key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="admin-toggle-sub" style="margin:0 0 10px 2px;">Add a local model server (Ollama, llama.cpp, vLLM).</div>
|
||||
<div class="adm-add-section">
|
||||
<div class="admin-model-form">
|
||||
<div class="admin-model-form-row">
|
||||
<div class="adm-fused-group" style="display:flex;flex:1 1 180px;min-width:0;">
|
||||
<select id="adm-epLocalType" style="padding:5px;width:62px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:0;">
|
||||
<select id="adm-epLocalType" style="padding:5px;width:66px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:0;">
|
||||
<option value="llm" selected>LLM</option>
|
||||
<option value="image">Image</option>
|
||||
</select>
|
||||
@@ -2024,19 +2078,6 @@
|
||||
<div class="admin-model-form-row" id="adm-epLocalApiKey-row" style="display:none;">
|
||||
<input id="adm-epLocalApiKey" type="password" placeholder="API key (optional — for protected local endpoints)" autocomplete="off" style="flex:1">
|
||||
</div>
|
||||
<div class="admin-model-form-row">
|
||||
<button class="admin-btn-sm" id="adm-epDiscoverBtn" title="Scan your network for running model servers" style="display:inline-flex;align-items:center;gap:4px;">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Scan
|
||||
</button>
|
||||
<button class="admin-btn-sm" id="adm-epOllamaBtn" title="Fill the default Ollama endpoint" style="display:inline-flex;align-items:center;gap:5px;"><span class="adm-ollama-logo" style="display:inline-flex;width:13px;height:13px;"></span>Ollama</button>
|
||||
<span style="flex:1"></span>
|
||||
<button class="admin-btn-sm" id="adm-epLocalKeyBtn" title="Show / hide the API key field" aria-expanded="false" aria-controls="adm-epLocalApiKey-row" style="opacity:0.75;display:inline-flex;align-items:center;gap:4px;">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/><path d="M15.5 7.5l3 3"/></svg>API
|
||||
</button>
|
||||
<button class="admin-btn-sm" id="adm-epLocalTestBtn" style="min-width:55px;text-align:center;display:inline-flex;align-items:center;justify-content:center;gap:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test
|
||||
</button>
|
||||
</div>
|
||||
<div id="adm-epLocalMsg" class="adm-ep-inline-msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2044,18 +2085,48 @@
|
||||
|
||||
<!-- ── API card ───────────────────────────────────────────── -->
|
||||
<div class="admin-card">
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>API Models</h2>
|
||||
<h2 style="display:flex;align-items:center;gap:8px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>Add API Models <span style="opacity:0.45;font-weight:normal;font-size:0.82em">(Endpoint)</span>
|
||||
<span style="flex:1"></span>
|
||||
<button class="admin-btn-sm" id="adm-epApiTestBtn" style="font-size:11px;font-weight:normal;display:inline-flex;align-items:center;gap:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test
|
||||
</button>
|
||||
<button class="admin-btn-sm hidden" id="adm-epApiCancelTestBtn" style="font-size:11px;font-weight:normal;">Cancel</button>
|
||||
<div style="position:relative;display:inline-block;">
|
||||
<button class="admin-btn-sm" id="adm-epApiMoreBtn" title="More options" aria-haspopup="true" aria-expanded="false" style="font-size:11px;font-weight:normal;padding:4px 8px;line-height:1;">
|
||||
<svg width="14" height="4" viewBox="0 0 14 4" fill="currentColor"><circle cx="2" cy="2" r="1.4"/><circle cx="7" cy="2" r="1.4"/><circle cx="12" cy="2" r="1.4"/></svg>
|
||||
</button>
|
||||
<div id="adm-epApiMoreMenu" style="display:none;position:absolute;top:calc(100% + 4px);right:0;z-index:50;min-width:200px;padding:4px;background:var(--panel,var(--bg));border:1px solid var(--border);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.22);flex-direction:column;gap:1px;">
|
||||
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;opacity:0.55;padding:6px 9px 2px;">Connection mode</div>
|
||||
<button class="admin-btn-sm adm-more-item adm-kind-opt" data-kind="proxy" style="background:none;border:0;border-radius:5px;padding:7px 9px;display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;font-weight:normal;color:var(--fg);cursor:pointer;">
|
||||
<svg class="adm-kind-check" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<span>Proxy</span>
|
||||
<span style="margin-left:auto;opacity:0.5;font-size:10px;">routed via server</span>
|
||||
</button>
|
||||
<button class="admin-btn-sm adm-more-item adm-kind-opt" data-kind="api" style="background:none;border:0;border-radius:5px;padding:7px 9px;display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;font-weight:normal;color:var(--fg);cursor:pointer;">
|
||||
<svg class="adm-kind-check" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="visibility:hidden;"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<span>API (direct)</span>
|
||||
<span style="margin-left:auto;opacity:0.5;font-size:10px;">browser→provider</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="admin-toggle-sub" style="margin:0 0 10px 2px;">Connect a cloud provider (OpenAI, Anthropic, DeepSeek, OpenRouter, etc.).</div>
|
||||
<div class="adm-add-section">
|
||||
<div class="admin-model-form">
|
||||
<div class="adm-provider-picker adm-provider-combo" id="adm-provider-picker">
|
||||
<input id="adm-epUrl" type="text" placeholder="Base URL or pick provider" autocomplete="off">
|
||||
<button type="button" class="adm-provider-btn" id="adm-provider-btn" title="Pick provider">
|
||||
<span class="adm-provider-current"><span class="adm-provider-logo"></span><span class="adm-provider-name">Provider</span></span>
|
||||
<svg class="adm-provider-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="adm-provider-menu hidden" id="adm-provider-menu"></div>
|
||||
<div class="admin-model-form-row">
|
||||
<div class="adm-provider-picker adm-provider-combo" id="adm-provider-picker" style="flex:1 1 220px;min-width:0;margin-bottom:0;">
|
||||
<button type="button" class="adm-provider-btn" id="adm-provider-btn" title="Pick provider" style="border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:6px;border-bottom-left-radius:6px;border-left:1px solid var(--border);border-right:1px solid var(--border);">
|
||||
<span class="adm-provider-current"><span class="adm-provider-logo"></span><span class="adm-provider-name">Provider</span></span>
|
||||
<svg class="adm-provider-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<input id="adm-epUrl" type="text" placeholder="Base URL or pick provider" autocomplete="off" style="border-left:0;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:6px;border-bottom-right-radius:6px;">
|
||||
<div class="adm-provider-menu hidden" id="adm-provider-menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select id="adm-epKind" style="display:none">
|
||||
<option value="proxy">proxy</option>
|
||||
<option value="api" selected>api</option>
|
||||
</select>
|
||||
<select id="adm-epProvider" style="display:none">
|
||||
<option value="">Custom URL</option>
|
||||
<option value="https://api.anthropic.com" data-logo="anthropic">Anthropic</option>
|
||||
@@ -2077,28 +2148,16 @@
|
||||
<option value="https://api.z.ai/api/coding/paas/v4" data-logo="zhipu">Z.AI Coding Plan</option>
|
||||
</select>
|
||||
<div class="admin-model-form-row" id="adm-epApiKey-row">
|
||||
<input id="adm-epApiKey" type="password" placeholder="API key" autocomplete="off" style="flex:1">
|
||||
</div>
|
||||
<div class="admin-model-form-row" style="margin-top:-4px;">
|
||||
<select id="adm-epKind" style="padding:5px;width:82px;">
|
||||
<option value="proxy">Proxy</option>
|
||||
<option value="api">API</option>
|
||||
</select>
|
||||
<label style="display:inline-flex;align-items:center;gap:4px;font-size:11px;opacity:0.6;flex-shrink:0;">Type:<select id="adm-epType" style="padding:5px;width:80px;flex-shrink:0;">
|
||||
<option value="llm" selected>LLM</option>
|
||||
<option value="image">Image</option>
|
||||
</select></label>
|
||||
<span style="flex:1"></span>
|
||||
<button class="admin-btn-sm" id="adm-epApiTestBtn" style="min-width:55px;text-align:center;display:inline-flex;align-items:center;justify-content:center;gap:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>Test
|
||||
</button>
|
||||
<button class="admin-btn-sm hidden" id="adm-epApiCancelTestBtn" style="width:62px;text-align:center;">Cancel</button>
|
||||
<button class="admin-btn-add" id="adm-epAddBtn" style="min-width:55px;text-align:center;display:inline-flex;align-items:center;justify-content:center;gap:4px;">
|
||||
<div style="position:relative;flex:1;display:flex;align-items:center;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;left:9px;top:50%;transform:translateY(-50%);opacity:0.55;pointer-events:none;"><path d="M21 2l-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/><path d="M15.5 7.5l3 3"/></svg>
|
||||
<input id="adm-epApiKey" type="password" placeholder="API key, e.g. sk-proj-AbCdEf…" autocomplete="off" style="flex:1;padding-left:28px;height:32px;box-sizing:border-box;">
|
||||
</div>
|
||||
<button class="admin-btn-add" id="adm-epAddBtn" style="height:32px;min-width:55px;text-align:center;display:inline-flex;align-items:center;justify-content:center;gap:4px;flex-shrink:0;box-sizing:border-box;">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>Add
|
||||
</button>
|
||||
</div>
|
||||
<div id="adm-epApiMsg" class="adm-ep-inline-msg"></div>
|
||||
<div id="adm-deviceAuthStatus" class="adm-ep-inline-msg"></div>
|
||||
<div id="adm-deviceAuthStatus" class="adm-ep-inline-msg" style="min-height:0;margin-top:0;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2141,24 +2200,8 @@
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">All external service connections in one place.</div>
|
||||
<div id="unified-integrations-list"></div>
|
||||
<div id="unified-intg-form" style="display:none"></div>
|
||||
<div style="text-align:center;padding:8px 0;">
|
||||
<button type="button" class="admin-btn-sm" id="unified-intg-add-btn" style="display:inline-flex;align-items:center;gap:6px;">+ Add Integration<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.7;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card admin-only" style="margin-top:12px;">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>API Tokens</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Bearer tokens for external integrations (scripts, Codex, headless agent runs). Token value shown ONCE on create — copy it then.</div>
|
||||
<div id="adm-tokenList" style="margin-bottom:8px;"></div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-start;">
|
||||
<input type="text" id="adm-tokenName" placeholder="Token name (e.g. agent-test)" class="settings-select" style="flex:1;min-width:160px;">
|
||||
<input type="text" id="adm-tokenScopes" placeholder="scopes (comma-separated, blank = chat)" class="settings-select" style="flex:2;min-width:220px;" title="Allowed: chat, cookbook:read, cookbook:launch, documents:read|write, todos:read|write, email:read|draft|send, calendar:read|write, memory:read|write">
|
||||
<button class="admin-btn-add" id="adm-tokenAddBtn">Create token</button>
|
||||
</div>
|
||||
<div id="adm-tokenMsg" style="font-size:11px;margin-top:6px;"></div>
|
||||
<div id="adm-tokenReveal" style="display:none;margin-top:8px;padding:8px 10px;background:color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);border:1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);border-radius:6px;">
|
||||
<div style="font-size:11px;font-weight:600;margin-bottom:4px;">Copy now — this is the only time you'll see it:</div>
|
||||
<code id="adm-tokenValue" style="font-family:'Berkeley Mono','SF Mono','Fira Code',monospace;font-size:11px;word-break:break-all;display:block;background:var(--bg);padding:6px 8px;border-radius:4px;margin-bottom:6px;user-select:all;"></code>
|
||||
<button class="admin-btn-sm" id="adm-tokenCopyBtn">Copy</button>
|
||||
<div style="text-align:right;padding:8px 0;">
|
||||
<button type="button" class="admin-btn-add" id="unified-intg-add-btn" style="text-align:center;display:inline-flex;align-items:center;justify-content:center;gap:5px;flex-shrink:0;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>Add Integration</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2180,10 +2223,6 @@
|
||||
<div id="set-agentMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card" style="margin-bottom:12px;">
|
||||
<h2 style="display:flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:1px;opacity:0.6;flex-shrink:0"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>Agent loop<span style="flex:1"></span><label class="admin-switch" title="On a failing effectful turn, climb verify → different-method → teacher → stop-and-summarize instead of silently quitting." style="flex-shrink:0"><input type="checkbox" id="set-agentSupervisorLadder"><span class="admin-slider"></span></label></h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Supervisor ladder. When on, every effectful agent turn that claims done is verified; on FAIL the ladder escalates verify → different method → teacher → stop-with-blocker, each rung visible in chat. Teacher rung requires <code>teacher_model</code> to be set.</div>
|
||||
</div>
|
||||
<div class="admin-card" style="margin-bottom:12px;">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>Built-in Tools</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Enable or disable tools available to the AI agent.</div>
|
||||
@@ -2210,68 +2249,76 @@
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all chats</div>
|
||||
<div class="admin-toggle-label">Delete all chats</div>
|
||||
<div class="admin-toggle-sub">Every session, message, and chat history. Documents/notes/etc. stay.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="chats" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="chats" title="Delete all chats" aria-label="Delete all chats" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all memory</div>
|
||||
<div class="admin-toggle-label">Delete all memory</div>
|
||||
<div class="admin-toggle-sub">Clears `memory.json`, the Memory table, and the vector store. Skills not affected.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="memory" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="memory" title="Delete all memory" aria-label="Delete all memory" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all skills</div>
|
||||
<div class="admin-toggle-label">Delete all skills</div>
|
||||
<div class="admin-toggle-sub">Drops `data/skills/` (all SKILL.md files). Memory not affected.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="skills" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="skills" title="Delete all skills" aria-label="Delete all skills" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all notes</div>
|
||||
<div class="admin-toggle-label">Delete all notes</div>
|
||||
<div class="admin-toggle-sub">Every note, todo, and checklist.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="notes" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="notes" title="Delete all notes" aria-label="Delete all notes" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all tasks</div>
|
||||
<div class="admin-toggle-label">Delete all tasks</div>
|
||||
<div class="admin-toggle-sub">Every scheduled task and its run history (Tasks tool).</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="tasks" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="tasks" title="Delete all tasks" aria-label="Delete all tasks" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all documents</div>
|
||||
<div class="admin-toggle-label">Delete all documents</div>
|
||||
<div class="admin-toggle-sub">Every document and version. Drafts, exports, library — all gone.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="documents" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="documents" title="Delete all documents" aria-label="Delete all documents" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all gallery</div>
|
||||
<div class="admin-toggle-label">Delete all gallery</div>
|
||||
<div class="admin-toggle-sub">Every image record and the upload directory on disk.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="gallery" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="gallery" title="Delete all gallery" aria-label="Delete all gallery" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Wipe all calendar</div>
|
||||
<div class="admin-toggle-label">Delete all calendar</div>
|
||||
<div class="admin-toggle-sub">Every event and every calendar (incl. CalDAV-synced ones; resync to restore).</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="calendar" style="white-space:nowrap;">Wipe</button>
|
||||
<button class="admin-btn-delete" data-wipe-kind="calendar" title="Delete all calendar" aria-label="Delete all calendar" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete</button>
|
||||
</div>
|
||||
|
||||
<hr style="border:0;border-top:1px solid color-mix(in srgb, #e55 25%, var(--border));margin:14px 0 10px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<div class="admin-toggle-label" style="color:#e55;">Delete everything</div>
|
||||
<div class="admin-toggle-sub">All eight categories above, in one go. Same effect as wiping each one in sequence.</div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-wipe-kind="__all__" title="Delete every category" aria-label="Delete everything" style="display:inline-flex;align-items:center;gap:5px;white-space:nowrap;font-weight:600;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete All</button>
|
||||
</div>
|
||||
<div id="adm-wipeMsg" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+323
-43
@@ -3,7 +3,7 @@
|
||||
|
||||
import uiModule from './ui.js';
|
||||
import settingsModule from './settings.js';
|
||||
import { providerLogo } from './providers.js';
|
||||
import { providerLogo, providerLogoFromUrl } from './providers.js';
|
||||
import { sortModelObjects } from './modelSort.js';
|
||||
import { PROVIDER_DEVICE_FLOWS, formatDeviceFlowError, runProviderDeviceFlow } from './providerDeviceFlow.js';
|
||||
|
||||
@@ -449,13 +449,14 @@ async function loadEndpoints() {
|
||||
return `
|
||||
<div class="admin-user-row${ep.is_enabled ? '' : ' admin-ep-disabled'}${justAddedClass}" data-adm-ep-id="${ep.id}">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;${hasModels ? 'cursor:pointer;' : ''}padding:4px 0;" data-adm-ep-header="${ep.id}">
|
||||
<div class="admin-user-info" style="flex:1;flex-wrap:wrap;gap:0.3rem;">
|
||||
<div class="admin-user-info" style="flex:1;flex-wrap:wrap;gap:0.3rem;align-items:center;">
|
||||
<span class="adm-ep-row-logo" style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex-shrink:0;opacity:0.9;">${providerLogoFromUrl(ep.base_url) || ''}</span>
|
||||
<span class="admin-user-name">${esc(ep.name)}</span>
|
||||
${ep.model_type === 'image' ? '<span class="admin-badge" style="background:color-mix(in srgb, var(--accent) 20%, transparent);color:var(--accent);">Image</span>' : ''}
|
||||
${kindLabel ? `<span class="admin-badge">${esc(kindLabel)}</span>` : ''}
|
||||
${statusBadge}
|
||||
${ep.is_enabled ? '' : '<span class="admin-badge admin-badge-off">disabled</span>'}
|
||||
${hasModels ? '<span style="font-size:10px;opacity:0.4;">Click to manage models</span>' : ''}
|
||||
${hasModels ? `<span style="font-size:10px;opacity:0.4;${category === 'api' ? 'flex-basis:100%;' : ''}">Click to manage models</span>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;align-items:center;">
|
||||
<button class="admin-btn-sm" data-adm-toggle-ep="${ep.id}">${ep.is_enabled ? 'Disable' : 'Enable'}</button>
|
||||
@@ -828,6 +829,14 @@ function initEndpointForm() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!picker.contains(e.target)) pickerMenu.classList.add('hidden');
|
||||
});
|
||||
// Capture-phase Esc: dismiss the picker menu without bubbling to the
|
||||
// settings-modal handler that would otherwise close the whole modal.
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (pickerMenu.classList.contains('hidden')) return;
|
||||
e.stopPropagation();
|
||||
pickerMenu.classList.add('hidden');
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
provider.addEventListener('change', () => {
|
||||
@@ -1022,14 +1031,15 @@ function initEndpointForm() {
|
||||
if (d.id) _recentlyAddedEpId = String(d.id);
|
||||
await loadEndpoints();
|
||||
await _selectAddedModelInChat(d);
|
||||
const goLink = ' <a href="#" data-go-added-models style="margin-left:6px;text-decoration:underline;color:inherit;font-weight:600;">Added Models →</a>';
|
||||
if (!d.online) {
|
||||
msg.textContent = 'Added (endpoint offline — will retry on next load)';
|
||||
msg.innerHTML = 'Added (endpoint offline — will retry on next load)' + goLink;
|
||||
msg.className = 'admin-error';
|
||||
} else if (d.status === 'empty') {
|
||||
msg.textContent = 'Added — endpoint reachable, no models found';
|
||||
msg.innerHTML = 'Added — endpoint reachable, no models found' + goLink;
|
||||
msg.className = 'admin-success';
|
||||
} else {
|
||||
msg.textContent = `Added — found ${count} model${count !== 1 ? 's' : ''}`;
|
||||
msg.innerHTML = `Added — found ${count} model${count !== 1 ? 's' : ''}` + goLink;
|
||||
msg.className = 'admin-success';
|
||||
}
|
||||
} else { msg.textContent = d.detail || 'Failed'; msg.className = 'admin-error'; }
|
||||
@@ -1169,6 +1179,125 @@ function initEndpointForm() {
|
||||
};
|
||||
_wireKeyToggle('adm-epLocalKeyBtn', 'adm-epLocalApiKey-row');
|
||||
|
||||
// Delegated link handler for jumping between settings tabs.
|
||||
// [data-go-added-models] → quick shortcut for the Added Models tab
|
||||
// [data-go-settings-tab="X"] → any tab whose nav button has data-settings-tab="X"
|
||||
// [data-go-scroll-to="#elementId"] → after switching, scroll the element into view
|
||||
document.addEventListener('click', (e) => {
|
||||
const explicit = e.target.closest('[data-go-settings-tab]');
|
||||
if (explicit) {
|
||||
e.preventDefault();
|
||||
const tab = explicit.getAttribute('data-go-settings-tab');
|
||||
const scrollTo = explicit.getAttribute('data-go-scroll-to');
|
||||
const btn = document.querySelector(`[data-settings-tab="${tab}"]`);
|
||||
if (btn) btn.click();
|
||||
if (scrollTo) {
|
||||
// Defer to the next frame so the panel has actually become visible
|
||||
// before we try to scroll into it.
|
||||
requestAnimationFrame(() => {
|
||||
const target = document.querySelector(scrollTo);
|
||||
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const link = e.target.closest('[data-go-added-models]');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
const btn = document.querySelector('[data-settings-tab="added-models"]');
|
||||
if (btn) btn.click();
|
||||
});
|
||||
|
||||
// Generic open/close helper for the kebab dropdowns in this card.
|
||||
// Both the Local and API cards use the same shape: an h2-anchored button
|
||||
// with id "<prefix>MoreBtn" toggles a sibling menu with id "<prefix>MoreMenu".
|
||||
// Global Esc handler: close any currently-open kebab menu in the admin
|
||||
// panel regardless of which _wireKebab instance owns it. Belt-and-braces
|
||||
// backup for the per-instance handler below — registered once.
|
||||
if (!document._admKebabEscWired) {
|
||||
document._admKebabEscWired = true;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
// Any visible kebab dropdown in the admin panel — match by id pattern
|
||||
// so adding a new kebab elsewhere automatically benefits.
|
||||
const menus = document.querySelectorAll(
|
||||
'#adm-epLocalMoreMenu, #adm-epApiMoreMenu'
|
||||
);
|
||||
let closed = false;
|
||||
menus.forEach((m) => {
|
||||
if (m && m.style.display !== 'none') {
|
||||
m.style.display = 'none';
|
||||
// Sync the associated button's aria-expanded when we can find it.
|
||||
const btn = document.getElementById(m.id.replace('Menu', 'Btn'));
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
if (closed) e.stopPropagation();
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
const _wireKebab = (btnId, menuId, onItem) => {
|
||||
const btn = el(btnId);
|
||||
const menu = el(menuId);
|
||||
if (!btn || !menu) return;
|
||||
const isOpen = () => menu.style.display !== 'none';
|
||||
const close = () => { menu.style.display = 'none'; btn.setAttribute('aria-expanded', 'false'); };
|
||||
const open = () => { menu.style.display = 'flex'; btn.setAttribute('aria-expanded', 'true'); };
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (isOpen()) close(); else open();
|
||||
});
|
||||
menu.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.adm-more-item');
|
||||
if (!item) return;
|
||||
if (onItem) onItem(item, e);
|
||||
close();
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!isOpen()) return;
|
||||
if (e.target.closest('#' + menuId + ', #' + btnId)) return;
|
||||
close();
|
||||
});
|
||||
// Use capture phase so this fires before the settings-modal Esc handler
|
||||
// (which is in bubble phase). stopPropagation prevents the modal from
|
||||
// closing when the user only meant to dismiss this menu.
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && isOpen()) {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
}
|
||||
}, { capture: true });
|
||||
};
|
||||
|
||||
// API card "..." menu: contains the Proxy/API connection-mode toggle.
|
||||
// Sync the visible checkmarks with the hidden #adm-epKind select so
|
||||
// downstream code (which reads kindSel.value) keeps working.
|
||||
(function wireApiKindMenu() {
|
||||
const kind = el('adm-epKind');
|
||||
if (!kind) return;
|
||||
const opts = document.querySelectorAll('#adm-epApiMoreMenu .adm-kind-opt');
|
||||
const sync = () => {
|
||||
opts.forEach((o) => {
|
||||
const check = o.querySelector('.adm-kind-check');
|
||||
if (check) check.style.visibility = (o.dataset.kind === kind.value) ? 'visible' : 'hidden';
|
||||
});
|
||||
};
|
||||
sync();
|
||||
kind.addEventListener('change', sync);
|
||||
_wireKebab('adm-epApiMoreBtn', 'adm-epApiMoreMenu', (item) => {
|
||||
const k = item.dataset.kind;
|
||||
if (!k) return;
|
||||
kind.value = k;
|
||||
kind.dispatchEvent(new Event('change'));
|
||||
});
|
||||
})();
|
||||
|
||||
// Local card "..." kebab: holds Scan network / Ollama / API key reveal.
|
||||
// Item buttons keep their own click handlers; the helper just handles
|
||||
// open/close + outside-click + Esc.
|
||||
_wireKebab('adm-epLocalMoreBtn', 'adm-epLocalMoreMenu');
|
||||
|
||||
// ── Added Models toolbar: Probe + Clear offline ────────────────────
|
||||
// Both buttons act over the currently-rendered endpoint list. The
|
||||
// online/offline marker is stamped on each row's [data-adm-ep-online]
|
||||
@@ -1179,10 +1308,10 @@ function initEndpointForm() {
|
||||
if (!lbl) return;
|
||||
const n = document.querySelectorAll('[data-adm-ep-id] [data-adm-ep-online="0"]').length;
|
||||
lbl.textContent = n > 0 ? `(${n})` : '';
|
||||
// Keep the button enabled even when there are no offline rows — a
|
||||
// click on the empty case fires a toast instead of feeling dead.
|
||||
// Hide the button entirely when there's nothing offline — no point
|
||||
// showing an action that has nothing to act on.
|
||||
const btn = el('adm-epClearOfflineBtn');
|
||||
if (btn) btn.style.opacity = n === 0 ? '0.55' : '0.85';
|
||||
if (btn) btn.style.display = n === 0 ? 'none' : '';
|
||||
};
|
||||
// Wire after every loadEndpoints() run by patching the render hook —
|
||||
// simplest path: MutationObserver on the two list containers.
|
||||
@@ -1199,7 +1328,17 @@ function initEndpointForm() {
|
||||
probeAllBtn.addEventListener('click', async () => {
|
||||
probeAllBtn.disabled = true;
|
||||
const origHTML = probeAllBtn.innerHTML;
|
||||
probeAllBtn.innerHTML = '<span style="opacity:0.7;">Probing…</span>';
|
||||
let _wp = null;
|
||||
try {
|
||||
const sp = window.spinnerModule || (await import('./spinner.js')).default;
|
||||
_wp = sp.createWhirlpool(11);
|
||||
_wp.element.style.cssText = 'display:inline-flex;width:11px;height:11px;margin:0 4px 0 0;';
|
||||
probeAllBtn.innerHTML = '';
|
||||
probeAllBtn.appendChild(_wp.element);
|
||||
probeAllBtn.appendChild(document.createTextNode('Probing'));
|
||||
} catch (_) {
|
||||
probeAllBtn.innerHTML = '<span style="opacity:0.7;">Probing…</span>';
|
||||
}
|
||||
try {
|
||||
// Hit the bulk local probe (same one the model picker uses).
|
||||
await fetch('/api/model-endpoints/probe-local', { credentials: 'same-origin' }).catch(() => {});
|
||||
@@ -1221,6 +1360,7 @@ function initEndpointForm() {
|
||||
await loadEndpoints();
|
||||
if (uiModule && uiModule.showToast) uiModule.showToast('Endpoint status refreshed', 1800);
|
||||
} finally {
|
||||
if (_wp) { try { _wp.destroy(); } catch (_) {} }
|
||||
probeAllBtn.innerHTML = origHTML;
|
||||
probeAllBtn.disabled = false;
|
||||
}
|
||||
@@ -1290,16 +1430,17 @@ function initEndpointForm() {
|
||||
const localAddBtn = el('adm-epLocalAddBtn');
|
||||
const localTestBtn = el('adm-epLocalTestBtn');
|
||||
if (localTestBtn) {
|
||||
const testOriginalHtml = localTestBtn.innerHTML;
|
||||
localTestBtn.addEventListener('click', async () => {
|
||||
const msg = _endpointMsg('local');
|
||||
msg.textContent = ''; msg.className = '';
|
||||
msg.textContent = ''; msg.className = 'adm-ep-inline-msg';
|
||||
const raw = (el('adm-epLocalUrl').value || '').trim();
|
||||
if (!raw) { msg.textContent = 'Enter a base URL to test'; msg.className = 'admin-error'; return; }
|
||||
const url = _normalizeBaseUrl(raw);
|
||||
const keyEl = el('adm-epLocalApiKey');
|
||||
const apiKey = keyEl ? keyEl.value.trim() : '';
|
||||
localTestBtn.disabled = true;
|
||||
localTestBtn.textContent = 'Testing...';
|
||||
localTestBtn.innerHTML = testOriginalHtml.replace(/>Test\s*$/, '>Testing...');
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('base_url', url);
|
||||
@@ -1312,19 +1453,21 @@ function initEndpointForm() {
|
||||
msg.className = 'admin-error';
|
||||
}
|
||||
localTestBtn.disabled = false;
|
||||
localTestBtn.textContent = 'Test';
|
||||
localTestBtn.innerHTML = testOriginalHtml;
|
||||
});
|
||||
}
|
||||
if (localAddBtn) {
|
||||
const addOriginalHtml = localAddBtn.innerHTML;
|
||||
localAddBtn.addEventListener('click', async () => {
|
||||
const msg = _endpointMsg('local');
|
||||
msg.textContent = ''; msg.className = '';
|
||||
msg.textContent = ''; msg.className = 'adm-ep-inline-msg';
|
||||
const raw = (el('adm-epLocalUrl').value || '').trim();
|
||||
if (!raw) { msg.textContent = 'Enter a base URL (e.g. http://localhost:8002/v1)'; msg.className = 'admin-error'; return; }
|
||||
const url = _normalizeBaseUrl(raw);
|
||||
const keyEl = el('adm-epLocalApiKey');
|
||||
const apiKey = keyEl ? keyEl.value.trim() : '';
|
||||
localAddBtn.disabled = true; localAddBtn.textContent = 'Adding...';
|
||||
localAddBtn.disabled = true;
|
||||
localAddBtn.innerHTML = addOriginalHtml.replace(/>Add\s*$/, '>Adding...');
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('base_url', url);
|
||||
@@ -1344,15 +1487,17 @@ function initEndpointForm() {
|
||||
await loadEndpoints();
|
||||
await _selectAddedModelInChat(d);
|
||||
const count = (d.models || []).length;
|
||||
msg.textContent = d.status === 'empty'
|
||||
const baseText = d.status === 'empty'
|
||||
? 'Added — Ollama is running, no models pulled yet'
|
||||
: d.online
|
||||
? `Added — found ${count} model${count !== 1 ? 's' : ''}`
|
||||
: 'Added (offline — will retry on next load)';
|
||||
msg.innerHTML = `${baseText} <a href="#" data-go-added-models style="margin-left:6px;text-decoration:underline;color:inherit;font-weight:600;">Added Models →</a>`;
|
||||
msg.className = d.online ? 'admin-success' : 'admin-error';
|
||||
} else { msg.textContent = d.detail || 'Failed'; msg.className = 'admin-error'; }
|
||||
} catch (e) { msg.textContent = 'Request failed'; msg.className = 'admin-error'; }
|
||||
localAddBtn.disabled = false; localAddBtn.textContent = 'Add';
|
||||
localAddBtn.disabled = false;
|
||||
localAddBtn.innerHTML = addOriginalHtml;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1378,10 +1523,7 @@ function initEndpointForm() {
|
||||
discoverBtn.addEventListener('click', async () => {
|
||||
const msg = _endpointMsg('local');
|
||||
discoverBtn.disabled = true;
|
||||
// Keep the button's icon as-is while scanning; the whirlpool +
|
||||
// status text below is enough feedback. (Two spinning indicators
|
||||
// at once looks busy.)
|
||||
msg.className = '';
|
||||
msg.className = 'adm-ep-inline-msg';
|
||||
msg.innerHTML = '';
|
||||
try {
|
||||
const sp = window.spinnerModule || (await import('./spinner.js')).default;
|
||||
@@ -1392,7 +1534,7 @@ function initEndpointForm() {
|
||||
wrap.appendChild(wp.element);
|
||||
const txt = document.createElement('span');
|
||||
txt.textContent = 'Scanning ports 8000-8020 and 11434 for model servers...';
|
||||
txt.style.cssText = 'font-size:12px;opacity:0.7;';
|
||||
txt.style.cssText = 'opacity:0.7;';
|
||||
wrap.appendChild(txt);
|
||||
msg.appendChild(wrap);
|
||||
discoverBtn._wp = wp;
|
||||
@@ -2158,28 +2300,126 @@ function initRag() {
|
||||
/* ═══════════════════════════════════════════
|
||||
SYSTEM TAB — Tokens
|
||||
═══════════════════════════════════════════ */
|
||||
// Catalog mirrors the one in settings.js integration form. Keep keys in
|
||||
// sync with the backend scope allowlist.
|
||||
const _TOKEN_SCOPES = [
|
||||
{ key: 'todos:read', label: 'Todos read', detail: 'Read notes and checklists' },
|
||||
{ key: 'todos:write', label: 'Todos write', detail: 'Create, update, delete, and toggle todo items' },
|
||||
{ key: 'documents:read', label: 'Documents read', detail: 'Read documents when a document API is enabled' },
|
||||
{ key: 'documents:write', label: 'Documents write', detail: 'Create and update draft documents' },
|
||||
{ key: 'email:read', label: 'Email read', detail: 'Read email when an email API is enabled' },
|
||||
{ key: 'email:draft', label: 'Email draft', detail: 'Create email reply drafts without sending' },
|
||||
{ key: 'email:send', label: 'Email send', detail: 'Send email directly' },
|
||||
{ key: 'calendar:read', label: 'Calendar read', detail: 'Read calendar events when enabled' },
|
||||
{ key: 'calendar:write', label: 'Calendar write', detail: 'Create and update calendar events' },
|
||||
{ key: 'memory:read', label: 'Memory read', detail: 'Read memory when enabled' },
|
||||
{ key: 'memory:write', label: 'Memory write', detail: 'Write memory when enabled' },
|
||||
{ key: 'cookbook:read', label: 'Cookbook read', detail: 'List cookbook tasks + tail their tmux output' },
|
||||
{ key: 'cookbook:launch', label: 'Cookbook launch', detail: 'Launch and stop cookbook serve tasks' },
|
||||
];
|
||||
|
||||
function _renderTokenScopeRows(t) {
|
||||
const have = new Set(t.scopes || []);
|
||||
return _TOKEN_SCOPES.map(s => {
|
||||
const action = (s.key.split(':')[1] || '').toLowerCase();
|
||||
const pill = action === 'read'
|
||||
? 'background:rgba(150,150,150,0.18);color:var(--fg-muted,#888);'
|
||||
: 'background:color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);color:var(--accent, var(--red));';
|
||||
const tool = s.label.replace(/\s+(read|write|draft|send|launch)$/i, '');
|
||||
return `
|
||||
<label style="display:flex;align-items:center;gap:8px;min-height:28px;padding:1px 0;">
|
||||
<span class="settings-label" style="width:90px;flex-shrink:0;padding:0;font-size:12px;">${esc(tool)}</span>
|
||||
<span style="font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;padding:1px 7px;border-radius:999px;flex-shrink:0;min-width:44px;text-align:center;box-sizing:border-box;${pill}">${esc(action)}</span>
|
||||
<span style="font-size:11px;line-height:1.35;opacity:0.62;flex:1;min-width:0;">${esc(s.detail)}</span>
|
||||
<label class="admin-switch" style="margin-left:auto;flex-shrink:0;"><input type="checkbox" class="adm-tok-scope" data-token-id="${esc(t.id)}" data-scope="${esc(s.key)}" ${have.has(s.key) ? 'checked' : ''}><span class="admin-slider"></span></label>
|
||||
</label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadTokens() {
|
||||
const list = el('adm-tokenList');
|
||||
if (!list) return;
|
||||
try {
|
||||
const res = await fetch('/api/tokens', { credentials: 'same-origin' });
|
||||
const tokens = await res.json();
|
||||
if (!tokens.length) { list.innerHTML = '<div class="admin-empty">No API tokens</div>'; return; }
|
||||
if (!tokens.length) { list.innerHTML = '<div class="admin-empty" style="color:var(--accent, var(--red));opacity:0.7;font-size:10px;">No API tokens</div>'; return; }
|
||||
list.innerHTML = tokens.map(t => `
|
||||
<div class="admin-user-row">
|
||||
<div class="admin-user-info" style="flex:1;flex-wrap:wrap;gap:0.3rem;">
|
||||
<span class="admin-user-name">${esc(t.name)}</span>
|
||||
<span class="admin-badge">${esc(t.token_prefix)}...</span>
|
||||
<span class="admin-badge" title="Allowed API scopes">${esc((t.scopes || ['chat']).join(', '))}</span>
|
||||
${t.owner ? `<span style="font-size:0.75rem;opacity:0.5;">Owner: ${esc(t.owner)}</span>` : ''}
|
||||
${t.last_used_at ? `<span style="font-size:0.75rem;opacity:0.5;">Last used: ${new Date(t.last_used_at).toLocaleDateString()}</span>` : '<span style="font-size:0.75rem;opacity:0.4;">Never used</span>'}
|
||||
<div class="admin-user-row" data-adm-tok-row="${esc(t.id)}" style="display:block;">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
<div class="admin-user-info" style="flex:1;min-width:0;flex-wrap:wrap;gap:0.3rem;">
|
||||
<input type="text" class="adm-tok-rename" data-token-id="${esc(t.id)}" value="${esc(t.name || '')}" placeholder="Token name" style="font-size:13px;font-weight:600;padding:3px 6px;background:transparent;border:1px solid transparent;border-radius:4px;min-width:160px;" title="Click to rename">
|
||||
<span class="admin-badge">${esc(t.token_prefix)}...</span>
|
||||
${t.owner ? `<span style="font-size:0.75rem;opacity:0.5;">Owner: ${esc(t.owner)}</span>` : ''}
|
||||
${t.last_used_at ? `<span style="font-size:0.75rem;opacity:0.5;">Last used: ${new Date(t.last_used_at).toLocaleDateString()}</span>` : '<span style="font-size:0.75rem;opacity:0.4;">Never used</span>'}
|
||||
</div>
|
||||
<button class="admin-btn-sm" data-adm-tok-toggle="${esc(t.id)}" style="opacity:0.75;">Permissions</button>
|
||||
<button class="admin-btn-delete" data-adm-del-token="${esc(t.id)}">Revoke</button>
|
||||
</div>
|
||||
<div data-adm-tok-perm="${esc(t.id)}" style="display:none;margin-top:8px;padding:8px 4px 0;border-top:1px solid var(--border);">
|
||||
${_renderTokenScopeRows(t)}
|
||||
<div class="adm-tok-scope-msg" data-token-id="${esc(t.id)}" style="font-size:11px;min-height:14px;margin-top:4px;"></div>
|
||||
</div>
|
||||
<button class="admin-btn-delete" data-adm-del-token="${t.id}">Revoke</button>
|
||||
</div>`).join('');
|
||||
|
||||
// Revoke
|
||||
list.querySelectorAll('[data-adm-del-token]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!await uiModule.styledConfirm('Revoke this API token? External integrations using it will stop working.', { confirmText: 'Revoke', danger: true })) return;
|
||||
await fetch(`/api/tokens/${btn.dataset.admDelToken}`, { method: 'DELETE', credentials: 'same-origin' });
|
||||
loadTokens();
|
||||
// Codex / Claude integration cards on the Integrations panel are
|
||||
// backed by these tokens — let them re-render so the deleted token
|
||||
// disappears there too.
|
||||
try { window.dispatchEvent(new CustomEvent('odysseus-integrations-changed')); } catch (_) {}
|
||||
});
|
||||
});
|
||||
// Toggle permissions panel
|
||||
list.querySelectorAll('[data-adm-tok-toggle]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const panel = list.querySelector(`[data-adm-tok-perm="${btn.dataset.admTokToggle}"]`);
|
||||
if (!panel) return;
|
||||
panel.style.display = panel.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
});
|
||||
// Rename
|
||||
list.querySelectorAll('.adm-tok-rename').forEach(input => {
|
||||
const original = input.value;
|
||||
const commit = async () => {
|
||||
const name = (input.value || '').trim();
|
||||
if (!name || name === original) return;
|
||||
try {
|
||||
const r = await fetch(`/api/tokens/${input.dataset.tokenId}`, {
|
||||
method: 'PATCH', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!r.ok) throw new Error('Save failed');
|
||||
loadTokens();
|
||||
} catch (_) { input.value = original; }
|
||||
};
|
||||
input.addEventListener('blur', commit);
|
||||
input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); input.blur(); } });
|
||||
});
|
||||
// Scope toggle change → PATCH the whole scopes array for this token.
|
||||
list.querySelectorAll('.adm-tok-scope').forEach(cb => {
|
||||
cb.addEventListener('change', async () => {
|
||||
const tokenId = cb.dataset.tokenId;
|
||||
const panel = list.querySelector(`[data-adm-tok-perm="${tokenId}"]`);
|
||||
const msg = list.querySelector(`.adm-tok-scope-msg[data-token-id="${tokenId}"]`);
|
||||
const scopes = Array.from(panel.querySelectorAll('.adm-tok-scope:checked')).map(input => input.dataset.scope);
|
||||
try {
|
||||
const r = await fetch(`/api/tokens/${tokenId}`, {
|
||||
method: 'PATCH', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scopes }),
|
||||
});
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(d.detail || 'Failed');
|
||||
if (msg) { msg.textContent = 'Saved'; msg.style.color = 'var(--green, #50fa7b)'; setTimeout(() => { msg.textContent = ''; }, 1200); }
|
||||
} catch (err) {
|
||||
cb.checked = !cb.checked;
|
||||
if (msg) { msg.textContent = (err && err.message) || 'Failed'; msg.style.color = 'var(--red)'; }
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) { list.innerHTML = '<div class="admin-error">Failed to load tokens</div>'; }
|
||||
@@ -2211,11 +2451,20 @@ function initTokenForm() {
|
||||
else { msg.textContent = data.detail || 'Failed'; msg.className = 'admin-error'; }
|
||||
} catch (e) { msg.textContent = 'Request failed'; msg.className = 'admin-error'; }
|
||||
});
|
||||
const TOKEN_COPY_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
const TOKEN_CHECK_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
el('adm-tokenCopyBtn').addEventListener('click', () => {
|
||||
const val = el('adm-tokenValue').textContent;
|
||||
const btn = el('adm-tokenCopyBtn');
|
||||
navigator.clipboard.writeText(val).then(() => {
|
||||
el('adm-tokenCopyBtn').textContent = 'Copied!';
|
||||
setTimeout(() => { el('adm-tokenCopyBtn').textContent = 'Copy'; }, 2000);
|
||||
btn.innerHTML = TOKEN_CHECK_ICON;
|
||||
btn.style.color = 'var(--accent, var(--red))';
|
||||
btn.style.opacity = '1';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = TOKEN_COPY_ICON;
|
||||
btn.style.color = '';
|
||||
btn.style.opacity = '0.7';
|
||||
}, 1600);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2442,23 +2691,54 @@ function initDangerZone() {
|
||||
modalEl.querySelectorAll('[data-wipe-kind]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const kind = btn.dataset.wipeKind;
|
||||
const label = _LABELS[kind] || kind;
|
||||
if (!await uiModule.styledConfirm(`Wipe ALL ${label}? This cannot be undone.`, { confirmText: 'Wipe', danger: true })) return;
|
||||
if (!await uiModule.styledConfirm(`Really wipe every one of your ${label}?`, { confirmText: 'Yes, wipe everything', danger: true })) return;
|
||||
btn.disabled = true; const prev = btn.textContent; btn.textContent = 'Wiping…';
|
||||
const isAll = kind === '__all__';
|
||||
const label = isAll ? 'data across every category' : (_LABELS[kind] || kind);
|
||||
if (!await uiModule.styledConfirm(`Delete ALL ${label}? This cannot be undone.`, { confirmText: 'Delete', danger: true })) return;
|
||||
if (!await uiModule.styledConfirm(`Really delete every one of your ${label}?`, { confirmText: isAll ? 'Yes, delete everything' : 'Yes, delete everything', danger: true })) return;
|
||||
btn.disabled = true;
|
||||
const prevHtml = btn.innerHTML;
|
||||
btn.innerHTML = isAll ? 'Deleting all…' : 'Deleting…';
|
||||
if (_wipeMsg) { _wipeMsg.textContent = ''; _wipeMsg.className = ''; }
|
||||
try {
|
||||
const res = await fetch(`/api/admin/wipe/${kind}`, { method: 'DELETE', credentials: 'same-origin' });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
if (_wipeMsg) { _wipeMsg.textContent = `Wiped ${data.count ?? 0} ${label}.`; _wipeMsg.className = 'admin-success'; }
|
||||
if (isAll) {
|
||||
// Iterate every known category. Failures in one shouldn't stop
|
||||
// the rest — record per-category counts and surface a summary.
|
||||
const kinds = Object.keys(_LABELS);
|
||||
const results = [];
|
||||
for (const k of kinds) {
|
||||
try {
|
||||
const r = await fetch(`/api/admin/wipe/${k}`, { method: 'DELETE', credentials: 'same-origin' });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
results.push({ k, ok: r.ok, count: d.count ?? 0, error: r.ok ? null : (d.detail || 'failed') });
|
||||
} catch (e) {
|
||||
results.push({ k, ok: false, count: 0, error: e.message });
|
||||
}
|
||||
}
|
||||
const okCount = results.filter(r => r.ok).length;
|
||||
const total = results.reduce((n, r) => n + (r.ok ? r.count : 0), 0);
|
||||
const fails = results.filter(r => !r.ok).map(r => r.k);
|
||||
if (_wipeMsg) {
|
||||
if (!fails.length) {
|
||||
_wipeMsg.textContent = `Deleted ${total} items across all ${okCount} categories.`;
|
||||
_wipeMsg.className = 'admin-success';
|
||||
} else {
|
||||
_wipeMsg.textContent = `Deleted ${total} items; failed: ${fails.join(', ')}.`;
|
||||
_wipeMsg.className = 'admin-error';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_wipeMsg) { _wipeMsg.textContent = data.detail || 'Failed'; _wipeMsg.className = 'admin-error'; }
|
||||
const res = await fetch(`/api/admin/wipe/${kind}`, { method: 'DELETE', credentials: 'same-origin' });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
if (_wipeMsg) { _wipeMsg.textContent = `Deleted ${data.count ?? 0} ${label}.`; _wipeMsg.className = 'admin-success'; }
|
||||
} else {
|
||||
if (_wipeMsg) { _wipeMsg.textContent = data.detail || 'Failed'; _wipeMsg.className = 'admin-error'; }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (_wipeMsg) { _wipeMsg.textContent = 'Request failed: ' + e.message; _wipeMsg.className = 'admin-error'; }
|
||||
}
|
||||
btn.disabled = false; btn.textContent = prev;
|
||||
btn.disabled = false; btn.innerHTML = prevHtml;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
_tryFoldHintSig, _foldSignature, _SIG_ICON, _QUOTE_ICON,
|
||||
} from './emailLibrary/signatureFold.js';
|
||||
import { state } from './emailLibrary/state.js';
|
||||
import { collapseSidebarToRail } from './modalSnap.js';
|
||||
|
||||
const API_BASE = window.location.origin;
|
||||
let _emailUnreadChipClickWired = false;
|
||||
@@ -406,7 +407,14 @@ function _clearEmailDocumentSplit() {
|
||||
].forEach(prop => docPane.style.removeProperty(prop));
|
||||
}
|
||||
|
||||
function _hasDesktopRoomForEmailAndDocument(modal) {
|
||||
// Compute the left-edge x assuming the wide sidebar has collapsed to the
|
||||
// rail. Used by the "try collapsing the sidebar first" path so we can decide
|
||||
// whether collapsing recovers enough room before minimizing email.
|
||||
function _emailSplitLeftEdgeIfSidebarCollapsed() {
|
||||
return _readCssPx('--icon-rail-w');
|
||||
}
|
||||
|
||||
function _hasDesktopRoomForEmailAndDocument(modal, opts = {}) {
|
||||
if (window.innerWidth <= 768) return false;
|
||||
if (window.innerWidth >= 1100) return true;
|
||||
const content = modal?.querySelector?.('.modal-content');
|
||||
@@ -416,9 +424,12 @@ function _hasDesktopRoomForEmailAndDocument(modal) {
|
||||
const emailWidth = isFullscreen
|
||||
? Math.min(440, Math.max(360, Math.round(window.innerWidth * 0.30)))
|
||||
: Math.max(360, Math.round(rect?.width || 440));
|
||||
const docMinWidth = 560;
|
||||
const breathingRoom = 72;
|
||||
const leftEdge = isFullscreen ? _emailSplitLeftEdge() : Math.max(0, Math.round(rect?.left || _emailSplitLeftEdge()));
|
||||
// Relaxed thresholds — the old 560 + 72 forced an unnecessary tab-down
|
||||
// on ~1200–1300px viewports where there was visually plenty of room.
|
||||
const docMinWidth = 460;
|
||||
const breathingRoom = 40;
|
||||
const leftEdgeNow = isFullscreen ? _emailSplitLeftEdge() : Math.max(0, Math.round(rect?.left || _emailSplitLeftEdge()));
|
||||
const leftEdge = opts.assumeSidebarCollapsed ? _emailSplitLeftEdgeIfSidebarCollapsed() : leftEdgeNow;
|
||||
return (window.innerWidth - leftEdge - emailWidth) >= (docMinWidth + breathingRoom);
|
||||
}
|
||||
|
||||
@@ -426,8 +437,18 @@ function _prepareEmailWindowForDocument(modal) {
|
||||
if (window.innerWidth <= 768) return true;
|
||||
if (!modal) return false;
|
||||
if (!_hasDesktopRoomForEmailAndDocument(modal)) {
|
||||
_clearEmailDocumentSplit();
|
||||
return true;
|
||||
// Before giving up and minimizing email, see if collapsing the wide
|
||||
// sidebar to the rail would recover enough space. The route-collapse
|
||||
// marker that collapseSidebarToRail() sets makes the existing
|
||||
// auto-restore logic put the sidebar back when the doc closes.
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarWasOpen = sidebar && !sidebar.classList.contains('hidden');
|
||||
if (sidebarWasOpen && _hasDesktopRoomForEmailAndDocument(modal, { assumeSidebarCollapsed: true })) {
|
||||
try { collapseSidebarToRail(); } catch (_) {}
|
||||
} else {
|
||||
_clearEmailDocumentSplit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (modal.classList.contains('modal-left-docked')) {
|
||||
const content = modal.querySelector('.modal-content');
|
||||
|
||||
@@ -302,6 +302,7 @@ function _anchorLeftDock(content) {
|
||||
}
|
||||
}
|
||||
|
||||
export function collapseSidebarToRail() { return _collapseSidebarToRail(); }
|
||||
function _collapseSidebarToRail() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rail = document.getElementById('icon-rail');
|
||||
|
||||
+28
-1
@@ -146,4 +146,31 @@ export function providerLabel(endpointUrl) {
|
||||
return host.replace(/^api\./i, "");
|
||||
}
|
||||
|
||||
export default { providerLogo, providerLabel };
|
||||
// Map endpoint URL → logo SVG using the same model-id regex catalog.
|
||||
// Tests host + port + path so loopback servers (e.g. Ollama on
|
||||
// localhost:11434) still match by port. Falls back to null when nothing
|
||||
// recognises the URL, so callers can render a neutral placeholder.
|
||||
export function providerLogoFromUrl(url) {
|
||||
if (!url) return null;
|
||||
let host = '', port = '', path = '';
|
||||
try {
|
||||
const u = new URL(url);
|
||||
host = u.hostname; port = u.port; path = u.pathname || '';
|
||||
} catch (_) {
|
||||
const raw = String(url).replace(/^[a-z]+:\/\//i, '');
|
||||
const slashIdx = raw.indexOf('/');
|
||||
const hostport = slashIdx >= 0 ? raw.slice(0, slashIdx) : raw;
|
||||
path = slashIdx >= 0 ? raw.slice(slashIdx) : '';
|
||||
const colon = hostport.lastIndexOf(':');
|
||||
host = colon >= 0 ? hostport.slice(0, colon) : hostport;
|
||||
port = colon >= 0 ? hostport.slice(colon + 1) : '';
|
||||
}
|
||||
// Build candidate strings to test against the provider catalog.
|
||||
const candidates = [host, port ? `${host}:${port}` : '', port ? `:${port}` : '', path].filter(Boolean);
|
||||
for (const [re, svg] of _PROVIDERS) {
|
||||
if (candidates.some(c => re.test(c))) return svg;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default { providerLogo, providerLabel, providerLogoFromUrl };
|
||||
|
||||
+590
-295
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -2482,12 +2482,15 @@ function _renderMainView() {
|
||||
|
||||
// ---- Modal ----
|
||||
|
||||
export function openTasks(focusId) {
|
||||
export function openTasks(focusId, opts) {
|
||||
const o = opts || {};
|
||||
if (_open) {
|
||||
// Already open — just focus the requested task.
|
||||
// Already open — just focus the requested task / apply filter.
|
||||
if (o.filter !== undefined) { _taskFilter = o.filter; _renderList(); }
|
||||
if (focusId) _focusTask(focusId);
|
||||
return;
|
||||
}
|
||||
if (o.filter !== undefined) _taskFilter = o.filter;
|
||||
_pendingFocusTaskId = focusId || null;
|
||||
_open = true;
|
||||
_tasksCascadeNext = true;
|
||||
|
||||
+41
-22
@@ -1239,23 +1239,27 @@ body.bg-pattern-sparkles {
|
||||
.section-header-btn.active { opacity: 0.9; color: var(--accent); }
|
||||
.section-header-btn svg { width: 12px; height: 12px; }
|
||||
|
||||
/* Chats library — grid icon, hover-reveal so the header only toggles collapse */
|
||||
/* Chats library — grid icon, hover-reveal so the header only toggles
|
||||
collapse. Uses !important to win over .list-item-plus-btn's
|
||||
opacity:1!important (the email-style plus button forces always-on,
|
||||
which we don't want here — the manage button should fade until the
|
||||
user actually hovers the section). */
|
||||
#sessions-section .chats-manage-btn {
|
||||
opacity: 0;
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.12s, background 0.08s;
|
||||
}
|
||||
#sessions-section .section-header-flex:hover .chats-manage-btn,
|
||||
#sessions-section .chats-manage-btn:hover,
|
||||
#sessions-section .chats-manage-btn:focus-visible {
|
||||
opacity: 0.45;
|
||||
opacity: 0.45 !important;
|
||||
}
|
||||
#sessions-section .chats-manage-btn:hover,
|
||||
#sessions-section .chats-manage-btn:focus-visible {
|
||||
opacity: 1;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@media (hover: none) {
|
||||
#sessions-section .chats-manage-btn { opacity: 0.35; }
|
||||
#sessions-section .chats-manage-btn:active { opacity: 1; }
|
||||
#sessions-section .chats-manage-btn { opacity: 0.35 !important; }
|
||||
#sessions-section .chats-manage-btn:active { opacity: 1 !important; }
|
||||
}
|
||||
|
||||
/* Collapse chevron */
|
||||
@@ -14011,7 +14015,7 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
|
||||
background: color-mix(in srgb, var(--fg) 12%, transparent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.adm-provider-caret { flex-shrink: 0; opacity: 0.5; transition: transform 0.15s; }
|
||||
.adm-provider-caret { flex-shrink: 0; transition: transform 0.15s; }
|
||||
.adm-provider-picker:has(.adm-provider-menu:not(.hidden)) .adm-provider-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -14273,6 +14277,25 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* Settings panel — tint each card's h2 title icon with the user's accent
|
||||
colour. Direct-child selector (`> h2 > svg`) so SVGs nested inside
|
||||
header buttons (Test, kebab, etc.) are untouched. Each title icon uses
|
||||
stroke="currentColor", so setting color propagates; existing inline
|
||||
opacity:0.6 stays so the tint reads soft. */
|
||||
/* Selected integration card — class beats inline borderColor reset */
|
||||
.intg-card.intg-card-active { border-color: var(--accent, var(--red)) !important; }
|
||||
|
||||
[data-settings-panel="ai"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="services"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="added-models"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="search"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="integrations"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="reminders"] .admin-card > h2 > svg,
|
||||
[data-settings-panel="added-models"] .adm-ep-section-head > svg {
|
||||
color: var(--accent, var(--red));
|
||||
}
|
||||
.admin-ep-actions {
|
||||
display: flex;
|
||||
@@ -21463,7 +21486,7 @@ body.gallery-selecting .gallery-dl-btn,
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-muted);
|
||||
color: color-mix(in srgb, var(--fg) 60%, transparent);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@@ -21565,9 +21588,11 @@ body.gallery-selecting .gallery-dl-btn,
|
||||
background: transparent; color: var(--fg); cursor: pointer; font-size: 13px;
|
||||
display: flex; align-items: center; justify-content: center; transition: all 0.15s;
|
||||
}
|
||||
.shortcut-action-btn:hover { border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, var(--bg)); }
|
||||
.shortcut-action-btn.is-reset { opacity: 0.5; }
|
||||
.shortcut-action-btn.is-reset:hover { opacity: 1; }
|
||||
.shortcut-action-btn:not(.is-reset):hover { border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, var(--bg)); }
|
||||
.shortcut-action-btn.is-reset { opacity: 0.55; border-color: transparent; background: transparent; }
|
||||
.shortcut-action-btn.is-reset:hover,
|
||||
.shortcut-action-btn.is-reset:focus,
|
||||
.shortcut-action-btn.is-reset:active { opacity: 0.55; background: transparent; border-color: transparent; }
|
||||
@keyframes shortcut-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
@@ -22659,23 +22684,17 @@ details.hwfit-serve-advanced > .hwfit-serve-checks:last-of-type {
|
||||
.settings-fallback-remove {
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
line-height: 1;
|
||||
font-size: 15px;
|
||||
/* Nudge the × glyph 5px left within the button (button size unchanged). */
|
||||
text-indent: -5px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: color-mix(in srgb, var(--fg) 55%, transparent);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, color 0.12s, background 0.12s;
|
||||
position: relative;
|
||||
top: -6px;
|
||||
/* Glyph baseline trim: nudge × up 1px inside the button without moving the
|
||||
button. line-height < 1 lets the glyph float toward the top of its line box. */
|
||||
line-height: 0.85;
|
||||
}
|
||||
.settings-fallback-remove:hover {
|
||||
border-color: var(--red);
|
||||
|
||||
Reference in New Issue
Block a user