diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index 6f39b6e4..825dcae8 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -26,6 +26,10 @@ import { Plus, Send, Terminal, + Search, + History, + Keyboard, + FileText, } from 'lucide-react'; import type { OllamaData, @@ -58,6 +62,10 @@ export default function Dashboard() { const [copied, setCopied] = useState(false); const [streamMetrics, setStreamMetrics] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [modelSearch, setModelSearch] = useState(''); + const [showHistory, setShowHistory] = useState(false); + const [showShortcuts, setShowShortcuts] = useState(false); + const [modelfileData, setModelfileData] = useState>({}); const responseRef = useRef(null); const abortRef = useRef(null); @@ -91,10 +99,49 @@ export default function Dashboard() { return () => clearInterval(interval); }, [fetchAll, promptLoading, pullLoading]); - // Escape key closes modals (respects streaming state) + // Prompt history helpers (F3) + const getPromptHistory = (): Array<{ prompt: string; model: string; ts: number }> => { + try { + return JSON.parse(localStorage.getItem('llm-prompt-history') || '[]'); + } catch { + return []; + } + }; + const saveToHistory = (prompt: string, model: string) => { + const history = getPromptHistory().filter(h => h.prompt !== prompt || h.model !== model); + history.unshift({ prompt, model, ts: Date.now() }); + localStorage.setItem('llm-prompt-history', JSON.stringify(history.slice(0, 20))); + }; + + // Fetch modelfile for expanded model (F9) + const fetchModelfile = async (modelName: string) => { + if (modelfileData[modelName]) return; + try { + const res = await fetch('/api/ollama', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'show', model: modelName }), + }); + const data = await res.json(); + if (data.modelfile) { + setModelfileData(prev => ({ ...prev, [modelName]: data.modelfile })); + } + } catch { + /* ignore */ + } + }; + + // Keyboard shortcuts (F11) + Escape handler useEffect(() => { const handler = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + const inInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'; + if (e.key === 'Escape') { + if (showShortcuts) { + setShowShortcuts(false); + return; + } if (promptLoading) { abortRef.current?.abort(); setPromptLoading(false); @@ -104,11 +151,27 @@ export default function Dashboard() { setPromptText(''); } setDeleteConfirm(null); + return; + } + if (inInput) return; + if (e.key === '?') { + setShowShortcuts(s => !s); + return; + } + if (e.key === 'r' || e.key === 'R') { + fetchAll(); + return; + } + if (e.key === '/') { + e.preventDefault(); + setModelSearch(''); + document.querySelector('[data-model-search]')?.focus(); + return; } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [promptLoading]); + }, [promptLoading, showShortcuts, fetchAll]); const handleModelAction = async (action: string, model: string) => { setActionLoading(`${action}-${model}`); @@ -193,6 +256,7 @@ export default function Dashboard() { // Streaming prompt const handlePrompt = async () => { if (!promptModel || !promptText.trim()) return; + saveToHistory(promptText.trim(), promptModel); setPromptLoading(true); setPromptResponse(''); setStreamMetrics(null); @@ -433,6 +497,30 @@ export default function Dashboard() { )} + {/* Model Search (F2) */} + {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" + style={{ + background: 'var(--surface-muted)', + border: '1px solid var(--border-subtle)', + color: 'var(--text-primary)', + caretColor: 'var(--accent-primary)', + }} + /> +
+ )} + {/* Pull Model Input */} {ollama?.status === 'online' && (
@@ -508,192 +596,230 @@ export default function Dashboard() {
) : (
- {ollama.models.map(model => { - const running = isRunning(model.name); - const expanded = expandedModel === model.name; - return ( -
-
-
-
- {running ? ( - - ) : ( - - )} + {ollama.models + .filter( + m => + !modelSearch || + m.name.toLowerCase().includes(modelSearch.toLowerCase()) || + m.details?.family?.toLowerCase().includes(modelSearch.toLowerCase()) || + m.details?.quantization_level?.toLowerCase().includes(modelSearch.toLowerCase()) + ) + .map(model => { + const running = isRunning(model.name); + const expanded = expandedModel === model.name; + return ( +
+
+
+
+ {running ? ( + + ) : ( + + )} +
+
+
+ + {model.name} + + {running && ( + + LOADED + + )} +
+
+ {formatBytes(model.size)} + {model.details?.parameter_size && ( + {model.details.parameter_size} + )} + {model.details?.quantization_level && ( + {model.details.quantization_level} + )} +
+
-
-
- - {model.name} - - {running && ( - + {running ? ( + <> + +
-
- {formatBytes(model.size)} - {model.details?.parameter_size && ( - {model.details.parameter_size} - )} - {model.details?.quantization_level && ( - {model.details.quantization_level} - )} -
-
-
-
- {running ? ( - <> + {actionLoading === `unload-${model.name}` ? ( + + ) : ( + + )} + Unload + + + ) : ( - - - ) : ( - - )} - -
-
- {/* Running model VRAM info */} - {running && - (() => { - const info = getRunningInfo(model.name); - return info ? ( -
{ + const next = expanded ? null : model.name; + setExpandedModel(next); + if (next) fetchModelfile(model.name); + }} + className="p-1.5 rounded-md transition-colors" style={{ color: 'var(--text-tertiary)' }} > - {info.size_vram > 0 && VRAM: {formatBytes(info.size_vram)}} - {info.expires_at && ( - Expires: {new Date(info.expires_at).toLocaleTimeString()} + {expanded ? ( + + ) : ( + )} -
- ) : null; - })()} - {expanded && ( -
-

Digest: {model.digest?.substring(0, 16)}...

-

Modified: {new Date(model.modified_at).toLocaleString()}

- {model.details?.family &&

Family: {model.details.family}

} -
- {deleteConfirm === model.name ? ( -
- - Delete this model? - - - -
- ) : ( - - )} +
- )} -
- ); - })} + {/* Running model VRAM info */} + {running && + (() => { + const info = getRunningInfo(model.name); + return info ? ( +
+ {info.size_vram > 0 && ( + VRAM: {formatBytes(info.size_vram)} + )} + {info.expires_at && ( + + Expires: {new Date(info.expires_at).toLocaleTimeString()} + + )} +
+ ) : null; + })()} + {expanded && ( +
+

Digest: {model.digest?.substring(0, 16)}...

+

Modified: {new Date(model.modified_at).toLocaleString()}

+ {model.details?.family &&

Family: {model.details.family}

} + {modelfileData[model.name] && ( +
+ + Modelfile + +
+                                {modelfileData[model.name]}
+                              
+
+ )} +
+ {deleteConfirm === model.name ? ( +
+ + Delete this model? + + + +
+ ) : ( + + )} +
+
+ )} +
+ ); + })} {ollama.models.length === 0 && (

No models installed. Run "ollama pull <model>" to get started. @@ -987,24 +1113,73 @@ export default function Dashboard() {

-
-