Commit f0886c8a by Seefs

fix

parent 6b58648d
...@@ -20,10 +20,10 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -20,10 +20,10 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Banner,
Button, Button,
Card, Card,
Col, Col,
Collapse,
Input, Input,
Modal, Modal,
Row, Row,
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
Space, Space,
Switch, Switch,
Tag, Tag,
TextArea,
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus } from '@douyinfe/semi-icons'; import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
...@@ -204,6 +205,11 @@ const OPERATION_TEMPLATE = { ...@@ -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; let localIdSeed = 0;
const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`; const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`;
...@@ -274,6 +280,28 @@ const normalizeOperation = (operation = {}) => ({ ...@@ -274,6 +280,28 @@ const normalizeOperation = (operation = {}) => ({
const createDefaultOperation = () => normalizeOperation({ mode: 'set' }); 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 parseInitialState = (rawValue) => {
const text = typeof rawValue === 'string' ? rawValue : ''; const text = typeof rawValue === 'string' ? rawValue : '';
const trimmed = text.trim(); const trimmed = text.trim();
...@@ -439,6 +467,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -439,6 +467,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
const [operations, setOperations] = useState([createDefaultOperation()]); const [operations, setOperations] = useState([createDefaultOperation()]);
const [jsonText, setJsonText] = useState(''); const [jsonText, setJsonText] = useState('');
const [jsonError, setJsonError] = useState(''); const [jsonError, setJsonError] = useState('');
const [operationSearch, setOperationSearch] = useState('');
const [selectedOperationId, setSelectedOperationId] = useState('');
const [expandedConditionMap, setExpandedConditionMap] = useState({});
const [templateLibraryKey, setTemplateLibraryKey] = useState('operations');
useEffect(() => { useEffect(() => {
if (!visible) return; if (!visible) return;
...@@ -449,13 +481,73 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -449,13 +481,73 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
setOperations(nextState.operations); setOperations(nextState.operations);
setJsonText(nextState.jsonText); setJsonText(nextState.jsonText);
setJsonError(nextState.jsonError); setJsonError(nextState.jsonError);
setOperationSearch('');
setSelectedOperationId(nextState.operations[0]?.id || '');
setExpandedConditionMap({});
setTemplateLibraryKey(
nextState.visualMode === 'legacy' ? 'legacy' : 'operations',
);
}, [visible, value]); }, [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( const operationCount = useMemo(
() => operations.filter((item) => !isOperationBlank(item)).length, () => operations.filter((item) => !isOperationBlank(item)).length,
[operations], [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(() => { const buildVisualJson = useCallback(() => {
if (visualMode === 'legacy') { if (visualMode === 'legacy') {
const trimmed = legacyValue.trim(); const trimmed = legacyValue.trim();
...@@ -545,8 +637,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -545,8 +637,10 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
if (editMode === 'visual') return; if (editMode === 'visual') return;
const trimmed = jsonText.trim(); const trimmed = jsonText.trim();
if (!trimmed) { if (!trimmed) {
const fallback = createDefaultOperation();
setVisualMode('operations'); setVisualMode('operations');
setOperations([createDefaultOperation()]); setOperations([fallback]);
setSelectedOperationId(fallback.id);
setLegacyValue(''); setLegacyValue('');
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
...@@ -563,21 +657,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -563,21 +657,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
!Array.isArray(parsed) && !Array.isArray(parsed) &&
Array.isArray(parsed.operations) Array.isArray(parsed.operations)
) { ) {
setVisualMode('operations'); const nextOperations =
setOperations(
parsed.operations.length > 0 parsed.operations.length > 0
? parsed.operations.map(normalizeOperation) ? parsed.operations.map(normalizeOperation)
: [createDefaultOperation()], : [createDefaultOperation()];
); setVisualMode('operations');
setOperations(nextOperations);
setSelectedOperationId(nextOperations[0]?.id || '');
setLegacyValue(''); setLegacyValue('');
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
return; return;
} }
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const fallback = createDefaultOperation();
setVisualMode('legacy'); setVisualMode('legacy');
setLegacyValue(JSON.stringify(parsed, null, 2)); setLegacyValue(JSON.stringify(parsed, null, 2));
setOperations([createDefaultOperation()]); setOperations([fallback]);
setSelectedOperationId(fallback.id);
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
return; return;
...@@ -587,27 +684,54 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -587,27 +684,54 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
const setOldTemplate = () => { const setOldTemplate = () => {
const text = JSON.stringify(LEGACY_TEMPLATE, null, 2); const text = JSON.stringify(LEGACY_TEMPLATE, null, 2);
const fallback = createDefaultOperation();
setVisualMode('legacy'); setVisualMode('legacy');
setLegacyValue(text); setLegacyValue(text);
setOperations([fallback]);
setSelectedOperationId(fallback.id);
setExpandedConditionMap({});
setJsonText(text); setJsonText(text);
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
setTemplateLibraryKey('legacy');
}; };
const setNewTemplate = () => { const setNewTemplate = () => {
const nextOperations =
OPERATION_TEMPLATE.operations.map(normalizeOperation);
setVisualMode('operations'); setVisualMode('operations');
setOperations(OPERATION_TEMPLATE.operations.map(normalizeOperation)); setOperations(nextOperations);
setSelectedOperationId(nextOperations[0]?.id || '');
setExpandedConditionMap({});
setJsonText(JSON.stringify(OPERATION_TEMPLATE, null, 2)); setJsonText(JSON.stringify(OPERATION_TEMPLATE, null, 2));
setJsonError(''); setJsonError('');
setEditMode('visual'); setEditMode('visual');
setTemplateLibraryKey('operations');
}; };
const clearValue = () => { const clearValue = () => {
const fallback = createDefaultOperation();
setVisualMode('operations'); setVisualMode('operations');
setLegacyValue(''); setLegacyValue('');
setOperations([createDefaultOperation()]); setOperations([fallback]);
setSelectedOperationId(fallback.id);
setExpandedConditionMap({});
setJsonText(''); setJsonText('');
setJsonError(''); setJsonError('');
setTemplateLibraryKey('operations');
};
const applyTemplateFromLibrary = () => {
if (templateLibraryKey === 'legacy') {
setOldTemplate();
return;
}
setNewTemplate();
};
const resetEditorState = () => {
clearValue();
setEditMode('visual');
}; };
const updateOperation = (operationId, patch) => { const updateOperation = (operationId, patch) => {
...@@ -619,10 +743,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -619,10 +743,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
}; };
const addOperation = () => { const addOperation = () => {
setOperations((prev) => [...prev, createDefaultOperation()]); const created = createDefaultOperation();
setOperations((prev) => [...prev, created]);
setSelectedOperationId(created.id);
}; };
const duplicateOperation = (operationId) => { const duplicateOperation = (operationId) => {
let insertedId = '';
setOperations((prev) => { setOperations((prev) => {
const index = prev.findIndex((item) => item.id === operationId); const index = prev.findIndex((item) => item.id === operationId);
if (index < 0) return prev; if (index < 0) return prev;
...@@ -643,10 +770,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -643,10 +770,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
pass_missing_key: condition.pass_missing_key, pass_missing_key: condition.pass_missing_key,
})), })),
}); });
insertedId = cloned.id;
const next = [...prev]; const next = [...prev];
next.splice(index + 1, 0, cloned); next.splice(index + 1, 0, cloned);
return next; return next;
}); });
if (insertedId) {
setSelectedOperationId(insertedId);
}
}; };
const removeOperation = (operationId) => { const removeOperation = (operationId) => {
...@@ -654,19 +785,32 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -654,19 +785,32 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
if (prev.length <= 1) return [createDefaultOperation()]; if (prev.length <= 1) return [createDefaultOperation()];
return prev.filter((item) => item.id !== operationId); 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 addCondition = (operationId) => {
const createdCondition = createDefaultCondition();
setOperations((prev) => setOperations((prev) =>
prev.map((operation) => prev.map((operation) =>
operation.id === operationId operation.id === operationId
? { ? {
...operation, ...operation,
conditions: [...(operation.conditions || []), createDefaultCondition()], conditions: [...(operation.conditions || []), createdCondition],
} }
: operation, : operation,
), ),
); );
setExpandedConditionMap((prev) => ({
...prev,
[operationId]: [...(prev[operationId] || []), createdCondition.id],
}));
}; };
const updateCondition = (operationId, conditionId, patch) => { const updateCondition = (operationId, conditionId, patch) => {
...@@ -697,8 +841,50 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -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) => { const handleJsonChange = (nextValue) => {
setJsonText(nextValue); setJsonText(nextValue);
const trimmed = String(nextValue || '').trim(); const trimmed = String(nextValue || '').trim();
...@@ -761,21 +947,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -761,21 +947,13 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
<Modal <Modal
title={t('参数覆盖')} title={t('参数覆盖')}
visible={visible} visible={visible}
width={980} width={1120}
onCancel={onCancel} onCancel={onCancel}
onOk={handleSave} onOk={handleSave}
okText={t('保存')} okText={t('保存')}
cancelText={t('取消')} cancelText={t('取消')}
> >
<Space vertical align='start' spacing={12} style={{ width: '100%' }}> <Space vertical align='start' spacing={12} style={{ width: '100%' }}>
<Banner
fullMode={false}
type='info'
description={t(
'支持旧格式直接覆盖,也支持新格式 operations 条件编辑;可在可视化和 JSON 之间双向切换。',
)}
/>
<Space wrap> <Space wrap>
<Button <Button
type={editMode === 'visual' ? 'primary' : 'tertiary'} type={editMode === 'visual' ? 'primary' : 'tertiary'}
...@@ -789,32 +967,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -789,32 +967,24 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
> >
{t('JSON 模式')} {t('JSON 模式')}
</Button> </Button>
<Button onClick={setOldTemplate}>{t('旧格式模板')}</Button> <Select
<Button onClick={setNewTemplate}>{t('新格式模板')}</Button> value={templateLibraryKey}
<Button onClick={clearValue}>{t('不更改')}</Button> optionList={TEMPLATE_LIBRARY_OPTIONS}
onChange={(nextValue) =>
setTemplateLibraryKey(nextValue || 'operations')
}
style={{ width: 240 }}
/>
<Button onClick={applyTemplateFromLibrary}>{t('应用模板')}</Button>
<Button onClick={resetEditorState}>{t('重置')}</Button>
</Space> </Space>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Space wrap style={{ marginBottom: 12 }}>
<Button
type={visualMode === 'operations' ? 'primary' : 'tertiary'}
onClick={() => setVisualMode('operations')}
>
{t('新格式模板')}
</Button>
<Button
type={visualMode === 'legacy' ? 'primary' : 'tertiary'}
onClick={() => setVisualMode('legacy')}
>
{t('旧格式模板')}
</Button>
</Space>
{visualMode === 'legacy' ? ( {visualMode === 'legacy' ? (
<div> <div>
<Text className='mb-2 block'>{t('旧格式(直接覆盖):')}</Text> <Text className='mb-2 block'>{t('旧格式(直接覆盖):')}</Text>
<Input.TextArea <TextArea
value={legacyValue} value={legacyValue}
autosize={{ minRows: 10, maxRows: 20 }} autosize={{ minRows: 10, maxRows: 20 }}
placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)} placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)}
...@@ -830,462 +1000,731 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -830,462 +1000,731 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
<div className='flex items-center justify-between mb-3'> <div className='flex items-center justify-between mb-3'>
<Space> <Space>
<Text>{t('新格式(支持条件判断与json自定义):')}</Text> <Text>{t('新格式(支持条件判断与json自定义):')}</Text>
<Tag color='cyan'> <Tag color='cyan'>{`${t('规则')}: ${operationCount}`}</Tag>
{`${t('规则')}: ${operationCount}`}
</Tag>
</Space> </Space>
<Button icon={<IconPlus />} onClick={addOperation}> <Button icon={<IconPlus />} onClick={addOperation}>
{t('新增规则')} {t('新增规则')}
</Button> </Button>
</div> </div>
<Space vertical spacing={8} style={{ width: '100%' }}> <Row gutter={12}>
{operations.map((operation, index) => { <Col xs={24} md={8}>
const mode = operation.mode || 'set'; <Card
const meta = MODE_META[mode] || MODE_META.set; className='!rounded-2xl !border-0 h-full'
const conditions = operation.conditions || []; bodyStyle={{
const syncFromTarget = padding: 12,
mode === 'sync_fields' background: 'var(--semi-color-fill-0)',
? parseSyncTargetSpec(operation.from) display: 'flex',
: null; flexDirection: 'column',
const syncToTarget = gap: 10,
mode === 'sync_fields' minHeight: 520,
? parseSyncTargetSpec(operation.to) }}
: null; >
return ( <div className='flex items-center justify-between'>
<Card key={operation.id} className='!rounded-xl border'> <Text strong>{t('规则导航')}</Text>
<div className='flex items-center justify-between mb-2'> <Tag color='grey'>{`${operationCount}/${operations.length}`}</Tag>
<Space> </div>
<Tag>{`#${index + 1}`}</Tag>
<Text>{mode}</Text> {topOperationModes.length > 0 ? (
</Space> <Space wrap spacing={6}>
<Space> {topOperationModes.map(([mode, count]) => (
<Button <Tag
key={`mode_stat_${mode}`}
size='small' size='small'
type='tertiary' color={getOperationModeTagColor(mode)}
onClick={() => duplicateOperation(operation.id)}
> >
{t('复制')} {`${mode} · ${count}`}
</Button> </Tag>
<Button ))}
theme='borderless' </Space>
type='danger' ) : null}
icon={<IconDelete />}
onClick={() => removeOperation(operation.id)} <Input
/> value={operationSearch}
</Space> placeholder={t('搜索规则(mode/path/from/to)')}
</div> onChange={(nextValue) =>
setOperationSearch(nextValue || '')
<Row gutter={12}> }
<Col xs={24} md={8}> showClear
<Text type='tertiary' size='small'> />
mode
</Text> <div
<Select className='overflow-auto'
value={mode} style={{ flex: 1, minHeight: 320, paddingRight: 2 }}
optionList={OPERATION_MODE_OPTIONS} >
onChange={(nextMode) => {filteredOperations.length === 0 ? (
updateOperation(operation.id, { mode: nextMode }) <Text type='tertiary' size='small'>
} {t('没有匹配的规则')}
style={{ width: '100%' }} </Text>
/> ) : (
</Col> <div
{meta.path || meta.pathOptional ? ( style={{
<Col xs={24} md={16}> display: 'flex',
<Text type='tertiary' size='small'> flexDirection: 'column',
{meta.pathOptional ? 'path (optional)' : 'path'} gap: 8,
</Text> width: '100%',
<Input }}
value={operation.path} >
placeholder={ {filteredOperations.map((operation) => {
mode.includes('header') const index = operations.findIndex(
? 'X-Debug-Mode' (item) => item.id === operation.id,
: mode === 'prune_objects' );
? 'messages (optional)' const isActive =
: 'temperature' operation.id === selectedOperationId;
} return (
onChange={(nextValue) => <div
updateOperation(operation.id, { key={operation.id}
path: nextValue, role='button'
}) tabIndex={0}
} onClick={() =>
/> setSelectedOperationId(operation.id)
<Space wrap style={{ marginTop: 6 }}> }
{OPERATION_PATH_SUGGESTIONS.map((pathItem) => ( onKeyDown={(event) => {
<Tag if (
key={`${operation.id}_${pathItem}`} event.key === 'Enter' ||
size='small' event.key === ' '
color='grey' ) {
className='cursor-pointer' event.preventDefault();
onClick={() => setSelectedOperationId(operation.id);
updateOperation(operation.id, {
path: pathItem,
})
} }
> }}
{pathItem} className='w-full rounded-xl px-3 py-3 cursor-pointer transition-colors'
</Tag> style={{
))} background: isActive
</Space> ? 'var(--semi-color-primary-light-default)'
</Col> : 'var(--semi-color-bg-2)',
) : null} border: isActive
</Row> ? '1px solid var(--semi-color-primary)'
<Text type='tertiary' size='small' className='mt-1 block'> : '1px solid var(--semi-color-border)',
{MODE_DESCRIPTIONS[mode] || ''} }}
</Text> >
<div className='flex items-start justify-between gap-2'>
{meta.value ? ( <div>
<div className='mt-2'> <Text strong>{`#${index + 1}`}</Text>
<Text type='tertiary' size='small'> <Text
value (JSON or plain text) type='tertiary'
</Text> size='small'
<Input.TextArea className='block mt-1'
value={operation.value_text} >
autosize={{ minRows: 1, maxRows: 4 }} {getOperationSummary(operation, index)}
placeholder='0.7' </Text>
onChange={(nextValue) => </div>
updateOperation(operation.id, { <Tag size='small' color='grey'>
value_text: nextValue, {(operation.conditions || []).length}
}) </Tag>
} </div>
/> <Space spacing={6} style={{ marginTop: 8 }}>
</div> <Tag
) : null} size='small'
color={getOperationModeTagColor(
{meta.keepOrigin ? ( operation.mode || 'set',
<div className='mt-2'> )}
<Switch >
checked={operation.keep_origin} {operation.mode || 'set'}
checkedText={t('开')} </Tag>
uncheckedText={t('关')} <Text type='tertiary' size='small'>
onChange={(nextValue) => {t('条件')}
updateOperation(operation.id, { </Text>
keep_origin: nextValue, </Space>
}) </div>
} );
/> })}
<Text type='tertiary' size='small' className='ml-2'>
keep_origin
</Text>
</div> </div>
) : null} )}
</div>
</Card>
</Col>
<Col xs={24} md={16}>
{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 (
<Card
className='!rounded-2xl !border-0'
bodyStyle={{
padding: 14,
background: 'var(--semi-color-fill-0)',
}}
>
<div className='flex items-center justify-between mb-3'>
<Space>
<Tag color='blue'>{`#${selectedOperationIndex + 1}`}</Tag>
<Text strong>
{getOperationSummary(
selectedOperation,
selectedOperationIndex,
)}
</Text>
</Space>
<Space>
<Button
size='small'
type='tertiary'
onClick={() =>
duplicateOperation(selectedOperation.id)
}
>
{t('复制')}
</Button>
<Button
size='small'
type='danger'
theme='borderless'
icon={<IconDelete />}
onClick={() =>
removeOperation(selectedOperation.id)
}
/>
</Space>
</div>
{mode === 'sync_fields' ? ( <Row gutter={12}>
<div className='mt-2'> <Col xs={24} md={8}>
<Text type='tertiary' size='small'>
sync endpoints
</Text>
<Row gutter={12} style={{ marginTop: 6 }}>
<Col xs={24} md={12}>
<Text type='tertiary' size='small'> <Text type='tertiary' size='small'>
from endpoint mode
</Text> </Text>
<div className='flex gap-2'> <Select
<Select value={mode}
value={syncFromTarget?.type || 'json'} optionList={OPERATION_MODE_OPTIONS}
optionList={SYNC_TARGET_TYPE_OPTIONS} onChange={(nextMode) =>
style={{ width: 120 }} updateOperation(selectedOperation.id, {
onChange={(nextType) => mode: nextMode,
updateOperation(operation.id, { })
from: buildSyncTargetSpec( }
nextType, style={{ width: '100%' }}
syncFromTarget?.key || '', />
),
})
}
/>
<Input
value={syncFromTarget?.key || ''}
placeholder='session_id'
onChange={(nextKey) =>
updateOperation(operation.id, {
from: buildSyncTargetSpec(
syncFromTarget?.type || 'json',
nextKey,
),
})
}
/>
</div>
</Col> </Col>
<Col xs={24} md={12}> {meta.path || meta.pathOptional ? (
<Text type='tertiary' size='small'> <Col xs={24} md={16}>
to endpoint <Text type='tertiary' size='small'>
</Text> {meta.pathOptional
<div className='flex gap-2'> ? 'path (optional)'
<Select : 'path'}
value={syncToTarget?.type || 'json'} </Text>
optionList={SYNC_TARGET_TYPE_OPTIONS}
style={{ width: 120 }}
onChange={(nextType) =>
updateOperation(operation.id, {
to: buildSyncTargetSpec(
nextType,
syncToTarget?.key || '',
),
})
}
/>
<Input <Input
value={syncToTarget?.key || ''} value={selectedOperation.path}
placeholder='prompt_cache_key' placeholder={
onChange={(nextKey) => mode.includes('header')
updateOperation(operation.id, { ? 'X-Debug-Mode'
to: buildSyncTargetSpec( : mode === 'prune_objects'
syncToTarget?.type || 'json', ? 'messages (optional)'
nextKey, : 'temperature'
), }
onChange={(nextValue) =>
updateOperation(selectedOperation.id, {
path: nextValue,
}) })
} }
/> />
</div> <Space wrap style={{ marginTop: 6 }}>
</Col> {OPERATION_PATH_SUGGESTIONS.map(
(pathItem) => (
<Tag
key={`${selectedOperation.id}_${pathItem}`}
size='small'
color='grey'
className='cursor-pointer'
onClick={() =>
updateOperation(
selectedOperation.id,
{
path: pathItem,
},
)
}
>
{pathItem}
</Tag>
),
)}
</Space>
</Col>
) : null}
</Row> </Row>
<Space wrap style={{ marginTop: 8 }}>
<Tag <Text
size='small' type='tertiary'
color='cyan' size='small'
className='cursor-pointer' className='mt-1 block'
onClick={() => >
updateOperation(operation.id, { {MODE_DESCRIPTIONS[mode] || ''}
from: 'header:session_id', </Text>
to: 'json:prompt_cache_key',
}) {meta.value ? (
} <div className='mt-2'>
>
{'header:session_id -> json:prompt_cache_key'}
</Tag>
<Tag
size='small'
color='cyan'
className='cursor-pointer'
onClick={() =>
updateOperation(operation.id, {
from: 'json:prompt_cache_key',
to: 'header:session_id',
})
}
>
{'json:prompt_cache_key -> header:session_id'}
</Tag>
</Space>
</div>
) : meta.from || meta.to === false || meta.to ? (
<Row gutter={12} style={{ marginTop: 8 }}>
{meta.from || meta.to === false ? (
<Col xs={24} md={12}>
<Text type='tertiary' size='small'> <Text type='tertiary' size='small'>
from value (JSON or plain text)
</Text> </Text>
<Input <TextArea
value={operation.from} value={selectedOperation.value_text}
placeholder={ autosize={{ minRows: 1, maxRows: 4 }}
mode.includes('header') placeholder='0.7'
? 'Authorization'
: 'model'
}
onChange={(nextValue) => onChange={(nextValue) =>
updateOperation(operation.id, { updateOperation(selectedOperation.id, {
from: nextValue, value_text: nextValue,
}) })
} }
/> />
</Col> </div>
) : null} ) : null}
{meta.to || meta.to === false ? (
<Col xs={24} md={12}> {meta.keepOrigin ? (
<Text type='tertiary' size='small'> <div className='mt-2'>
to <Switch
</Text> checked={Boolean(
<Input selectedOperation.keep_origin,
value={operation.to} )}
placeholder={ checkedText={t('开')}
mode.includes('header') uncheckedText={t('关')}
? 'X-Upstream-Auth'
: 'original_model'
}
onChange={(nextValue) => onChange={(nextValue) =>
updateOperation(operation.id, { to: nextValue }) updateOperation(selectedOperation.id, {
keep_origin: nextValue,
})
} }
/> />
</Col> <Text
type='tertiary'
size='small'
className='ml-2'
>
keep_origin
</Text>
</div>
) : null} ) : null}
</Row>
) : null}
<div className='mt-3 border rounded-lg p-2'>
<div className='flex items-center justify-between mb-2'>
<Space>
<Text>{t('条件')}</Text>
<Select
value={operation.logic || 'OR'}
optionList={[
{ label: 'OR', value: 'OR' },
{ label: 'AND', value: 'AND' },
]}
style={{ width: 96 }}
onChange={(nextValue) =>
updateOperation(operation.id, {
logic: nextValue,
})
}
/>
</Space>
<Button
icon={<IconPlus />}
size='small'
onClick={() => addCondition(operation.id)}
>
{t('新增条件')}
</Button>
</div>
{conditions.length === 0 ? ( {mode === 'sync_fields' ? (
<Text type='tertiary' size='small'> <div className='mt-2'>
{t('没有条件时,默认总是执行该操作。')} <Text type='tertiary' size='small'>
</Text> sync endpoints
) : ( </Text>
<Space vertical spacing={8} style={{ width: '100%' }}> <Row gutter={12} style={{ marginTop: 6 }}>
{conditions.map((condition, conditionIndex) => ( <Col xs={24} md={12}>
<Card <Text type='tertiary' size='small'>
key={condition.id} from endpoint
bodyStyle={{ padding: 10 }} </Text>
className='!rounded-lg' <div className='flex gap-2'>
> <Select
<div className='flex items-center justify-between mb-2'> value={syncFromTarget?.type || 'json'}
<Tag size='small'>{`C${conditionIndex + 1}`}</Tag> optionList={SYNC_TARGET_TYPE_OPTIONS}
<Button style={{ width: 120 }}
theme='borderless' onChange={(nextType) =>
type='danger' updateOperation(
icon={<IconDelete />} selectedOperation.id,
size='small' {
onClick={() => from: buildSyncTargetSpec(
removeCondition(operation.id, condition.id) nextType,
} syncFromTarget?.key || '',
/> ),
</div> },
<Row gutter={12}> )
<Col xs={24} md={10}> }
<Text type='tertiary' size='small'> />
path
</Text>
<Input <Input
value={condition.path} value={syncFromTarget?.key || ''}
placeholder='model' placeholder='session_id'
onChange={(nextValue) => onChange={(nextKey) =>
updateCondition( updateOperation(
operation.id, selectedOperation.id,
condition.id, {
{ path: nextValue }, from: buildSyncTargetSpec(
syncFromTarget?.type || 'json',
nextKey,
),
},
) )
} }
/> />
<Space wrap style={{ marginTop: 6 }}> </div>
{CONDITION_PATH_SUGGESTIONS.map( </Col>
(pathItem) => ( <Col xs={24} md={12}>
<Tag <Text type='tertiary' size='small'>
key={`${condition.id}_${pathItem}`} to endpoint
size='small' </Text>
color='grey' <div className='flex gap-2'>
className='cursor-pointer'
onClick={() =>
updateCondition(
operation.id,
condition.id,
{ path: pathItem },
)
}
>
{pathItem}
</Tag>
),
)}
</Space>
</Col>
<Col xs={24} md={8}>
<Text type='tertiary' size='small'>
mode
</Text>
<Select <Select
value={condition.mode} value={syncToTarget?.type || 'json'}
optionList={CONDITION_MODE_OPTIONS} optionList={SYNC_TARGET_TYPE_OPTIONS}
onChange={(nextValue) => style={{ width: 120 }}
updateCondition( onChange={(nextType) =>
operation.id, updateOperation(
condition.id, selectedOperation.id,
{ mode: nextValue }, {
to: buildSyncTargetSpec(
nextType,
syncToTarget?.key || '',
),
},
) )
} }
style={{ width: '100%' }}
/> />
</Col>
<Col xs={24} md={6}>
<Text type='tertiary' size='small'>
value
</Text>
<Input <Input
value={condition.value_text} value={syncToTarget?.key || ''}
placeholder='gpt' placeholder='prompt_cache_key'
onChange={(nextValue) => onChange={(nextKey) =>
updateCondition( updateOperation(
operation.id, selectedOperation.id,
condition.id, {
{ value_text: nextValue }, to: buildSyncTargetSpec(
syncToTarget?.type || 'json',
nextKey,
),
},
) )
} }
/> />
</Col> </div>
</Row> </Col>
<Space style={{ marginTop: 8 }}> </Row>
<Switch <Space wrap style={{ marginTop: 8 }}>
checked={condition.invert} <Tag
checkedText={t('开')} size='small'
uncheckedText={t('关')} color='cyan'
className='cursor-pointer'
onClick={() =>
updateOperation(selectedOperation.id, {
from: 'header:session_id',
to: 'json:prompt_cache_key',
})
}
>
{
'header:session_id -> json:prompt_cache_key'
}
</Tag>
<Tag
size='small'
color='cyan'
className='cursor-pointer'
onClick={() =>
updateOperation(selectedOperation.id, {
from: 'json:prompt_cache_key',
to: 'header:session_id',
})
}
>
{
'json:prompt_cache_key -> header:session_id'
}
</Tag>
</Space>
</div>
) : meta.from || meta.to === false || meta.to ? (
<Row gutter={12} style={{ marginTop: 8 }}>
{meta.from || meta.to === false ? (
<Col xs={24} md={12}>
<Text type='tertiary' size='small'>
from
</Text>
<Input
value={selectedOperation.from}
placeholder={
mode.includes('header')
? 'Authorization'
: 'model'
}
onChange={(nextValue) => onChange={(nextValue) =>
updateCondition( updateOperation(selectedOperation.id, {
operation.id, from: nextValue,
condition.id, })
{ invert: nextValue },
)
} }
/> />
</Col>
) : null}
{meta.to || meta.to === false ? (
<Col xs={24} md={12}>
<Text type='tertiary' size='small'> <Text type='tertiary' size='small'>
invert to
</Text> </Text>
<Switch <Input
checked={condition.pass_missing_key} value={selectedOperation.to}
checkedText={t('开')} placeholder={
uncheckedText={t('关')} mode.includes('header')
? 'X-Upstream-Auth'
: 'original_model'
}
onChange={(nextValue) => onChange={(nextValue) =>
updateCondition( updateOperation(selectedOperation.id, {
operation.id, to: nextValue,
condition.id, })
{ pass_missing_key: nextValue },
)
} }
/> />
<Text type='tertiary' size='small'> </Col>
pass_missing_key ) : null}
</Text> </Row>
</Space> ) : null}
</Card>
))} <div
</Space> className='mt-3 rounded-xl p-2'
)} style={{
</div> background: 'rgba(127, 127, 127, 0.08)',
}}
>
<div className='flex items-center justify-between mb-2'>
<Space>
<Text>{t('条件')}</Text>
<Select
value={selectedOperation.logic || 'OR'}
optionList={[
{ label: 'OR', value: 'OR' },
{ label: 'AND', value: 'AND' },
]}
style={{ width: 96 }}
onChange={(nextValue) =>
updateOperation(selectedOperation.id, {
logic: nextValue,
})
}
/>
</Space>
<Space spacing={6}>
<Button
size='small'
type='tertiary'
onClick={expandAllSelectedConditions}
>
{t('全部展开')}
</Button>
<Button
size='small'
type='tertiary'
onClick={collapseAllSelectedConditions}
>
{t('全部收起')}
</Button>
<Button
icon={<IconPlus />}
size='small'
onClick={() =>
addCondition(selectedOperation.id)
}
>
{t('新增条件')}
</Button>
</Space>
</div>
{conditions.length === 0 ? (
<Text type='tertiary' size='small'>
{t('没有条件时,默认总是执行该操作。')}
</Text>
) : (
<Collapse
keepDOM
activeKey={selectedConditionKeys}
onChange={(activeKeys) =>
handleConditionCollapseChange(
selectedOperation.id,
activeKeys,
)
}
>
{conditions.map(
(condition, conditionIndex) => (
<Collapse.Panel
key={condition.id}
itemKey={condition.id}
header={
<Space spacing={8}>
<Tag size='small'>
{`C${conditionIndex + 1}`}
</Tag>
<Text type='tertiary' size='small'>
{condition.path ||
t('未设置 path')}
</Text>
</Space>
}
>
<div>
<div className='flex justify-end mb-2'>
<Button
theme='borderless'
type='danger'
icon={<IconDelete />}
size='small'
onClick={() =>
removeCondition(
selectedOperation.id,
condition.id,
)
}
/>
</div>
<Row gutter={12}>
<Col xs={24} md={10}>
<Text
type='tertiary'
size='small'
>
path
</Text>
<Input
value={condition.path}
placeholder='model'
onChange={(nextValue) =>
updateCondition(
selectedOperation.id,
condition.id,
{ path: nextValue },
)
}
/>
<Space
wrap
style={{ marginTop: 6 }}
>
{CONDITION_PATH_SUGGESTIONS.map(
(pathItem) => (
<Tag
key={`${condition.id}_${pathItem}`}
size='small'
color='grey'
className='cursor-pointer'
onClick={() =>
updateCondition(
selectedOperation.id,
condition.id,
{ path: pathItem },
)
}
>
{pathItem}
</Tag>
),
)}
</Space>
</Col>
<Col xs={24} md={8}>
<Text
type='tertiary'
size='small'
>
mode
</Text>
<Select
value={condition.mode}
optionList={
CONDITION_MODE_OPTIONS
}
onChange={(nextValue) =>
updateCondition(
selectedOperation.id,
condition.id,
{ mode: nextValue },
)
}
style={{ width: '100%' }}
/>
</Col>
<Col xs={24} md={6}>
<Text
type='tertiary'
size='small'
>
value
</Text>
<Input
value={condition.value_text}
placeholder='gpt'
onChange={(nextValue) =>
updateCondition(
selectedOperation.id,
condition.id,
{ value_text: nextValue },
)
}
/>
</Col>
</Row>
<Space style={{ marginTop: 8 }}>
<Switch
checked={Boolean(
condition.invert,
)}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(nextValue) =>
updateCondition(
selectedOperation.id,
condition.id,
{ invert: nextValue },
)
}
/>
<Text type='tertiary' size='small'>
invert
</Text>
<Switch
checked={Boolean(
condition.pass_missing_key,
)}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(nextValue) =>
updateCondition(
selectedOperation.id,
condition.id,
{
pass_missing_key: nextValue,
},
)
}
/>
<Text type='tertiary' size='small'>
pass_missing_key
</Text>
</Space>
</div>
</Collapse.Panel>
),
)}
</Collapse>
)}
</div>
</Card>
);
})()
) : (
<Card
className='!rounded-2xl !border-0'
bodyStyle={{
padding: 14,
background: 'var(--semi-color-fill-0)',
}}
>
<Text type='tertiary'>
{t('请选择一条规则进行编辑。')}
</Text>
</Card> </Card>
); )}
})}
</Space> <Card
<Card className='!rounded-xl border mt-3'> className='!rounded-2xl !border-0 mt-3'
<div className='flex items-center justify-between mb-2'> bodyStyle={{
<Text>{t('实时 JSON 预览')}</Text> padding: 12,
<Tag color='grey'>{t('预览')}</Tag> background: 'var(--semi-color-fill-0)',
</div> }}
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-64 overflow-auto'> >
{visualPreview || '{}'} <div className='flex items-center justify-between mb-2'>
</pre> <Text>{t('实时 JSON 预览')}</Text>
</Card> <Tag color='grey'>{t('预览')}</Tag>
</div>
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-64 overflow-auto'>
{visualPreview || '{}'}
</pre>
</Card>
</Col>
</Row>
</div> </div>
)} )}
</div> </div>
...@@ -1295,7 +1734,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => { ...@@ -1295,7 +1734,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
<Button onClick={formatJson}>{t('格式化')}</Button> <Button onClick={formatJson}>{t('格式化')}</Button>
<Tag color='grey'>{t('普通编辑')}</Tag> <Tag color='grey'>{t('普通编辑')}</Tag>
</Space> </Space>
<Input.TextArea <TextArea
value={jsonText} value={jsonText}
autosize={{ minRows: 18, maxRows: 28 }} autosize={{ minRows: 18, maxRows: 28 }}
onChange={(nextValue) => handleJsonChange(nextValue ?? '')} onChange={(nextValue) => handleJsonChange(nextValue ?? '')}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment