"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { flushSync } from "react-dom"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Trash2 } from "lucide-react"; 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 { Model } from "llama-stack-client/resources/models"; import type { TurnCreateParams } from "llama-stack-client/resources/agents/turn"; import { SessionUtils, type ChatSession, } from "@/components/chat-playground/conversations"; 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 [selectedModel, setSelectedModel] = useState(""); const [modelsLoading, setModelsLoading] = useState(true); const [modelsError, setModelsError] = useState(null); const [agents, setAgents] = useState< Array<{ agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }> >([]); const [selectedAgentConfig, setSelectedAgentConfig] = useState<{ toolgroups?: Array< string | { name: string; args: Record } >; } | null>(null); const [selectedAgentId, setSelectedAgentId] = useState(""); const [agentsLoading, setAgentsLoading] = useState(true); const [showCreateAgent, setShowCreateAgent] = useState(false); const [newAgentName, setNewAgentName] = useState(""); const [newAgentInstructions, setNewAgentInstructions] = useState( "You are a helpful assistant." ); const [selectedToolgroups, setSelectedToolgroups] = useState([]); const [availableToolgroups, setAvailableToolgroups] = useState< Array<{ identifier: string; provider_id: string; type: string; provider_resource_id?: string; }> >([]); const client = useAuthClient(); const abortControllerRef = useRef(null); const isModelsLoading = modelsLoading ?? true; const loadAgentConfig = useCallback( async (agentId: string) => { try { console.log("Loading agent config for:", agentId); // try to load from cache first const cachedConfig = SessionUtils.loadAgentConfig(agentId); if (cachedConfig) { console.log("✅ Loaded agent config from cache:", cachedConfig); setSelectedAgentConfig({ toolgroups: cachedConfig.toolgroups, }); return; } console.log("📡 Fetching agent config from API..."); const agentDetails = await client.agents.retrieve(agentId); console.log("Agent details retrieved:", agentDetails); console.log("Agent config:", agentDetails.agent_config); console.log("Agent toolgroups:", agentDetails.agent_config?.toolgroups); // cache the config SessionUtils.saveAgentConfig(agentId, agentDetails.agent_config); setSelectedAgentConfig({ toolgroups: agentDetails.agent_config?.toolgroups, }); } catch (error) { console.error("Error loading agent config:", error); setSelectedAgentConfig(null); } }, [client] ); const createDefaultSession = useCallback( async (agentId: string) => { try { const response = await client.agents.session.create(agentId, { session_name: "Default Session", }); const defaultSession: ChatSession = { id: response.session_id, name: "Default Session", messages: [], selectedModel: selectedModel, // Use current selected model systemMessage: "You are a helpful assistant.", agentId, createdAt: Date.now(), updatedAt: Date.now(), }; setCurrentSession(defaultSession); console.log( `💾 Saving default session ID for agent ${agentId}:`, defaultSession.id ); SessionUtils.saveCurrentSessionId(defaultSession.id, agentId); // cache entire session data SessionUtils.saveSessionData(agentId, defaultSession); } catch (error) { console.error("Error creating default session:", error); } }, [client, selectedModel] ); const loadSessionMessages = useCallback( async (agentId: string, sessionId: string): Promise => { try { const session = await client.agents.session.retrieve( agentId, sessionId ); if (!session || !session.turns || !Array.isArray(session.turns)) { return []; } const messages: Message[] = []; for (const turn of session.turns) { // add user messages if (turn.input_messages && Array.isArray(turn.input_messages)) { for (const input of turn.input_messages) { if (input.role === "user" && input.content) { messages.push({ id: `${turn.turn_id}-user-${messages.length}`, role: "user", content: typeof input.content === "string" ? input.content : JSON.stringify(input.content), createdAt: new Date(turn.started_at || Date.now()), }); } } } // add assistant message from output_message if (turn.output_message && turn.output_message.content) { messages.push({ id: `${turn.turn_id}-assistant-${messages.length}`, role: "assistant", content: typeof turn.output_message.content === "string" ? turn.output_message.content : JSON.stringify(turn.output_message.content), createdAt: new Date( turn.completed_at || turn.started_at || Date.now() ), }); } } return messages; } catch (error) { console.error("Error loading session messages:", error); return []; } }, [client] ); const loadAgentSessions = useCallback( async (agentId: string) => { try { console.log("Loading sessions for agent:", agentId); const response = await client.agents.session.list(agentId); console.log("Available sessions:", response.data); if ( response.data && Array.isArray(response.data) && response.data.length > 0 ) { // check for a previously saved session ID for this specific agent const savedSessionId = SessionUtils.loadCurrentSessionId(agentId); console.log(`Saved session ID for agent ${agentId}:`, savedSessionId); // try to load cached session data first if (savedSessionId) { const cachedSession = SessionUtils.loadSessionData( agentId, savedSessionId ); if (cachedSession) { console.log("✅ Loaded session from cache:", cachedSession.id); setCurrentSession(cachedSession); SessionUtils.saveCurrentSessionId(cachedSession.id, agentId); return; } console.log("📡 Cache miss, fetching session from API..."); } let sessionToLoad = response.data[0] as { session_id: string; session_name?: string; started_at?: string; }; console.log( "Default session to load (first in list):", sessionToLoad.session_id ); // try to find saved session id in available sessions if (savedSessionId) { const foundSession = response.data.find( (s: { session_id: string }) => s.session_id === savedSessionId ); console.log("Found saved session in list:", foundSession); if (foundSession) { sessionToLoad = foundSession as { session_id: string; session_name?: string; started_at?: string; }; console.log( "✅ Restored previously selected session:", savedSessionId ); } else { console.log( "❌ Previously selected session not found, using latest session" ); } } else { console.log("❌ No saved session ID found, using latest session"); } const messages = await loadSessionMessages( agentId, sessionToLoad.session_id ); const session: ChatSession = { id: sessionToLoad.session_id, name: sessionToLoad.session_name || "Session", messages, selectedModel: selectedModel || "", // Preserve current model or use empty systemMessage: "You are a helpful assistant.", agentId, createdAt: sessionToLoad.started_at ? new Date(sessionToLoad.started_at).getTime() : Date.now(), updatedAt: Date.now(), }; setCurrentSession(session); console.log(`💾 Saving session ID for agent ${agentId}:`, session.id); SessionUtils.saveCurrentSessionId(session.id, agentId); // cache session data SessionUtils.saveSessionData(agentId, session); } else { // no sessions, create a new one await createDefaultSession(agentId); } } catch (error) { console.error("Error loading agent sessions:", error); // fallback to creating a new session await createDefaultSession(agentId); } }, [client, loadSessionMessages, createDefaultSession, selectedModel] ); useEffect(() => { const fetchAgents = async () => { try { setAgentsLoading(true); const agentList = await client.agents.list(); setAgents( (agentList.data as Array<{ agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }>) || [] ); if (agentList.data && agentList.data.length > 0) { // check if there's a previously selected agent const savedAgentId = SessionUtils.loadCurrentAgentId(); let agentToSelect = agentList.data[0] as { agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }; // if we have a saved agent ID, find it in the available agents if (savedAgentId) { const foundAgent = agentList.data.find( (a: { agent_id: string }) => a.agent_id === savedAgentId ); if (foundAgent) { agentToSelect = foundAgent as typeof agentToSelect; } else { console.log("Previously slelected agent not found:"); } } setSelectedAgentId(agentToSelect.agent_id); SessionUtils.saveCurrentAgentId(agentToSelect.agent_id); // load agent config immediately await loadAgentConfig(agentToSelect.agent_id); // Note: loadAgentSessions will be called after models are loaded } } catch (error) { console.error("Error fetching agents:", error); } finally { setAgentsLoading(false); } }; fetchAgents(); // fetch available toolgroups const fetchToolgroups = async () => { try { console.log("Fetching toolgroups..."); const toolgroups = await client.toolgroups.list(); console.log("Toolgroups response:", toolgroups); // The client returns data directly, not wrapped in .data const toolGroupsArray = Array.isArray(toolgroups) ? toolgroups : toolgroups && typeof toolgroups === "object" && "data" in toolgroups && Array.isArray((toolgroups as { data: unknown }).data) ? ( toolgroups as { data: Array<{ identifier: string; provider_id: string; type: string; provider_resource_id?: string; }>; } ).data : []; if (toolGroupsArray && Array.isArray(toolGroupsArray)) { setAvailableToolgroups(toolGroupsArray); console.log("Set toolgroups:", toolGroupsArray); } else { console.error("Invalid toolgroups data format:", toolgroups); } } catch (error) { console.error("Error fetching toolgroups:", error); if (error instanceof Error) { console.error("Error details:", { name: error.name, message: error.message, stack: error.stack, }); } } }; fetchToolgroups(); }, [client, loadAgentSessions, loadAgentConfig]); const createNewAgent = useCallback( async ( name: string, instructions: string, model: string, toolgroups: string[] = [] ) => { try { console.log("Creating agent with toolgroups:", toolgroups); const agentConfig = { model, instructions, name: name || undefined, enable_session_persistence: true, toolgroups: toolgroups.length > 0 ? toolgroups : undefined, }; console.log("Agent config being sent:", agentConfig); const response = await client.agents.create({ agent_config: agentConfig, }); // refresh agents list const agentList = await client.agents.list(); setAgents( (agentList.data as Array<{ agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }>) || [] ); // set the new agent as selected setSelectedAgentId(response.agent_id); await loadAgentConfig(response.agent_id); await loadAgentSessions(response.agent_id); return response.agent_id; } catch (error) { console.error("Error creating agent:", error); throw error; } }, [client, loadAgentSessions, loadAgentConfig] ); const deleteAgent = useCallback( async (agentId: string) => { if (agents.length <= 1) { return; } if ( confirm( "Are you sure you want to delete this agent? This action cannot be undone and will delete all associated sessions." ) ) { try { await client.agents.delete(agentId); // clear cached data for agent SessionUtils.clearAgentCache(agentId); // Refresh agents list const agentList = await client.agents.list(); setAgents( (agentList.data as Array<{ agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }>) || [] ); // if we deleted the current agent, switch to another one if (selectedAgentId === agentId) { const remainingAgents = agentList.data?.filter( (a: { agent_id: string }) => a.agent_id !== agentId ); if (remainingAgents && remainingAgents.length > 0) { const newAgent = remainingAgents[0] as { agent_id: string; agent_config?: { agent_name?: string; name?: string; instructions?: string; }; [key: string]: unknown; }; setSelectedAgentId(newAgent.agent_id); SessionUtils.saveCurrentAgentId(newAgent.agent_id); await loadAgentConfig(newAgent.agent_id); await loadAgentSessions(newAgent.agent_id); } else { // No agents left setSelectedAgentId(""); setCurrentSession(null); setSelectedAgentConfig(null); } } } catch (error) { console.error("Error deleting agent:", error); } } }, [agents.length, client, selectedAgentId, loadAgentConfig, loadAgentSessions] ); const handleModelChange = useCallback((newModel: string) => { setSelectedModel(newModel); setCurrentSession(prev => prev ? { ...prev, selectedModel: newModel, updatedAt: Date.now(), } : prev ); }, []); useEffect(() => { if (currentSession) { console.log( `💾 Auto-saving session ID for agent ${currentSession.agentId}:`, currentSession.id ); SessionUtils.saveCurrentSessionId( currentSession.id, currentSession.agentId ); // cache session data SessionUtils.saveSessionData(currentSession.agentId, currentSession); // only update selectedModel if the session has a valid model and it's different from current if ( currentSession.selectedModel && currentSession.selectedModel !== selectedModel ) { setSelectedModel(currentSession.selectedModel); } } }, [currentSession, selectedModel]); useEffect(() => { const fetchModels = async () => { try { setModelsLoading(true); setModelsError(null); const modelList = await client.models.list(); const llmModels = modelList.filter(model => model.model_type === "llm"); setModels(llmModels); if (llmModels.length > 0) { handleModelChange(llmModels[0].identifier); } } catch (err) { console.error("Error fetching models:", err); setModelsError("Failed to fetch available models"); } finally { setModelsLoading(false); } }; fetchModels(); }, [client, handleModelChange]); // load agent sessions after both agents and models are ready useEffect(() => { if ( selectedAgentId && !agentsLoading && !modelsLoading && selectedModel && !currentSession ) { loadAgentSessions(selectedAgentId); } }, [ selectedAgentId, agentsLoading, modelsLoading, selectedModel, currentSession, loadAgentSessions, ]); const handleInputChange = (e: React.ChangeEvent) => { setInput(e.target.value); }; const handleSubmit = async (event?: { preventDefault?: () => void }) => { event?.preventDefault?.(); if (!input.trim()) return; const userMessage: Message = { id: Date.now().toString(), role: "user", content: input.trim(), createdAt: new Date(), }; setCurrentSession(prev => { if (!prev) return prev; const updatedSession = { ...prev, messages: [...prev.messages, userMessage], updatedAt: Date.now(), }; // Update cache with new message SessionUtils.saveSessionData(prev.agentId, updatedSession); return updatedSession; }); setInput(""); await handleSubmitWithContent(userMessage.content); }; const handleSubmitWithContent = async (content: string) => { if (!currentSession || !selectedAgentId) return; setIsGenerating(true); setError(null); if (abortControllerRef.current) { abortControllerRef.current.abort(); } const abortController = new AbortController(); abortControllerRef.current = abortController; try { const userMessage = { role: "user" as const, content, }; const turnParams: TurnCreateParams = { messages: [userMessage], stream: true, }; const response = await client.agents.turn.create( selectedAgentId, currentSession.id, turnParams, { signal: abortController.signal, } as { signal: AbortSignal } ); const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: "", createdAt: new Date(), }; const extractDeltaText = (chunk: unknown): string | null => { // this is an awful way to handle different chunk formats, but i'm not sure if there's much of a better way if (chunk?.delta?.text && typeof chunk.delta.text === "string") { return chunk.delta.text; } if ( chunk?.event?.delta?.text && typeof chunk.event.delta.text === "string" ) { return chunk.event.delta.text; } if ( chunk?.choices?.[0]?.delta?.content && typeof chunk.choices[0].delta.content === "string" ) { return chunk.choices[0].delta.content; } if (typeof chunk === "string") { return chunk; } if ( chunk?.event?.payload?.delta?.text && typeof chunk.event.payload.delta.text === "string" ) { return chunk.event.payload.delta.text; } if (process.env.NODE_ENV !== "production") { console.debug("Unrecognized chunk format:", chunk); } return null; }; setCurrentSession(prev => { if (!prev) return null; const updatedSession = { ...prev, messages: [...prev.messages, assistantMessage], updatedAt: Date.now(), }; // update cache with assistant message SessionUtils.saveSessionData(prev.agentId, updatedSession); return updatedSession; }); let fullContent = ""; for await (const chunk of response) { const deltaText = extractDeltaText(chunk); if (deltaText) { fullContent += deltaText; flushSync(() => { setCurrentSession(prev => { if (!prev) return null; const newMessages = [...prev.messages]; const last = newMessages[newMessages.length - 1]; if (last.role === "assistant") { last.content = fullContent; } const updatedSession = { ...prev, messages: newMessages, updatedAt: Date.now(), }; // update cache with streaming content (throttled) if (fullContent.length % 100 === 0) { // Only cache every 100 characters to avoid spam SessionUtils.saveSessionData(prev.agentId, updatedSession); } return updatedSession; }); }); } } } catch (err) { if (err instanceof Error && err.name === "AbortError") { console.log("Request aborted"); return; } 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); abortControllerRef.current = null; // cache final session state after streaming completes setCurrentSession(prev => { if (prev) { SessionUtils.saveSessionData(prev.agentId, prev); } return prev; }); } }; 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 = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; setIsGenerating(false); } setCurrentSession(prev => prev ? { ...prev, messages: [], updatedAt: Date.now() } : prev ); setError(null); }; return (
{/* Header */}

