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 && (