import { useState, useEffect } from "react"; import { AssistantInfo } from "@/types/chat"; import { getPresetsFromConfigs } from "@/config/assistants"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Label } from "@/components/ui/label"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Chat } from "@/components/ui/chat"; import { useChat } from "@/hooks/useChat"; import { Plus, Trash2, Save, Settings, Sliders, BookOpen, MessageSquare, ChevronLeft, ChevronRight, } from "lucide-react"; // Import refactored components import { ModelParametersTab, AssistantSelector, SystemInstructionsTab, DocumentsTab, } from "@/components/playground"; interface ModelInfo { model_name: string; name: string; supports_thinking: boolean; description: string; size_gb: string; is_loaded: boolean; type: "local" | "api"; } interface ModelsResponse { models: ModelInfo[]; current_model: string; } export function Playground() { // Chat functionality const { sessions, currentSessionId, selectSession, deleteSession, clearCurrentSession, messages, input, setInput, sendMessage, stopGeneration, isLoading, selectedModel, setSelectedModel, systemPrompt, setSystemPrompt, temperature, setTemperature, maxTokens, setMaxTokens, } = useChat(); // RAG configuration state const [ragEnabled, setRagEnabled] = useState(false); const [retrievalCount, setRetrievalCount] = useState(3); // UI state - sidebar collapse states const [sessionsCollapsed, setSessionsCollapsed] = useState(false); const [configCollapsed, setConfigCollapsed] = useState(false); const [autoLoadingModel, setAutoLoadingModel] = useState(null); const [showLoadConfirm, setShowLoadConfirm] = useState(false); const [pendingModelToLoad, setPendingModelToLoad] = useState(null); // Model management state const [models, setModels] = useState([]); // Saved assistants state const [savedAssistants, setSavedAssistants] = useState([]); const [selectedAssistant, setSelectedAssistant] = useState<{ id: string; name: string; type: "user" | "template" | "new"; originalTemplate?: string; } | null>(null); // Save assistant dialog state const [showSaveDialog, setShowSaveDialog] = useState(false); const [saveAssistantName, setSaveAssistantName] = useState(""); // Rename assistant dialog state const [showRenameDialog, setShowRenameDialog] = useState(false); const [renameAssistantName, setRenameAssistantName] = useState(""); // Load saved assistants const loadSavedAssistants = () => { try { const assistants = JSON.parse( localStorage.getItem("savedAssistants") || "[]" ); setSavedAssistants(assistants); } catch (error) { console.error("Failed to load saved assistants:", error); } }; // Open save assistant dialog const openSaveDialog = () => { if (!systemPrompt.trim()) return; // Set default name based on selected assistant or create a default let defaultName = "My Assistant"; if (selectedAssistant) { if (selectedAssistant.type === "new") { defaultName = selectedAssistant.name; } else if (selectedAssistant.type === "template") { defaultName = `My ${selectedAssistant.name}`; } else { defaultName = selectedAssistant.name + " Copy"; } } setSaveAssistantName(defaultName); setShowSaveDialog(true); }; // Actually save the assistant with user-defined name const confirmSaveAssistant = () => { if (!saveAssistantName.trim() || !systemPrompt.trim()) return; const newAssistant = { id: Date.now().toString(), name: saveAssistantName.trim(), systemPrompt, temperature, maxTokens, model: selectedModel, ragEnabled, retrievalCount, documents: [], // Assistant-specific documents (future: populated from DocumentsTab) createdAt: new Date().toISOString(), }; const updatedAssistants = [...savedAssistants, newAssistant]; setSavedAssistants(updatedAssistants); localStorage.setItem("savedAssistants", JSON.stringify(updatedAssistants)); // Update current selection to the newly saved assistant setSelectedAssistant({ id: newAssistant.id, name: newAssistant.name, type: "user", }); // Close dialog setShowSaveDialog(false); setSaveAssistantName(""); }; // Cancel save dialog const cancelSaveDialog = () => { setShowSaveDialog(false); setSaveAssistantName(""); }; // Load saved assistant const loadSavedAssistant = (assistantId: string) => { const assistant = savedAssistants.find((a) => a.id === assistantId); if (assistant) { setSystemPrompt(assistant.systemPrompt); setTemperature(assistant.temperature); setMaxTokens(assistant.maxTokens); // Load RAG settings with defaults for backwards compatibility setRagEnabled(assistant.ragEnabled || false); setRetrievalCount(assistant.retrievalCount || 3); if (assistant.model) { setSelectedModel(assistant.model); } setSelectedAssistant({ id: assistant.id, name: assistant.name, type: "user", }); } }; // Preset system prompts from shared config const systemPromptPresets = getPresetsFromConfigs(); // Handle preset selection const handlePresetSelect = (presetName: string) => { const preset = systemPromptPresets.find((p) => p.name === presetName); if (preset) { setSystemPrompt(preset.prompt); // Reset RAG settings to defaults for templates setRagEnabled(false); setRetrievalCount(3); setSelectedAssistant({ id: presetName, name: preset.name, type: "template", }); } }; // Create new assistant (clear all settings but keep current session) const createNewAssistant = () => { setSystemPrompt(""); setTemperature(0.7); setMaxTokens(1024); // Reset RAG settings to defaults setRagEnabled(false); setRetrievalCount(3); setSelectedAssistant({ id: "new_assistant", name: "New Assistant", type: "new", }); // Keep current session - only clear assistant settings }; // Clear current assistant const clearCurrentAssistant = () => { setSelectedAssistant(null); }; // Open rename assistant dialog const openRenameDialog = () => { if (selectedAssistant) { setRenameAssistantName(selectedAssistant.name); setShowRenameDialog(true); } }; // Confirm rename assistant const confirmRenameAssistant = () => { if (!selectedAssistant || !renameAssistantName.trim()) return; // Update selectedAssistant state const updatedAssistant = { ...selectedAssistant, name: renameAssistantName.trim(), }; setSelectedAssistant(updatedAssistant); // If it's a saved user assistant, update localStorage if (selectedAssistant.type === "user") { const updatedAssistants = savedAssistants.map((assistant) => assistant.id === selectedAssistant.id ? { ...assistant, name: renameAssistantName.trim() } : assistant ); setSavedAssistants(updatedAssistants); localStorage.setItem( "savedAssistants", JSON.stringify(updatedAssistants) ); } // Close dialog setShowRenameDialog(false); setRenameAssistantName(""); }; // Cancel rename dialog const cancelRenameDialog = () => { setShowRenameDialog(false); setRenameAssistantName(""); }; // Get current assistant information const getCurrentAssistantInfo = (): AssistantInfo => { return { name: selectedAssistant?.name || "Default Assistant", type: selectedAssistant?.type || "default", systemPrompt, temperature, maxTokens, originalTemplate: selectedAssistant?.originalTemplate, }; }; // Convert template to new assistant when settings change const convertTemplateToNew = () => { if (selectedAssistant && selectedAssistant.type === "template") { setSelectedAssistant({ id: "new_assistant", name: `Custom ${selectedAssistant.name}`, type: "new", originalTemplate: selectedAssistant.name, }); } }; // Wrapped setters that trigger template conversion const handleSystemPromptChange = (prompt: string) => { setSystemPrompt(prompt); convertTemplateToNew(); }; const handleTemperatureChange = (temp: number) => { setTemperature(temp); convertTemplateToNew(); }; const handleMaxTokensChange = (tokens: number) => { setMaxTokens(tokens); convertTemplateToNew(); }; const handleRagEnabledChange = (enabled: boolean) => { setRagEnabled(enabled); convertTemplateToNew(); }; const handleRetrievalCountChange = (count: number) => { setRetrievalCount(count); convertTemplateToNew(); }; // Load available models and saved assistants on startup useEffect(() => { fetchModels(); loadSavedAssistants(); // Check if there's a template configuration to load const loadConfig = localStorage.getItem("loadAssistantConfig"); if (loadConfig) { try { const config = JSON.parse(loadConfig); setSystemPrompt(config.systemPrompt || ""); setTemperature(config.temperature || 0.7); setMaxTokens(config.maxTokens || 1024); if (config.model) { setSelectedModel(config.model); } setSelectedAssistant({ id: "loaded_template", name: config.name || "Loaded Template", type: "template", }); // Clear the config after loading localStorage.removeItem("loadAssistantConfig"); } catch (error) { console.error("Failed to load assistant config:", error); localStorage.removeItem("loadAssistantConfig"); } } }, []); // Debug logs for Session issue useEffect(() => { console.log("Sidebar states:", { sessionsCollapsed, configCollapsed, sessionsCount: sessions.length, currentSessionId, }); }, [sessionsCollapsed, configCollapsed, sessions.length, currentSessionId]); useEffect(() => { console.log( "Rendering sessions:", sessions.length, sessions.map((s) => ({ id: s.id, title: s.title })) ); }, [sessions]); // Update selected model when models change useEffect(() => { // Only reset if the selected model no longer exists in the models list if (selectedModel && !models.find((m) => m.model_name === selectedModel)) { const firstModel = models[0]; if (firstModel) { setSelectedModel(firstModel.model_name); } } }, [models, selectedModel, setSelectedModel]); // Auto-load/unload local models when selection changes useEffect(() => { const handleModelChange = async () => { if (!selectedModel || !models.length) return; const selectedModelInfo = models.find( (m) => m.model_name === selectedModel ); if (!selectedModelInfo) return; const baseUrl = `${window.location.protocol}//${window.location.host}`; // If selected model is a local model and not loaded, show confirmation if (selectedModelInfo.type === "local" && !selectedModelInfo.is_loaded) { setPendingModelToLoad(selectedModelInfo); setShowLoadConfirm(true); return; // Don't auto-load, wait for user confirmation } // Unload other local models that are loaded but not selected const loadedLocalModels = models.filter( (m) => m.type === "local" && m.is_loaded && m.model_name !== selectedModel ); for (const model of loadedLocalModels) { try { const response = await fetch(`${baseUrl}/unload-model`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_name: model.model_name }), }); if (response.ok) { console.log(`✅ Auto-unloaded local model: ${model.model_name}`); } } catch (error) { console.error( `Error auto-unloading model ${model.model_name}:`, error ); } } // Refresh models after any unloading if (loadedLocalModels.length > 0) { fetchModels(); } }; handleModelChange(); }, [selectedModel, models]); const handleLoadModelConfirm = async () => { if (!pendingModelToLoad) return; setShowLoadConfirm(false); setAutoLoadingModel(pendingModelToLoad.model_name); try { const baseUrl = `${window.location.protocol}//${window.location.host}`; const response = await fetch(`${baseUrl}/load-model`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_name: pendingModelToLoad.model_name }), }); if (response.ok) { console.log( `✅ User confirmed and loaded: ${pendingModelToLoad.model_name}` ); fetchModels(); // Refresh model states } else { console.error( `❌ Failed to load model: ${pendingModelToLoad.model_name}` ); // Revert to an API model if load failed const apiModel = models.find((m) => m.type === "api"); if (apiModel) { setSelectedModel(apiModel.model_name); } } } catch (error) { console.error("Error loading model:", error); // Revert to an API model if error const apiModel = models.find((m) => m.type === "api"); if (apiModel) { setSelectedModel(apiModel.model_name); } } finally { setAutoLoadingModel(null); setPendingModelToLoad(null); } }; const handleLoadModelCancel = () => { setShowLoadConfirm(false); setPendingModelToLoad(null); // Revert to an API model const apiModel = models.find((m) => m.type === "api"); if (apiModel) { setSelectedModel(apiModel.model_name); } }; // Cleanup: unload all local models when component unmounts or user leaves useEffect(() => { const handlePageUnload = async () => { const baseUrl = `${window.location.protocol}//${window.location.host}`; const loadedLocalModels = models.filter( (m) => m.type === "local" && m.is_loaded ); for (const model of loadedLocalModels) { try { await fetch(`${baseUrl}/unload-model`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_name: model.model_name }), }); console.log(`✅ Cleanup: unloaded ${model.model_name}`); } catch (error) { console.error(`Error cleaning up model ${model.model_name}:`, error); } } }; // Cleanup on component unmount return () => { handlePageUnload(); }; }, [models]); const fetchModels = async () => { try { const baseUrl = `${window.location.protocol}//${window.location.host}`; const res = await fetch(`${baseUrl}/models`); if (res.ok) { const data: ModelsResponse = await res.json(); setModels(data.models); // Set selected model to current model if available, otherwise first API model if (data.current_model && selectedModel !== data.current_model) { setSelectedModel(data.current_model); } else if (!selectedModel && data.models.length > 0) { // Prefer API models as default const apiModel = data.models.find((m) => m.type === "api"); const defaultModel = apiModel || data.models[0]; setSelectedModel(defaultModel.model_name); } } } catch (err) { console.error("Failed to fetch models:", err); } }; return (
{/* Chat Sessions Sidebar */}
{!sessionsCollapsed && (

Chat Sessions

)}
{!sessionsCollapsed && ( )}
{sessionsCollapsed && ( )}
{!sessionsCollapsed && sessions.map((session) => ( { console.log( "Session card clicked:", session.id, session.title ); selectSession(session.id); }} >
{session.title}
{session.messages.length} messages
))} {sessionsCollapsed && sessions.map((session) => ( ))}
{/* Main Content */}
{/* Content Area - Responsive layout */}
{/* Chat Area */}
{/* Chat Messages and Input */} ({ id: msg.id, role: msg.role as "user" | "assistant" | "system", content: msg.content, createdAt: new Date(msg.timestamp), assistantInfo: msg.assistantInfo, }))} input={input} handleInputChange={(e) => setInput(e.target.value)} handleSubmit={async (e) => { e.preventDefault(); if ( !selectedModel || !models.find((m) => m.model_name === selectedModel) ) return; const assistantInfo = getCurrentAssistantInfo(); const ragConfig = { useRag: ragEnabled, retrievalCount }; await sendMessage(assistantInfo, ragConfig); }} isGenerating={isLoading} stop={stopGeneration} className="h-full w-full" />
{/* Settings Panel - Tabbed Configuration Sidebar */}
{!configCollapsed && (

Configuration

)}
{configCollapsed && ( )}
{!configCollapsed && ( <> {/* Assistant Selection Section */}
Parameters Instructions Documents
)}
{/* Model Load Confirmation Dialog */} Load Local Model Loading {pendingModelToLoad?.name} will use approximately {pendingModelToLoad?.size_gb} of RAM and storage. First-time loading may require downloading the model. Cancel Load Model {/* Save Assistant Dialog */} Save Assistant Give your assistant a custom name. This will be displayed in your assistant list.
setSaveAssistantName(e.target.value)} className="mt-2 w-full px-3 py-2 text-sm border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring" placeholder="Enter a name for your assistant..." maxLength={100} autoFocus onKeyDown={(e) => { if (e.key === "Enter" && saveAssistantName.trim()) { confirmSaveAssistant(); } if (e.key === "Escape") { cancelSaveDialog(); } }} />
{saveAssistantName.length}/100 characters
Cancel Save Assistant
{/* Rename Assistant Dialog */} Rename Assistant Enter a new name for "{selectedAssistant?.name}".
setRenameAssistantName(e.target.value)} className="mt-2 w-full px-3 py-2 text-sm border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring" placeholder="Enter new assistant name..." maxLength={100} autoFocus onKeyDown={(e) => { if (e.key === "Enter" && renameAssistantName.trim()) { confirmRenameAssistant(); } if (e.key === "Escape") { cancelRenameDialog(); } }} />
{renameAssistantName.length}/100 characters
Cancel Rename
); }