diff --git a/CHANGELOG.md b/CHANGELOG.md index 0696a4c..7ecb982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi - Conversation Playground: a Thinking on/off switch next to the Language Model (default off, setting `conversation_thinking`). Off sends `chat_template_kwargs.enable_thinking=false` (plus `think:false` for Ollama) with each reply request and never touches the system prompt; the empty-reply error now says to turn Thinking off. ### Changed +- Conversation Playground: the Language Model base URL is now picked from a dropdown of the language models configured under Engines (Docker LLM containers incl. saved URL overrides, and custom LLM cards; running/stopped marked), with a "Custom…" option that reveals the free-text URL. Selecting an entry saves `conv_llm_url` and fetches the models. - Playwright specs now import `test` from `tests/_guard.js`, whose auto fixture aborts and fails any unmocked non-GET request to `/api/**` so a spec can no longer modify live settings. See `architecture/qa.md`. --- diff --git a/Manual.md b/Manual.md index 43d2f95..c9caa06 100644 --- a/Manual.md +++ b/Manual.md @@ -844,6 +844,22 @@ same library and reading progress. Playback needs the server; there is no offline cache yet. +## Conversation Playground: Language Model endpoint picker + +**Purpose:** choose the LLM endpoint from the language models you configured under Engines → Language Models instead of typing a URL. + +**Prerequisites:** at least one language model (Docker container or custom card) under Engines; otherwise only "Custom…" is offered, with a link to Engines. + +**Usage:** in Engines & Prompt open the Language Model dropdown (entries show label and URL; ✓ = running, ✗ = stopped). Picking an entry saves it and loads its models into the model list. Choose "Custom…" to type any URL; the field appears right after the dropdown and takes focus. The refresh button re-fetches models. + +**Expected result:** the saved URL is preselected on reload; an unknown saved URL shows "Custom…" with the URL in the field. + +**Errors:** an unreachable endpoint shows "Fetch failed" in the model list. If the container list cannot be loaded, "Custom…" still works. + +**Mobile / accessibility:** controls stack on phone width, 44 px targets, visible label, keyboard operable, German and English. + +**Offline / privacy:** only your configured endpoint is contacted; the URL is stored in local app settings. + ## Conversation Playground: Thinking switch **Purpose:** choose whether a reasoning model (e.g. Qwen3) thinks before replying. Off (default) gives faster answers; on gives better reasoning but is slower and some models then return an empty reply. diff --git a/static/js/conversation.js b/static/js/conversation.js index b102ac4..e8f1a72 100644 --- a/static/js/conversation.js +++ b/static/js/conversation.js @@ -198,10 +198,62 @@ $('s-import-voices-file')?.addEventListener('change', async function () { // Restore conv LLM URL from server settings — fall back to the global Active LLM endpoint if (llmUrlInp && _appSettings) llmUrlInp.value = _appSettings.conv_llm_url || _appSettings.llm_url || ''; - if (llmUrlInp) llmUrlInp.addEventListener('input', () => { + const saveLlmUrl = () => { _patchSettings({ conv_llm_url: llmUrlInp.value }); if (_appSettings) _appSettings.conv_llm_url = llmUrlInp.value; - }); + }; + if (llmUrlInp) llmUrlInp.addEventListener('input', saveLlmUrl); + + // Endpoint picker: language models configured under Engines → Language Models + // (running/stopped Docker LLM containers from /api/local-containers with their saved + // URL override or default port, plus custom LLM cards). "Custom…" shows the free-text URL. + const llmEpSel = $('conv-llm-endpoint'); + const llmEpHint = $('conv-llm-empty-hint'); + const CUSTOM = '__custom__'; + const _tr = s => (window.t ? window.t(s) : s); + const syncLlmPicker = () => { + if (!llmEpSel || !llmUrlInp) return; + const cur = llmUrlInp.value.trim(); + const hit = cur && [...llmEpSel.options].find(o => o.value === cur); + llmEpSel.value = hit ? cur : CUSTOM; + llmUrlInp.hidden = !!hit; + }; + async function loadLlmEndpoints() { + if (!llmEpSel) return; + const list = []; + const seen = new Set(); + const add = (label, url, status) => { if (url && !seen.has(url)) { seen.add(url); list.push({ label, url, status }); } }; + try { + const r = await fetch('/api/local-containers', { cache: 'no-store' }); + const d = r.ok ? await r.json() : {}; + const over = (_appSettings && _appSettings.engine_local_urls) || {}; + for (const c of (d.containers || [])) { + if (c.role !== 'llm' || c.status === 'not_found') continue; + add(c.label || c.name, over['dc-' + c.name] || (c.port ? `http://host.docker.internal:${c.port}/v1` : ''), c.status); + } + } catch (_) { /* list stays partial; Custom… still works */ } + try { + const custom = typeof loadCustomEngineCards === 'function' ? loadCustomEngineCards() : []; + for (const c of custom) if (c && c.role === 'llm') add(c.label || c.name, c.url, 'custom'); + } catch (_) {} + llmEpSel.innerHTML = list.map(e => { + const mark = e.status === 'running' ? '✓ ' : (e.status === 'custom' ? '' : '✗ '); + return ``; + }).join('') + ``; + if (llmEpHint) llmEpHint.hidden = list.length > 0; + syncLlmPicker(); + } + if (llmEpSel && llmUrlInp) { + llmUrlInp.addEventListener('url-sync', syncLlmPicker); + llmEpSel.addEventListener('change', () => { + if (llmEpSel.value === CUSTOM) { llmUrlInp.hidden = false; llmUrlInp.focus(); return; } + llmUrlInp.value = llmEpSel.value; + llmUrlInp.hidden = true; + saveLlmUrl(); + fetchLlmModels(); + }); + loadLlmEndpoints(); + } // Thinking switch (server setting conversation_thinking; default off = faster answers) const thinkingSw = $('conv-thinking-switch'); diff --git a/static/js/i18n.js b/static/js/i18n.js index 06661ba..07474e3 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -85,6 +85,11 @@ const I18N_DICT = { 'No candidate could be generated.': 'Es konnte kein Kandidat erzeugt werden.', 'Speech-to-text unavailable: scored by level and audio problems only.': 'Sprache-zu-Text nicht verfügbar: Wertung nur nach Pegel und Audioproblemen.', 'Auto-pick failed': 'Automatische Wahl fehlgeschlagen', + // Conversation LLM endpoint picker + 'Custom…': 'Benutzerdefiniert…', + 'Custom LLM base URL': 'Eigene LLM-Basis-URL', + 'Fetch models': 'Modelle abrufen', + 'Add language models under Engines → Language Models': 'Sprachmodelle unter Engines → Language Models hinzufügen', // Conversation thinking switch 'Thinking': 'Denken', 'on': 'an', 'off': 'aus', diff --git a/static/js/settings.js b/static/js/settings.js index 3b98b47..0c765c8 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -549,7 +549,7 @@ async function loadSettings() { const refineInp = $('refine-llm-url'); if (refineInp && s.refine_llm_url) refineInp.value = s.refine_llm_url; const convInp = $('conv-llm-url'); - if (convInp && s.conv_llm_url) convInp.value = s.conv_llm_url; + if (convInp && s.conv_llm_url) { convInp.value = s.conv_llm_url; convInp.dispatchEvent(new Event('url-sync')); } sv('s-tts-stream-url', s.tts_stream_url || ''); sv('s-customvoice-url', s.customvoice_url || 'http://host.docker.internal:8022'); sv('s-nvidia-router-url', s.nvidia_router_url || 'http://host.docker.internal:8090'); diff --git a/static/sections/s-conversation.html b/static/sections/s-conversation.html index 336959d..d3aba3b 100644 --- a/static/sections/s-conversation.html +++ b/static/sections/s-conversation.html @@ -17,15 +17,17 @@
Add language models under Engines → Language Models
Off = faster answers. On = better reasoning but slower, and some models may return an empty reply.