From 1fbf809a2db58e7946600ae42f1bbfc4e036b1ba Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 28 Sep 2025 14:40:04 +0200 Subject: [PATCH] Add EnvironmentVariablesInput component and integrate into InstanceSettingsCard --- .../form/EnvironmentVariablesInput.tsx | 144 ++++++++++++++++++ .../instance/InstanceSettingsCard.tsx | 9 ++ webui/src/lib/zodFormUtils.ts | 51 ------- webui/src/schemas/instanceOptions.ts | 4 + 4 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 webui/src/components/form/EnvironmentVariablesInput.tsx diff --git a/webui/src/components/form/EnvironmentVariablesInput.tsx b/webui/src/components/form/EnvironmentVariablesInput.tsx new file mode 100644 index 0000000..47739f0 --- /dev/null +++ b/webui/src/components/form/EnvironmentVariablesInput.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { X, Plus } from 'lucide-react' + +interface EnvironmentVariablesInputProps { + id: string + label: string + value: Record | undefined + onChange: (value: Record | undefined) => void + description?: string + disabled?: boolean + className?: string +} + +interface EnvVar { + key: string + value: string +} + +const EnvironmentVariablesInput: React.FC = ({ + id, + label, + value, + onChange, + description, + disabled = false, + className +}) => { + // Convert the value object to an array of key-value pairs for editing + const envVarsFromValue = value + ? Object.entries(value).map(([key, val]) => ({ key, value: val })) + : [] + + const [envVars, setEnvVars] = useState( + envVarsFromValue.length > 0 ? envVarsFromValue : [{ key: '', value: '' }] + ) + + // Update parent component when env vars change + const updateParent = (newEnvVars: EnvVar[]) => { + // Filter out empty entries + const validVars = newEnvVars.filter(env => env.key.trim() !== '' && env.value.trim() !== '') + + if (validVars.length === 0) { + onChange(undefined) + } else { + const envObject = validVars.reduce((acc, env) => { + acc[env.key.trim()] = env.value.trim() + return acc + }, {} as Record) + onChange(envObject) + } + } + + const handleKeyChange = (index: number, newKey: string) => { + const newEnvVars = [...envVars] + newEnvVars[index].key = newKey + setEnvVars(newEnvVars) + updateParent(newEnvVars) + } + + const handleValueChange = (index: number, newValue: string) => { + const newEnvVars = [...envVars] + newEnvVars[index].value = newValue + setEnvVars(newEnvVars) + updateParent(newEnvVars) + } + + const addEnvVar = () => { + const newEnvVars = [...envVars, { key: '', value: '' }] + setEnvVars(newEnvVars) + } + + const removeEnvVar = (index: number) => { + if (envVars.length === 1) { + // Reset to empty if it's the last one + const newEnvVars = [{ key: '', value: '' }] + setEnvVars(newEnvVars) + updateParent(newEnvVars) + } else { + const newEnvVars = envVars.filter((_, i) => i !== index) + setEnvVars(newEnvVars) + updateParent(newEnvVars) + } + } + + return ( +
+ +
+ {envVars.map((envVar, index) => ( +
+ handleKeyChange(index, e.target.value)} + disabled={disabled} + className="flex-1" + /> + handleValueChange(index, e.target.value)} + disabled={disabled} + className="flex-1" + /> + +
+ ))} + +
+ {description && ( +

{description}

+ )} +

+ Environment variables that will be passed to the backend process +

+
+ ) +} + +export default EnvironmentVariablesInput \ No newline at end of file diff --git a/webui/src/components/instance/InstanceSettingsCard.tsx b/webui/src/components/instance/InstanceSettingsCard.tsx index d997a8c..c85eda9 100644 --- a/webui/src/components/instance/InstanceSettingsCard.tsx +++ b/webui/src/components/instance/InstanceSettingsCard.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input' import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration' import NumberInput from '@/components/form/NumberInput' import CheckboxInput from '@/components/form/CheckboxInput' +import EnvironmentVariablesInput from '@/components/form/EnvironmentVariablesInput' interface InstanceSettingsCardProps { instanceName: string @@ -75,6 +76,14 @@ const InstanceSettingsCard: React.FC = ({ onChange={(value) => onChange('on_demand_start', value)} description="Start instance only when needed" /> + + onChange('environment', value)} + description="Custom environment variables for the instance" + /> diff --git a/webui/src/lib/zodFormUtils.ts b/webui/src/lib/zodFormUtils.ts index 88294c4..6d959d6 100644 --- a/webui/src/lib/zodFormUtils.ts +++ b/webui/src/lib/zodFormUtils.ts @@ -1,12 +1,10 @@ import { - type CreateInstanceOptions, type LlamaCppBackendOptions, type MlxBackendOptions, type VllmBackendOptions, LlamaCppBackendOptionsSchema, MlxBackendOptionsSchema, VllmBackendOptionsSchema, - getAllFieldKeys, getAllLlamaCppFieldKeys, getAllMlxFieldKeys, getAllVllmFieldKeys, @@ -15,41 +13,6 @@ import { getVllmFieldType } from '@/schemas/instanceOptions' -// Instance-level basic fields (not backend-specific) -export const basicFieldsConfig: Record = { - auto_restart: { - label: 'Auto Restart', - description: 'Automatically restart the instance on failure' - }, - max_restarts: { - label: 'Max Restarts', - placeholder: '3', - description: 'Maximum number of restart attempts (0 = unlimited)' - }, - restart_delay: { - label: 'Restart Delay (seconds)', - placeholder: '5', - description: 'Delay in seconds before attempting restart' - }, - idle_timeout: { - label: 'Idle Timeout (minutes)', - placeholder: '60', - description: 'Time in minutes before instance is considered idle and stopped' - }, - on_demand_start: { - label: 'On-Demand Start', - description: 'Start instance upon receiving OpenAI-compatible API request' - }, - backend_type: { - label: 'Backend Type', - description: 'Type of backend to use for this instance' - } -} - // LlamaCpp backend-specific basic fields const basicLlamaCppFieldsConfig: Record !isBasicField(key)) -} - export function getBasicBackendFields(backendType?: string): string[] { const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldConfigs const config = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig @@ -222,5 +173,3 @@ export function getBackendFieldType(key: string): 'text' | 'number' | 'boolean' return 'text' } -// Re-export the Zod-based functions -export { getFieldType } from '@/schemas/instanceOptions' \ No newline at end of file diff --git a/webui/src/schemas/instanceOptions.ts b/webui/src/schemas/instanceOptions.ts index 3d2df94..0af09c1 100644 --- a/webui/src/schemas/instanceOptions.ts +++ b/webui/src/schemas/instanceOptions.ts @@ -33,6 +33,9 @@ export const CreateInstanceOptionsSchema = z.object({ idle_timeout: z.number().optional(), on_demand_start: z.boolean().optional(), + // Environment variables + environment: z.record(z.string(), z.string()).optional(), + // Backend configuration backend_type: z.enum([BackendType.LLAMA_CPP, BackendType.MLX_LM, BackendType.VLLM]).optional(), backend_options: BackendOptionsSchema.optional(), @@ -75,5 +78,6 @@ export function getFieldType(key: keyof CreateInstanceOptions): 'text' | 'number if (innerSchema instanceof z.ZodNumber) return 'number' if (innerSchema instanceof z.ZodArray) return 'array' if (innerSchema instanceof z.ZodObject) return 'object' + if (innerSchema instanceof z.ZodRecord) return 'object' // Handle ZodRecord as object return 'text' // ZodString and others default to text } \ No newline at end of file