diff --git a/__LOCAL_LLMs/dashboard/src/app/api/ollama/pull/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/ollama/pull/route.ts index 43b3b063..51fbfc24 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/ollama/pull/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/ollama/pull/route.ts @@ -1,6 +1,5 @@ import { NextRequest } from 'next/server'; - -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; +import { OLLAMA_URL } from '../../../lib/ollama-config'; export async function POST(request: NextRequest) { try { diff --git a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts index 6007ae7f..4cf60520 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/ollama/route.ts @@ -1,6 +1,5 @@ import { NextResponse } from 'next/server'; - -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; +import { OLLAMA_URL } from '../../lib/ollama-config'; async function fetchOllama(path: string, options?: RequestInit) { const controller = new AbortController(); @@ -52,11 +51,17 @@ export async function GET() { } } +const MODEL_NAME_RE = /^[a-zA-Z0-9._:/-]{1,256}$/; + export async function POST(request: Request) { try { const body = await request.json(); const { action, model } = body; + if (!model || typeof model !== 'string' || !MODEL_NAME_RE.test(model)) { + return NextResponse.json({ error: 'Invalid model name' }, { status: 400 }); + } + if (action === 'load') { await fetchOllama('/api/generate', { method: 'POST', diff --git a/__LOCAL_LLMs/dashboard/src/app/api/ollama/stream/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/ollama/stream/route.ts index 6ffc50e4..4add315c 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/ollama/stream/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/ollama/stream/route.ts @@ -1,6 +1,5 @@ import { NextRequest } from 'next/server'; - -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; +import { OLLAMA_URL } from '../../../lib/ollama-config'; export async function POST(request: NextRequest) { try { diff --git a/__LOCAL_LLMs/dashboard/src/app/api/system/route.ts b/__LOCAL_LLMs/dashboard/src/app/api/system/route.ts index ab279f25..5d44624b 100644 --- a/__LOCAL_LLMs/dashboard/src/app/api/system/route.ts +++ b/__LOCAL_LLMs/dashboard/src/app/api/system/route.ts @@ -1,9 +1,10 @@ import { NextResponse } from 'next/server'; -import { exec } from 'child_process'; +import { exec, execFile } from 'child_process'; import { promisify } from 'util'; import os from 'os'; const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); // Cache slow commands (chip, gpu, brew don't change during a session) let staticCache: { @@ -64,7 +65,7 @@ async function getBrewPackages(): Promise = []; for (const pkg of targets) { try { - const { stdout } = await execAsync(`brew list --versions ${pkg} 2>/dev/null`, { + const { stdout } = await execFileAsync('brew', ['list', '--versions', pkg], { timeout: 3000, }); const parts = stdout.trim().split(' '); diff --git a/__LOCAL_LLMs/dashboard/src/app/components/ProgressBar.tsx b/__LOCAL_LLMs/dashboard/src/app/components/ProgressBar.tsx new file mode 100644 index 00000000..819d9a6e --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/components/ProgressBar.tsx @@ -0,0 +1,18 @@ +'use client'; + +export function ProgressBar({ + value, + max, + color = 'var(--accent-primary)', +}: { + value: number; + max: number; + color?: string; +}) { + const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0; + return ( +
+
+
+ ); +} diff --git a/__LOCAL_LLMs/dashboard/src/app/components/StatusDot.tsx b/__LOCAL_LLMs/dashboard/src/app/components/StatusDot.tsx new file mode 100644 index 00000000..02a5de2b --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/components/StatusDot.tsx @@ -0,0 +1,10 @@ +'use client'; + +export function StatusDot({ status }: { status: 'online' | 'offline' | 'warning' }) { + const colors = { + online: 'bg-[var(--success)]', + offline: 'bg-[var(--danger)]', + warning: 'bg-[var(--warning)]', + }; + return ; +} diff --git a/__LOCAL_LLMs/dashboard/src/app/error.tsx b/__LOCAL_LLMs/dashboard/src/app/error.tsx new file mode 100644 index 00000000..7477850c --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/error.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('Dashboard error:', error); + }, [error]); + + return ( +
+
+
+ ⚠️ +
+

+ Something went wrong +

+

+ {error.message || 'An unexpected error occurred in the dashboard.'} +

+
+          {error.stack?.split('\n').slice(0, 5).join('\n')}
+        
+ +
+
+ ); +} diff --git a/__LOCAL_LLMs/dashboard/src/app/lib/format.ts b/__LOCAL_LLMs/dashboard/src/app/lib/format.ts new file mode 100644 index 00000000..7ba29664 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/lib/format.ts @@ -0,0 +1,16 @@ +export function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +export function formatUptime(seconds: number): string { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}d ${h}h ${m}m`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} diff --git a/__LOCAL_LLMs/dashboard/src/app/lib/ollama-config.ts b/__LOCAL_LLMs/dashboard/src/app/lib/ollama-config.ts new file mode 100644 index 00000000..9a346df3 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/lib/ollama-config.ts @@ -0,0 +1 @@ +export const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; diff --git a/__LOCAL_LLMs/dashboard/src/app/lib/types.ts b/__LOCAL_LLMs/dashboard/src/app/lib/types.ts new file mode 100644 index 00000000..c9ad5f55 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/lib/types.ts @@ -0,0 +1,75 @@ +export interface OllamaModel { + name: string; + size: number; + digest: string; + modified_at: string; + details?: { + family?: string; + parameter_size?: string; + quantization_level?: string; + }; +} + +export interface RunningModel { + name: string; + size: number; + digest: string; + expires_at: string; + size_vram: number; +} + +export interface OllamaData { + status: string; + url: string; + models: OllamaModel[]; + running: RunningModel[]; + totalModels: number; + totalSize: number; + runningCount: number; +} + +export interface WhisperModel { + name: string; + size: number; + path: string; +} + +export interface WhisperData { + installed: boolean; + version: string; + binaries: string[]; + models: WhisperModel[]; + modelsDir: string; +} + +export interface SystemData { + chip: string; + gpu: string; + memory: { total: number; appMemory: number; cached: number; free: number; pressure: string }; + disk: { total: number; used: number; free: number }; + ollamaDiskUsage: number; + cpuCores: number; + uptime: number; + platform: string; + arch: string; + nodeVersion: string; + brewPackages: Array<{ name: string; version: string }>; +} + +export interface Toast { + id: number; + message: string; + type: 'success' | 'error' | 'info'; +} + +export interface PullProgress { + status: string; + completed: number; + total: number; +} + +export interface StreamMetrics { + tokensPerSec: number; + totalTokens: number; + durationMs: number; +} diff --git a/__LOCAL_LLMs/dashboard/src/app/page.tsx b/__LOCAL_LLMs/dashboard/src/app/page.tsx index 678710ba..6f39b6e4 100644 --- a/__LOCAL_LLMs/dashboard/src/app/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/page.tsx @@ -27,113 +27,17 @@ import { Send, Terminal, } from 'lucide-react'; - -interface OllamaModel { - name: string; - size: number; - digest: string; - modified_at: string; - details?: { - family?: string; - parameter_size?: string; - quantization_level?: string; - }; -} - -interface RunningModel { - name: string; - size: number; - digest: string; - expires_at: string; - size_vram: number; -} - -interface OllamaData { - status: string; - url: string; - models: OllamaModel[]; - running: RunningModel[]; - totalModels: number; - totalSize: number; - runningCount: number; -} - -interface WhisperModel { - name: string; - size: number; - path: string; -} - -interface WhisperData { - installed: boolean; - version: string; - binaries: string[]; - models: WhisperModel[]; - modelsDir: string; -} - -interface SystemData { - chip: string; - gpu: string; - memory: { total: number; appMemory: number; cached: number; free: number; pressure: string }; - disk: { total: number; used: number; free: number }; - ollamaDiskUsage: number; - cpuCores: number; - uptime: number; - platform: string; - arch: string; - nodeVersion: string; - brewPackages: Array<{ name: string; version: string }>; -} - -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; -} - -function formatUptime(seconds: number): string { - const d = Math.floor(seconds / 86400); - const h = Math.floor((seconds % 86400) / 3600); - const m = Math.floor((seconds % 3600) / 60); - if (d > 0) return `${d}d ${h}h ${m}m`; - if (h > 0) return `${h}h ${m}m`; - return `${m}m`; -} - -function StatusDot({ status }: { status: 'online' | 'offline' | 'warning' }) { - const colors = { - online: 'bg-[var(--success)]', - offline: 'bg-[var(--danger)]', - warning: 'bg-[var(--warning)]', - }; - return ; -} - -function ProgressBar({ - value, - max, - color = 'var(--accent-primary)', -}: { - value: number; - max: number; - color?: string; -}) { - const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0; - return ( -
-
-
- ); -} - -interface Toast { - id: number; - message: string; - type: 'success' | 'error' | 'info'; -} +import type { + OllamaData, + WhisperData, + SystemData, + Toast, + PullProgress, + StreamMetrics, +} from './lib/types'; +import { formatBytes, formatUptime } from './lib/format'; +import { StatusDot } from './components/StatusDot'; +import { ProgressBar } from './components/ProgressBar'; export default function Dashboard() { const [ollama, setOllama] = useState(null); @@ -150,17 +54,9 @@ export default function Dashboard() { const [toasts, setToasts] = useState([]); const [pullInput, setPullInput] = useState(''); const [pullLoading, setPullLoading] = useState(false); - const [pullProgress, setPullProgress] = useState<{ - status: string; - completed: number; - total: number; - } | null>(null); + const [pullProgress, setPullProgress] = useState(null); const [copied, setCopied] = useState(false); - const [streamMetrics, setStreamMetrics] = useState<{ - tokensPerSec: number; - totalTokens: number; - durationMs: number; - } | null>(null); + const [streamMetrics, setStreamMetrics] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const responseRef = useRef(null); const abortRef = useRef(null);