Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions prototype/ui-redesign/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion prototype/ui-redesign/src/components/ModelManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1801,14 +1801,17 @@ const ModelManager: React.FC<ModelManagerProps> = ({ 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]);

Expand Down
44 changes: 39 additions & 5 deletions prototype/ui-redesign/src/components/PresetManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ const PresetManager: React.FC<PresetManagerProps> = ({ loadedModels }) => {
const [applyBackendSuccess, setApplyBackendSuccess] = useState<string | null>(null);
const [autoRailCollapsed, setAutoRailCollapsed] = useState(false);
const [selectedAutoRunId, setSelectedAutoRunId] = useState(AUTO_OPT_RUNS[0]?.id || '');
const [autoOptRuns, setAutoOptRuns] = useState<AutoOptRun[]>(AUTO_OPT_RUNS);
const [autoOptRunning, setAutoOptRunning] = useState(false);
const slideoverRef = useRef<HTMLElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);

Expand Down Expand Up @@ -483,14 +485,14 @@ const PresetManager: React.FC<PresetManagerProps> = ({ 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);
Expand Down Expand Up @@ -623,7 +625,35 @@ const PresetManager: React.FC<PresetManagerProps> = ({ 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 (
<>
Expand All @@ -638,8 +668,11 @@ const PresetManager: React.FC<PresetManagerProps> = ({ loadedModels }) => {
</div>
<div className="context-rail__body">
<p className="context-rail__hint">Select a safe local optimization result and attach it to editable presets. Manual args override AutoOpt.</p>
<button type="button" className="btn btn--primary btn--small" style={{ width: '100%', marginBottom: '0.75rem' }} onClick={handleRunAutoOpt} disabled={autoOptRunning}>
{autoOptRunning ? '⏳ Running…' : '▶ Run optimizer'}
</button>
<div className="auto-run-list">
{AUTO_OPT_RUNS.map(run => (
{autoOptRuns.map(run => (
<article key={run.id} className={`auto-run-card${selectedAutoRunId === run.id ? ' is-active' : ''}`}>
<button type="button" className="auto-run-card__main" onClick={() => setSelectedAutoRunId(run.id)} aria-pressed={selectedAutoRunId === run.id}>
<span className="auto-run-card__icon">⚙️</span>
Expand All @@ -656,6 +689,7 @@ const PresetManager: React.FC<PresetManagerProps> = ({ loadedModels }) => {
{run.backends.map(backend => <li key={`${run.id}-${backend.name}-${backend.version}`}>{backend.name} {backend.version} · {backend.device}</li>)}
</ul>
</details>
<button type="button" className="btn btn--ghost btn--tiny auto-run-card__delete" onClick={(e) => { e.stopPropagation(); handleDeleteAutoOptRun(run.id); }} aria-label={`Delete ${run.name}`} title="Delete this run">✕</button>
</article>
))}
</div>
Expand Down Expand Up @@ -842,7 +876,7 @@ const PresetManager: React.FC<PresetManagerProps> = ({ loadedModels }) => {
onExport={handleExport}
onDelete={handleDelete}
onClose={closeSlideover}
autoRuns={AUTO_OPT_RUNS}
autoRuns={autoOptRuns}
/>
)}
</aside>
Expand Down
11 changes: 11 additions & 0 deletions prototype/ui-redesign/src/hooks/useDashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions prototype/ui-redesign/src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading