Files
Claude-Code-Workflow/ccw/frontend/src/components/codexlens/SchemaFormRenderer.tsx

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;