From 93d17d31e3289f7b9fa77c46732a98265de5c7ba Mon Sep 17 00:00:00 2001 From: Kyle Poineal Date: Mon, 6 Jul 2026 16:01:28 -0600 Subject: [PATCH] fix(gui3): resolve 4 bugs from owner feedback - Auto-discovery: derive default base URL from window.location.origin when served by lemond, and read mock API's apiUrl field as fallback. Fixes #2561. - Dashboard port: listen for connection status changes and resume polling when connection is re-established after failures. Fixes #2562. - Models filtering: hide models with suggested:false unless they are downloaded or currently loaded. Fixes #2560. - AutoOpt rail: add Run Optimizer button and per-run Delete action so the panel is actually usable. Fixes #2567. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- prototype/ui-redesign/src/api.ts | 24 ++++++++-- .../src/components/ModelManager.tsx | 5 ++- .../src/components/PresetManager.tsx | 44 ++++++++++++++++--- .../ui-redesign/src/hooks/useDashboardData.ts | 11 +++++ prototype/ui-redesign/src/styles/styles.css | 19 ++++++++ 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/prototype/ui-redesign/src/api.ts b/prototype/ui-redesign/src/api.ts index 15bdc164d7..0b24d40d6a 100644 --- a/prototype/ui-redesign/src/api.ts +++ b/prototype/ui-redesign/src/api.ts @@ -7,7 +7,20 @@ import { recipeOptionsForModel, samplingForModel, type RecipeOptions } from './presetStore'; import { COLLECTION_IMAGE_SIZE } from './features/collections/collectionImageConfig'; -const DEFAULT_BASE_URL = 'http://localhost:13305'; +function detectDefaultBaseUrl(): string { + if (typeof window !== 'undefined' && window.location) { + // When served by lemond at /app, window.location.origin IS the API server. + // Detect this by checking for the lemond-injected window.api shim or /app path. + const servedByLemond = window.location.pathname.startsWith('/app') + || (window as any).api?.getServerPort; + if (servedByLemond && window.location.port) { + return `${window.location.protocol}//${window.location.hostname}:${window.location.port}`; + } + } + return 'http://localhost:13305'; +} + +const DEFAULT_BASE_URL = detectDefaultBaseUrl(); const LS_BASE_URL = 'lemonade_base_url'; const LS_API_KEY = 'lemonade_api_key'; @@ -585,8 +598,13 @@ class LemonadeAPI { const settings = await hostApi.getSettings(); const baseUrl = typedSettingString(settings, 'baseURL'); const apiKey = typedSettingString(settings, 'apiKey'); - if (baseUrl.trim()) { - this._hostBaseUrl = normalizeBaseUrl(baseUrl); + // Fallback: web-app mock provides apiUrl as a plain string (not typed setting) + const fallbackUrl = !baseUrl.trim() && isObject(settings) && typeof (settings as any).apiUrl === 'string' + ? (settings as any).apiUrl.trim() + : ''; + const resolvedUrl = baseUrl.trim() || fallbackUrl; + if (resolvedUrl) { + this._hostBaseUrl = normalizeBaseUrl(resolvedUrl); safeSetLocalStorage(LS_BASE_URL, this._hostBaseUrl); } this._sessionApiKey = apiKey; diff --git a/prototype/ui-redesign/src/components/ModelManager.tsx b/prototype/ui-redesign/src/components/ModelManager.tsx index 50606e77a1..7b399a2092 100644 --- a/prototype/ui-redesign/src/components/ModelManager.tsx +++ b/prototype/ui-redesign/src/components/ModelManager.tsx @@ -1801,14 +1801,17 @@ const ModelManager: React.FC = ({ onModelSelect, selectedMode seen.add(name); merged.push(m); } + const loadedNames = new Set(loadedModels.map(lm => lm.model_name.toLowerCase())); for (const m of models) { const name = modelName(m).toLowerCase(); if (!name || seen.has(name)) continue; + // Hide models explicitly marked as not suggested, unless they are downloaded or loaded + if ((m as any).suggested === false && !(m as any).downloaded && !loadedNames.has(name)) continue; seen.add(name); merged.push(m); } return merged; - }, [customModels, models]); + }, [customModels, models, loadedModels]); const allPresets = useMemo(() => [DEFAULT_PRESET, ...STARTERS, ...userPresets], [userPresets]); diff --git a/prototype/ui-redesign/src/components/PresetManager.tsx b/prototype/ui-redesign/src/components/PresetManager.tsx index b485b5736f..8794ba5a74 100644 --- a/prototype/ui-redesign/src/components/PresetManager.tsx +++ b/prototype/ui-redesign/src/components/PresetManager.tsx @@ -415,6 +415,8 @@ const PresetManager: React.FC = ({ loadedModels }) => { const [applyBackendSuccess, setApplyBackendSuccess] = useState(null); const [autoRailCollapsed, setAutoRailCollapsed] = useState(false); const [selectedAutoRunId, setSelectedAutoRunId] = useState(AUTO_OPT_RUNS[0]?.id || ''); + const [autoOptRuns, setAutoOptRuns] = useState(AUTO_OPT_RUNS); + const [autoOptRunning, setAutoOptRunning] = useState(false); const slideoverRef = useRef(null); const triggerRef = useRef(null); @@ -483,14 +485,14 @@ const PresetManager: React.FC = ({ loadedModels }) => { engine_hint: 'auto', starter: false, auto_opt_enabled: true, - auto_opt_run_id: AUTO_OPT_RUNS[0]?.id || null, + auto_opt_run_id: autoOptRuns[0]?.id || null, system_prompt_id: 'general', system_prompts: cloneSystemPrompts(CUSTOM_PRESET_PROMPTS), tools_enabled: true, }; setUserPresets(prev => [newPreset, ...prev]); openSlideover(newPreset); - }, [openSlideover]); + }, [openSlideover, autoOptRuns]); const importPresets = useCallback((raw: string) => { const data = JSON.parse(raw); @@ -623,7 +625,35 @@ const PresetManager: React.FC = ({ loadedModels }) => { }); }, []); - const selectedAutoRun = AUTO_OPT_RUNS.find(run => run.id === selectedAutoRunId) || AUTO_OPT_RUNS[0]; + const handleRunAutoOpt = useCallback(() => { + setAutoOptRunning(true); + setTimeout(() => { + const newRun: AutoOptRun = { + id: `autoopt-${Date.now()}`, + name: `AutoOpt #${autoOptRuns.length + 1}`, + date: new Date().toISOString().split('T')[0], + lemonadeVersion: '0.6.0-prototype', + summary: 'Fresh optimization based on current system hardware and loaded backends.', + args: '--threads auto --batch-size 512 --ubatch-size 256 --ctx-size 4096', + backends: [{ name: 'llama.cpp', version: 'latest', device: 'Auto-detected' }], + }; + setAutoOptRuns(prev => [newRun, ...prev]); + setSelectedAutoRunId(newRun.id); + setAutoOptRunning(false); + }, 2000); + }, [autoOptRuns.length]); + + const handleDeleteAutoOptRun = useCallback((runId: string) => { + setAutoOptRuns(prev => { + const next = prev.filter(r => r.id !== runId); + if (selectedAutoRunId === runId) { + setSelectedAutoRunId(next[0]?.id || ''); + } + return next; + }); + }, [selectedAutoRunId]); + + const selectedAutoRun = autoOptRuns.find(run => run.id === selectedAutoRunId) || autoOptRuns[0]; return ( <> @@ -638,8 +668,11 @@ const PresetManager: React.FC = ({ loadedModels }) => {

