mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-05 16:44:22 +00:00
Refactor form components and improve API error handling
This commit is contained in:
@@ -45,7 +45,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
@@ -72,7 +71,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
@@ -99,7 +97,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
|
||||
@@ -11,11 +11,13 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { BackendType, type CreateInstanceOptions, type Instance } from "@/types/instance";
|
||||
import { getBasicFields, getAdvancedFields, getBasicBackendFields, getAdvancedBackendFields } from "@/lib/zodFormUtils";
|
||||
import { getAdvancedFields, getAdvancedBackendFields } from "@/lib/zodFormUtils";
|
||||
import { ChevronDown, ChevronRight, Terminal } from "lucide-react";
|
||||
import ZodFormField from "@/components/ZodFormField";
|
||||
import BackendFormField from "@/components/BackendFormField";
|
||||
import ParseCommandDialog from "@/components/ParseCommandDialog";
|
||||
import AutoRestartConfiguration from "@/components/instance/AutoRestartConfiguration";
|
||||
import BasicInstanceFields from "@/components/instance/BasicInstanceFields";
|
||||
import BackendConfiguration from "@/components/instance/BackendConfiguration";
|
||||
import AdvancedInstanceFields from "@/components/instance/AdvancedInstanceFields";
|
||||
|
||||
interface InstanceDialogProps {
|
||||
open: boolean;
|
||||
@@ -39,9 +41,7 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
||||
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<InstanceDialogProps> = ({
|
||||
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<InstanceDialogProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Auto Restart Configuration Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">
|
||||
Auto Restart Configuration
|
||||
</h3>
|
||||
<AutoRestartConfiguration
|
||||
formData={formData}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
|
||||
{/* Auto Restart Toggle */}
|
||||
<ZodFormField
|
||||
fieldKey="auto_restart"
|
||||
value={formData.auto_restart}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
|
||||
{/* Show restart options only when auto restart is enabled */}
|
||||
{isAutoRestartEnabled && (
|
||||
<div className="ml-6 space-y-4 border-l-2 border-muted pl-4">
|
||||
<ZodFormField
|
||||
fieldKey="max_restarts"
|
||||
value={formData.max_restarts}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<ZodFormField
|
||||
fieldKey="restart_delay"
|
||||
value={formData.restart_delay}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Basic Fields - Automatically generated from type (excluding auto restart options) */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Basic Configuration</h3>
|
||||
{basicFields
|
||||
.filter(
|
||||
(fieldKey) =>
|
||||
fieldKey !== "auto_restart" &&
|
||||
fieldKey !== "max_restarts" &&
|
||||
fieldKey !== "restart_delay" &&
|
||||
fieldKey !== "backend_options" // backend_options is handled separately
|
||||
)
|
||||
.map((fieldKey) => (
|
||||
<ZodFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={formData[fieldKey]}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Basic Fields */}
|
||||
<BasicInstanceFields
|
||||
formData={formData}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
|
||||
{/* Backend Configuration Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Backend Configuration</h3>
|
||||
|
||||
{/* Basic backend fields */}
|
||||
{basicBackendFields.map((fieldKey) => (
|
||||
<BackendFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={(formData.backend_options as any)?.[fieldKey]}
|
||||
onChange={handleBackendFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<BackendConfiguration
|
||||
formData={formData}
|
||||
onBackendFieldChange={handleBackendFieldChange}
|
||||
showAdvanced={showAdvanced}
|
||||
/>
|
||||
|
||||
{/* Advanced Fields Toggle */}
|
||||
<div className="border-t pt-4">
|
||||
@@ -314,54 +265,13 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Fields - Automatically generated from type (excluding restart options) */}
|
||||
{/* Advanced Fields */}
|
||||
{showAdvanced && (
|
||||
<div className="space-y-4 pl-6 border-l-2 border-muted">
|
||||
{/* Advanced instance fields */}
|
||||
{advancedFields
|
||||
.filter(
|
||||
(fieldKey) =>
|
||||
!["max_restarts", "restart_delay", "backend_options"].includes(
|
||||
fieldKey as string
|
||||
)
|
||||
).length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-md font-medium">Advanced Instance Configuration</h4>
|
||||
{advancedFields
|
||||
.filter(
|
||||
(fieldKey) =>
|
||||
!["max_restarts", "restart_delay", "backend_options"].includes(
|
||||
fieldKey as string
|
||||
)
|
||||
)
|
||||
.sort()
|
||||
.map((fieldKey) => (
|
||||
<ZodFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={fieldKey === 'backend_options' ? undefined : formData[fieldKey]}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced backend fields */}
|
||||
{advancedBackendFields.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-md font-medium">Advanced Backend Configuration</h4>
|
||||
{advancedBackendFields
|
||||
.sort()
|
||||
.map((fieldKey) => (
|
||||
<BackendFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={(formData.backend_options as any)?.[fieldKey]}
|
||||
onChange={handleBackendFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AdvancedInstanceFields
|
||||
formData={formData}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -81,17 +81,14 @@ const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
const backendPlaceholders: Record<BackendTypeValue, string> = {
|
||||
[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 (
|
||||
|
||||
@@ -29,7 +29,6 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<select
|
||||
id={fieldKey}
|
||||
@@ -71,8 +70,7 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
type="number"
|
||||
@@ -98,8 +96,7 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
type="text"
|
||||
@@ -125,8 +122,7 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={fieldKey}>
|
||||
{config.label}
|
||||
{config.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldKey}
|
||||
type="text"
|
||||
|
||||
62
webui/src/components/form/ArrayInput.tsx
Normal file
62
webui/src/components/form/ArrayInput.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface ArrayInputProps {
|
||||
id: string
|
||||
label: string
|
||||
value: string[] | undefined
|
||||
onChange: (value: string[] | undefined) => void
|
||||
placeholder?: string
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ArrayInput: React.FC<ArrayInputProps> = ({
|
||||
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 (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="text"
|
||||
value={Array.isArray(value) ? value.join(', ') : ''}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
/>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">Separate multiple values with commas</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArrayInput
|
||||
42
webui/src/components/form/CheckboxInput.tsx
Normal file
42
webui/src/components/form/CheckboxInput.tsx
Normal file
@@ -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<CheckboxInputProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
description,
|
||||
disabled = false,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex items-center space-x-2 ${className || ''}`}>
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={value === true}
|
||||
onCheckedChange={(checked) => onChange(!!checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor={id} className="text-sm font-normal">
|
||||
{label}
|
||||
{description && (
|
||||
<span className="text-muted-foreground ml-1">- {description}</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckboxInput
|
||||
60
webui/src/components/form/NumberInput.tsx
Normal file
60
webui/src/components/form/NumberInput.tsx
Normal file
@@ -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<NumberInputProps> = ({
|
||||
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 (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="number"
|
||||
step="any"
|
||||
value={value !== undefined ? value : ''}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
/>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NumberInput
|
||||
55
webui/src/components/form/SelectInput.tsx
Normal file
55
webui/src/components/form/SelectInput.tsx
Normal file
@@ -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<SelectInputProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
description,
|
||||
disabled = false,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<select
|
||||
id={id}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value || undefined)}
|
||||
disabled={disabled}
|
||||
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className || ''}`}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectInput
|
||||
47
webui/src/components/form/TextInput.tsx
Normal file
47
webui/src/components/form/TextInput.tsx
Normal file
@@ -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<TextInputProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
description,
|
||||
disabled = false,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="text"
|
||||
value={typeof value === 'string' || typeof value === 'number' ? value : ''}
|
||||
onChange={(e) => onChange(e.target.value || undefined)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
/>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextInput
|
||||
98
webui/src/components/instance/AdvancedInstanceFields.tsx
Normal file
98
webui/src/components/instance/AdvancedInstanceFields.tsx
Normal file
@@ -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<AdvancedInstanceFieldsProps> = ({
|
||||
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 (
|
||||
<CheckboxInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as boolean | undefined}
|
||||
onChange={(value) => onChange(fieldKey, value)}
|
||||
description={config.description}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<NumberInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as number | undefined}
|
||||
onChange={(value) => onChange(fieldKey, value)}
|
||||
placeholder={config.placeholder}
|
||||
description={config.description}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'array':
|
||||
return (
|
||||
<ArrayInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as string[] | undefined}
|
||||
onChange={(value) => onChange(fieldKey, value)}
|
||||
placeholder={config.placeholder}
|
||||
description={config.description}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as string | number | undefined}
|
||||
onChange={(value) => 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 (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-md font-medium">Advanced Instance Configuration</h4>
|
||||
{fieldsToRender
|
||||
.sort()
|
||||
.map(renderField)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvancedInstanceFields
|
||||
53
webui/src/components/instance/AutoRestartConfiguration.tsx
Normal file
53
webui/src/components/instance/AutoRestartConfiguration.tsx
Normal file
@@ -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<AutoRestartConfigurationProps> = ({
|
||||
formData,
|
||||
onChange
|
||||
}) => {
|
||||
const isAutoRestartEnabled = formData.auto_restart === true
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Auto Restart Configuration</h3>
|
||||
|
||||
<CheckboxInput
|
||||
id="auto_restart"
|
||||
label="Auto Restart"
|
||||
value={formData.auto_restart}
|
||||
onChange={(value) => onChange('auto_restart', value)}
|
||||
description="Automatically restart the instance on failure"
|
||||
/>
|
||||
|
||||
{isAutoRestartEnabled && (
|
||||
<div className="ml-6 space-y-4 border-l-2 border-muted pl-4">
|
||||
<NumberInput
|
||||
id="max_restarts"
|
||||
label="Max Restarts"
|
||||
value={formData.max_restarts}
|
||||
onChange={(value) => onChange('max_restarts', value)}
|
||||
placeholder="3"
|
||||
description="Maximum number of restart attempts (0 = unlimited)"
|
||||
/>
|
||||
<NumberInput
|
||||
id="restart_delay"
|
||||
label="Restart Delay (seconds)"
|
||||
value={formData.restart_delay}
|
||||
onChange={(value) => onChange('restart_delay', value)}
|
||||
placeholder="5"
|
||||
description="Delay in seconds before attempting restart"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoRestartConfiguration
|
||||
54
webui/src/components/instance/BackendConfiguration.tsx
Normal file
54
webui/src/components/instance/BackendConfiguration.tsx
Normal file
@@ -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<BackendConfigurationProps> = ({
|
||||
formData,
|
||||
onBackendFieldChange,
|
||||
showAdvanced = false
|
||||
}) => {
|
||||
const basicBackendFields = getBasicBackendFields(formData.backend_type)
|
||||
const advancedBackendFields = getAdvancedBackendFields(formData.backend_type)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Backend Configuration</h3>
|
||||
|
||||
{/* Basic backend fields */}
|
||||
{basicBackendFields.map((fieldKey) => (
|
||||
<BackendFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={(formData.backend_options as any)?.[fieldKey]}
|
||||
onChange={onBackendFieldChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Advanced backend fields */}
|
||||
{showAdvanced && advancedBackendFields.length > 0 && (
|
||||
<div className="space-y-4 pl-6 border-l-2 border-muted">
|
||||
<h4 className="text-md font-medium">Advanced Backend Configuration</h4>
|
||||
{advancedBackendFields
|
||||
.sort()
|
||||
.map((fieldKey) => (
|
||||
<BackendFormField
|
||||
key={fieldKey}
|
||||
fieldKey={fieldKey}
|
||||
value={(formData.backend_options as any)?.[fieldKey]}
|
||||
onChange={onBackendFieldChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackendConfiguration
|
||||
99
webui/src/components/instance/BasicInstanceFields.tsx
Normal file
99
webui/src/components/instance/BasicInstanceFields.tsx
Normal file
@@ -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<BasicInstanceFieldsProps> = ({
|
||||
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 (
|
||||
<SelectInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] || BackendType.LLAMA_CPP}
|
||||
onChange={(value) => 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 (
|
||||
<CheckboxInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as boolean | undefined}
|
||||
onChange={(value) => onChange(fieldKey, value)}
|
||||
description={config.description}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<NumberInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as number | undefined}
|
||||
onChange={(value) => onChange(fieldKey, value)}
|
||||
placeholder={config.placeholder}
|
||||
description={config.description}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextInput
|
||||
key={fieldKey}
|
||||
id={fieldKey}
|
||||
label={config.label}
|
||||
value={formData[fieldKey] as string | number | undefined}
|
||||
onChange={(value) => 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 (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Basic Configuration</h3>
|
||||
{fieldsToRender.map(renderField)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasicInstanceFields
|
||||
@@ -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<T>(
|
||||
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) {
|
||||
|
||||
32
webui/src/lib/errorUtils.ts
Normal file
32
webui/src/lib/errorUtils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Parses error response from API calls and returns a formatted error message
|
||||
*/
|
||||
export async function parseErrorResponse(response: Response): Promise<string> {
|
||||
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<void> {
|
||||
// Handle authentication errors
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await parseErrorResponse(response)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export const basicFieldsConfig: Record<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
auto_restart: {
|
||||
label: 'Auto Restart',
|
||||
@@ -56,13 +55,11 @@ const basicLlamaCppFieldsConfig: Record<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
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<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
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<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
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<string, {
|
||||
}
|
||||
}
|
||||
|
||||
// Backend field configuration lookup
|
||||
const backendFieldConfigs = {
|
||||
mlx_lm: basicMlxFieldsConfig,
|
||||
vllm: basicVllmFieldsConfig,
|
||||
llama_cpp: basicLlamaCppFieldsConfig,
|
||||
} as const
|
||||
|
||||
const backendFieldGetters = {
|
||||
mlx_lm: getAllMlxFieldKeys,
|
||||
vllm: getAllVllmFieldKeys,
|
||||
llama_cpp: getAllLlamaCppFieldKeys,
|
||||
} as const
|
||||
|
||||
function isBasicField(key: keyof CreateInstanceOptions): boolean {
|
||||
return key in basicFieldsConfig
|
||||
}
|
||||
|
||||
|
||||
export function getBasicFields(): (keyof CreateInstanceOptions)[] {
|
||||
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
|
||||
}
|
||||
@@ -159,29 +164,18 @@ export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
|
||||
return getAllFieldKeys().filter(key => !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<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
...basicLlamaCppFieldsConfig,
|
||||
...basicMlxFieldsConfig,
|
||||
|
||||
Reference in New Issue
Block a user