import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconChevronDown } from '@/components/ui/icons'; import { ConfigSection } from '@/components/config/ConfigSection'; import type { PayloadFilterRule, PayloadModelEntry, PayloadParamEntry, PayloadParamValueType, PayloadRule, VisualConfigValues, } from '@/types/visualConfig'; import { makeClientId } from '@/types/visualConfig'; import { VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS, VISUAL_CONFIG_PROTOCOL_OPTIONS, } from '@/hooks/useVisualConfig'; import { maskApiKey } from '@/utils/format'; import { isValidApiKeyCharset } from '@/utils/validation'; interface VisualConfigEditorProps { values: VisualConfigValues; disabled?: boolean; onChange: (values: Partial) => void; } type ToggleRowProps = { title: string; description?: string; checked: boolean; disabled?: boolean; onChange: (value: boolean) => void; }; function ToggleRow({ title, description, checked, disabled, onChange }: ToggleRowProps) { return (
{title}
{description && (
{description}
)}
); } function SectionGrid({ children }: { children: ReactNode }) { return (
{children}
); } function Divider() { return
; } type ToastSelectOption = { value: string; label: string }; function ToastSelect({ value, options, disabled, ariaLabel, onChange, }: { value: string; options: ReadonlyArray; disabled?: boolean; ariaLabel: string; onChange: (value: string) => void; }) { const [open, setOpen] = useState(false); const containerRef = useRef(null); const selectedOption = options.find((opt) => opt.value === value) ?? options[0]; useEffect(() => { if (!open) return; const handleClickOutside = (event: MouseEvent) => { if (!containerRef.current) return; if (!containerRef.current.contains(event.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [open]); return (
{open && !disabled && (
{options.map((opt) => { const active = opt.value === value; return ( ); })}
)}
); } function ApiKeysCardEditor({ value, disabled, onChange, }: { value: string; disabled?: boolean; onChange: (nextValue: string) => void; }) { const { t } = useTranslation(); const apiKeys = useMemo( () => value .split('\n') .map((key) => key.trim()) .filter(Boolean), [value] ); const [modalOpen, setModalOpen] = useState(false); const [editingIndex, setEditingIndex] = useState(null); const [inputValue, setInputValue] = useState(''); const [formError, setFormError] = useState(''); const openAddModal = () => { setEditingIndex(null); setInputValue(''); setFormError(''); setModalOpen(true); }; const openEditModal = (index: number) => { setEditingIndex(index); setInputValue(apiKeys[index] ?? ''); setFormError(''); setModalOpen(true); }; const closeModal = () => { setModalOpen(false); setInputValue(''); setEditingIndex(null); setFormError(''); }; const updateApiKeys = (nextKeys: string[]) => { onChange(nextKeys.join('\n')); }; const handleDelete = (index: number) => { updateApiKeys(apiKeys.filter((_, i) => i !== index)); }; const handleSave = () => { const trimmed = inputValue.trim(); if (!trimmed) { setFormError(t('config_management.visual.api_keys.error_empty')); return; } if (!isValidApiKeyCharset(trimmed)) { setFormError(t('config_management.visual.api_keys.error_invalid')); return; } const nextKeys = editingIndex === null ? [...apiKeys, trimmed] : apiKeys.map((key, idx) => (idx === editingIndex ? trimmed : key)); updateApiKeys(nextKeys); closeModal(); }; return (
{apiKeys.length === 0 ? (
{t('config_management.visual.api_keys.empty')}
) : (
{apiKeys.map((key, index) => (
#{index + 1}
API Key
{maskApiKey(String(key || ''))}
))}
)}
{t('config_management.visual.api_keys.hint')}
} > setInputValue(e.target.value)} disabled={disabled} error={formError || undefined} hint={t('config_management.visual.api_keys.input_hint')} />
); } function StringListEditor({ value, disabled, placeholder, onChange, }: { value: string[]; disabled?: boolean; placeholder?: string; onChange: (next: string[]) => void; }) { const { t } = useTranslation(); const items = value.length ? value : []; const updateItem = (index: number, nextValue: string) => onChange(items.map((item, i) => (i === index ? nextValue : item))); const addItem = () => onChange([...items, '']); const removeItem = (index: number) => onChange(items.filter((_, i) => i !== index)); return (
{items.map((item, index) => (
updateItem(index, e.target.value)} disabled={disabled} style={{ flex: 1 }} />
))}
); } function PayloadRulesEditor({ value, disabled, protocolFirst = false, onChange, }: { value: PayloadRule[]; disabled?: boolean; protocolFirst?: boolean; onChange: (next: PayloadRule[]) => void; }) { const { t } = useTranslation(); const rules = value.length ? value : []; const addRule = () => onChange([...rules, { id: makeClientId(), models: [], params: [] }]); const removeRule = (ruleIndex: number) => onChange(rules.filter((_, i) => i !== ruleIndex)); const updateRule = (ruleIndex: number, patch: Partial) => onChange(rules.map((rule, i) => (i === ruleIndex ? { ...rule, ...patch } : rule))); const addModel = (ruleIndex: number) => { const rule = rules[ruleIndex]; const nextModel: PayloadModelEntry = { id: makeClientId(), name: '', protocol: undefined }; updateRule(ruleIndex, { models: [...rule.models, nextModel] }); }; const removeModel = (ruleIndex: number, modelIndex: number) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { models: rule.models.filter((_, i) => i !== modelIndex) }); }; const updateModel = (ruleIndex: number, modelIndex: number, patch: Partial) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { models: rule.models.map((m, i) => (i === modelIndex ? { ...m, ...patch } : m)), }); }; const addParam = (ruleIndex: number) => { const rule = rules[ruleIndex]; const nextParam: PayloadParamEntry = { id: makeClientId(), path: '', valueType: 'string', value: '', }; updateRule(ruleIndex, { params: [...rule.params, nextParam] }); }; const removeParam = (ruleIndex: number, paramIndex: number) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { params: rule.params.filter((_, i) => i !== paramIndex) }); }; const updateParam = (ruleIndex: number, paramIndex: number, patch: Partial) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { params: rule.params.map((p, i) => (i === paramIndex ? { ...p, ...patch } : p)), }); }; const getValuePlaceholder = (valueType: PayloadParamValueType) => { switch (valueType) { case 'string': return t('config_management.visual.payload_rules.value_string'); case 'number': return t('config_management.visual.payload_rules.value_number'); case 'boolean': return t('config_management.visual.payload_rules.value_boolean'); case 'json': return t('config_management.visual.payload_rules.value_json'); default: return t('config_management.visual.payload_rules.value_default'); } }; return (
{rules.map((rule, ruleIndex) => (
{t('config_management.visual.payload_rules.rule')} {ruleIndex + 1}
{t('config_management.visual.payload_rules.models')}
{(rule.models.length ? rule.models : []).map((model, modelIndex) => (
{protocolFirst ? ( <> updateModel(ruleIndex, modelIndex, { protocol: (nextValue || undefined) as PayloadModelEntry['protocol'], }) } /> updateModel(ruleIndex, modelIndex, { name: e.target.value })} disabled={disabled} /> ) : ( <> updateModel(ruleIndex, modelIndex, { name: e.target.value })} disabled={disabled} /> updateModel(ruleIndex, modelIndex, { protocol: (nextValue || undefined) as PayloadModelEntry['protocol'], }) } /> )}
))}
{t('config_management.visual.payload_rules.params')}
{(rule.params.length ? rule.params : []).map((param, paramIndex) => (
updateParam(ruleIndex, paramIndex, { path: e.target.value })} disabled={disabled} /> updateParam(ruleIndex, paramIndex, { valueType: nextValue as PayloadParamValueType }) } /> updateParam(ruleIndex, paramIndex, { value: e.target.value })} disabled={disabled} />
))}
))} {rules.length === 0 && (
{t('config_management.visual.payload_rules.no_rules')}
)}
); } function PayloadFilterRulesEditor({ value, disabled, onChange, }: { value: PayloadFilterRule[]; disabled?: boolean; onChange: (next: PayloadFilterRule[]) => void; }) { const { t } = useTranslation(); const rules = value.length ? value : []; const addRule = () => onChange([...rules, { id: makeClientId(), models: [], params: [] }]); const removeRule = (ruleIndex: number) => onChange(rules.filter((_, i) => i !== ruleIndex)); const updateRule = (ruleIndex: number, patch: Partial) => onChange(rules.map((rule, i) => (i === ruleIndex ? { ...rule, ...patch } : rule))); const addModel = (ruleIndex: number) => { const rule = rules[ruleIndex]; const nextModel: PayloadModelEntry = { id: makeClientId(), name: '', protocol: undefined }; updateRule(ruleIndex, { models: [...rule.models, nextModel] }); }; const removeModel = (ruleIndex: number, modelIndex: number) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { models: rule.models.filter((_, i) => i !== modelIndex) }); }; const updateModel = (ruleIndex: number, modelIndex: number, patch: Partial) => { const rule = rules[ruleIndex]; updateRule(ruleIndex, { models: rule.models.map((m, i) => (i === modelIndex ? { ...m, ...patch } : m)), }); }; return (
{rules.map((rule, ruleIndex) => (
{t('config_management.visual.payload_rules.rule')} {ruleIndex + 1}
{t('config_management.visual.payload_rules.models')}
{rule.models.map((model, modelIndex) => (
updateModel(ruleIndex, modelIndex, { name: e.target.value })} disabled={disabled} /> updateModel(ruleIndex, modelIndex, { protocol: (nextValue || undefined) as PayloadModelEntry['protocol'], }) } />
))}
{t('config_management.visual.payload_rules.remove_params')}
updateRule(ruleIndex, { params })} />
))} {rules.length === 0 && (
{t('config_management.visual.payload_rules.no_rules')}
)}
); } export function VisualConfigEditor({ values, disabled = false, onChange }: VisualConfigEditorProps) { const { t } = useTranslation(); const isKeepaliveDisabled = values.streaming.keepaliveSeconds === '' || values.streaming.keepaliveSeconds === '0'; const isNonstreamKeepaliveDisabled = values.streaming.nonstreamKeepaliveInterval === '' || values.streaming.nonstreamKeepaliveInterval === '0'; return (
onChange({ host: e.target.value })} disabled={disabled} /> onChange({ port: e.target.value })} disabled={disabled} />
onChange({ tlsEnable })} /> {values.tlsEnable && ( <> onChange({ tlsCert: e.target.value })} disabled={disabled} /> onChange({ tlsKey: e.target.value })} disabled={disabled} /> )}
onChange({ rmAllowRemote })} /> onChange({ rmDisableControlPanel })} /> onChange({ rmSecretKey: e.target.value })} disabled={disabled} /> onChange({ rmPanelRepo: e.target.value })} disabled={disabled} />
onChange({ authDir: e.target.value })} disabled={disabled} hint={t('config_management.visual.sections.auth.auth_dir_hint')} /> onChange({ apiKeysText })} />
onChange({ debug })} /> onChange({ commercialMode })} /> onChange({ loggingToFile })} /> onChange({ usageStatisticsEnabled })} /> onChange({ logsMaxTotalSizeMb: e.target.value })} disabled={disabled} /> onChange({ usageRecordsRetentionDays: e.target.value })} disabled={disabled} hint={t('config_management.visual.sections.system.usage_retention_hint')} />
onChange({ proxyUrl: e.target.value })} disabled={disabled} /> onChange({ requestRetry: e.target.value })} disabled={disabled} /> onChange({ maxRetryInterval: e.target.value })} disabled={disabled} />
onChange({ routingStrategy: nextValue as VisualConfigValues['routingStrategy'] }) } />
{t('config_management.visual.sections.network.routing_strategy_hint')}
onChange({ forceModelPrefix })} /> onChange({ wsAuth })} />
onChange({ quotaSwitchProject })} /> onChange({ quotaSwitchPreviewModel })} />
onChange({ streaming: { ...values.streaming, keepaliveSeconds: e.target.value } }) } disabled={disabled} /> {isKeepaliveDisabled && ( {t('config_management.visual.sections.streaming.disabled')} )}
{t('config_management.visual.sections.streaming.keepalive_hint')}
onChange({ streaming: { ...values.streaming, bootstrapRetries: e.target.value } })} disabled={disabled} hint={t('config_management.visual.sections.streaming.bootstrap_hint')} />
onChange({ streaming: { ...values.streaming, nonstreamKeepaliveInterval: e.target.value }, }) } disabled={disabled} /> {isNonstreamKeepaliveDisabled && ( {t('config_management.visual.sections.streaming.disabled')} )}
{t('config_management.visual.sections.streaming.nonstream_keepalive_hint')}
{t('config_management.visual.sections.payload.default_rules')}
{t('config_management.visual.sections.payload.default_rules_desc')}
onChange({ payloadDefaultRules })} />
{t('config_management.visual.sections.payload.override_rules')}
{t('config_management.visual.sections.payload.override_rules_desc')}
onChange({ payloadOverrideRules })} />
{t('config_management.visual.sections.payload.filter_rules')}
{t('config_management.visual.sections.payload.filter_rules_desc')}
onChange({ payloadFilterRules })} />
); }