diff --git a/webui/src/components/BackendFormField.tsx b/webui/src/components/BackendFormField.tsx index 3dd7af0..e66fedd 100644 --- a/webui/src/components/BackendFormField.tsx +++ b/webui/src/components/BackendFormField.tsx @@ -45,7 +45,6 @@ const BackendFormField: React.FC = ({ fieldKey, value, on
= ({ fieldKey, value, on
= ({ fieldKey, value, on
= ({ const [showParseDialog, setShowParseDialog] = useState(false); // Get field lists dynamically from the type - const basicFields = getBasicFields(); const advancedFields = getAdvancedFields(); - const basicBackendFields = getBasicBackendFields(formData.backend_type); const advancedBackendFields = getAdvancedBackendFields(formData.backend_type); // Reset form when dialog opens/closes or when instance changes @@ -163,8 +163,6 @@ const InstanceDialog: React.FC = ({ setShowParseDialog(false); }; - // Check if auto_restart is enabled - const isAutoRestartEnabled = formData.auto_restart === true; // Save button label logic let saveButtonLabel = "Create Instance"; @@ -212,70 +210,23 @@ const InstanceDialog: React.FC = ({
{/* Auto Restart Configuration Section */} -
-

- Auto Restart Configuration -

+ - {/* Auto Restart Toggle */} - - - {/* Show restart options only when auto restart is enabled */} - {isAutoRestartEnabled && ( -
- - -
- )} -
- - {/* Basic Fields - Automatically generated from type (excluding auto restart options) */} -
-

Basic Configuration

- {basicFields - .filter( - (fieldKey) => - fieldKey !== "auto_restart" && - fieldKey !== "max_restarts" && - fieldKey !== "restart_delay" && - fieldKey !== "backend_options" // backend_options is handled separately - ) - .map((fieldKey) => ( - - ))} -
+ {/* Basic Fields */} + {/* Backend Configuration Section */} -
-

Backend Configuration

- - {/* Basic backend fields */} - {basicBackendFields.map((fieldKey) => ( - - ))} -
+ {/* Advanced Fields Toggle */}
@@ -314,54 +265,13 @@ const InstanceDialog: React.FC = ({
- {/* Advanced Fields - Automatically generated from type (excluding restart options) */} + {/* Advanced Fields */} {showAdvanced && (
- {/* Advanced instance fields */} - {advancedFields - .filter( - (fieldKey) => - !["max_restarts", "restart_delay", "backend_options"].includes( - fieldKey as string - ) - ).length > 0 && ( -
-

Advanced Instance Configuration

- {advancedFields - .filter( - (fieldKey) => - !["max_restarts", "restart_delay", "backend_options"].includes( - fieldKey as string - ) - ) - .sort() - .map((fieldKey) => ( - - ))} -
- )} - - {/* Advanced backend fields */} - {advancedBackendFields.length > 0 && ( -
-

Advanced Backend Configuration

- {advancedBackendFields - .sort() - .map((fieldKey) => ( - - ))} -
- )} +
)}
diff --git a/webui/src/components/ParseCommandDialog.tsx b/webui/src/components/ParseCommandDialog.tsx index fcf79e6..c6abbf9 100644 --- a/webui/src/components/ParseCommandDialog.tsx +++ b/webui/src/components/ParseCommandDialog.tsx @@ -81,17 +81,14 @@ const ParseCommandDialog: React.FC = ({ onOpenChange(open); }; + const backendPlaceholders: Record = { + [BackendType.LLAMA_CPP]: "llama-server --model /path/to/model.gguf --gpu-layers 32 --ctx-size 4096", + [BackendType.MLX_LM]: "mlx_lm.server --model mlx-community/Mistral-7B-Instruct-v0.3-4bit --host 0.0.0.0 --port 8080", + [BackendType.VLLM]: "vllm serve --model microsoft/DialoGPT-medium --tensor-parallel-size 2 --gpu-memory-utilization 0.9", + }; + const getPlaceholderForBackend = (backendType: BackendTypeValue): string => { - switch (backendType) { - case BackendType.LLAMA_CPP: - return "llama-server --model /path/to/model.gguf --gpu-layers 32 --ctx-size 4096"; - case BackendType.MLX_LM: - return "mlx_lm.server --model mlx-community/Mistral-7B-Instruct-v0.3-4bit --host 0.0.0.0 --port 8080"; - case BackendType.VLLM: - return "vllm serve --model microsoft/DialoGPT-medium --tensor-parallel-size 2 --gpu-memory-utilization 0.9"; - default: - return "Enter your command here..."; - } + return backendPlaceholders[backendType] || "Enter your command here..."; }; return ( diff --git a/webui/src/components/ZodFormField.tsx b/webui/src/components/ZodFormField.tsx index 9f4ea21..594d907 100644 --- a/webui/src/components/ZodFormField.tsx +++ b/webui/src/components/ZodFormField.tsx @@ -29,7 +29,6 @@ const ZodFormField: React.FC = ({ fieldKey, value, onChange }
= ({ fieldKey, value, onChange }
+ = ({ fieldKey, value, onChange }
+ void + placeholder?: string + description?: string + disabled?: boolean + className?: string +} + +const ArrayInput: React.FC = ({ + id, + label, + value, + onChange, + placeholder = "item1, item2, item3", + description, + disabled = false, + className +}) => { + const handleChange = (inputValue: string) => { + if (inputValue === '') { + onChange(undefined) + return + } + + const arrayValue = inputValue + .split(',') + .map(s => s.trim()) + .filter(Boolean) + + onChange(arrayValue.length > 0 ? arrayValue : undefined) + } + + return ( +
+ + handleChange(e.target.value)} + placeholder={placeholder} + disabled={disabled} + className={className} + /> + {description && ( +

{description}

+ )} +

Separate multiple values with commas

+
+ ) +} + +export default ArrayInput \ No newline at end of file diff --git a/webui/src/components/form/CheckboxInput.tsx b/webui/src/components/form/CheckboxInput.tsx new file mode 100644 index 0000000..5107bde --- /dev/null +++ b/webui/src/components/form/CheckboxInput.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' + +interface CheckboxInputProps { + id: string + label: string + value: boolean | undefined + onChange: (value: boolean) => void + description?: string + disabled?: boolean + className?: string +} + +const CheckboxInput: React.FC = ({ + id, + label, + value, + onChange, + description, + disabled = false, + className +}) => { + return ( +
+ onChange(!!checked)} + disabled={disabled} + /> + +
+ ) +} + +export default CheckboxInput \ No newline at end of file diff --git a/webui/src/components/form/NumberInput.tsx b/webui/src/components/form/NumberInput.tsx new file mode 100644 index 0000000..74bb9f7 --- /dev/null +++ b/webui/src/components/form/NumberInput.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' + +interface NumberInputProps { + id: string + label: string + value: number | undefined + onChange: (value: number | undefined) => void + placeholder?: string + description?: string + disabled?: boolean + className?: string +} + +const NumberInput: React.FC = ({ + id, + label, + value, + onChange, + placeholder, + description, + disabled = false, + className +}) => { + const handleChange = (inputValue: string) => { + if (inputValue === '') { + onChange(undefined) + return + } + + const numValue = parseFloat(inputValue) + if (!isNaN(numValue)) { + onChange(numValue) + } + } + + return ( +
+ + handleChange(e.target.value)} + placeholder={placeholder} + disabled={disabled} + className={className} + /> + {description && ( +

{description}

+ )} +
+ ) +} + +export default NumberInput \ No newline at end of file diff --git a/webui/src/components/form/SelectInput.tsx b/webui/src/components/form/SelectInput.tsx new file mode 100644 index 0000000..461e865 --- /dev/null +++ b/webui/src/components/form/SelectInput.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { Label } from '@/components/ui/label' + +interface SelectOption { + value: string + label: string +} + +interface SelectInputProps { + id: string + label: string + value: string | undefined + onChange: (value: string | undefined) => void + options: SelectOption[] + description?: string + disabled?: boolean + className?: string +} + +const SelectInput: React.FC = ({ + id, + label, + value, + onChange, + options, + description, + disabled = false, + className +}) => { + return ( +
+ + + {description && ( +

{description}

+ )} +
+ ) +} + +export default SelectInput \ No newline at end of file diff --git a/webui/src/components/form/TextInput.tsx b/webui/src/components/form/TextInput.tsx new file mode 100644 index 0000000..63378b1 --- /dev/null +++ b/webui/src/components/form/TextInput.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' + +interface TextInputProps { + id: string + label: string + value: string | number | undefined + onChange: (value: string | undefined) => void + placeholder?: string + description?: string + disabled?: boolean + className?: string +} + +const TextInput: React.FC = ({ + id, + label, + value, + onChange, + placeholder, + description, + disabled = false, + className +}) => { + return ( +
+ + onChange(e.target.value || undefined)} + placeholder={placeholder} + disabled={disabled} + className={className} + /> + {description && ( +

{description}

+ )} +
+ ) +} + +export default TextInput \ No newline at end of file diff --git a/webui/src/components/instance/AdvancedInstanceFields.tsx b/webui/src/components/instance/AdvancedInstanceFields.tsx new file mode 100644 index 0000000..16ea9af --- /dev/null +++ b/webui/src/components/instance/AdvancedInstanceFields.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import type { CreateInstanceOptions } from '@/types/instance' +import { getAdvancedFields, basicFieldsConfig } from '@/lib/zodFormUtils' +import { getFieldType } from '@/schemas/instanceOptions' +import TextInput from '@/components/form/TextInput' +import NumberInput from '@/components/form/NumberInput' +import CheckboxInput from '@/components/form/CheckboxInput' +import ArrayInput from '@/components/form/ArrayInput' + +interface AdvancedInstanceFieldsProps { + formData: CreateInstanceOptions + onChange: (key: keyof CreateInstanceOptions, value: any) => void +} + +const AdvancedInstanceFields: React.FC = ({ + formData, + onChange +}) => { + const advancedFields = getAdvancedFields() + + const renderField = (fieldKey: keyof CreateInstanceOptions) => { + const config = basicFieldsConfig[fieldKey as string] || { label: fieldKey } + const fieldType = getFieldType(fieldKey) + + switch (fieldType) { + case 'boolean': + return ( + onChange(fieldKey, value)} + description={config.description} + /> + ) + + case 'number': + return ( + onChange(fieldKey, value)} + placeholder={config.placeholder} + description={config.description} + /> + ) + + case 'array': + return ( + onChange(fieldKey, value)} + placeholder={config.placeholder} + description={config.description} + /> + ) + + default: + return ( + onChange(fieldKey, value)} + placeholder={config.placeholder} + description={config.description} + /> + ) + } + } + + // Filter out restart options and backend_options (handled separately) + const fieldsToRender = advancedFields.filter( + fieldKey => !['max_restarts', 'restart_delay', 'backend_options'].includes(fieldKey as string) + ) + + if (fieldsToRender.length === 0) { + return null + } + + return ( +
+

Advanced Instance Configuration

+ {fieldsToRender + .sort() + .map(renderField)} +
+ ) +} + +export default AdvancedInstanceFields \ No newline at end of file diff --git a/webui/src/components/instance/AutoRestartConfiguration.tsx b/webui/src/components/instance/AutoRestartConfiguration.tsx new file mode 100644 index 0000000..fe3a900 --- /dev/null +++ b/webui/src/components/instance/AutoRestartConfiguration.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import type { CreateInstanceOptions } from '@/types/instance' +import CheckboxInput from '@/components/form/CheckboxInput' +import NumberInput from '@/components/form/NumberInput' + +interface AutoRestartConfigurationProps { + formData: CreateInstanceOptions + onChange: (key: keyof CreateInstanceOptions, value: any) => void +} + +const AutoRestartConfiguration: React.FC = ({ + formData, + onChange +}) => { + const isAutoRestartEnabled = formData.auto_restart === true + + return ( +
+

Auto Restart Configuration

+ + onChange('auto_restart', value)} + description="Automatically restart the instance on failure" + /> + + {isAutoRestartEnabled && ( +
+ onChange('max_restarts', value)} + placeholder="3" + description="Maximum number of restart attempts (0 = unlimited)" + /> + onChange('restart_delay', value)} + placeholder="5" + description="Delay in seconds before attempting restart" + /> +
+ )} +
+ ) +} + +export default AutoRestartConfiguration \ No newline at end of file diff --git a/webui/src/components/instance/BackendConfiguration.tsx b/webui/src/components/instance/BackendConfiguration.tsx new file mode 100644 index 0000000..cfcee86 --- /dev/null +++ b/webui/src/components/instance/BackendConfiguration.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import type { CreateInstanceOptions } from '@/types/instance' +import { getBasicBackendFields, getAdvancedBackendFields } from '@/lib/zodFormUtils' +import BackendFormField from '@/components/BackendFormField' + +interface BackendConfigurationProps { + formData: CreateInstanceOptions + onBackendFieldChange: (key: string, value: any) => void + showAdvanced?: boolean +} + +const BackendConfiguration: React.FC = ({ + formData, + onBackendFieldChange, + showAdvanced = false +}) => { + const basicBackendFields = getBasicBackendFields(formData.backend_type) + const advancedBackendFields = getAdvancedBackendFields(formData.backend_type) + + return ( +
+

Backend Configuration

+ + {/* Basic backend fields */} + {basicBackendFields.map((fieldKey) => ( + + ))} + + {/* Advanced backend fields */} + {showAdvanced && advancedBackendFields.length > 0 && ( +
+

Advanced Backend Configuration

+ {advancedBackendFields + .sort() + .map((fieldKey) => ( + + ))} +
+ )} +
+ ) +} + +export default BackendConfiguration \ No newline at end of file diff --git a/webui/src/components/instance/BasicInstanceFields.tsx b/webui/src/components/instance/BasicInstanceFields.tsx new file mode 100644 index 0000000..9dce284 --- /dev/null +++ b/webui/src/components/instance/BasicInstanceFields.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import { BackendType, type CreateInstanceOptions } from '@/types/instance' +import { getBasicFields, basicFieldsConfig } from '@/lib/zodFormUtils' +import { getFieldType } from '@/schemas/instanceOptions' +import TextInput from '@/components/form/TextInput' +import NumberInput from '@/components/form/NumberInput' +import CheckboxInput from '@/components/form/CheckboxInput' +import SelectInput from '@/components/form/SelectInput' + +interface BasicInstanceFieldsProps { + formData: CreateInstanceOptions + onChange: (key: keyof CreateInstanceOptions, value: any) => void +} + +const BasicInstanceFields: React.FC = ({ + formData, + onChange +}) => { + const basicFields = getBasicFields() + + const renderField = (fieldKey: keyof CreateInstanceOptions) => { + const config = basicFieldsConfig[fieldKey as string] || { label: fieldKey } + const fieldType = getFieldType(fieldKey) + + // Special handling for backend_type field + if (fieldKey === 'backend_type') { + return ( + onChange(fieldKey, value)} + options={[ + { value: BackendType.LLAMA_CPP, label: 'Llama Server' }, + { value: BackendType.MLX_LM, label: 'MLX LM' }, + { value: BackendType.VLLM, label: 'vLLM' } + ]} + description={config.description} + /> + ) + } + + // Render based on field type + switch (fieldType) { + case 'boolean': + return ( + onChange(fieldKey, value)} + description={config.description} + /> + ) + + case 'number': + return ( + onChange(fieldKey, value)} + placeholder={config.placeholder} + description={config.description} + /> + ) + + default: + return ( + onChange(fieldKey, value)} + placeholder={config.placeholder} + description={config.description} + /> + ) + } + } + + // Filter out auto restart fields and backend_options (handled separately) + const fieldsToRender = basicFields.filter( + fieldKey => !['auto_restart', 'max_restarts', 'restart_delay', 'backend_options'].includes(fieldKey as string) + ) + + return ( +
+

Basic Configuration

+ {fieldsToRender.map(renderField)} +
+ ) +} + +export default BasicInstanceFields \ No newline at end of file diff --git a/webui/src/lib/api.ts b/webui/src/lib/api.ts index c465033..a3ca0ee 100644 --- a/webui/src/lib/api.ts +++ b/webui/src/lib/api.ts @@ -1,4 +1,5 @@ import type { CreateInstanceOptions, Instance } from "@/types/instance"; +import { handleApiError } from "./errorUtils"; const API_BASE = "/api/v1"; @@ -30,25 +31,8 @@ async function apiCall( headers, }); - // Handle authentication errors - if (response.status === 401) { - throw new Error('Authentication required'); - } - - if (!response.ok) { - // Try to get error message from response - let errorMessage = `HTTP ${response.status}`; - try { - const errorText = await response.text(); - if (errorText) { - errorMessage += `: ${errorText}`; - } - } catch { - // If we can't read the error, just use status - } - - throw new Error(errorMessage); - } + // Handle errors using centralized error handler + await handleApiError(response); // Handle empty responses (like DELETE) if (response.status === 204) { diff --git a/webui/src/lib/errorUtils.ts b/webui/src/lib/errorUtils.ts new file mode 100644 index 0000000..1860bf9 --- /dev/null +++ b/webui/src/lib/errorUtils.ts @@ -0,0 +1,32 @@ +/** + * Parses error response from API calls and returns a formatted error message + */ +export async function parseErrorResponse(response: Response): Promise { + let errorMessage = `HTTP ${response.status}` + + try { + const errorText = await response.text() + if (errorText) { + errorMessage += `: ${errorText}` + } + } catch { + // If we can't read the error, just use status + } + + return errorMessage +} + +/** + * Handles common API call errors and throws appropriate Error objects + */ +export async function handleApiError(response: Response): Promise { + // Handle authentication errors + if (response.status === 401) { + throw new Error('Authentication required') + } + + if (!response.ok) { + const errorMessage = await parseErrorResponse(response) + throw new Error(errorMessage) + } +} \ No newline at end of file diff --git a/webui/src/lib/zodFormUtils.ts b/webui/src/lib/zodFormUtils.ts index c2e2c91..88294c4 100644 --- a/webui/src/lib/zodFormUtils.ts +++ b/webui/src/lib/zodFormUtils.ts @@ -20,7 +20,6 @@ export const basicFieldsConfig: Record = { auto_restart: { label: 'Auto Restart', @@ -56,13 +55,11 @@ const basicLlamaCppFieldsConfig: Record = { model: { label: 'Model Path', placeholder: '/path/to/model.gguf', - description: 'Path to the model file', - required: true + description: 'Path to the model file' }, hf_repo: { label: 'Hugging Face Repository', @@ -86,13 +83,11 @@ const basicMlxFieldsConfig: Record = { model: { label: 'Model', placeholder: 'mlx-community/Mistral-7B-Instruct-v0.3-4bit', - description: 'The path to the MLX model weights, tokenizer, and config', - required: true + description: 'The path to the MLX model weights, tokenizer, and config' }, temp: { label: 'Temperature', @@ -126,13 +121,11 @@ const basicVllmFieldsConfig: Record = { model: { label: 'Model', placeholder: 'microsoft/DialoGPT-medium', - description: 'The name or path of the Hugging Face model to use', - required: true + description: 'The name or path of the Hugging Face model to use' }, tensor_parallel_size: { label: 'Tensor Parallel Size', @@ -146,11 +139,23 @@ const basicVllmFieldsConfig: Record !isBasicField(key)) } - export function getBasicBackendFields(backendType?: string): string[] { - if (backendType === 'mlx_lm') { - return Object.keys(basicMlxFieldsConfig) - } else if (backendType === 'vllm') { - return Object.keys(basicVllmFieldsConfig) - } else if (backendType === 'llama_cpp') { - return Object.keys(basicLlamaCppFieldsConfig) - } - // Default to LlamaCpp for backward compatibility - return Object.keys(basicLlamaCppFieldsConfig) + const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldConfigs + const config = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig + return Object.keys(config) } export function getAdvancedBackendFields(backendType?: string): string[] { - if (backendType === 'mlx_lm') { - return getAllMlxFieldKeys().filter(key => !(key in basicMlxFieldsConfig)) - } else if (backendType === 'vllm') { - return getAllVllmFieldKeys().filter(key => !(key in basicVllmFieldsConfig)) - } else if (backendType === 'llama_cpp') { - return getAllLlamaCppFieldKeys().filter(key => !(key in basicLlamaCppFieldsConfig)) - } - // Default to LlamaCpp for backward compatibility - return getAllLlamaCppFieldKeys().filter(key => !(key in basicLlamaCppFieldsConfig)) + const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldGetters + const fieldGetter = backendFieldGetters[normalizedType] || getAllLlamaCppFieldKeys + const basicConfig = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig + + return fieldGetter().filter(key => !(key in basicConfig)) } // Combined backend fields config for use in BackendFormField @@ -189,7 +183,6 @@ export const basicBackendFieldsConfig: Record = { ...basicLlamaCppFieldsConfig, ...basicMlxFieldsConfig,