From 6f6baf99c8df8fbc53dd81d9c8581407ba013950 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 19 Feb 2026 23:17:07 -0800 Subject: [PATCH] =?UTF-8?q?feat(local-llm):=20Phase=203=20=E2=80=94=20mode?= =?UTF-8?q?l=20intelligence=20badges=20+=20sort=20+=20version=20(N6-N10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N6: warning badge for DeepSeek R1 and distilled variants N7: Vision model indicator for llava, bakllava, moondream, qwen-vl, etc. N8: Architecture/family badge as pill on every model card N9: Sort dropdown (A-Z, size, params, running, recent) with localStorage persist N10: Ollama server version fetched from /api/version, shown in stats card --- .../dashboard/src/app/api/ollama/route.ts | 5 +- __LOCAL_LLMs/dashboard/src/app/lib/format.ts | 33 +++++ __LOCAL_LLMs/dashboard/src/app/lib/types.ts | 1 + __LOCAL_LLMs/dashboard/src/app/page.tsx | 121 +++++++++++++++--- 4 files changed, 142 insertions(+), 18 deletions(-) diff --git a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts index 3056c848..7c29ae34 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts @@ -29,13 +29,15 @@ async function fetchOllama(path: string, options?: RequestInit) { export async function GET() { try { - const [modelsRes, psRes] = await Promise.allSettled([ + const [modelsRes, psRes, versionRes] = await Promise.allSettled([ fetchOllama('/api/tags'), fetchOllama('/api/ps'), + fetchOllama('/api/version'), ]); const models = modelsRes.status === 'fulfilled' ? (modelsRes.value.models ?? []) : []; const running = psRes.status === 'fulfilled' ? (psRes.value.models ?? []) : []; + const version = versionRes.status === 'fulfilled' ? (versionRes.value.version ?? null) : null; const totalSize = models.reduce((acc: number, m: { size: number }) => acc + (m.size || 0), 0); @@ -47,6 +49,7 @@ export async function GET() { totalModels: models.length, totalSize, runningCount: running.length, + version, }); } catch { return NextResponse.json({ diff --git a/__LOCAL_LLMs/dashboard/src/app/lib/format.ts b/__LOCAL_LLMs/dashboard/src/app/lib/format.ts index b86ca543..043d5d13 100644 --- a/__LOCAL_LLMs/dashboard/src/app/lib/format.ts +++ b/__LOCAL_LLMs/dashboard/src/app/lib/format.ts @@ -32,6 +32,39 @@ export function checkMemoryFit( return 'no'; } +// N6/N7: Detect model capabilities from name and family +export interface ModelBadge { + label: string; + color: string; + tooltip: string; +} + +export function getModelBadges(name: string, family?: string): ModelBadge[] { + const badges: ModelBadge[] = []; + const n = name.toLowerCase(); + const f = (family || '').toLowerCase(); + + // N6: DeepSeek R1 reasoning models emit traces + if (n.includes('deepseek-r1') || n.includes('deepseek-r1-distill') || f.includes('deepseek')) { + badges.push({ + label: '', + color: 'var(--warning)', + tooltip: 'Emits reasoning traces — strip before JSON.parse', + }); + } + + // N7: Vision/multimodal models + if (/llava|bakllava|moondream|qwen.*vl|minicpm-v|llama.*vision/i.test(n)) { + badges.push({ + label: '👁 Vision', + color: 'var(--accent-secondary)', + tooltip: 'Vision model — supports image input', + }); + } + + return badges; +} + export function formatUptime(seconds: number): string { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); diff --git a/__LOCAL_LLMs/dashboard/src/app/lib/types.ts b/__LOCAL_LLMs/dashboard/src/app/lib/types.ts index c9ad5f55..1a32eb24 100644 --- a/__LOCAL_LLMs/dashboard/src/app/lib/types.ts +++ b/__LOCAL_LLMs/dashboard/src/app/lib/types.ts @@ -26,6 +26,7 @@ export interface OllamaData { totalModels: number; totalSize: number; runningCount: number; + version?: string; } export interface WhisperModel { diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index a531ab37..2f7ef77c 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -44,7 +44,13 @@ import type { PullProgress, StreamMetrics, } from './lib/types'; -import { formatBytes, formatUptime, estimateRam, checkMemoryFit } from './lib/format'; +import { + formatBytes, + formatUptime, + estimateRam, + checkMemoryFit, + getModelBadges, +} from './lib/format'; import { StatusDot } from './components/StatusDot'; import { ProgressBar } from './components/ProgressBar'; import { Sparkline } from './components/Sparkline'; @@ -98,6 +104,9 @@ export default function Dashboard() { const [modelMetadata, setModelMetadata] = useState>( {} ); + const [modelSort, setModelSort] = useState<'name' | 'size' | 'params' | 'running' | 'modified'>( + 'name' + ); const responseRef = useRef(null); const abortRef = useRef(null); const compareAbortRef = useRef(null); @@ -153,6 +162,8 @@ export default function Dashboard() { /* ignore */ } setAutoLoadModel(localStorage.getItem('llm-auto-load-model')); + const savedSort = localStorage.getItem('llm-model-sort'); + if (savedSort) setModelSort(savedSort as typeof modelSort); }, []); useEffect(() => { @@ -723,6 +734,14 @@ export default function Dashboard() { {ollama?.status === 'online' ? 'Online' : 'Offline'} + {ollama?.version && ( + + v{ollama.version} + + )}

{ollama?.url} @@ -833,27 +852,50 @@ export default function Dashboard() { )} - {/* Model Search (F2) */} + {/* Model Search (F2) + Sort (N9) */} {ollama?.status === 'online' && ollama.models.length > 3 && ( -

- - setModelSearch(e.target.value)} - placeholder="Filter models... (press / to focus)" - className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono focus:outline-none focus:ring-2" +
+
+ + setModelSearch(e.target.value)} + placeholder="Filter models... (press / to focus)" + className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono focus:outline-none focus:ring-2" + style={{ + background: 'var(--surface-muted)', + border: '1px solid var(--border-subtle)', + color: 'var(--text-primary)', + caretColor: 'var(--accent-primary)', + }} + /> +
+
)} @@ -949,10 +991,31 @@ export default function Dashboard() { m.details?.family?.toLowerCase().includes(modelSearch.toLowerCase()) || m.details?.quantization_level?.toLowerCase().includes(modelSearch.toLowerCase()) ) + .sort((a, b) => { + switch (modelSort) { + case 'size': + return b.size - a.size; + case 'params': { + const pa = parseFloat(a.details?.parameter_size || '0'); + const pb = parseFloat(b.details?.parameter_size || '0'); + return pb - pa; + } + case 'running': { + const ra = isRunning(a.name) ? 0 : 1; + const rb = isRunning(b.name) ? 0 : 1; + return ra !== rb ? ra - rb : a.name.localeCompare(b.name); + } + case 'modified': + return new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime(); + default: + return a.name.localeCompare(b.name); + } + }) .map(model => { const running = isRunning(model.name); const expanded = expandedModel === model.name; const estRam = estimateRam(model.size, model.details?.quantization_level); + const badges = getModelBadges(model.name, model.details?.family); const fitStatus = system ? checkMemoryFit(estRam, system.memory.free, system.memory.cached) : null; @@ -997,6 +1060,30 @@ export default function Dashboard() { LOADED )} + {badges.map(b => ( + + {b.label} + + ))} + {model.details?.family && ( + + {model.details.family} + + )}