Agent Session

{!agentsLoading && agents.length > 0 && (
{selectedAgentId && agents.length > 1 && ( )}
)} {!agentsLoading && agents.length > 0 && ( )}
{/* Main Two-Column Layout */}
{/* Left Column - Configuration Panel */}

Settings

{/* Model Configuration */}

Model Configuration

{modelsError && (

{modelsError}

)}
{(selectedAgentId && agents.find(a => a.agent_id === selectedAgentId) ?.agent_config?.instructions) || "No agent selected"}

Instructions are set when creating an agent and cannot be changed.

{/* Agent Tools */}

Agent Tools

{selectedAgentConfig?.toolgroups && selectedAgentConfig.toolgroups.length > 0 ? ( selectedAgentConfig.toolgroups.map( ( toolgroup: | string | { name: string; args: Record }, index: number ) => { const toolName = typeof toolgroup === "string" ? toolgroup : toolgroup.name; const toolArgs = typeof toolgroup === "object" ? toolgroup.args : null; return (
{toolName} {toolName.includes("rag") ? "🔍 RAG" : toolName.includes("search") ? "🌐 Search" : "🔧 Tool"}
{toolArgs && Object.keys(toolArgs).length > 0 && (
Args:{" "} {Object.entries(toolArgs) .map( ([key, value]) => `${key}: ${JSON.stringify(value)}` ) .join(", ")}
)}
); } ) ) : (

No tools configured

This agent only has text generation capabilities

)}

Tools are configured when creating an agent and provide additional capabilities like web search, math calculations, or RAG document retrieval.

{/* Right Column - Chat Interface */}
{error && (

{error}

)} setCurrentSession(prev => prev ? { ...prev, messages, updatedAt: Date.now() } : prev ) } />
{/* Create Agent Modal */} {showCreateAgent && (

Create New Agent

setNewAgentName(e.target.value)} placeholder="My Custom Agent" />