'use client' import type { EnvVarInput, EnvVarSlot } from '@dify/contracts/enterprise/types.gen' import type { InputHTMLAttributes } from 'react' import { EnvVarValueSource as ApiEnvVarValueSource } from '@dify/contracts/enterprise/types.gen' import { cn } from '@langgenius/dify-ui/cn' import { Input } from '@langgenius/dify-ui/input' import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' import { TitleTooltip } from './title-tooltip' export type EnvVarValueSource = NonNullable type EnvVarValueType = 'string' | 'number' | 'secret' // Contract EnvVarSlot carries an EnvVarValueType enum and signals default/last // presence via optional fields; binding slots keep the legacy lowercase value // type plus explicit has* flags because DSL-metadata slots share this shape. // Adapters live in env-var-bindings-utils.ts. export type EnvVarBindingSlot = Omit & { key: string valueType: EnvVarValueType hasDefaultValue?: boolean hasLastValue?: boolean } export type EnvVarValueSelection = { valueSource: EnvVarValueSource value?: string } export type EnvVarValues = Record type EnvVarDefaultSourcePriority = 'dslDefault' | 'lastDeployment' type EnvVarValueSourceOption = { value: EnvVarValueSource label: string } type EnvVarBindingsPanelProps = { slots: EnvVarBindingSlot[] values: EnvVarValues title: string hint: string envVarPlaceholder: string literalSourceLabel: string defaultSourceLabel: string lastDeploymentSourceLabel: string valueTypeLabels: Record sourceAriaLabel: (key: string) => string defaultSourcePriority?: EnvVarDefaultSourcePriority envVarCountLabel?: string missingRequiredLabel?: string showMissingRequired?: boolean listScrollable?: boolean onChange: (key: string, value: EnvVarValueSelection) => void className?: string listClassName?: string } const ENV_VAR_INPUT_TYPES = { string: 'text', number: 'number', secret: 'password', } satisfies Record['type']> function envVarInputId(index: number, key: string) { const safeKey = key.replace(/[^\w-]/g, '-') return `env-var-binding-${index}-${safeKey}` } function envVarValueSourceOptions( slot: EnvVarBindingSlot, labels: { literal: string defaultValue: string lastDeployment: string }, ): EnvVarValueSourceOption[] { const options: EnvVarValueSourceOption[] = [ { value: ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LITERAL, label: labels.literal, }, ] if (slot.hasDefaultValue) { options.push({ value: ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_DSL_DEFAULT, label: labels.defaultValue, }) } if (slot.hasLastValue) { options.push({ value: ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LAST_DEPLOYMENT, label: labels.lastDeployment, }) } return options } export function EnvVarBindingsPanel({ slots, values, title, hint, envVarPlaceholder, literalSourceLabel, defaultSourceLabel, lastDeploymentSourceLabel, valueTypeLabels, sourceAriaLabel, defaultSourcePriority, envVarCountLabel, missingRequiredLabel, showMissingRequired = false, listScrollable = true, onChange, className, listClassName, }: EnvVarBindingsPanelProps) { if (slots.length === 0) return null return (
{title}
{envVarCountLabel ?? slots.length}
{hint}
{slots.map((slot, index) => { const description = slot.description const inputId = envVarInputId(index, slot.key) const selection = values[slot.key] const defaultValueSource = defaultSourcePriority === 'lastDeployment' && slot.hasLastValue ? ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LAST_DEPLOYMENT : slot.hasDefaultValue ? ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_DSL_DEFAULT : slot.hasLastValue ? ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LAST_DEPLOYMENT : ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LITERAL const valueSource = selection?.valueSource ?? defaultValueSource const invalidLiteralNumber = slot.valueType === 'number' && Number.isNaN(Number(selection?.value)) const missing = showMissingRequired && valueSource === ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LITERAL && (!selection?.value || invalidLiteralNumber) const sourceOptions = envVarValueSourceOptions(slot, { literal: literalSourceLabel, defaultValue: defaultSourceLabel, lastDeployment: lastDeploymentSourceLabel, }) const isLiteralValue = valueSource === ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LITERAL const displayValue = valueSource === ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_DSL_DEFAULT ? slot.defaultValue : valueSource === ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LAST_DEPLOYMENT ? slot.lastValue : selection?.value return (
{sourceOptions.length > 1 && ( aria-label={sourceAriaLabel(slot.key)} value={[valueSource]} onValueChange={(value) => { const nextSource = value[0] if (!nextSource) return onChange(slot.key, { value: selection?.value, valueSource: nextSource, }) }} className="shrink-0" > {sourceOptions.map((option) => ( {option.label} ))} )}
{description && (
{description}
)} onChange(slot.key, { value: event.target.value, valueSource: ApiEnvVarValueSource.ENV_VAR_VALUE_SOURCE_LITERAL, }) } placeholder={envVarPlaceholder} autoComplete="off" disabled={!isLiteralValue} required className="h-8" />
{missing && missingRequiredLabel && (
{missingRequiredLabel}
)}
) })}
) }