import React, { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { BackendType, type CreateInstanceOptions, type Instance } from "@/types/instance"; import type { BackendOptions } from "@/schemas/instanceOptions"; import ParseCommandDialog from "@/components/ParseCommandDialog"; import InstanceSettingsCard from "@/components/instance/InstanceSettingsCard"; import BackendConfigurationCard from "@/components/instance/BackendConfigurationCard"; import { Upload } from "lucide-react"; interface InstanceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSave: (name: string, options: CreateInstanceOptions) => void; instance?: Instance; // For editing existing instance } const InstanceDialog: React.FC = ({ open, onOpenChange, onSave, instance, }) => { const isEditing = !!instance; const [instanceName, setInstanceName] = useState(""); const [formData, setFormData] = useState({}); const [nameError, setNameError] = useState(""); const [showParseDialog, setShowParseDialog] = useState(false); const fileInputRef = useRef(null); // Reset form when dialog opens/closes or when instance changes useEffect(() => { if (open) { if (instance) { // Populate form with existing instance data setInstanceName(instance.name); setFormData(instance.options || {}); } else { // Reset form for new instance setInstanceName(""); setFormData({ auto_restart: true, // Default value backend_type: BackendType.LLAMA_CPP, // Default backend type backend_options: {}, }); } setNameError(""); // Reset any name errors } }, [open, instance]); const handleFieldChange = (key: keyof CreateInstanceOptions, value: unknown) => { setFormData((prev) => { // If backend_type is changing, clear backend_options if (key === 'backend_type' && prev.backend_type !== value) { return { ...prev, backend_type: value as CreateInstanceOptions['backend_type'], backend_options: {}, // Clear backend options when backend type changes }; } return { ...prev, [key]: value, } as CreateInstanceOptions; }); }; const handleBackendFieldChange = (key: string, value: unknown) => { setFormData((prev) => ({ ...prev, backend_options: { ...prev.backend_options, [key]: value, } as BackendOptions, })); }; const handleNameChange = (name: string) => { setInstanceName(name); // Validate instance name if (!name.trim()) { setNameError("Instance name is required"); } else if (!/^[a-zA-Z0-9-_]+$/.test(name)) { setNameError( "Instance name can only contain letters, numbers, hyphens, and underscores" ); } else { setNameError(""); } }; const handleSave = () => { // Validate instance name before saving if (!instanceName.trim()) { setNameError("Instance name is required"); return; } // Clean up undefined values to avoid sending empty fields const cleanOptions: CreateInstanceOptions = {} as CreateInstanceOptions; Object.entries(formData).forEach(([key, value]) => { const typedKey = key as keyof CreateInstanceOptions; if (key === 'backend_options' && value && typeof value === 'object' && !Array.isArray(value)) { // Handle backend_options specially - clean nested object const cleanBackendOptions: Record = {}; Object.entries(value).forEach(([backendKey, backendValue]) => { if (backendValue !== undefined && backendValue !== null && (typeof backendValue !== 'string' || backendValue.trim() !== "")) { // Handle arrays - don't include empty arrays if (Array.isArray(backendValue) && backendValue.length === 0) { return; } cleanBackendOptions[backendKey] = backendValue; } }); // Only include backend_options if it has content if (Object.keys(cleanBackendOptions).length > 0) { (cleanOptions as Record)[typedKey] = cleanBackendOptions as BackendOptions; } } else if (value !== undefined && value !== null) { // Skip empty strings if (typeof value === 'string' && value.trim() === "") { return; } // Skip empty arrays if (Array.isArray(value) && value.length === 0) { return; } (cleanOptions as Record)[typedKey] = value; } }); onSave(instanceName, cleanOptions); onOpenChange(false); }; const handleCancel = () => { onOpenChange(false); }; const handleCommandParsed = (parsedOptions: CreateInstanceOptions) => { setFormData(prev => ({ ...prev, ...parsedOptions, })); setShowParseDialog(false); }; const handleImportFile = () => { fileInputRef.current?.click(); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const content = e.target?.result as string; const importedData = JSON.parse(content) as { name?: string; options?: CreateInstanceOptions }; // Validate that it's an instance export if (!importedData.name || !importedData.options) { alert('Invalid instance file: Missing required fields (name, options)'); return; } // Set the instance name (only for new instances, not editing) if (!isEditing && typeof importedData.name === 'string') { handleNameChange(importedData.name); } // Populate all the options from the imported file if (importedData.options) { setFormData(prev => ({ ...prev, ...importedData.options, })); } // Reset the file input event.target.value = ''; } catch (error) { console.error('Failed to parse instance file:', error); alert(`Failed to parse instance file: ${error instanceof Error ? error.message : 'Invalid JSON'}`); } }; reader.readAsText(file); }; // Save button label logic let saveButtonLabel = "Create Instance"; if (isEditing) { if (instance?.status === "running") { saveButtonLabel = "Update & Restart Instance"; } else { saveButtonLabel = "Update Instance"; } } return (
{isEditing ? "Edit Instance" : "Create New Instance"} {isEditing ? "Modify the instance configuration below." : "Configure your new llama-server instance below."}
{!isEditing && ( )}
{/* Instance Settings Card */} {/* Backend Configuration Card */} setShowParseDialog(true)} />
); }; export default InstanceDialog;