diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx
index e397205..9695c3d 100644
--- a/src/components/config/VisualConfigEditor.tsx
+++ b/src/components/config/VisualConfigEditor.tsx
@@ -1,33 +1,22 @@
-import { memo, useCallback, useId, useMemo, useState, type ReactNode } from 'react';
+import { useCallback, useId, 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 { Select } from '@/components/ui/Select';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { ConfigSection } from '@/components/config/ConfigSection';
-import { useNotificationStore } from '@/stores';
-import styles from './VisualConfigEditor.module.scss';
-import { copyToClipboard } from '@/utils/clipboard';
import type {
PayloadFilterRule,
- PayloadModelEntry,
- PayloadParamEntry,
PayloadParamValidationErrorCode,
- PayloadParamValueType,
PayloadRule,
VisualConfigValidationErrorCode,
VisualConfigValidationErrors,
VisualConfigValues,
} 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';
+ ApiKeysCardEditor,
+ PayloadFilterRulesEditor,
+ PayloadRulesEditor,
+} from './VisualConfigEditorBlocks';
interface VisualConfigEditorProps {
values: VisualConfigValues;
@@ -94,748 +83,6 @@ function Divider() {
return
;
}
-const ApiKeysCardEditor = memo(function ApiKeysCardEditor({
- value,
- disabled,
- onChange,
-}: {
- value: string;
- disabled?: boolean;
- onChange: (nextValue: string) => void;
-}) {
- const { t } = useTranslation();
- const showNotification = useNotificationStore((state) => state.showNotification);
- const apiKeys = useMemo(
- () =>
- value
- .split('\n')
- .map((key) => key.trim())
- .filter(Boolean),
- [value]
- );
- const [apiKeyIds, setApiKeyIds] = useState(() => apiKeys.map(() => makeClientId()));
- const renderApiKeyIds = useMemo(() => {
- if (apiKeyIds.length === apiKeys.length) return apiKeyIds;
- if (apiKeyIds.length > apiKeys.length) return apiKeyIds.slice(0, apiKeys.length);
- return [...apiKeyIds, ...Array.from({ length: apiKeys.length - apiKeyIds.length }, () => makeClientId())];
- }, [apiKeyIds, apiKeys.length]);
-
- const apiKeyInputId = useId();
- const apiKeyHintId = `${apiKeyInputId}-hint`;
- const apiKeyErrorId = `${apiKeyInputId}-error`;
- const [modalOpen, setModalOpen] = useState(false);
- const [editingApiKeyId, setEditingApiKeyId] = useState(null);
- const [inputValue, setInputValue] = useState('');
- const [formError, setFormError] = useState('');
-
- function generateSecureApiKey(): string {
- const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- const array = new Uint8Array(17);
- crypto.getRandomValues(array);
- return 'sk-' + Array.from(array, (b) => charset[b % charset.length]).join('');
- }
-
- const openAddModal = () => {
- setEditingApiKeyId(null);
- setInputValue('');
- setFormError('');
- setModalOpen(true);
- };
-
- const openEditModal = (apiKeyId: string) => {
- const editingIndex = renderApiKeyIds.findIndex((id) => id === apiKeyId);
- setEditingApiKeyId(apiKeyId);
- setInputValue(apiKeys[editingIndex] ?? '');
- setFormError('');
- setModalOpen(true);
- };
-
- const closeModal = () => {
- setModalOpen(false);
- setInputValue('');
- setEditingApiKeyId(null);
- setFormError('');
- };
-
- const updateApiKeys = (nextKeys: string[]) => {
- onChange(nextKeys.join('\n'));
- };
-
- const handleDelete = (apiKeyId: string) => {
- const index = renderApiKeyIds.findIndex((id) => id === apiKeyId);
- if (index < 0) return;
- setApiKeyIds(renderApiKeyIds.filter((id) => id !== apiKeyId));
- 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 editingIndex = editingApiKeyId ? renderApiKeyIds.findIndex((id) => id === editingApiKeyId) : -1;
- const nextKeys =
- editingApiKeyId === null
- ? [...apiKeys, trimmed]
- : apiKeys.map((key, idx) => (idx === editingIndex ? trimmed : key));
- if (editingApiKeyId === null) {
- setApiKeyIds([...renderApiKeyIds, makeClientId()]);
- }
- updateApiKeys(nextKeys);
- closeModal();
- };
-
- const handleCopy = async (apiKey: string) => {
- const copied = await copyToClipboard(apiKey);
- showNotification(
- t(copied ? 'notification.link_copied' : 'notification.copy_failed'),
- copied ? 'success' : 'error'
- );
- };
-
- const handleGenerate = () => {
- setInputValue(generateSecureApiKey());
- setFormError('');
- };
-
- return (
-
-
-
-
-
-
- {apiKeys.length === 0 ? (
-
- {t('config_management.visual.api_keys.empty')}
-
- ) : (
-
- {apiKeys.map((key, index) => (
-
-
-
#{index + 1}
-
{t('config_management.visual.api_keys.input_label')}
-
{maskApiKey(String(key || ''))}
-
-
-
-
-
-
-
- ))}
-
- )}
-
-
{t('config_management.visual.api_keys.hint')}
-
-
-
-
- >
- }
- >
-
-
-
- setInputValue(e.target.value)}
- disabled={disabled}
- aria-describedby={formError ? `${apiKeyErrorId} ${apiKeyHintId}` : apiKeyHintId}
- aria-invalid={Boolean(formError)}
- />
-
-
-
{t('config_management.visual.api_keys.input_hint')}
- {formError &&
{formError}
}
-
-
-
- );
-});
-
-const StringListEditor = memo(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 [itemIds, setItemIds] = useState(() => items.map(() => makeClientId()));
- const renderItemIds = useMemo(() => {
- if (itemIds.length === items.length) return itemIds;
- if (itemIds.length > items.length) return itemIds.slice(0, items.length);
- return [...itemIds, ...Array.from({ length: items.length - itemIds.length }, () => makeClientId())];
- }, [itemIds, items.length]);
-
- const updateItem = (index: number, nextValue: string) =>
- onChange(items.map((item, i) => (i === index ? nextValue : item)));
- const addItem = () => {
- setItemIds([...renderItemIds, makeClientId()]);
- onChange([...items, '']);
- };
- const removeItem = (index: number) => {
- setItemIds(renderItemIds.filter((_, i) => i !== index));
- onChange(items.filter((_, i) => i !== index));
- };
-
- return (
-
- {items.map((item, index) => (
-
- updateItem(index, e.target.value)}
- disabled={disabled}
- style={{ flex: 1 }}
- />
-
-
- ))}
-
-
-
-
- );
-});
-
-const PayloadRulesEditor = memo(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 protocolOptions = useMemo(
- () =>
- VISUAL_CONFIG_PROTOCOL_OPTIONS.map((option) => ({
- value: option.value,
- label: t(option.labelKey, { defaultValue: option.defaultLabel }),
- })),
- [t]
- );
- const payloadValueTypeOptions = useMemo(
- () =>
- VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS.map((option) => ({
- value: option.value,
- label: t(option.labelKey, { defaultValue: option.defaultLabel }),
- })),
- [t]
- );
- const booleanValueOptions = useMemo(
- () => [
- { value: 'true', label: t('config_management.visual.payload_rules.boolean_true') },
- { value: 'false', label: t('config_management.visual.payload_rules.boolean_false') },
- ],
- [t]
- );
-
- 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');
- }
- };
-
- const getParamErrorMessage = (param: PayloadParamEntry) => {
- const errorCode = getPayloadParamValidationError(param);
- return getValidationMessage(t, errorCode);
- };
-
- const renderParamValueEditor = (
- ruleIndex: number,
- paramIndex: number,
- param: PayloadParamEntry
- ) => {
- if (param.valueType === 'boolean') {
- return (
-