import { useState, useEffect, useCallback } from 'react'; import { Settings, Server, Cpu, Activity, MessageSquare, Send, RefreshCw, Check, X, Zap, Database, ChevronDown, ChevronUp, Loader2, AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { useToast } from '@/hooks/use-toast'; import { SmartComponentRenderer, parseMessageForComponents, isUIComponentMessage, UIComponentMessage, } from '@/components/smart-ui'; interface LocalAIModel { id: string; object: string; owned_by?: string; } interface ChatMessage { role: 'user' | 'assistant'; content: string; uiComponents?: UIComponentMessage[]; } interface LocalAIStatus { connected: boolean; status?: string; error?: string; } export default function LocalAIWidget() { const { toast } = useToast(); const [endpoint, setEndpoint] = useState(() => localStorage.getItem('localai-endpoint') || 'http://localhost:8080' ); const [tempEndpoint, setTempEndpoint] = useState(endpoint); const [showSettings, setShowSettings] = useState(false); const [status, setStatus] = useState({ connected: false }); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState('status'); // Chat state const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [isSending, setIsSending] = useState(false); // Check LocalAI status const checkStatus = useCallback(async () => { setIsLoading(true); try { // Try health endpoint first const healthResponse = await fetch(`${endpoint}/readyz`, { method: 'GET', signal: AbortSignal.timeout(5000), }); if (healthResponse.ok) { setStatus({ connected: true, status: 'healthy' }); } else { // Try alternative health check const altResponse = await fetch(`${endpoint}/v1/models`, { method: 'GET', signal: AbortSignal.timeout(5000), }); if (altResponse.ok) { setStatus({ connected: true, status: 'healthy' }); } else { setStatus({ connected: false, error: 'Server unhealthy' }); } } } catch (error) { setStatus({ connected: false, error: error instanceof Error ? error.message : 'Connection failed' }); } finally { setIsLoading(false); } }, [endpoint]); // Fetch available models const fetchModels = useCallback(async () => { try { const response = await fetch(`${endpoint}/v1/models`, { method: 'GET', signal: AbortSignal.timeout(10000), }); if (response.ok) { const data = await response.json(); const modelList = data.data || []; setModels(modelList); if (modelList.length > 0 && !selectedModel) { setSelectedModel(modelList[0].id); } } } catch (error) { console.error('Failed to fetch models:', error); } }, [endpoint, selectedModel]); // Send chat message const sendMessage = async () => { if (!inputMessage.trim() || !selectedModel || isSending) return; const userMessage: ChatMessage = { role: 'user', content: inputMessage }; setMessages(prev => [...prev, userMessage]); setInputMessage(''); setIsSending(true); try { const response = await fetch(`${endpoint}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: selectedModel, messages: [...messages, userMessage].map(m => ({ role: m.role, content: m.content })), max_tokens: 500, }), signal: AbortSignal.timeout(30000), }); if (response.ok) { const data = await response.json(); const assistantContent = data.choices?.[0]?.message?.content || 'No response'; setMessages(prev => [...prev, { role: 'assistant', content: assistantContent }]); } else { throw new Error('Failed to get response'); } } catch (error) { toast({ title: "Error", description: error instanceof Error ? error.message : 'Failed to send message', variant: "destructive", }); // Remove the user message if it failed setMessages(prev => prev.slice(0, -1)); } finally { setIsSending(false); } }; // Save endpoint const saveEndpoint = () => { setEndpoint(tempEndpoint); localStorage.setItem('localai-endpoint', tempEndpoint); setShowSettings(false); setStatus({ connected: false }); setModels([]); toast({ title: "Endpoint opdateret", description: `Forbinder til ${tempEndpoint}`, }); }; // Initial check useEffect(() => { checkStatus(); fetchModels(); }, [checkStatus, fetchModels]); return (
{/* Header */}
{status.connected ? 'CONNECTED' : 'DISCONNECTED'}
{/* Settings Panel */} {showSettings && (
setTempEndpoint(e.target.value)} placeholder="http://localhost:8080" className="h-7 text-xs font-mono bg-background/50" />
)} {/* Main Content */} Status Models Chat {/* Status Tab */}
Server {status.connected ? 'Online' : 'Offline'}
Endpoint {endpoint}
Models {models.length}
{status.error && (
{status.error}
)}
{/* Models Tab */} {models.length === 0 ? (
{status.connected ? 'Ingen modeller fundet' : 'Forbind til LocalAI først'}
) : (
{models.map((model) => (
setSelectedModel(model.id)} className={cn( "p-2 rounded cursor-pointer transition-colors text-xs font-mono", selectedModel === model.id ? "bg-primary/20 border border-primary/30" : "bg-background/50 hover:bg-background/80" )} >
{model.id} {selectedModel === model.id && }
))}
)}
{/* Chat Tab */} {!status.connected ? (
Forbind til LocalAI først
) : !selectedModel ? (
Vælg en model først
) : ( <>
{messages.length === 0 ? (
Start en samtale med {selectedModel}
) : ( messages.map((msg, i) => { // Parse message for UI components const parts = parseMessageForComponents(msg.content); const hasComponents = parts.some(p => isUIComponentMessage(p)); return (
{msg.role} {hasComponents && ( UI )}
{parts.map((part, j) => { if (isUIComponentMessage(part)) { return ( ); } // Render text parts return part ? (
{part}
) : null; })}
); }) )} {isSending && (
Tænker...
)}
setInputMessage(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && sendMessage()} placeholder="Skriv besked..." className="h-7 text-xs bg-background/50" disabled={isSending} />
)}
); }