/** * React Hooks for LLM & Grafana Integration * * Usage: * import { useChat, useLLMMetrics } from '@/lib/hooks'; * * function ChatComponent() { * const { messages, send, isLoading } = useChat(); * // ... * } */ import { useState, useCallback, useRef, useEffect } from 'react'; export interface Message { role: 'system' | 'user' | 'assistant'; content: string; } export interface UseChatOptions { model?: 'reasoning' | 'ornith:35b' | 'qwen2.5:3b-instruct'; systemPrompt?: string; onError?: (error: Error) => void; } export interface UseChatReturn { messages: Message[]; send: (content: string) => Promise; isLoading: boolean; error: Error | null; clear: () => void; } /** * Chat hook with streaming support */ export function useChat(options: UseChatOptions = {}): UseChatReturn { const { model = 'reasoning', systemPrompt, onError } = options; const [messages, setMessages] = useState( systemPrompt ? [{ role: 'system', content: systemPrompt }] : [] ); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const abortRef = useRef(null); const send = useCallback( async (content: string) => { const userMessage: Message = { role: 'user', content }; setMessages((prev) => [...prev, userMessage]); setIsLoading(true); setError(null); abortRef.current = new AbortController(); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages: [...messages, userMessage], stream: true, }), signal: abortRef.current.signal, }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) throw new Error('No response body'); const decoder = new TextDecoder(); let assistantContent = ''; // Add placeholder assistant message setMessages((prev) => [...prev, { role: 'assistant', content: '' }]); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value, { stream: true }); const lines = text.split('\n').filter((line) => line.startsWith('data: ')); for (const line of lines) { const data = line.slice(6); if (data === '[DONE]') break; try { const parsed = JSON.parse(data); if (parsed.content) { assistantContent += parsed.content; setMessages((prev) => { const updated = [...prev]; updated[updated.length - 1] = { role: 'assistant', content: assistantContent, }; return updated; }); } } catch { // Skip malformed chunks } } } } catch (err) { if (err instanceof Error && err.name !== 'AbortError') { setError(err); onError?.(err); } } finally { setIsLoading(false); abortRef.current = null; } }, [messages, model, onError] ); const clear = useCallback(() => { abortRef.current?.abort(); setMessages(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []); setError(null); }, [systemPrompt]); return { messages, send, isLoading, error, clear }; } export interface LLMMetrics { podsReady: number; cpuByPod: Record; memoryByPod: Record; } /** * Metrics hook with auto-refresh */ export function useLLMMetrics(refreshInterval = 30000) { const [metrics, setMetrics] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const fetchMetrics = useCallback(async () => { try { const response = await fetch('/api/metrics'); if (!response.ok) throw new Error(`API error: ${response.status}`); const data = await response.json(); setMetrics(data); setError(null); } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsLoading(false); } }, []); useEffect(() => { fetchMetrics(); const interval = setInterval(fetchMetrics, refreshInterval); return () => clearInterval(interval); }, [fetchMetrics, refreshInterval]); return { metrics, isLoading, error, refresh: fetchMetrics }; } /** * Grafana iframe embed component props generator */ export function useGrafanaEmbed(dashboardUid: string, panelId?: number) { const baseUrl = process.env.NEXT_PUBLIC_GRAFANA_URL || 'https://grafana.riotpiao.com'; const dashboardUrl = `${baseUrl}/d/${dashboardUid}`; const embedUrl = panelId ? `${baseUrl}/d-solo/${dashboardUid}?panelId=${panelId}&from=now-6h&to=now&refresh=30s` : null; return { dashboardUrl, embedUrl, iframeProps: embedUrl ? { src: embedUrl, width: '100%', height: '400', frameBorder: 0, style: { border: 'none' }, } : null, }; }