|
|
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 { |
|
|
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() { |
|
|
|
|
|
const { |
|
|
sessions, |
|
|
currentSessionId, |
|
|
selectSession, |
|
|
deleteSession, |
|
|
clearCurrentSession, |
|
|
messages, |
|
|
input, |
|
|
setInput, |
|
|
sendMessage, |
|
|
stopGeneration, |
|
|
isLoading, |
|
|
selectedModel, |
|
|
setSelectedModel, |
|
|
systemPrompt, |
|
|
setSystemPrompt, |
|
|
temperature, |
|
|
setTemperature, |
|
|
maxTokens, |
|
|
setMaxTokens, |
|
|
} = useChat(); |
|
|
|
|
|
|
|
|
const [ragEnabled, setRagEnabled] = useState(false); |
|
|
const [retrievalCount, setRetrievalCount] = useState(3); |
|
|
|
|
|
|
|
|
const [sessionsCollapsed, setSessionsCollapsed] = useState(false); |
|
|
const [configCollapsed, setConfigCollapsed] = useState(false); |
|
|
const [autoLoadingModel, setAutoLoadingModel] = useState<string | null>(null); |
|
|
const [showLoadConfirm, setShowLoadConfirm] = useState(false); |
|
|
const [pendingModelToLoad, setPendingModelToLoad] = |
|
|
useState<ModelInfo | null>(null); |
|
|
|
|
|
|
|
|
const [models, setModels] = useState<ModelInfo[]>([]); |
|
|
|
|
|
|
|
|
const [savedAssistants, setSavedAssistants] = useState<any[]>([]); |
|
|
const [selectedAssistant, setSelectedAssistant] = useState<{ |
|
|
id: string; |
|
|
name: string; |
|
|
type: "user" | "template" | "new"; |
|
|
originalTemplate?: string; |
|
|
} | null>(null); |
|
|
|
|
|
|
|
|
const [showSaveDialog, setShowSaveDialog] = useState(false); |
|
|
const [saveAssistantName, setSaveAssistantName] = useState(""); |
|
|
|
|
|
|
|
|
const [showRenameDialog, setShowRenameDialog] = useState(false); |
|
|
const [renameAssistantName, setRenameAssistantName] = useState(""); |
|
|
|
|
|
|
|
|
const loadSavedAssistants = () => { |
|
|
try { |
|
|
const assistants = JSON.parse( |
|
|
localStorage.getItem("savedAssistants") || "[]" |
|
|
); |
|
|
setSavedAssistants(assistants); |
|
|
} catch (error) { |
|
|
console.error("Failed to load saved assistants:", error); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const openSaveDialog = () => { |
|
|
if (!systemPrompt.trim()) return; |
|
|
|
|
|
|
|
|
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); |
|
|
}; |
|
|
|
|
|
|
|
|
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: [], |
|
|
createdAt: new Date().toISOString(), |
|
|
}; |
|
|
|
|
|
const updatedAssistants = [...savedAssistants, newAssistant]; |
|
|
setSavedAssistants(updatedAssistants); |
|
|
localStorage.setItem("savedAssistants", JSON.stringify(updatedAssistants)); |
|
|
|
|
|
|
|
|
setSelectedAssistant({ |
|
|
id: newAssistant.id, |
|
|
name: newAssistant.name, |
|
|
type: "user", |
|
|
}); |
|
|
|
|
|
|
|
|
setShowSaveDialog(false); |
|
|
setSaveAssistantName(""); |
|
|
}; |
|
|
|
|
|
|
|
|
const cancelSaveDialog = () => { |
|
|
setShowSaveDialog(false); |
|
|
setSaveAssistantName(""); |
|
|
}; |
|
|
|
|
|
|
|
|
const loadSavedAssistant = (assistantId: string) => { |
|
|
const assistant = savedAssistants.find((a) => a.id === assistantId); |
|
|
if (assistant) { |
|
|
setSystemPrompt(assistant.systemPrompt); |
|
|
setTemperature(assistant.temperature); |
|
|
setMaxTokens(assistant.maxTokens); |
|
|
|
|
|
setRagEnabled(assistant.ragEnabled || false); |
|
|
setRetrievalCount(assistant.retrievalCount || 3); |
|
|
if (assistant.model) { |
|
|
setSelectedModel(assistant.model); |
|
|
} |
|
|
setSelectedAssistant({ |
|
|
id: assistant.id, |
|
|
name: assistant.name, |
|
|
type: "user", |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const systemPromptPresets = getPresetsFromConfigs(); |
|
|
|
|
|
|
|
|
const handlePresetSelect = (presetName: string) => { |
|
|
const preset = systemPromptPresets.find((p) => p.name === presetName); |
|
|
if (preset) { |
|
|
setSystemPrompt(preset.prompt); |
|
|
|
|
|
setRagEnabled(false); |
|
|
setRetrievalCount(3); |
|
|
setSelectedAssistant({ |
|
|
id: presetName, |
|
|
name: preset.name, |
|
|
type: "template", |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const createNewAssistant = () => { |
|
|
setSystemPrompt(""); |
|
|
setTemperature(0.7); |
|
|
setMaxTokens(1024); |
|
|
|
|
|
setRagEnabled(false); |
|
|
setRetrievalCount(3); |
|
|
setSelectedAssistant({ |
|
|
id: "new_assistant", |
|
|
name: "New Assistant", |
|
|
type: "new", |
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const clearCurrentAssistant = () => { |
|
|
setSelectedAssistant(null); |
|
|
}; |
|
|
|
|
|
|
|
|
const openRenameDialog = () => { |
|
|
if (selectedAssistant) { |
|
|
setRenameAssistantName(selectedAssistant.name); |
|
|
setShowRenameDialog(true); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const confirmRenameAssistant = () => { |
|
|
if (!selectedAssistant || !renameAssistantName.trim()) return; |
|
|
|
|
|
|
|
|
const updatedAssistant = { |
|
|
...selectedAssistant, |
|
|
name: renameAssistantName.trim(), |
|
|
}; |
|
|
setSelectedAssistant(updatedAssistant); |
|
|
|
|
|
|
|
|
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) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
setShowRenameDialog(false); |
|
|
setRenameAssistantName(""); |
|
|
}; |
|
|
|
|
|
|
|
|
const cancelRenameDialog = () => { |
|
|
setShowRenameDialog(false); |
|
|
setRenameAssistantName(""); |
|
|
}; |
|
|
|
|
|
|
|
|
const getCurrentAssistantInfo = (): AssistantInfo => { |
|
|
return { |
|
|
name: selectedAssistant?.name || "Default Assistant", |
|
|
type: selectedAssistant?.type || "default", |
|
|
systemPrompt, |
|
|
temperature, |
|
|
maxTokens, |
|
|
originalTemplate: selectedAssistant?.originalTemplate, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const convertTemplateToNew = () => { |
|
|
if (selectedAssistant && selectedAssistant.type === "template") { |
|
|
setSelectedAssistant({ |
|
|
id: "new_assistant", |
|
|
name: `Custom ${selectedAssistant.name}`, |
|
|
type: "new", |
|
|
originalTemplate: selectedAssistant.name, |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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(); |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
fetchModels(); |
|
|
loadSavedAssistants(); |
|
|
|
|
|
|
|
|
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", |
|
|
}); |
|
|
|
|
|
|
|
|
localStorage.removeItem("loadAssistantConfig"); |
|
|
} catch (error) { |
|
|
console.error("Failed to load assistant config:", error); |
|
|
localStorage.removeItem("loadAssistantConfig"); |
|
|
} |
|
|
} |
|
|
}, []); |
|
|
|
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (selectedModel && !models.find((m) => m.model_name === selectedModel)) { |
|
|
const firstModel = models[0]; |
|
|
if (firstModel) { |
|
|
setSelectedModel(firstModel.model_name); |
|
|
} |
|
|
} |
|
|
}, [models, selectedModel, setSelectedModel]); |
|
|
|
|
|
|
|
|
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 (selectedModelInfo.type === "local" && !selectedModelInfo.is_loaded) { |
|
|
setPendingModelToLoad(selectedModelInfo); |
|
|
setShowLoadConfirm(true); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
} else { |
|
|
console.error( |
|
|
`❌ Failed to load model: ${pendingModelToLoad.model_name}` |
|
|
); |
|
|
|
|
|
const apiModel = models.find((m) => m.type === "api"); |
|
|
if (apiModel) { |
|
|
setSelectedModel(apiModel.model_name); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Error loading model:", 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); |
|
|
|
|
|
|
|
|
const apiModel = models.find((m) => m.type === "api"); |
|
|
if (apiModel) { |
|
|
setSelectedModel(apiModel.model_name); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
if (data.current_model && selectedModel !== data.current_model) { |
|
|
setSelectedModel(data.current_model); |
|
|
} else if (!selectedModel && data.models.length > 0) { |
|
|
|
|
|
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 ( |
|
|
<div className="h-screen bg-background flex"> |
|
|
{/* Chat Sessions Sidebar */} |
|
|
<div |
|
|
className={` |
|
|
bg-background border-r flex-shrink-0 transition-all duration-300 ease-in-out |
|
|
${sessionsCollapsed ? "w-12" : "w-80"} |
|
|
`} |
|
|
> |
|
|
<div className="p-4 space-y-4 h-full"> |
|
|
<div |
|
|
className={`flex items-center ${ |
|
|
sessionsCollapsed ? "justify-center" : "justify-between" |
|
|
}`} |
|
|
> |
|
|
{!sessionsCollapsed && ( |
|
|
<h2 className="font-semibold">Chat Sessions</h2> |
|
|
)} |
|
|
<div className="flex gap-1"> |
|
|
{!sessionsCollapsed && ( |
|
|
<Button onClick={clearCurrentSession} size="sm"> |
|
|
<Plus className="h-4 w-4 mr-1" /> |
|
|
New |
|
|
</Button> |
|
|
)} |
|
|
<Button |
|
|
onClick={() => setSessionsCollapsed(!sessionsCollapsed)} |
|
|
size="sm" |
|
|
variant="ghost" |
|
|
className="h-8 w-8 p-0 hover:bg-gray-100" |
|
|
title={ |
|
|
sessionsCollapsed ? "Expand Sessions" : "Collapse Sessions" |
|
|
} |
|
|
> |
|
|
{sessionsCollapsed ? ( |
|
|
<ChevronRight className="h-4 w-4" /> |
|
|
) : ( |
|
|
<ChevronLeft className="h-4 w-4" /> |
|
|
)} |
|
|
</Button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{sessionsCollapsed && ( |
|
|
<Button |
|
|
onClick={clearCurrentSession} |
|
|
size="sm" |
|
|
variant="ghost" |
|
|
className="w-full p-2" |
|
|
> |
|
|
<Plus className="h-4 w-4" /> |
|
|
</Button> |
|
|
)} |
|
|
<div className="space-y-2"> |
|
|
{!sessionsCollapsed && |
|
|
sessions.map((session) => ( |
|
|
<Card |
|
|
key={session.id} |
|
|
className={`p-3 cursor-pointer transition-colors hover:bg-accent ${ |
|
|
currentSessionId === session.id |
|
|
? "bg-accent border-primary" |
|
|
: "" |
|
|
}`} |
|
|
onClick={() => { |
|
|
console.log( |
|
|
"Session card clicked:", |
|
|
session.id, |
|
|
session.title |
|
|
); |
|
|
selectSession(session.id); |
|
|
}} |
|
|
> |
|
|
<div className="flex items-center justify-between"> |
|
|
<span className="text-sm font-medium truncate"> |
|
|
{session.title} |
|
|
</span> |
|
|
<Button |
|
|
size="sm" |
|
|
variant="ghost" |
|
|
onClick={(e) => { |
|
|
e.stopPropagation(); |
|
|
deleteSession(session.id); |
|
|
}} |
|
|
className="h-6 w-6 p-0" |
|
|
> |
|
|
<Trash2 className="h-3 w-3" /> |
|
|
</Button> |
|
|
</div> |
|
|
<div className="text-xs text-muted-foreground"> |
|
|
{session.messages.length} messages |
|
|
</div> |
|
|
</Card> |
|
|
))} |
|
|
|
|
|
{sessionsCollapsed && |
|
|
sessions.map((session) => ( |
|
|
<Button |
|
|
key={session.id} |
|
|
variant={ |
|
|
currentSessionId === session.id ? "default" : "ghost" |
|
|
} |
|
|
size="sm" |
|
|
className="w-full p-1 h-8 relative" |
|
|
title={session.title} |
|
|
onClick={() => { |
|
|
console.log( |
|
|
"Session icon clicked:", |
|
|
session.id, |
|
|
session.title |
|
|
); |
|
|
selectSession(session.id); |
|
|
}} |
|
|
> |
|
|
<MessageSquare className="h-4 w-4" /> |
|
|
{session.messages && session.messages.length > 0 && ( |
|
|
<div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 text-white text-xs rounded-full flex items-center justify-center"> |
|
|
{Math.min(session.messages.length, 9)} |
|
|
</div> |
|
|
)} |
|
|
</Button> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Main Content */} |
|
|
<div className="flex-1 flex flex-col h-full"> |
|
|
{/* Content Area - Responsive layout */} |
|
|
<div className="flex-1 flex"> |
|
|
{/* Chat Area */} |
|
|
<div className="flex-1 flex flex-col min-h-0"> |
|
|
{/* Chat Messages and Input */} |
|
|
<Chat |
|
|
messages={messages.map((msg) => ({ |
|
|
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" |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{/* Settings Panel - Tabbed Configuration Sidebar */} |
|
|
<div |
|
|
className={` |
|
|
border-l bg-background flex-shrink-0 transition-all duration-300 ease-in-out |
|
|
${configCollapsed ? "w-12" : "w-[480px] xl:w-[520px]"} |
|
|
`} |
|
|
> |
|
|
<div className="p-4 space-y-4 h-full"> |
|
|
<div |
|
|
className={`flex items-center ${ |
|
|
configCollapsed ? "justify-center" : "justify-between" |
|
|
}`} |
|
|
> |
|
|
{!configCollapsed && ( |
|
|
<h2 className="font-semibold">Configuration</h2> |
|
|
)} |
|
|
<div className="flex gap-1"> |
|
|
<Button |
|
|
onClick={() => setConfigCollapsed(!configCollapsed)} |
|
|
size="sm" |
|
|
variant="ghost" |
|
|
className="h-8 w-8 p-0 hover:bg-gray-100" |
|
|
title={ |
|
|
configCollapsed |
|
|
? "Expand Configuration" |
|
|
: "Collapse Configuration" |
|
|
} |
|
|
> |
|
|
{configCollapsed ? ( |
|
|
<ChevronLeft className="h-4 w-4" /> |
|
|
) : ( |
|
|
<ChevronRight className="h-4 w-4" /> |
|
|
)} |
|
|
</Button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{configCollapsed && ( |
|
|
<Button |
|
|
size="sm" |
|
|
variant="ghost" |
|
|
className="w-full p-2 invisible" |
|
|
> |
|
|
<Settings className="h-4 w-4" /> |
|
|
</Button> |
|
|
)} |
|
|
<div className="space-y-2"> |
|
|
{!configCollapsed && ( |
|
|
<> |
|
|
{/* Assistant Selection Section */} |
|
|
<div className="border-b bg-white p-4 -mx-4"> |
|
|
<AssistantSelector |
|
|
savedAssistants={savedAssistants} |
|
|
loadSavedAssistant={loadSavedAssistant} |
|
|
openSaveDialog={openSaveDialog} |
|
|
presets={systemPromptPresets} |
|
|
onPresetSelect={handlePresetSelect} |
|
|
isLoading={isLoading} |
|
|
selectedAssistant={selectedAssistant} |
|
|
createNewAssistant={createNewAssistant} |
|
|
clearCurrentAssistant={clearCurrentAssistant} |
|
|
openRenameDialog={openRenameDialog} |
|
|
systemPrompt={systemPrompt} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
<Tabs |
|
|
defaultValue="parameters" |
|
|
className="flex-1 flex flex-col -mx-4" |
|
|
> |
|
|
<TabsList className="grid w-full grid-cols-3 m-4 mb-0"> |
|
|
<TabsTrigger |
|
|
value="parameters" |
|
|
className="flex items-center gap-2" |
|
|
> |
|
|
<Sliders className="h-4 w-4" /> |
|
|
<span className="hidden sm:inline">Parameters</span> |
|
|
</TabsTrigger> |
|
|
<TabsTrigger |
|
|
value="instructions" |
|
|
className="flex items-center gap-2" |
|
|
> |
|
|
<Settings className="h-4 w-4" /> |
|
|
<span className="hidden sm:inline">Instructions</span> |
|
|
</TabsTrigger> |
|
|
<TabsTrigger |
|
|
value="documents" |
|
|
className="flex items-center gap-2" |
|
|
> |
|
|
<BookOpen className="h-4 w-4" /> |
|
|
<span className="hidden sm:inline">Documents</span> |
|
|
</TabsTrigger> |
|
|
</TabsList> |
|
|
|
|
|
<div className="flex-1 overflow-hidden"> |
|
|
<TabsContent |
|
|
value="parameters" |
|
|
className="p-6 space-y-6 m-0 h-full overflow-y-auto" |
|
|
> |
|
|
<ModelParametersTab |
|
|
models={models} |
|
|
selectedModel={selectedModel} |
|
|
setSelectedModel={setSelectedModel} |
|
|
autoLoadingModel={autoLoadingModel} |
|
|
temperature={temperature} |
|
|
setTemperature={handleTemperatureChange} |
|
|
maxTokens={maxTokens} |
|
|
setMaxTokens={handleMaxTokensChange} |
|
|
/> |
|
|
</TabsContent> |
|
|
|
|
|
<TabsContent |
|
|
value="instructions" |
|
|
className="p-6 space-y-6 m-0 h-full overflow-y-auto" |
|
|
> |
|
|
<SystemInstructionsTab |
|
|
systemPrompt={systemPrompt} |
|
|
setSystemPrompt={handleSystemPromptChange} |
|
|
isLoading={isLoading} |
|
|
/> |
|
|
</TabsContent> |
|
|
|
|
|
<TabsContent |
|
|
value="documents" |
|
|
className="p-6 space-y-6 m-0 h-full overflow-y-auto" |
|
|
> |
|
|
<DocumentsTab |
|
|
isLoading={isLoading} |
|
|
ragEnabled={ragEnabled} |
|
|
setRagEnabled={handleRagEnabledChange} |
|
|
retrievalCount={retrievalCount} |
|
|
setRetrievalCount={handleRetrievalCountChange} |
|
|
currentAssistant={selectedAssistant} |
|
|
/> |
|
|
</TabsContent> |
|
|
</div> |
|
|
</Tabs> |
|
|
</> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{} |
|
|
<AlertDialog open={showLoadConfirm} onOpenChange={setShowLoadConfirm}> |
|
|
<AlertDialogContent> |
|
|
<AlertDialogHeader> |
|
|
<AlertDialogTitle>Load Local Model</AlertDialogTitle> |
|
|
<AlertDialogDescription> |
|
|
Loading <strong>{pendingModelToLoad?.name}</strong> will use |
|
|
approximately <strong>{pendingModelToLoad?.size_gb}</strong> of |
|
|
RAM and storage. First-time loading may require downloading the |
|
|
model. |
|
|
</AlertDialogDescription> |
|
|
</AlertDialogHeader> |
|
|
<AlertDialogFooter> |
|
|
<AlertDialogCancel onClick={handleLoadModelCancel}> |
|
|
Cancel |
|
|
</AlertDialogCancel> |
|
|
<AlertDialogAction onClick={handleLoadModelConfirm}> |
|
|
Load Model |
|
|
</AlertDialogAction> |
|
|
</AlertDialogFooter> |
|
|
</AlertDialogContent> |
|
|
</AlertDialog> |
|
|
|
|
|
{} |
|
|
<AlertDialog open={showSaveDialog} onOpenChange={setShowSaveDialog}> |
|
|
<AlertDialogContent> |
|
|
<AlertDialogHeader> |
|
|
<AlertDialogTitle>Save Assistant</AlertDialogTitle> |
|
|
<AlertDialogDescription> |
|
|
Give your assistant a custom name. This will be displayed in your |
|
|
assistant list. |
|
|
</AlertDialogDescription> |
|
|
</AlertDialogHeader> |
|
|
<div className="py-4"> |
|
|
<Label htmlFor="assistant-name" className="text-sm font-medium"> |
|
|
Assistant Name |
|
|
</Label> |
|
|
<input |
|
|
id="assistant-name" |
|
|
type="text" |
|
|
value={saveAssistantName} |
|
|
onChange={(e) => 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(); |
|
|
} |
|
|
}} |
|
|
/> |
|
|
<div className="mt-1 text-xs text-muted-foreground"> |
|
|
{saveAssistantName.length}/100 characters |
|
|
</div> |
|
|
</div> |
|
|
<AlertDialogFooter> |
|
|
<AlertDialogCancel onClick={cancelSaveDialog}> |
|
|
Cancel |
|
|
</AlertDialogCancel> |
|
|
<AlertDialogAction |
|
|
onClick={confirmSaveAssistant} |
|
|
disabled={!saveAssistantName.trim()} |
|
|
> |
|
|
<Save className="h-4 w-4 mr-2" /> |
|
|
Save Assistant |
|
|
</AlertDialogAction> |
|
|
</AlertDialogFooter> |
|
|
</AlertDialogContent> |
|
|
</AlertDialog> |
|
|
|
|
|
{} |
|
|
<AlertDialog open={showRenameDialog} onOpenChange={setShowRenameDialog}> |
|
|
<AlertDialogContent> |
|
|
<AlertDialogHeader> |
|
|
<AlertDialogTitle>Rename Assistant</AlertDialogTitle> |
|
|
<AlertDialogDescription> |
|
|
Enter a new name for "{selectedAssistant?.name}". |
|
|
</AlertDialogDescription> |
|
|
</AlertDialogHeader> |
|
|
<div className="py-4"> |
|
|
<Label |
|
|
htmlFor="rename-assistant-name" |
|
|
className="text-sm font-medium" |
|
|
> |
|
|
Assistant Name |
|
|
</Label> |
|
|
<input |
|
|
id="rename-assistant-name" |
|
|
type="text" |
|
|
value={renameAssistantName} |
|
|
onChange={(e) => 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(); |
|
|
} |
|
|
}} |
|
|
/> |
|
|
<div className="mt-1 text-xs text-muted-foreground"> |
|
|
{renameAssistantName.length}/100 characters |
|
|
</div> |
|
|
</div> |
|
|
<AlertDialogFooter> |
|
|
<AlertDialogCancel onClick={cancelRenameDialog}> |
|
|
Cancel |
|
|
</AlertDialogCancel> |
|
|
<AlertDialogAction |
|
|
onClick={confirmRenameAssistant} |
|
|
disabled={!renameAssistantName.trim()} |
|
|
> |
|
|
<Settings className="h-4 w-4 mr-2" /> |
|
|
Rename |
|
|
</AlertDialogAction> |
|
|
</AlertDialogFooter> |
|
|
</AlertDialogContent> |
|
|
</AlertDialog> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|