Models: the card list preselects the card a loaded model runs on; the sidebar model card says the version and the card (Q2_0 · GPU 2)

This commit is contained in:
Niko1221 committed 2026-10-18 14:46:30 +02:00
1 parent 261145baaf
commit cd1b3bd31a
4 files changed
+43 -2

No files matched your search

+4 -1
View File
@@ -59,7 +59,10 @@ export function Sidebar({ containerRef }: {
const current = library?.variants.find(v => v.id === library.loaded);
const native = useNative(), nativeCurrent = native?.instances.find(i => i.id === native.defaultId);
const modelName = nativeCurrent?.label ?? (current?.local ? current.name : current?.family) ?? tr('No model loaded');
const modelQuant = nativeCurrent?.apiModelId ?? current?.quant ?? tr('View model versions');
// The version from the catalog and the card it runs on ("Q2_0 · GPU 2"), not the file id.
const nativeSize = nativeCurrent ? native?.catalog?.families.flatMap(f => f.sizes).find(s => s.id === nativeCurrent.model) : undefined;
const nativeSecond = nativeCurrent ? [nativeSize?.model ?? nativeCurrent.model.toUpperCase(), nativeCurrent.gpus.length ? nativeCurrent.gpus.map(g => `GPU ${g + 1}`).join(' + ') : ''].filter(Boolean).join(' · ') : null;
const modelQuant = nativeSecond ?? current?.quant ?? tr('View model versions');
const phase = nativeCurrent ? (nativeCurrent.status === 'ready' ? nativeCurrent.phase : nativeCurrent.status) : monitor?.phase ?? 'idle';
const status = phase === 'unavailable' ? 'Status unavailable' : phase === 'error' ? 'Error' : phase === 'loading' ? 'Loading' : !current && !nativeCurrent ? 'No model loaded' : phase === 'reading' ? 'Reading prompt' : phase === 'thinking' ? 'Thinking' : phase === 'generating' ? 'Generating' : 'Ready';
const menuSession = sessions.find(s => s.id === menuId);
+2 -1
View File
@@ -84,7 +84,8 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
// A card a loaded model already uses (a second model on it usually does not fit): said on its row, and not the pre-selected one while another is free.
const usedBy = (cards: number[]) => (native?.instances ?? []).filter(i => i.status !== 'error' && i.model !== selectedSize?.id && i.gpus.some(g => cards.includes(g))).map(i => i.label);
const cardOptions = selectedSize?.gpu_options && selectedSize.gpu_options.options.length >= 2 ? selectedSize.gpu_options : null;
const activeCards = cardOptions ? (cardOptions.options.find(o => o.key === cardsKey) ?? (multiple ? cardOptions.options.find(o => o.recommended && !usedBy(o.cards).length) ?? cardOptions.options.find(o => !usedBy(o.cards).length) : undefined) ?? cardOptions.options.find(o => o.recommended) ?? cardOptions.options[0]) : null;
const loadedCards = loadedSelected ? cardOptions?.options.find(o => o.cards.length === loadedSelected.gpus.length && o.cards.every(c => loadedSelected.gpus.includes(c))) : undefined;
const activeCards = cardOptions ? (cardOptions.options.find(o => o.key === cardsKey) ?? loadedCards ?? (multiple ? cardOptions.options.find(o => o.recommended && !usedBy(o.cards).length) ?? cardOptions.options.find(o => !usedBy(o.cards).length) : undefined) ?? cardOptions.options.find(o => o.recommended) ?? cardOptions.options[0]) : null;
const cardsGpus: number[] = activeCards?.cards ?? [];
const chosenCards = expert && gpus.length ? gpus : cardsGpus;
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, pcVram - keptVramOn(chosenCards)) };
+22
View File
@@ -0,0 +1,22 @@
/** UI check fixture (not a test): the native fixture with one model loaded on GPU 2, every other API answers empty. Serves on 5271. */
import http from 'node:http';
import { readFile } from 'node:fs/promises';
import { resolve, extname } from 'node:path';
import { nativeFixture, finishJob } from '../helpers/native-fixture.mjs';
const fixture = nativeFixture(), control = fixture.control, root = resolve(import.meta.dirname, '../../dist');
const opts = [{ key: 'c0', cards: [0], label: 'RTX 4090 + RAM', verdict: 'fits', words: 'fits: needs 40 GB of the 152 GB available.', notes: [], recommended: true }, { key: 'c1', cards: [1], label: 'RTX 3090 + RAM', verdict: 'fits', words: 'fits: needs 40 GB of the 152 GB available.', notes: [], recommended: false }];
for (const s of fixture.catalog.families.flatMap(f => f.sizes)) s.gpu_options = { options: opts, recommended: 'c0', ignored: [], cards: 2 };
await control.refresh(true);
await finishJob(control, control.operation('load', { model: 'iq2_xs', gpus: [1], multiple: true }).id);
const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2', '.json': 'application/json' };
http.createServer(async (req, res) => {
const path = new URL(req.url, 'http://x').pathname;
const json = v => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(v)); };
try {
if (path === '/api/native') return json(await control.refresh());
if (path === '/api/hardware') return json({ gpus: [{ index: 0, id: 'g0', vendor: 'nvidia', name: 'RTX 4090', vramGb: 24, backend: 'cuda' }, { index: 1, id: 'g1', vendor: 'nvidia', name: 'RTX 3090', vramGb: 24, backend: 'cuda' }], memory: { totalGb: 128 } });
if (path.startsWith('/api/')) return json({});
const file = path === '/' ? '/index.html' : path;
const body = await readFile(root + file); res.writeHead(200, { 'content-type': types[extname(file)] ?? 'application/octet-stream' }); res.end(body);
} catch { res.writeHead(404); res.end(); }
}).listen(5271, '127.0.0.1', () => console.log('mock on 5271'));
+15
View File
@@ -0,0 +1,15 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
test('the card list preselects the card the loaded model runs on, and the sidebar card says the version and the card', async () => {
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
assert.ok(models.indexOf('const loadedSelected = ') < models.indexOf('const loadedCards = '));
assert.ok(models.indexOf('const loadedCards = ') < models.indexOf('const activeCards = '));
assert.match(models, /cardOptions\.options\.find\(o => o\.key === cardsKey\) \?\? loadedCards \?\?/);
assert.match(models, /o\.cards\.every\(c => loadedSelected\.gpus\.includes\(c\)\)/);
const side = await readFile(new URL('../src/app/Sidebar.tsx', import.meta.url), 'utf8');
assert.match(side, /nativeSize\?\.model \?\? nativeCurrent\.model\.toUpperCase\(\)/);
assert.match(side, /`GPU \$\{g \+ 1\}`/);
assert.ok(!side.includes('nativeCurrent?.apiModelId'));
});