import { useState, useRef, useEffect, useCallback } from 'react'; import { cn } from '@/lib/utils'; import { Send, Loader2, Swords, Sparkles } from 'lucide-react'; interface PlaygroundTabProps { providerIds: string[]; } interface ModelEntry { id: string; providerId: string; } interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; } export function PlaygroundTab({ providerIds }: PlaygroundTabProps) { const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [selectedProvider, setSelectedProvider] = useState(''); const [temperature, setTemperature] = useState(0.7); const [topP, setTopP] = useState(0.9); const [maxTokens, setMaxTokens] = useState(1024); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [streaming, setStreaming] = useState(false); const [abMode, setAbMode] = useState(false); const [modelB, setModelB] = useState(''); const [providerB, setProviderB] = useState(''); const [responseA, setResponseA] = useState(''); const [responseB, setResponseB] = useState(''); const [streamingAb, setStreamingAb] = useState(false); const messagesEndRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useEffect(() => { fetchModels(); }, []); const fetchModels = useCallback(async () => { try { const res = await fetch('/api/control/playground/models'); if (!res.ok) return; const data = await res.json() as { models: Array<{ providerId: string; models: string[] }> }; const flattened: ModelEntry[] = []; for (const group of data.models) { for (const m of group.models) { flattened.push({ id: m, providerId: group.providerId }); } } setModels(flattened); if (flattened.length > 0 && !selectedModel) { const first = flattened[0]; if (first) { setSelectedModel(first.id); setSelectedProvider(first.providerId); } } } catch { // silent } }, [selectedModel]); const groupedModels = models.reduce((acc, m) => { if (!acc[m.providerId]) { acc[m.providerId] = []; } const group = acc[m.providerId]; if (group) { group.push(m); } return acc; }, {} as Record); const handleSend = async () => { if (!input.trim() || !selectedModel || streaming) return; const userMsg: ChatMessage = { role: 'user', content: input.trim() }; const newMessages = [...messages, userMsg, { role: 'assistant' as const, content: '' }]; setMessages(newMessages); setInput(''); setStreaming(true); try { const chatMessages = newMessages.slice(0, -1).map((m) => ({ role: m.role, content: m.content, })); const res = await fetch('/api/control/playground/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ providerId: selectedProvider, model: selectedModel, messages: chatMessages, temperature, topP, maxTokens, }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); setMessages((prev) => [...prev.slice(0, -1), { role: 'assistant', content: `Error: ${err.error || 'Request failed'}` }]); setStreaming(false); return; } const reader = res.body?.getReader(); if (!reader) { setStreaming(false); return; } const decoder = new TextDecoder(); let buffer = ''; let assistantContent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (trimmed === 'data: [DONE]') continue; const jsonStr = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; try { const parsed = JSON.parse(jsonStr); const delta = parsed.choices?.[0]?.delta?.content; if (delta) { assistantContent += delta; setMessages((prev) => { const updated = [...prev]; updated[updated.length - 1] = { role: 'assistant', content: assistantContent }; return updated; }); } } catch { // skip } } } setStreaming(false); } catch (err) { const msg = (err as Error).message ?? String(err); setMessages((prev) => [...prev.slice(0, -1), { role: 'assistant', content: `Error: ${msg}` }]); setStreaming(false); } }; const handleABCompare = async () => { if (!input.trim() || !selectedModel || !modelB || streamingAb) return; const userMsg: ChatMessage = { role: 'user', content: input.trim() }; setMessages([...messages, userMsg]); setInput(''); setResponseA(''); setResponseB(''); setStreamingAb(true); try { const res = await fetch('/api/control/playground/chat-ab', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ providerIdA: selectedProvider, modelA: selectedModel, providerIdB: providerB, modelB, messages: [...messages, userMsg], temperature, topP, maxTokens, }), }); if (!res.ok) { setStreamingAb(false); return; } const reader = res.body?.getReader(); if (!reader) { setStreamingAb(false); return; } const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const jsonStr = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; try { const parsed = JSON.parse(jsonStr); if (parsed.done) { if (parsed.lane === 'A') setStreamingAb((p) => { // Check if B is also done elsewhere return p; }); continue; } if (parsed.raw) { const innerStr = parsed.raw.startsWith('data: ') ? parsed.raw.slice(6) : parsed.raw; const inner = JSON.parse(innerStr); const delta = inner.choices?.[0]?.delta?.content; if (delta) { if (parsed.lane === 'A') { setResponseA((p) => p + delta); } else { setResponseB((p) => p + delta); } } } } catch { // skip } } } setStreamingAb(false); } catch { setStreamingAb(false); } }; const getArenaBattleUrl = () => { const prompt = encodeURIComponent(input || messages[messages.length - 1]?.content || ''); const modelA = encodeURIComponent(selectedModel); const modelBParam = encodeURIComponent(modelB || ''); return `/arena?prompt=${prompt}&models=${modelA},${modelBParam}`; }; return (
{/* Model and param controls */}
setTemperature(parseFloat(e.target.value) || 0.7)} className="w-16 bg-muted/50 border border-border/50 rounded px-2 py-1 text-sm" />
setTopP(parseFloat(e.target.value) || 0.9)} className="w-16 bg-muted/50 border border-border/50 rounded px-2 py-1 text-sm" />
setMaxTokens(parseInt(e.target.value) || 1024)} className="w-20 bg-muted/50 border border-border/50 rounded px-2 py-1 text-sm" />
{/* A/B model B selector */} {abMode && (
)} {/* Chat area */}
{!abMode ? ( <> {/* Messages */}
{messages.map((msg, i) => (
{msg.content || (msg.role === 'assistant' && streaming ? : null)}
))}
{/* Input */}