edgellm / frontend /src /pages /Playground.tsx
wu981526092's picture
✨ UPDATE CHAT AND DEVICE COMPONENTS: Refactor for improved layout and functionality
27615c9
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<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 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 (
<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>
{/* 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>
);
}