diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index dc9807f3..5242db58 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -35,6 +35,7 @@ import { Tag, Star, MessageSquare, + Settings, } from 'lucide-react'; import type { OllamaData, @@ -116,6 +117,18 @@ export default function Dashboard() { >({}); const [countdownTick, setCountdownTick] = useState(0); const [visionImages, setVisionImages] = useState([]); + const [showSettings, setShowSettings] = useState(false); + const [inferenceLog, setInferenceLog] = useState< + Array<{ + model: string; + prompt: string; + response: string; + tokPerSec?: number; + timestamp: number; + }> + >([]); + const [showInferenceLog, setShowInferenceLog] = useState(false); + const [inferenceSearch, setInferenceSearch] = useState(''); const responseRef = useRef(null); const abortRef = useRef(null); const compareAbortRef = useRef(null); @@ -179,6 +192,12 @@ export default function Dashboard() { } catch { /* ignore */ } + try { + const savedLog = localStorage.getItem('llm-inference-log'); + if (savedLog) setInferenceLog(JSON.parse(savedLog)); + } catch { + /* ignore */ + } }, []); useEffect(() => { @@ -575,6 +594,21 @@ export default function Dashboard() { } } if (!fullResponse) setPromptResponse('(empty response)'); + // F30: Save to inference log + if (promptModel && fullResponse) { + setInferenceLog(prev => { + const entry = { + model: promptModel, + prompt: promptText.trim(), + response: fullResponse, + tokPerSec: streamMetrics?.tokensPerSec, + timestamp: Date.now(), + }; + const updated = [entry, ...prev].slice(0, 100); + localStorage.setItem('llm-inference-log', JSON.stringify(updated)); + return updated; + }); + } } catch (err) { if (controller.signal.aborted) { // User cancelled — keep partial response @@ -592,6 +626,67 @@ export default function Dashboard() { setTimeout(() => setCopied(false), 2000); }; + // F29: Export all llm-* localStorage keys as JSON + const exportSettings = () => { + const data: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.startsWith('llm-')) { + data[key] = localStorage.getItem(key) || ''; + } + } + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `llm-dashboard-settings-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + addToast('Settings exported', 'success'); + }; + + // F29: Import settings from JSON file + const importSettings = (file: File) => { + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result as string); + if (typeof data !== 'object' || data === null) throw new Error('Invalid format'); + let count = 0; + for (const [key, value] of Object.entries(data)) { + if (key.startsWith('llm-') && typeof value === 'string') { + localStorage.setItem(key, value); + count++; + } + } + addToast(`Imported ${count} settings — refresh to apply`, 'success'); + } catch { + addToast('Invalid settings file', 'error'); + } + }; + reader.readAsText(file); + }; + + // F31: Clear all llm-* localStorage keys + const factoryReset = () => { + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.startsWith('llm-')) keysToRemove.push(key); + } + keysToRemove.forEach(k => localStorage.removeItem(k)); + setModelTags({}); + setAutoLoadModel(null); + setModelBenchmarks({}); + setInferenceLog([]); + setModelSort('name'); + setChatMessages([]); + setTheme('dark'); + document.documentElement.classList.remove('light'); + addToast(`Cleared ${keysToRemove.length} settings`, 'success'); + setShowSettings(false); + }; + // Chat mode streaming (F4) const handleChat = async () => { if (!promptModel || !promptText.trim()) return; @@ -729,6 +824,95 @@ export default function Dashboard() { Last refresh: {lastRefresh.toLocaleTimeString()} + {/* F30: Inference log toggle */} + + {/* F29/F31: Settings popover toggle */} +
+ + {showSettings && ( +
+

+ Settings +

+ + +
+ +
+ )} +
+ + +
+ {inferenceLog + .filter( + e => + !inferenceSearch || + e.prompt.toLowerCase().includes(inferenceSearch.toLowerCase()) || + e.model.toLowerCase().includes(inferenceSearch.toLowerCase()) + ) + .map((entry, i) => ( +
+
+ + {entry.model} + + + {entry.tokPerSec ? `${entry.tokPerSec.toFixed(1)} tok/s · ` : ''} + {new Date(entry.timestamp).toLocaleString()} + +
+

+ Q: {entry.prompt} +

+

+ A: {entry.response.slice(0, 200)} +

+ +
+ ))} + {inferenceLog.length === 0 && ( +

+ No inference history yet. Send a prompt to start recording. +

+ )} +
+ + )} + {/* Top Stats Row (CQ5: skeleton loading when data not yet loaded) */}
{loading && !ollama ? (