diff --git a/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx b/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx index 50dc4949a..e33d68bef 100644 --- a/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx +++ b/web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx @@ -20,10 +20,10 @@ For commercial licensing, please contact support@quantumnous.com import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { - Banner, Button, Card, Col, + Collapse, Input, Modal, Row, @@ -31,6 +31,7 @@ import { Space, Switch, Tag, + TextArea, Typography, } from '@douyinfe/semi-ui'; import { IconDelete, IconPlus } from '@douyinfe/semi-icons'; @@ -204,6 +205,11 @@ const OPERATION_TEMPLATE = { ], }; +const TEMPLATE_LIBRARY_OPTIONS = [ + { label: 'Template · Operations', value: 'operations' }, + { label: 'Template · Legacy Object', value: 'legacy' }, +]; + let localIdSeed = 0; const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`; @@ -274,6 +280,28 @@ const normalizeOperation = (operation = {}) => ({ const createDefaultOperation = () => normalizeOperation({ mode: 'set' }); +const getOperationSummary = (operation = {}, index = 0) => { + const mode = operation.mode || 'set'; + if (mode === 'sync_fields') { + const from = String(operation.from || '').trim(); + const to = String(operation.to || '').trim(); + return `${index + 1}. ${mode} · ${from || to || '-'}`; + } + const path = String(operation.path || '').trim(); + const from = String(operation.from || '').trim(); + const to = String(operation.to || '').trim(); + return `${index + 1}. ${mode} · ${path || from || to || '-'}`; +}; + +const getOperationModeTagColor = (mode = 'set') => { + if (mode.includes('header')) return 'cyan'; + if (mode.includes('replace') || mode.includes('trim')) return 'violet'; + if (mode.includes('copy') || mode.includes('move')) return 'blue'; + if (mode.includes('error') || mode.includes('prune')) return 'red'; + if (mode.includes('sync')) return 'green'; + return 'grey'; +}; + const parseInitialState = (rawValue) => { const text = typeof rawValue === 'string' ? rawValue : ''; const trimmed = text.trim(); @@ -439,6 +467,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { const [operations, setOperations] = useState([createDefaultOperation()]); const [jsonText, setJsonText] = useState(''); const [jsonError, setJsonError] = useState(''); + const [operationSearch, setOperationSearch] = useState(''); + const [selectedOperationId, setSelectedOperationId] = useState(''); + const [expandedConditionMap, setExpandedConditionMap] = useState({}); + const [templateLibraryKey, setTemplateLibraryKey] = useState('operations'); useEffect(() => { if (!visible) return; @@ -449,13 +481,73 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { setOperations(nextState.operations); setJsonText(nextState.jsonText); setJsonError(nextState.jsonError); + setOperationSearch(''); + setSelectedOperationId(nextState.operations[0]?.id || ''); + setExpandedConditionMap({}); + setTemplateLibraryKey( + nextState.visualMode === 'legacy' ? 'legacy' : 'operations', + ); }, [visible, value]); + useEffect(() => { + if (operations.length === 0) { + setSelectedOperationId(''); + return; + } + if (!operations.some((item) => item.id === selectedOperationId)) { + setSelectedOperationId(operations[0].id); + } + }, [operations, selectedOperationId]); + + useEffect(() => { + setTemplateLibraryKey(visualMode === 'legacy' ? 'legacy' : 'operations'); + }, [visualMode]); + const operationCount = useMemo( () => operations.filter((item) => !isOperationBlank(item)).length, [operations], ); + const filteredOperations = useMemo(() => { + const keyword = operationSearch.trim().toLowerCase(); + if (!keyword) return operations; + return operations.filter((operation) => { + const searchableText = [ + operation.mode, + operation.path, + operation.from, + operation.to, + operation.value_text, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + return searchableText.includes(keyword); + }); + }, [operationSearch, operations]); + + const selectedOperation = useMemo( + () => operations.find((operation) => operation.id === selectedOperationId), + [operations, selectedOperationId], + ); + + const selectedOperationIndex = useMemo( + () => + operations.findIndex((operation) => operation.id === selectedOperationId), + [operations, selectedOperationId], + ); + + const topOperationModes = useMemo(() => { + const counts = operations.reduce((acc, operation) => { + const mode = operation.mode || 'set'; + acc[mode] = (acc[mode] || 0) + 1; + return acc; + }, {}); + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 4); + }, [operations]); + const buildVisualJson = useCallback(() => { if (visualMode === 'legacy') { const trimmed = legacyValue.trim(); @@ -545,8 +637,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { if (editMode === 'visual') return; const trimmed = jsonText.trim(); if (!trimmed) { + const fallback = createDefaultOperation(); setVisualMode('operations'); - setOperations([createDefaultOperation()]); + setOperations([fallback]); + setSelectedOperationId(fallback.id); setLegacyValue(''); setJsonError(''); setEditMode('visual'); @@ -563,21 +657,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { !Array.isArray(parsed) && Array.isArray(parsed.operations) ) { - setVisualMode('operations'); - setOperations( + const nextOperations = parsed.operations.length > 0 ? parsed.operations.map(normalizeOperation) - : [createDefaultOperation()], - ); + : [createDefaultOperation()]; + setVisualMode('operations'); + setOperations(nextOperations); + setSelectedOperationId(nextOperations[0]?.id || ''); setLegacyValue(''); setJsonError(''); setEditMode('visual'); return; } if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const fallback = createDefaultOperation(); setVisualMode('legacy'); setLegacyValue(JSON.stringify(parsed, null, 2)); - setOperations([createDefaultOperation()]); + setOperations([fallback]); + setSelectedOperationId(fallback.id); setJsonError(''); setEditMode('visual'); return; @@ -587,27 +684,54 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { const setOldTemplate = () => { const text = JSON.stringify(LEGACY_TEMPLATE, null, 2); + const fallback = createDefaultOperation(); setVisualMode('legacy'); setLegacyValue(text); + setOperations([fallback]); + setSelectedOperationId(fallback.id); + setExpandedConditionMap({}); setJsonText(text); setJsonError(''); setEditMode('visual'); + setTemplateLibraryKey('legacy'); }; const setNewTemplate = () => { + const nextOperations = + OPERATION_TEMPLATE.operations.map(normalizeOperation); setVisualMode('operations'); - setOperations(OPERATION_TEMPLATE.operations.map(normalizeOperation)); + setOperations(nextOperations); + setSelectedOperationId(nextOperations[0]?.id || ''); + setExpandedConditionMap({}); setJsonText(JSON.stringify(OPERATION_TEMPLATE, null, 2)); setJsonError(''); setEditMode('visual'); + setTemplateLibraryKey('operations'); }; const clearValue = () => { + const fallback = createDefaultOperation(); setVisualMode('operations'); setLegacyValue(''); - setOperations([createDefaultOperation()]); + setOperations([fallback]); + setSelectedOperationId(fallback.id); + setExpandedConditionMap({}); setJsonText(''); setJsonError(''); + setTemplateLibraryKey('operations'); + }; + + const applyTemplateFromLibrary = () => { + if (templateLibraryKey === 'legacy') { + setOldTemplate(); + return; + } + setNewTemplate(); + }; + + const resetEditorState = () => { + clearValue(); + setEditMode('visual'); }; const updateOperation = (operationId, patch) => { @@ -619,10 +743,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { }; const addOperation = () => { - setOperations((prev) => [...prev, createDefaultOperation()]); + const created = createDefaultOperation(); + setOperations((prev) => [...prev, created]); + setSelectedOperationId(created.id); }; const duplicateOperation = (operationId) => { + let insertedId = ''; setOperations((prev) => { const index = prev.findIndex((item) => item.id === operationId); if (index < 0) return prev; @@ -643,10 +770,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { pass_missing_key: condition.pass_missing_key, })), }); + insertedId = cloned.id; const next = [...prev]; next.splice(index + 1, 0, cloned); return next; }); + if (insertedId) { + setSelectedOperationId(insertedId); + } }; const removeOperation = (operationId) => { @@ -654,19 +785,32 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { if (prev.length <= 1) return [createDefaultOperation()]; return prev.filter((item) => item.id !== operationId); }); + setExpandedConditionMap((prev) => { + if (!Object.prototype.hasOwnProperty.call(prev, operationId)) { + return prev; + } + const next = { ...prev }; + delete next[operationId]; + return next; + }); }; const addCondition = (operationId) => { + const createdCondition = createDefaultCondition(); setOperations((prev) => prev.map((operation) => operation.id === operationId ? { ...operation, - conditions: [...(operation.conditions || []), createDefaultCondition()], + conditions: [...(operation.conditions || []), createdCondition], } : operation, ), ); + setExpandedConditionMap((prev) => ({ + ...prev, + [operationId]: [...(prev[operationId] || []), createdCondition.id], + })); }; const updateCondition = (operationId, conditionId, patch) => { @@ -697,8 +841,50 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { }; }), ); + setExpandedConditionMap((prev) => ({ + ...prev, + [operationId]: (prev[operationId] || []).filter( + (id) => id !== conditionId, + ), + })); }; + const selectedConditionKeys = useMemo( + () => expandedConditionMap[selectedOperationId] || [], + [expandedConditionMap, selectedOperationId], + ); + + const handleConditionCollapseChange = useCallback( + (operationId, activeKeys) => { + const keys = ( + Array.isArray(activeKeys) ? activeKeys : [activeKeys] + ).filter(Boolean); + setExpandedConditionMap((prev) => ({ + ...prev, + [operationId]: keys, + })); + }, + [], + ); + + const expandAllSelectedConditions = useCallback(() => { + if (!selectedOperationId || !selectedOperation) return; + setExpandedConditionMap((prev) => ({ + ...prev, + [selectedOperationId]: (selectedOperation.conditions || []).map( + (condition) => condition.id, + ), + })); + }, [selectedOperation, selectedOperationId]); + + const collapseAllSelectedConditions = useCallback(() => { + if (!selectedOperationId) return; + setExpandedConditionMap((prev) => ({ + ...prev, + [selectedOperationId]: [], + })); + }, [selectedOperationId]); + const handleJsonChange = (nextValue) => { setJsonText(nextValue); const trimmed = String(nextValue || '').trim(); @@ -761,21 +947,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { - - - - - + - updateOperation(operation.id, { mode: nextMode }) - } - style={{ width: '100%' }} - /> - - {meta.path || meta.pathOptional ? ( - - - {meta.pathOptional ? 'path (optional)' : 'path'} - - - updateOperation(operation.id, { - path: nextValue, - }) - } - /> - - {OPERATION_PATH_SUGGESTIONS.map((pathItem) => ( - - updateOperation(operation.id, { - path: pathItem, - }) - } - > - {pathItem} - - ))} - - - ) : null} - - - {MODE_DESCRIPTIONS[mode] || ''} - + + setOperationSearch(nextValue || '') + } + showClear + /> - {meta.value ? ( -
- - value (JSON or plain text) - - - updateOperation(operation.id, { - value_text: nextValue, - }) - } - /> -
- ) : null} - - {meta.keepOrigin ? ( -
- - updateOperation(operation.id, { - keep_origin: nextValue, - }) - } - /> - - keep_origin - -
- ) : null} - - {mode === 'sync_fields' ? ( -
- - sync endpoints - - - - - from endpoint - -
- - updateOperation(operation.id, { - from: buildSyncTargetSpec( - syncFromTarget?.type || 'json', - nextKey, - ), - }) - } - /> -
- - - - to endpoint - -
- - updateOperation(operation.id, { - to: buildSyncTargetSpec( - syncToTarget?.type || 'json', - nextKey, - ), - }) - } - /> -
- -
- - - updateOperation(operation.id, { - from: 'header:session_id', - to: 'json:prompt_cache_key', - }) - } - > - {'header:session_id -> json:prompt_cache_key'} - - - updateOperation(operation.id, { - from: 'json:prompt_cache_key', - to: 'header:session_id', - }) - } - > - {'json:prompt_cache_key -> header:session_id'} - - -
- ) : meta.from || meta.to === false || meta.to ? ( - - {meta.from || meta.to === false ? ( - - - from - - + {filteredOperations.length === 0 ? ( + + {t('没有匹配的规则')} + + ) : ( +
+ {filteredOperations.map((operation) => { + const index = operations.findIndex( + (item) => item.id === operation.id, + ); + const isActive = + operation.id === selectedOperationId; + return ( +
+ setSelectedOperationId(operation.id) } + onKeyDown={(event) => { + if ( + event.key === 'Enter' || + event.key === ' ' + ) { + event.preventDefault(); + setSelectedOperationId(operation.id); + } + }} + className='w-full rounded-xl px-3 py-3 cursor-pointer transition-colors' + style={{ + background: isActive + ? 'var(--semi-color-primary-light-default)' + : 'var(--semi-color-bg-2)', + border: isActive + ? '1px solid var(--semi-color-primary)' + : '1px solid var(--semi-color-border)', + }} + > +
+
+ {`#${index + 1}`} + + {getOperationSummary(operation, index)} + +
+ + {(operation.conditions || []).length} + +
+ + + {operation.mode || 'set'} + + + {t('条件')} + + +
+ ); + })} +
+ )} + + + + + {selectedOperation ? ( + (() => { + const mode = selectedOperation.mode || 'set'; + const meta = MODE_META[mode] || MODE_META.set; + const conditions = selectedOperation.conditions || []; + const syncFromTarget = + mode === 'sync_fields' + ? parseSyncTargetSpec(selectedOperation.from) + : null; + const syncToTarget = + mode === 'sync_fields' + ? parseSyncTargetSpec(selectedOperation.to) + : null; + return ( + +
+ + {`#${selectedOperationIndex + 1}`} + + {getOperationSummary( + selectedOperation, + selectedOperationIndex, + )} + + + + +
+ + + + + mode + + + updateOperation(selectedOperation.id, { + path: nextValue, + }) + } + /> + + {OPERATION_PATH_SUGGESTIONS.map( + (pathItem) => ( + + updateOperation( + selectedOperation.id, + { + path: pathItem, + }, + ) + } + > + {pathItem} + + ), + )} + + + ) : null} + + + + {MODE_DESCRIPTIONS[mode] || ''} + + + {meta.value ? ( +
+ + value (JSON or plain text) + +