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