"use client"; import { useState, useEffect } from "react"; import { flushSync } from "react-dom"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Chat } from "@/components/chat-playground/chat"; import { type Message } from "@/components/chat-playground/chat-message"; import { useAuthClient } from "@/hooks/use-auth-client"; import type { CompletionCreateParams } from "llama-stack-client/resources/chat/completions"; import type { Model } from "llama-stack-client/resources/models"; import type { VectorDBListResponse } from "llama-stack-client/resources/vector-dbs"; import { VectorDbManager } from "@/components/vector-db/vector-db-manager-simple"; import { SessionManager, SessionUtils, } from "@/components/chat-playground/session-manager"; import { DocumentUploader } from "@/components/chat-playground/document-uploader"; /** * Unified Chat Playground * - Keeps session + system message + VectorDB/RAG & document upload from version B * - Preserves simple message flow & suggestions/append helpers from version A * - Uses a single state source of truth: currentSession */ interface ChatSession { id: string; name: string; messages: Message[]; selectedModel: string; selectedVectorDb: string; // "none" disables RAG systemMessage: string; createdAt: number; updatedAt: number; } export default function ChatPlaygroundPage() { const [currentSession, setCurrentSession] = useState( null ); const [input, setInput] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); const [models, setModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(true); const [modelsError, setModelsError] = useState(null); const [vectorDbs, setVectorDbs] = useState([]); const [vectorDbsLoading, setVectorDbsLoading] = useState(true); const [vectorDbsError, setVectorDbsError] = useState(null); const client = useAuthClient(); const isModelsLoading = modelsLoading ?? true; // --- Session bootstrapping --- useEffect(() => { const saved = SessionUtils.loadCurrentSession(); if (saved) { setCurrentSession(saved); } else { const def = SessionUtils.createDefaultSession(); // ensure defaults align with our fields const defaultSession: ChatSession = { ...def, selectedModel: "", selectedVectorDb: "none", systemMessage: def.systemMessage || "You are a helpful assistant.", }; setCurrentSession(defaultSession); SessionUtils.saveCurrentSession(defaultSession); } }, []); // Persist session on change useEffect(() => { if (currentSession) SessionUtils.saveCurrentSession(currentSession); }, [currentSession]); // --- Fetch models & vector DBs --- useEffect(() => { const fetchModels = async () => { try { setModelsLoading(true); setModelsError(null); const list = await client.models.list(); const llms = list.filter(m => m.model_type === "llm"); setModels(llms); if (llms.length > 0) { setCurrentSession(prev => prev && !prev.selectedModel ? { ...prev, selectedModel: llms[0].identifier, updatedAt: Date.now(), } : prev ); } } catch (e) { console.error("Error fetching models:", e); setModelsError("Failed to fetch available models"); } finally { setModelsLoading(false); } }; const fetchVectorDbs = async () => { try { setVectorDbsLoading(true); setVectorDbsError(null); const list = await client.vectorDBs.list(); setVectorDbs(list); // default to "none" if not set setCurrentSession(prev => prev && !prev.selectedVectorDb ? { ...prev, selectedVectorDb: "none", updatedAt: Date.now() } : prev ); } catch (e) { console.error("Error fetching vector DBs:", e); setVectorDbsError("Failed to fetch available vector databases"); } finally { setVectorDbsLoading(false); } }; fetchModels(); fetchVectorDbs(); }, [client]); // --- Utilities --- const extractTextContent = (content: unknown): string => { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .filter( item => item && typeof item === "object" && "type" in item && (item as { type: string }).type === "text" ) .map(item => item && typeof item === "object" && "text" in item ? String((item as { text: unknown }).text) : "" ) .join(""); } if ( content && typeof content === "object" && "type" in content && (content as { type: string }).type === "text" && "text" in content ) { return String((content as { text: unknown }).text) || ""; } return ""; }; // --- Handlers --- const handleInputChange = (e: React.ChangeEvent) => setInput(e.target.value); const handleSubmit = async (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); if (!input.trim() || !currentSession || !currentSession.selectedModel) return; const userMessage: Message = { id: Date.now().toString(), role: "user", content: input.trim(), createdAt: new Date(), }; setCurrentSession(prev => prev ? { ...prev, messages: [...prev.messages, userMessage], updatedAt: Date.now(), } : prev ); setInput(""); // Use the helper function with the content await handleSubmitWithContent(userMessage.content); }; const handleSubmitWithContent = async (content: string) => { setIsGenerating(true); setError(null); try { let enhancedContent = content; // --- RAG augmentation (optional) --- if ( currentSession?.selectedVectorDb && currentSession.selectedVectorDb !== "none" ) { try { const vectorResponse = await client.vectorIo.query({ query: content, vector_db_id: currentSession.selectedVectorDb, }); if (vectorResponse.chunks && vectorResponse.chunks.length > 0) { const context = vectorResponse.chunks .map(chunk => typeof chunk.content === "string" ? chunk.content : extractTextContent(chunk.content) ) .join("\n\n"); enhancedContent = `Please answer the following query using the context below.\n\nCONTEXT:\n${context}\n\nQUERY:\n${content}`; } } catch (vectorErr) { console.error("Error querying vector DB:", vectorErr); // proceed without augmentation } } const messageParams: CompletionCreateParams["messages"] = [ ...(currentSession?.systemMessage ? [{ role: "system" as const, content: currentSession.systemMessage }] : []), ...(currentSession?.messages || []).map(msg => { const msgContent = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content); if (msg.role === "user") return { role: "user" as const, content: msgContent }; if (msg.role === "assistant") return { role: "assistant" as const, content: msgContent }; return { role: "system" as const, content: msgContent }; }), { role: "user" as const, content: enhancedContent }, ]; const response = await client.chat.completions.create({ model: currentSession?.selectedModel || "", messages: messageParams, stream: true, }); const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: "", createdAt: new Date(), }; setCurrentSession(prev => prev ? { ...prev, messages: [...prev.messages, assistantMessage], updatedAt: Date.now() } : null); let fullContent = ""; for await (const chunk of response) { if (chunk.choices && chunk.choices[0]?.delta?.content) { const deltaContent = chunk.choices[0].delta.content; fullContent += deltaContent; flushSync(() => { setCurrentSession(prev => { if (!prev) return null; const newMessages = [...prev.messages]; const last = newMessages[newMessages.length - 1]; if (last.role === "assistant") last.content = fullContent; return { ...prev, messages: newMessages, updatedAt: Date.now() }; }); }); } } } catch (err) { console.error("Error sending message:", err); setError("Failed to send message. Please try again."); setCurrentSession(prev => prev ? { ...prev, messages: prev.messages.slice(0, -1), updatedAt: Date.now(), } : prev ); } finally { setIsGenerating(false); } }; // --- UX helpers --- const suggestions = [ "Write a Python function that prints 'Hello, World!'", "Explain step-by-step how to solve this math problem: If x² + 6x + 9 = 25, what is x?", "Design a simple algorithm to find the longest palindrome in a string.", ]; const append = (message: { role: "user"; content: string }) => { const newMessage: Message = { id: Date.now().toString(), role: message.role, content: message.content, createdAt: new Date(), }; setCurrentSession(prev => prev ? { ...prev, messages: [...prev.messages, newMessage], updatedAt: Date.now(), } : prev ); handleSubmitWithContent(newMessage.content); }; const clearChat = () => { setCurrentSession(prev => prev ? { ...prev, messages: [], updatedAt: Date.now() } : prev ); setError(null); }; const handleSessionChange = (session: ChatSession) => { setCurrentSession(session); setError(null); }; const handleNewSession = () => { const defaultModel = currentSession?.selectedModel || (models.length > 0 ? models[0].identifier : ""); const defaultVectorDb = currentSession?.selectedVectorDb || "none"; const newSession: ChatSession = { ...SessionUtils.createDefaultSession(), selectedModel: defaultModel, selectedVectorDb: defaultVectorDb, systemMessage: currentSession?.systemMessage || "You are a helpful assistant.", messages: [], updatedAt: Date.now(), createdAt: Date.now(), }; setCurrentSession(newSession); SessionUtils.saveCurrentSession(newSession); }; const refreshVectorDbs = async () => { try { setVectorDbsLoading(true); setVectorDbsError(null); const vectorDbList = await client.vectorDBs.list(); setVectorDbs(vectorDbList); } catch (err) { console.error("Error refreshing vector DBs:", err); setVectorDbsError("Failed to refresh vector databases"); } finally { setVectorDbsLoading(false); } }; return (
{/* Header */}

Chat Playground

{/* Main Two-Column Layout */}
{/* Left Column - Configuration Panel */}

Settings

{/* Model Configuration */}

Model Configuration

{modelsError && (

{modelsError}

)}