import { memo, useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Modal } from '@/components/ui/Modal'; import { Select } from '@/components/ui/Select'; import { useNotificationStore } from '@/stores'; import styles from './VisualConfigEditor.module.scss'; import { copyToClipboard } from '@/utils/clipboard'; import type { PayloadFilterRule, PayloadModelEntry, PayloadParamEntry, PayloadParamValidationErrorCode, PayloadParamValueType, PayloadRule, } from '@/types/visualConfig'; import { makeClientId } from '@/types/visualConfig'; import { getPayloadParamValidationError, VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS, VISUAL_CONFIG_PROTOCOL_OPTIONS, } from '@/hooks/useVisualConfig'; import { maskApiKey } from '@/utils/format'; import { isValidApiKeyCharset } from '@/utils/validation'; /** Minimum character count before the expand/collapse toggle appears. */ const EXPAND_THRESHOLD = 30; /** Auto-expanding textarea that collapses back to a single-line input on demand. */ function ExpandableInput({ value, placeholder, ariaLabel, disabled, className, onChange, }: { value: string; placeholder?: string; ariaLabel?: string; disabled?: boolean; className?: string; onChange: (nextValue: string) => void; }) { const { t } = useTranslation(); const [collapsed, setCollapsed] = useState(true); const textareaRef = useRef(null); const autoResize = useCallback((el: HTMLTextAreaElement) => { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }, []); const handleChange = (e: React.ChangeEvent) => { // Strip newlines — these fields are single-line identifiers/paths that // would break YAML serialization if they contained line breaks. const sanitized = e.target.value.replace(/[\r\n]/g, ''); onChange(sanitized); // autoResize is handled by useLayoutEffect after React syncs the // sanitized value back to the DOM — calling it here would measure // stale content. }; // Resize synchronously before paint to avoid visual flicker. useLayoutEffect(() => { if (!collapsed && textareaRef.current) { autoResize(textareaRef.current); } }, [collapsed, value, autoResize]); if (collapsed) { return (
onChange(e.target.value.replace(/[\r\n]/g, ''))} disabled={disabled} /> {value.length > EXPAND_THRESHOLD && ( )}
); } return (