From 7f042975de2a58c9b8d39182a550754614fac89b Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 19 Feb 2026 23:13:22 -0800 Subject: [PATCH] =?UTF-8?q?feat(local-llm):=20Phase=202=20=E2=80=94=20rich?= =?UTF-8?q?=20metadata=20+=20persistence=20(N4-N5,=20BN3-BN4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N4: RamBudgetBar component — stacked horizontal bar showing OS+Apps, loaded models (by name with color), and free memory segments N5: Context window size — extract context_length from /api/show model_info, cache in modelMetadata state, display on card BN3: Persist chat messages to localStorage (llm-chat-{model}), restore on modal re-open, capped at 50 messages BN4: Logs panel refresh button — RefreshCw icon next to toggle --- .../src/app/components/RamBudgetBar.tsx | 105 ++++++++++++++++++ __LOCAL_LLMs/dashboard/src/app/page.tsx | 89 ++++++++++++--- 2 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 __LOCAL_LLMs/dashboard/src/app/components/RamBudgetBar.tsx diff --git a/__LOCAL_LLMs/dashboard/src/app/components/RamBudgetBar.tsx b/__LOCAL_LLMs/dashboard/src/app/components/RamBudgetBar.tsx new file mode 100644 index 00000000..dafcb2e9 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/components/RamBudgetBar.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { formatBytes } from '../lib/format'; + +interface RunningModelSegment { + name: string; + size_vram: number; +} + +interface RamBudgetBarProps { + totalRam: number; + appMemory: number; + runningModels: RunningModelSegment[]; + freeRam: number; +} + +const MODEL_COLORS = [ + 'var(--success)', + 'var(--accent-secondary)', + 'var(--accent-primary)', + 'var(--purple)', + 'var(--warning)', +]; + +export function RamBudgetBar({ totalRam, appMemory, runningModels, freeRam }: RamBudgetBarProps) { + if (totalRam <= 0) return null; + const modelTotal = runningModels.reduce((sum, m) => sum + m.size_vram, 0); + const osApps = Math.max(0, totalRam - modelTotal - freeRam); + const pct = (bytes: number) => Math.max(0.5, (bytes / totalRam) * 100); + + return ( +
+
+ + Memory Budget + + + {formatBytes(totalRam)} unified + +
+
+ {/* OS + Apps */} +
+ {pct(osApps) > 8 ? 'OS' : ''} +
+ {/* Loaded models */} + {runningModels.map((m, i) => ( +
+ {pct(m.size_vram) > 10 ? m.name.split(':')[0] : ''} +
+ ))} + {/* Free */} +
+ {pct(freeRam) > 8 ? `${formatBytes(freeRam)} free` : ''} +
+
+ {runningModels.length > 0 && ( +
+ {runningModels.map((m, i) => ( + + + {m.name.split(':')[0]}: {formatBytes(m.size_vram)} + + ))} +
+ )} +
+ ); +} diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index 8fdb5847..a531ab37 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -48,6 +48,7 @@ import { formatBytes, formatUptime, estimateRam, checkMemoryFit } from './lib/fo import { StatusDot } from './components/StatusDot'; import { ProgressBar } from './components/ProgressBar'; import { Sparkline } from './components/Sparkline'; +import { RamBudgetBar } from './components/RamBudgetBar'; export default function Dashboard() { const [ollama, setOllama] = useState(null); @@ -94,6 +95,9 @@ export default function Dashboard() { const [whisperTestLoading, setWhisperTestLoading] = useState(false); const [compareModel, setCompareModel] = useState(null); const [compareResponse, setCompareResponse] = useState(''); + const [modelMetadata, setModelMetadata] = useState>( + {} + ); const responseRef = useRef(null); const abortRef = useRef(null); const compareAbortRef = useRef(null); @@ -155,6 +159,26 @@ export default function Dashboard() { fetchAll(); }, [fetchAll]); + // BN3: Save chat messages to localStorage when they change + useEffect(() => { + if (promptModel && chatMode && chatMessages.length > 0) { + localStorage.setItem(`llm-chat-${promptModel}`, JSON.stringify(chatMessages.slice(-50))); + } + }, [chatMessages, promptModel, chatMode]); + + // BN3: Restore chat messages when prompt modal opens + useEffect(() => { + if (promptModel && chatMode) { + try { + const saved = localStorage.getItem(`llm-chat-${promptModel}`); + if (saved) setChatMessages(JSON.parse(saved)); + } catch { + /* ignore */ + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [promptModel, chatMode]); + // F16: Auto-load preferred model when Ollama is online but nothing loaded useEffect(() => { if (!autoLoadModel || !ollama || ollama.status !== 'online') return; @@ -289,7 +313,7 @@ export default function Dashboard() { else localStorage.removeItem('llm-auto-load-model'); }; - // Fetch modelfile for expanded model (F9) + // Fetch modelfile + metadata for expanded model (F9 + N5) const fetchModelfile = async (modelName: string) => { if (modelfileData[modelName]) return; try { @@ -302,6 +326,14 @@ export default function Dashboard() { if (data.modelfile) { setModelfileData(prev => ({ ...prev, [modelName]: data.modelfile })); } + // N5: Extract context_length from model_info + if (data.model_info) { + const ctxKey = Object.keys(data.model_info).find(k => k.endsWith('.context_length')); + const contextLength = ctxKey ? data.model_info[ctxKey] : undefined; + if (contextLength) { + setModelMetadata(prev => ({ ...prev, [modelName]: { contextLength } })); + } + } } catch { /* ignore */ } @@ -900,6 +932,15 @@ export default function Dashboard() { ) : (
+ {/* N4: RAM Budget Bar */} + {system && ollama.running.length > 0 && ( + + )} {ollama.models .filter( m => @@ -971,6 +1012,14 @@ export default function Dashboard() { ~{formatBytes(estRam)} RAM + {(() => { + const ctx = modelMetadata[model.name]?.contextLength; + return ctx ? ( + + {ctx >= 1024 ? `${Math.round(ctx / 1024)}k` : ctx} ctx + + ) : null; + })()}
@@ -1517,18 +1566,32 @@ export default function Dashboard() { {/* Ollama Logs Viewer (F8) */}
- +
+ + {showLogs && ( + + )} +
{showLogs && (