diff --git a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts index d10ac478..6007ae7f 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts @@ -3,12 +3,19 @@ import { NextResponse } from 'next/server'; const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; async function fetchOllama(path: string, options?: RequestInit) { - const res = await fetch(`${OLLAMA_URL}${path}`, { - ...options, - headers: { 'Content-Type': 'application/json', ...options?.headers }, - }); - if (!res.ok) throw new Error(`Ollama ${path}: ${res.status}`); - return res.json(); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + try { + const res = await fetch(`${OLLAMA_URL}${path}`, { + ...options, + signal: controller.signal, + headers: { 'Content-Type': 'application/json', ...options?.headers }, + }); + if (!res.ok) throw new Error(`Ollama ${path}: ${res.status}`); + return res.json(); + } finally { + clearTimeout(timeout); + } } export async function GET() { @@ -66,21 +73,6 @@ export async function POST(request: Request) { return NextResponse.json({ success: true, message: `Unloaded ${model}` }); } - if (action === 'generate') { - const res = await fetch(`${OLLAMA_URL}/api/generate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model, prompt: body.prompt, stream: false }), - }); - const data = await res.json(); - return NextResponse.json({ - success: true, - response: data.response, - eval_count: data.eval_count, - eval_duration: data.eval_duration, - }); - } - if (action === 'pull') { const res = await fetch(`${OLLAMA_URL}/api/pull`, { method: 'POST', diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index 157c99e5..80dc12ae 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -153,10 +153,10 @@ export default function Dashboard() { const [copied, setCopied] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(null); const responseRef = useRef(null); - const toastId = useRef(0); + const abortRef = useRef(null); const addToast = useCallback((message: string, type: Toast['type'] = 'info') => { - const id = ++toastId.current; + const id = Date.now() + Math.random(); setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000); }, []); @@ -180,21 +180,29 @@ export default function Dashboard() { }, [fetchAll]); useEffect(() => { + if (promptLoading || pullLoading) return; const interval = setInterval(fetchAll, 15000); return () => clearInterval(interval); - }, [fetchAll]); + }, [fetchAll, promptLoading, pullLoading]); - // Escape key closes modals + // Escape key closes modals (respects streaming state) useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { - setPromptModel(null); + if (promptLoading) { + abortRef.current?.abort(); + setPromptLoading(false); + } else { + setPromptModel(null); + setPromptResponse(''); + setPromptText(''); + } setDeleteConfirm(null); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, []); + }, [promptLoading]); const handleModelAction = async (action: string, model: string) => { setActionLoading(`${action}-${model}`); @@ -247,11 +255,14 @@ export default function Dashboard() { if (!promptModel || !promptText.trim()) return; setPromptLoading(true); setPromptResponse(''); + const controller = new AbortController(); + abortRef.current = controller; try { const res = await fetch('/api/ollama/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: promptModel, prompt: promptText }), + signal: controller.signal, }); if (!res.ok || !res.body) { setPromptResponse('Error: Failed to connect to Ollama'); @@ -284,8 +295,13 @@ export default function Dashboard() { } if (!fullResponse) setPromptResponse('(empty response)'); } catch (err) { - setPromptResponse(`Error: ${err}`); + if (controller.signal.aborted) { + // User cancelled — keep partial response + } else { + setPromptResponse(`Error: ${err}`); + } } + abortRef.current = null; setPromptLoading(false); }; @@ -314,7 +330,8 @@ export default function Dashboard() { Local LLM Mission Control

- Apple M4 Pro · 48 GB · {system?.platform || 'macOS'} + {system?.chip || 'Loading...'} · {formatBytes(system?.memory.total || 0)}{' '} + · {system?.platform || 'macOS'}

@@ -933,11 +950,16 @@ export default function Dashboard() { ))} - {(!system?.brewPackages || system.brewPackages.length === 0) && ( + {!system && (

Loading...

)} + {system && (!system.brewPackages || system.brewPackages.length === 0) && ( +

+ No tracked packages found +

+ )}