mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
Settings polish: /setup provider subs, Add API defaults to api kind, picker shows offline endpoints, doc library tracks sub-tab
- /setup gains explicit provider subcommands (deepseek, openai, anthropic, openrouter, groq, gemini, xai, ollama, copilot, local, endpoint) so the autocomplete popup surfaces "/setup de…" suggestions with format hints, and bare-provider invocations still prompt for the key. - Add API endpoint defaults to kind=api (auto-refresh /v1/models) instead of kind=proxy. Proxy was a frequent footgun for OpenAI- compatible endpoints that DO serve /v1/models — the user got an empty model list and had to flip the dropdown. - Model picker now includes offline endpoints with stale:true so a briefly-down local server doesn't vanish from the picker (it dims and shows the offline pill, clickable anyway). Dedup prefers the online entry when the same model is exposed by both. - Document library modal header reflects the active sub-tab via _TAB_HEADERS so it no longer shows the wrong section name when switching between Documents / Skills / Templates.
This commit is contained in:
+3
-3
@@ -731,14 +731,14 @@ function initEndpointForm() {
|
|||||||
urlInput.addEventListener('input', () => {
|
urlInput.addEventListener('input', () => {
|
||||||
if (provider.value && urlInput.value.trim() !== provider.value) {
|
if (provider.value && urlInput.value.trim() !== provider.value) {
|
||||||
provider.value = '';
|
provider.value = '';
|
||||||
if (kindSel) kindSel.value = 'proxy';
|
if (kindSel) kindSel.value = 'api';
|
||||||
_renderPickerMenu();
|
_renderPickerMenu();
|
||||||
_syncPickerCurrent();
|
_syncPickerCurrent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (kindSel) kindSel.value = provider.value ? 'api' : (kindSel.value || 'proxy');
|
if (kindSel) kindSel.value = kindSel.value || 'api';
|
||||||
function _apiEndpointKind() {
|
function _apiEndpointKind() {
|
||||||
return (kindSel && kindSel.value) ? kindSel.value : (provider.value ? 'api' : 'proxy');
|
return (kindSel && kindSel.value) ? kindSel.value : 'api';
|
||||||
}
|
}
|
||||||
function _normalizeBaseUrl(raw) {
|
function _normalizeBaseUrl(raw) {
|
||||||
let u = raw.trim();
|
let u = raw.trim();
|
||||||
|
|||||||
@@ -1598,7 +1598,11 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
|||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content doclib-modal-content" style="width:min(640px, 92vw);max-height:85vh;background:var(--bg);">
|
<div class="modal-content doclib-modal-content" style="width:min(640px, 92vw);max-height:85vh;background:var(--bg);">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><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"/><line x1="8" y1="7" x2="16" y2="7"/><line x1="8" y1="11" x2="14" y2="11"/></svg>Library</h4>
|
<!-- Header title + icon mirror the currently-active sub-tab (Chats /
|
||||||
|
Documents / Research / Archive) so the user sees ONE icon at
|
||||||
|
the top representing the section they're in, with the tab
|
||||||
|
strip below as sub-navigation. _switchLibTab() updates this. -->
|
||||||
|
<h4 id="doclib-header-title"><span id="doclib-header-icon" style="vertical-align:-2px;margin-right:4px;display:inline-flex;"></span><span id="doclib-header-text">Library</span></h4>
|
||||||
<button class="close-btn" id="doclib-close">\u2716</button>
|
<button class="close-btn" id="doclib-close">\u2716</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lib-tabs" id="doclib-lib-tabs" style="padding:0 10px;">
|
<div class="lib-tabs" id="doclib-lib-tabs" style="padding:0 10px;">
|
||||||
@@ -1831,6 +1835,27 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
|||||||
grid.parentElement.appendChild(btn);
|
grid.parentElement.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SVG markup + label for each tab — used to keep the modal header
|
||||||
|
// in sync with whichever sub-tab the user is on.
|
||||||
|
const _TAB_HEADERS = {
|
||||||
|
chats: {
|
||||||
|
label: 'Chats',
|
||||||
|
svg: '<svg width="16" height="16" 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>',
|
||||||
|
},
|
||||||
|
documents: {
|
||||||
|
label: 'Documents',
|
||||||
|
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>',
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
label: 'Research',
|
||||||
|
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>',
|
||||||
|
},
|
||||||
|
archive: {
|
||||||
|
label: 'Archive',
|
||||||
|
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function _switchLibTab(tab) {
|
function _switchLibTab(tab) {
|
||||||
_activeLibTab = tab;
|
_activeLibTab = tab;
|
||||||
_tabBtns.forEach(b => b.classList.toggle('active', b.dataset.doclibTab === tab));
|
_tabBtns.forEach(b => b.classList.toggle('active', b.dataset.doclibTab === tab));
|
||||||
@@ -1841,6 +1866,14 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
|||||||
p.style.display = 'none';
|
p.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Sync the modal header icon + label to match the active sub-tab.
|
||||||
|
const hdr = _TAB_HEADERS[tab];
|
||||||
|
if (hdr) {
|
||||||
|
const ico = document.getElementById('doclib-header-icon');
|
||||||
|
const txt = document.getElementById('doclib-header-text');
|
||||||
|
if (ico) ico.innerHTML = hdr.svg;
|
||||||
|
if (txt) txt.textContent = hdr.label;
|
||||||
|
}
|
||||||
if (tab === 'chats') _renderLibChats();
|
if (tab === 'chats') _renderLibChats();
|
||||||
else if (tab === 'archive') _renderLibArchive();
|
else if (tab === 'archive') _renderLibArchive();
|
||||||
else if (tab === 'research') _renderLibResearch();
|
else if (tab === 'research') _renderLibResearch();
|
||||||
@@ -3121,8 +3154,10 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
|||||||
return new Date(iso).toLocaleDateString();
|
return new Date(iso).toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to initial tab if not documents
|
// Switch to the initial tab. Always call this — even when the
|
||||||
if (_activeLibTab !== 'documents') _switchLibTab(_activeLibTab);
|
// default ('documents') matches — so the modal header's icon + label
|
||||||
|
// sync from "Library" to the active sub-tab on first open.
|
||||||
|
_switchLibTab(_activeLibTab);
|
||||||
|
|
||||||
const searchInput = document.getElementById('doclib-search');
|
const searchInput = document.getElementById('doclib-search');
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener('input', () => {
|
||||||
|
|||||||
+39
-14
@@ -179,14 +179,23 @@ function _initModelPickerDropdown() {
|
|||||||
const result = [];
|
const result = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (item.offline) return;
|
// Previously: offline endpoints were skipped entirely, so a server
|
||||||
|
// that briefly went down disappeared from the picker — confusing
|
||||||
|
// when the user can still see it (offline-tagged) in Settings.
|
||||||
|
// Now: include offline-endpoint models too but flag them
|
||||||
|
// `stale: true` so the row renderer dims them + shows the offline
|
||||||
|
// pill. The user can still click and try anyway (matches the
|
||||||
|
// existing "local server appears offline" path on line 301).
|
||||||
|
const epOffline = !!item.offline;
|
||||||
const allModels = (item.models || []).concat(item.models_extra || []);
|
const allModels = (item.models || []).concat(item.models_extra || []);
|
||||||
const allDisplay = (item.models_display || []).concat(item.models_extra_display || []);
|
const allDisplay = (item.models_display || []).concat(item.models_extra_display || []);
|
||||||
// Mark local endpoints whose live probe failed.
|
// Mark local endpoints whose live probe failed.
|
||||||
const probeResult = item.endpoint_id ? _localProbe[item.endpoint_id] : null;
|
const probeResult = item.endpoint_id ? _localProbe[item.endpoint_id] : null;
|
||||||
const isLocalDead = !!(probeResult && probeResult.alive === false);
|
const isLocalDead = !!(probeResult && probeResult.alive === false);
|
||||||
allModels.forEach((mid, i) => {
|
allModels.forEach((mid, i) => {
|
||||||
// Deduplicate by model ID — prefer DB endpoints over env-discovered
|
// Deduplicate by model ID — prefer ONLINE endpoint entries over
|
||||||
|
// offline duplicates so the user gets a working endpoint first
|
||||||
|
// when the same model is exposed by both.
|
||||||
if (seen.has(mid)) return;
|
if (seen.has(mid)) return;
|
||||||
seen.add(mid);
|
seen.add(mid);
|
||||||
result.push({
|
result.push({
|
||||||
@@ -201,8 +210,11 @@ function _initModelPickerDropdown() {
|
|||||||
item.host || '',
|
item.host || '',
|
||||||
item.url || '',
|
item.url || '',
|
||||||
].filter(Boolean).join(' '),
|
].filter(Boolean).join(' '),
|
||||||
stale: isLocalDead,
|
stale: isLocalDead || epOffline,
|
||||||
staleReason: isLocalDead ? (probeResult.error || 'not responding') : '',
|
staleReason: epOffline
|
||||||
|
? (item.ping_error || 'endpoint offline')
|
||||||
|
: (isLocalDead ? (probeResult.error || 'not responding') : ''),
|
||||||
|
offline: epOffline,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -377,22 +389,35 @@ function _initModelPickerDropdown() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Browse mode: Recent (auto) + Favorites (manual). No flat "All" dump. ──
|
// ── Browse mode: Favorites (manual) + Recent (auto), with dedupe. ──
|
||||||
|
// Rules:
|
||||||
|
// 1. Never list the same model twice in the dropdown. Favorites
|
||||||
|
// win over Recent (if you favorited it, that's where it
|
||||||
|
// belongs — Recent shouldn't show it again as duplicate).
|
||||||
|
// 2. Small catalogs (≤ BROWSE_ALL_LIMIT total) skip the Recent
|
||||||
|
// section entirely — when there's only ~10 models, the whole
|
||||||
|
// list fits below as "All models" and a separate Recent
|
||||||
|
// section just duplicates rows.
|
||||||
const shown = new Set();
|
const shown = new Set();
|
||||||
const recentModels = _loadRecent()
|
|
||||||
.map(id => byId.get(id))
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, RECENT_MAX);
|
|
||||||
const favModels = favs.map(id => byId.get(id)).filter(Boolean);
|
const favModels = favs.map(id => byId.get(id)).filter(Boolean);
|
||||||
|
|
||||||
if (recentModels.length) {
|
|
||||||
_addSection('Recent');
|
|
||||||
recentModels.forEach(m => { shown.add(m.mid); _addRow(m); });
|
|
||||||
}
|
|
||||||
if (favModels.length) {
|
if (favModels.length) {
|
||||||
_addSection('Favorites');
|
_addSection('Favorites');
|
||||||
favModels.forEach(m => { shown.add(m.mid); _addRow(m); });
|
favModels.forEach(m => { shown.add(m.mid); _addRow(m); });
|
||||||
}
|
}
|
||||||
|
// Recent: only render when the catalog is big enough that surfacing
|
||||||
|
// a recency shortlist is actually useful, AND only models that
|
||||||
|
// aren't already in Favorites (dedupe).
|
||||||
|
if (all.length > BROWSE_ALL_LIMIT) {
|
||||||
|
const recentModels = _loadRecent()
|
||||||
|
.map(id => byId.get(id))
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(m => !shown.has(m.mid))
|
||||||
|
.slice(0, RECENT_MAX);
|
||||||
|
if (recentModels.length) {
|
||||||
|
_addSection('Recent');
|
||||||
|
recentModels.forEach(m => { shown.add(m.mid); _addRow(m); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Small catalogs: still list everything so users aren't forced to search.
|
// Small catalogs: still list everything so users aren't forced to search.
|
||||||
if (all.length <= BROWSE_ALL_LIMIT) {
|
if (all.length <= BROWSE_ALL_LIMIT) {
|
||||||
|
|||||||
@@ -4815,7 +4815,15 @@ async function _cmdSetup(args, ctx) {
|
|||||||
} else {
|
} else {
|
||||||
pendingSetupProvider = provider;
|
pendingSetupProvider = provider;
|
||||||
setupMode = 'endpoint-key-for-provider';
|
setupMode = 'endpoint-key-for-provider';
|
||||||
await _setupReply(`Paste your ${provider.name} API key.`);
|
// Show the canonical "/setup <provider> <key>" usage so the user
|
||||||
|
// learns the one-shot form instead of relying on the pasted-key
|
||||||
|
// mode that always greets them with a generic prompt.
|
||||||
|
// _setupReply renders as plain text (no HTML) — use markdown
|
||||||
|
// backticks for the inline code instead of <code> + <>.
|
||||||
|
const _slug = (topic || '').toLowerCase();
|
||||||
|
await _setupReply(
|
||||||
|
`Paste your ${provider.name} API key, or run \`/setup ${_slug} <api-key>\` to set it in one step.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -5538,7 +5546,31 @@ const COMMANDS = {
|
|||||||
category: 'Getting started',
|
category: 'Getting started',
|
||||||
help: 'Add local or API model endpoints',
|
help: 'Add local or API model endpoints',
|
||||||
handler: _cmdSetup,
|
handler: _cmdSetup,
|
||||||
usage: '/setup local URL · /setup groq KEY · /setup copilot · /setup endpoint'
|
usage: '/setup local URL · /setup groq KEY · /setup copilot · /setup endpoint',
|
||||||
|
// Provider subs so the autocomplete popup surfaces "/setup deepseek",
|
||||||
|
// "/setup openai", etc. when the user types "/setup de". Each sub's
|
||||||
|
// handler is a thin wrapper that re-prepends the sub name and
|
||||||
|
// re-dispatches into _cmdSetup, which already knows how to handle
|
||||||
|
// bare-provider (prompts for the key) AND provider-with-key (saves it).
|
||||||
|
// Without the explicit handler, the slash-dispatcher errors with
|
||||||
|
// "subDef.handler is not a function".
|
||||||
|
subs: {
|
||||||
|
deepseek: { help: 'DeepSeek', usage: '/setup deepseek sk-...', handler: (a, c) => _cmdSetup(['deepseek', ...a], c) },
|
||||||
|
openai: { help: 'OpenAI', usage: '/setup openai sk-proj-...', handler: (a, c) => _cmdSetup(['openai', ...a], c) },
|
||||||
|
anthropic: { help: 'Anthropic', usage: '/setup anthropic sk-ant-...',handler: (a, c) => _cmdSetup(['anthropic', ...a], c) },
|
||||||
|
openrouter: { help: 'OpenRouter', usage: '/setup openrouter sk-or-...',handler: (a, c) => _cmdSetup(['openrouter', ...a], c) },
|
||||||
|
groq: { help: 'Groq', usage: '/setup groq gsk_...', handler: (a, c) => _cmdSetup(['groq', ...a], c) },
|
||||||
|
gemini: { help: 'Google Gemini', alias: ['google'], usage: '/setup gemini AIza...', handler: (a, c) => _cmdSetup(['gemini', ...a], c) },
|
||||||
|
xai: { help: 'xAI (Grok)', alias: ['grok'], usage: '/setup xai xai-...', handler: (a, c) => _cmdSetup(['xai', ...a], c) },
|
||||||
|
ollama: { help: 'Ollama Cloud', usage: '/setup ollama KEY', handler: (a, c) => _cmdSetup(['ollama', ...a], c) },
|
||||||
|
copilot: { help: 'GitHub Copilot', usage: '/setup copilot', handler: (a, c) => _cmdSetup(['copilot', ...a], c) },
|
||||||
|
local: { help: 'Local model server (vLLM / LM Studio / llama.cpp / Ollama)',
|
||||||
|
usage: '/setup local http://localhost:8000/v1',
|
||||||
|
handler: (a, c) => _cmdSetup(['local', ...a], c) },
|
||||||
|
endpoint: { help: 'Open the endpoint manager in Settings',
|
||||||
|
usage: '/setup endpoint',
|
||||||
|
handler: (a, c) => _cmdSetup(['endpoint', ...a], c) },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
demo: {
|
demo: {
|
||||||
alias: ['tour'],
|
alias: ['tour'],
|
||||||
|
|||||||
Reference in New Issue
Block a user