|
|
import { useState, useEffect } from 'react' |
|
|
import { AssistantInfo } from '@/types/chat' |
|
|
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 = [ |
|
|
{ |
|
|
name: "Default Assistant", |
|
|
prompt: "You are a helpful, harmless, and honest AI assistant. Provide clear, accurate, and well-structured responses." |
|
|
}, |
|
|
{ |
|
|
name: "Code Expert", |
|
|
prompt: "You are an expert software developer. Provide clean, efficient code with clear explanations. Always follow best practices and include comments where helpful." |
|
|
}, |
|
|
{ |
|
|
name: "Creative Writer", |
|
|
prompt: "You are a creative writer. Use vivid language, engaging storytelling, and imaginative descriptions. Be expressive and artistic in your responses." |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
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() |
|
|
}, []) |
|
|
|
|
|
|
|
|
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 overflow-hidden"> |
|
|
{/* Chat Area */} |
|
|
<div className="flex-1 flex flex-col"> |
|
|
{/* 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="flex-1" |
|
|
/> |
|
|
</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> |
|
|
) |
|
|
} |
|
|
|