mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-07 16:41:06 +08:00
267 lines
7.8 KiB
TypeScript
267 lines
7.8 KiB
TypeScript
// ========================================
|
|
// SchemaFormRenderer Component
|
|
// ========================================
|
|
// Renders structured form groups from EnvVarGroupsSchema definition
|
|
// Supports select, number, checkbox, text, and model-select field types
|
|
|
|
import { useMemo } from 'react';
|
|
import { useIntl } from 'react-intl';
|
|
import {
|
|
Box,
|
|
ArrowUpDown,
|
|
Cpu,
|
|
GitBranch,
|
|
Scissors,
|
|
type LucideIcon,
|
|
} from 'lucide-react';
|
|
import { Label } from '@/components/ui/Label';
|
|
import { Input } from '@/components/ui/Input';
|
|
import { Checkbox } from '@/components/ui/Checkbox';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/Select';
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from '@/components/ui/Collapsible';
|
|
import { cn } from '@/lib/utils';
|
|
import { evaluateShowWhen } from './envVarSchema';
|
|
import { ModelSelectField } from './ModelSelectField';
|
|
import type { EnvVarGroupsSchema, EnvVarFieldSchema } from '@/types/codexlens';
|
|
import type { CodexLensModel } from '@/lib/api';
|
|
|
|
// Icon mapping for group icons
|
|
const iconMap: Record<string, LucideIcon> = {
|
|
box: Box,
|
|
'arrow-up-down': ArrowUpDown,
|
|
cpu: Cpu,
|
|
'git-branch': GitBranch,
|
|
scissors: Scissors,
|
|
};
|
|
|
|
interface SchemaFormRendererProps {
|
|
/** The schema defining all groups and fields */
|
|
groups: EnvVarGroupsSchema;
|
|
/** Current form values keyed by env var name */
|
|
values: Record<string, string>;
|
|
/** Called when a field value changes */
|
|
onChange: (key: string, value: string) => void;
|
|
/** Whether the form is disabled (loading state) */
|
|
disabled?: boolean;
|
|
/** Local embedding models (installed) for model-select */
|
|
localEmbeddingModels?: CodexLensModel[];
|
|
/** Local reranker models (installed) for model-select */
|
|
localRerankerModels?: CodexLensModel[];
|
|
}
|
|
|
|
export function SchemaFormRenderer({
|
|
groups,
|
|
values,
|
|
onChange,
|
|
disabled = false,
|
|
localEmbeddingModels = [],
|
|
localRerankerModels = [],
|
|
}: SchemaFormRendererProps) {
|
|
const { formatMessage } = useIntl();
|
|
|
|
const groupEntries = useMemo(() => Object.entries(groups), [groups]);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{groupEntries.map(([groupKey, group]) => {
|
|
const IconComponent = iconMap[group.icon] || Box;
|
|
|
|
return (
|
|
<Collapsible key={groupKey} defaultOpen>
|
|
<div className="border border-border rounded-lg">
|
|
<CollapsibleTrigger className="flex w-full items-center gap-2 p-3 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors">
|
|
<IconComponent className="w-3.5 h-3.5" />
|
|
{formatMessage({ id: group.labelKey, defaultMessage: groupKey })}
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="px-3 pb-3 space-y-2">
|
|
{Object.entries(group.vars).map(([varKey, field]) => {
|
|
const visible = evaluateShowWhen(field, values);
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<FieldRenderer
|
|
key={varKey}
|
|
field={field}
|
|
value={values[varKey] ?? field.default ?? ''}
|
|
onChange={(val) => onChange(varKey, val)}
|
|
allValues={values}
|
|
disabled={disabled}
|
|
localModels={
|
|
varKey.includes('EMBEDDING')
|
|
? localEmbeddingModels
|
|
: localRerankerModels
|
|
}
|
|
formatMessage={formatMessage}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</div>
|
|
</Collapsible>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ========================================
|
|
// Individual Field Renderer
|
|
// ========================================
|
|
|
|
interface FieldRendererProps {
|
|
field: EnvVarFieldSchema;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
allValues: Record<string, string>;
|
|
disabled: boolean;
|
|
localModels: CodexLensModel[];
|
|
formatMessage: (descriptor: { id: string; defaultMessage?: string }) => string;
|
|
}
|
|
|
|
function FieldRenderer({
|
|
field,
|
|
value,
|
|
onChange,
|
|
allValues,
|
|
disabled,
|
|
localModels,
|
|
formatMessage,
|
|
}: FieldRendererProps) {
|
|
const label = formatMessage({ id: field.labelKey, defaultMessage: field.key });
|
|
|
|
switch (field.type) {
|
|
case 'select':
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="text-xs text-muted-foreground w-28 flex-shrink-0"
|
|
title={field.key}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<Select
|
|
value={value}
|
|
onValueChange={onChange}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger className={cn('flex-1 h-8 text-xs')}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(field.options || []).map((opt) => (
|
|
<SelectItem key={opt} value={opt} className="text-xs">
|
|
{opt}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
);
|
|
|
|
case 'number':
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="text-xs text-muted-foreground w-28 flex-shrink-0"
|
|
title={field.key}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<Input
|
|
type="number"
|
|
className="flex-1 h-8 text-xs"
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={field.placeholder}
|
|
min={field.min}
|
|
max={field.max}
|
|
step={field.step ?? 1}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
case 'checkbox':
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="text-xs text-muted-foreground w-28 flex-shrink-0"
|
|
title={field.key}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<div className="flex-1 flex items-center h-8">
|
|
<Checkbox
|
|
checked={value === 'true'}
|
|
onCheckedChange={(checked) => onChange(checked ? 'true' : 'false')}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'model-select': {
|
|
// Determine backend type from related backend env var
|
|
const isEmbedding = field.key.includes('EMBEDDING');
|
|
const backendKey = isEmbedding
|
|
? 'CODEXLENS_EMBEDDING_BACKEND'
|
|
: 'CODEXLENS_RERANKER_BACKEND';
|
|
const backendType = allValues[backendKey] === 'api' ? 'api' : 'local';
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="text-xs text-muted-foreground w-28 flex-shrink-0"
|
|
title={field.key}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<ModelSelectField
|
|
field={field}
|
|
value={value}
|
|
onChange={onChange}
|
|
localModels={localModels}
|
|
backendType={backendType}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
case 'text':
|
|
default:
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="text-xs text-muted-foreground w-28 flex-shrink-0"
|
|
title={field.key}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
className="flex-1 h-8 text-xs"
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={field.placeholder}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default SchemaFormRenderer;
|