edgellm / frontend /src /pages /Playground.tsx
wu981526092's picture
Refactored Playground.tsx into modular components + Assistant-specific documents
1425cf0
raw
history blame
30.3 kB
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 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<string | null>(null)
const [showLoadConfirm, setShowLoadConfirm] = useState(false)
const [pendingModelToLoad, setPendingModelToLoad] = useState<ModelInfo | null>(null)
// Model management state
const [models, setModels] = useState<ModelInfo[]>([])
// Saved assistants state
const [savedAssistants, setSavedAssistants] = useState<any[]>([])
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
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."
}
]
// 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()
}, [])
// 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 (
<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>
{/* Model Load Confirmation Dialog */}
<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>
{/* Save Assistant Dialog */}
<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>
{/* Rename Assistant Dialog */}
<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>
)
}