Refactor form components and improve API error handling

This commit is contained in:
2025-09-21 21:33:53 +02:00
parent b665194307
commit 501afb7f0d
16 changed files with 663 additions and 184 deletions

View File

@@ -45,7 +45,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}
@@ -72,7 +71,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}
@@ -99,7 +97,6 @@ const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, on
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}

View File

@@ -11,11 +11,13 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { BackendType, type CreateInstanceOptions, type Instance } from "@/types/instance"; 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 { ChevronDown, ChevronRight, Terminal } from "lucide-react";
import ZodFormField from "@/components/ZodFormField";
import BackendFormField from "@/components/BackendFormField";
import ParseCommandDialog from "@/components/ParseCommandDialog"; 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 { interface InstanceDialogProps {
open: boolean; open: boolean;
@@ -39,9 +41,7 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
const [showParseDialog, setShowParseDialog] = useState(false); const [showParseDialog, setShowParseDialog] = useState(false);
// Get field lists dynamically from the type // Get field lists dynamically from the type
const basicFields = getBasicFields();
const advancedFields = getAdvancedFields(); const advancedFields = getAdvancedFields();
const basicBackendFields = getBasicBackendFields(formData.backend_type);
const advancedBackendFields = getAdvancedBackendFields(formData.backend_type); const advancedBackendFields = getAdvancedBackendFields(formData.backend_type);
// Reset form when dialog opens/closes or when instance changes // Reset form when dialog opens/closes or when instance changes
@@ -163,8 +163,6 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
setShowParseDialog(false); setShowParseDialog(false);
}; };
// Check if auto_restart is enabled
const isAutoRestartEnabled = formData.auto_restart === true;
// Save button label logic // Save button label logic
let saveButtonLabel = "Create Instance"; let saveButtonLabel = "Create Instance";
@@ -212,70 +210,23 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
</div> </div>
{/* Auto Restart Configuration Section */} {/* Auto Restart Configuration Section */}
<div className="space-y-4"> <AutoRestartConfiguration
<h3 className="text-lg font-medium"> formData={formData}
Auto Restart Configuration
</h3>
{/* Auto Restart Toggle */}
<ZodFormField
fieldKey="auto_restart"
value={formData.auto_restart}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
{/* Show restart options only when auto restart is enabled */} {/* Basic Fields */}
{isAutoRestartEnabled && ( <BasicInstanceFields
<div className="ml-6 space-y-4 border-l-2 border-muted pl-4"> formData={formData}
<ZodFormField
fieldKey="max_restarts"
value={formData.max_restarts}
onChange={handleFieldChange} 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>
{/* Backend Configuration Section */} {/* Backend Configuration Section */}
<div className="space-y-4"> <BackendConfiguration
<h3 className="text-lg font-medium">Backend Configuration</h3> formData={formData}
onBackendFieldChange={handleBackendFieldChange}
{/* Basic backend fields */} showAdvanced={showAdvanced}
{basicBackendFields.map((fieldKey) => (
<BackendFormField
key={fieldKey}
fieldKey={fieldKey}
value={(formData.backend_options as any)?.[fieldKey]}
onChange={handleBackendFieldChange}
/> />
))}
</div>
{/* Advanced Fields Toggle */} {/* Advanced Fields Toggle */}
<div className="border-t pt-4"> <div className="border-t pt-4">
@@ -314,54 +265,13 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
</div> </div>
</div> </div>
{/* Advanced Fields - Automatically generated from type (excluding restart options) */} {/* Advanced Fields */}
{showAdvanced && ( {showAdvanced && (
<div className="space-y-4 pl-6 border-l-2 border-muted"> <div className="space-y-4 pl-6 border-l-2 border-muted">
{/* Advanced instance fields */} <AdvancedInstanceFields
{advancedFields formData={formData}
.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} 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>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -81,17 +81,14 @@ const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
onOpenChange(open); 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 => { const getPlaceholderForBackend = (backendType: BackendTypeValue): string => {
switch (backendType) { return backendPlaceholders[backendType] || "Enter your command here...";
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 ( return (

View File

@@ -29,7 +29,6 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<select <select
id={fieldKey} id={fieldKey}
@@ -71,7 +70,6 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}
@@ -98,7 +96,6 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}
@@ -125,7 +122,6 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor={fieldKey}> <Label htmlFor={fieldKey}>
{config.label} {config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldKey} id={fieldKey}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -1,4 +1,5 @@
import type { CreateInstanceOptions, Instance } from "@/types/instance"; import type { CreateInstanceOptions, Instance } from "@/types/instance";
import { handleApiError } from "./errorUtils";
const API_BASE = "/api/v1"; const API_BASE = "/api/v1";
@@ -30,25 +31,8 @@ async function apiCall<T>(
headers, headers,
}); });
// Handle authentication errors // Handle errors using centralized error handler
if (response.status === 401) { await handleApiError(response);
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 empty responses (like DELETE) // Handle empty responses (like DELETE)
if (response.status === 204) { if (response.status === 204) {

View 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)
}
}

View File

@@ -20,7 +20,6 @@ export const basicFieldsConfig: Record<string, {
label: string label: string
description?: string description?: string
placeholder?: string placeholder?: string
required?: boolean
}> = { }> = {
auto_restart: { auto_restart: {
label: 'Auto Restart', label: 'Auto Restart',
@@ -56,13 +55,11 @@ const basicLlamaCppFieldsConfig: Record<string, {
label: string label: string
description?: string description?: string
placeholder?: string placeholder?: string
required?: boolean
}> = { }> = {
model: { model: {
label: 'Model Path', label: 'Model Path',
placeholder: '/path/to/model.gguf', placeholder: '/path/to/model.gguf',
description: 'Path to the model file', description: 'Path to the model file'
required: true
}, },
hf_repo: { hf_repo: {
label: 'Hugging Face Repository', label: 'Hugging Face Repository',
@@ -86,13 +83,11 @@ const basicMlxFieldsConfig: Record<string, {
label: string label: string
description?: string description?: string
placeholder?: string placeholder?: string
required?: boolean
}> = { }> = {
model: { model: {
label: 'Model', label: 'Model',
placeholder: 'mlx-community/Mistral-7B-Instruct-v0.3-4bit', placeholder: 'mlx-community/Mistral-7B-Instruct-v0.3-4bit',
description: 'The path to the MLX model weights, tokenizer, and config', description: 'The path to the MLX model weights, tokenizer, and config'
required: true
}, },
temp: { temp: {
label: 'Temperature', label: 'Temperature',
@@ -126,13 +121,11 @@ const basicVllmFieldsConfig: Record<string, {
label: string label: string
description?: string description?: string
placeholder?: string placeholder?: string
required?: boolean
}> = { }> = {
model: { model: {
label: 'Model', label: 'Model',
placeholder: 'microsoft/DialoGPT-medium', placeholder: 'microsoft/DialoGPT-medium',
description: 'The name or path of the Hugging Face model to use', description: 'The name or path of the Hugging Face model to use'
required: true
}, },
tensor_parallel_size: { tensor_parallel_size: {
label: '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 { function isBasicField(key: keyof CreateInstanceOptions): boolean {
return key in basicFieldsConfig return key in basicFieldsConfig
} }
export function getBasicFields(): (keyof CreateInstanceOptions)[] { export function getBasicFields(): (keyof CreateInstanceOptions)[] {
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[] return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
} }
@@ -159,29 +164,18 @@ export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
return getAllFieldKeys().filter(key => !isBasicField(key)) return getAllFieldKeys().filter(key => !isBasicField(key))
} }
export function getBasicBackendFields(backendType?: string): string[] { export function getBasicBackendFields(backendType?: string): string[] {
if (backendType === 'mlx_lm') { const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldConfigs
return Object.keys(basicMlxFieldsConfig) const config = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
} else if (backendType === 'vllm') { return Object.keys(config)
return Object.keys(basicVllmFieldsConfig)
} else if (backendType === 'llama_cpp') {
return Object.keys(basicLlamaCppFieldsConfig)
}
// Default to LlamaCpp for backward compatibility
return Object.keys(basicLlamaCppFieldsConfig)
} }
export function getAdvancedBackendFields(backendType?: string): string[] { export function getAdvancedBackendFields(backendType?: string): string[] {
if (backendType === 'mlx_lm') { const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldGetters
return getAllMlxFieldKeys().filter(key => !(key in basicMlxFieldsConfig)) const fieldGetter = backendFieldGetters[normalizedType] || getAllLlamaCppFieldKeys
} else if (backendType === 'vllm') { const basicConfig = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
return getAllVllmFieldKeys().filter(key => !(key in basicVllmFieldsConfig))
} else if (backendType === 'llama_cpp') { return fieldGetter().filter(key => !(key in basicConfig))
return getAllLlamaCppFieldKeys().filter(key => !(key in basicLlamaCppFieldsConfig))
}
// Default to LlamaCpp for backward compatibility
return getAllLlamaCppFieldKeys().filter(key => !(key in basicLlamaCppFieldsConfig))
} }
// Combined backend fields config for use in BackendFormField // Combined backend fields config for use in BackendFormField
@@ -189,7 +183,6 @@ export const basicBackendFieldsConfig: Record<string, {
label: string label: string
description?: string description?: string
placeholder?: string placeholder?: string
required?: boolean
}> = { }> = {
...basicLlamaCppFieldsConfig, ...basicLlamaCppFieldsConfig,
...basicMlxFieldsConfig, ...basicMlxFieldsConfig,