Select a safe local optimization result and attach it to editable presets. Manual args override AutoOpt.

+
- {AUTO_OPT_RUNS.map(run => ( + {autoOptRuns.map(run => (
))}
@@ -842,7 +876,7 @@ const PresetManager: React.FC = ({ loadedModels }) => { onExport={handleExport} onDelete={handleDelete} onClose={closeSlideover} - autoRuns={AUTO_OPT_RUNS} + autoRuns={autoOptRuns} /> )} diff --git a/prototype/ui-redesign/src/hooks/useDashboardData.ts b/prototype/ui-redesign/src/hooks/useDashboardData.ts index 63e032fb57..7703598cc3 100644 --- a/prototype/ui-redesign/src/hooks/useDashboardData.ts +++ b/prototype/ui-redesign/src/hooks/useDashboardData.ts @@ -303,6 +303,17 @@ export function useDashboardData(): DashboardData { } }, [computeAggregates]); + // Resume polling when the global connection is re-established + useEffect(() => { + const unsub = api.onStatusChange((s) => { + if (s === 'connected' && paused) { + failureCountRef.current = 0; + setPaused(false); + } + }); + return unsub; + }, [paused]); + useEffect(() => { poll(); if (!paused) { diff --git a/prototype/ui-redesign/src/styles/styles.css b/prototype/ui-redesign/src/styles/styles.css index 39c8876b00..0ae73c029b 100644 --- a/prototype/ui-redesign/src/styles/styles.css +++ b/prototype/ui-redesign/src/styles/styles.css @@ -3615,6 +3615,25 @@ input, textarea { .auto-run-card { overflow: hidden; + position: relative; +} + +.auto-run-card__delete { + position: absolute; + top: var(--space-2); + right: var(--space-2); + opacity: 0; + color: var(--danger) !important; + font-size: 14px; + line-height: 1; + padding: 2px 5px !important; + transition: opacity 0.15s; +} + +.auto-run-card:hover .auto-run-card__delete, +.auto-run-card:focus-within .auto-run-card__delete, +.auto-run-card__delete:focus { + opacity: 1; } .auto-run-card__main {