feat: add visual editing mode for chat configurations

This commit is contained in:
HynoR
2025-10-01 18:22:32 +08:00
parent 96b172e93b
commit ec76b0f5e2

View File

@@ -18,7 +18,25 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Banner, Button, Form, Space, Spin } from '@douyinfe/semi-ui';
import {
Banner,
Button,
Form,
Space,
Spin,
RadioGroup,
Radio,
Table,
Modal,
Input,
Divider,
} from '@douyinfe/semi-ui';
import {
IconPlus,
IconEdit,
IconDelete,
IconSearch,
} from '@douyinfe/semi-icons';
import {
compareObjects,
API,
@@ -37,6 +55,52 @@ export default function SettingsChats(props) {
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
const [editMode, setEditMode] = useState('json');
const [chatConfigs, setChatConfigs] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
const [isEdit, setIsEdit] = useState(false);
const [searchText, setSearchText] = useState('');
const modalFormRef = useRef();
const jsonToConfigs = (jsonString) => {
try {
const configs = JSON.parse(jsonString);
return Array.isArray(configs)
? configs.map((config, index) => ({
id: index,
name: Object.keys(config)[0] || '',
url: Object.values(config)[0] || '',
}))
: [];
} catch (error) {
console.error('JSON parse error:', error);
return [];
}
};
const configsToJson = (configs) => {
const jsonArray = configs.map((config) => ({
[config.name]: config.url,
}));
return JSON.stringify(jsonArray, null, 2);
};
const syncJsonToConfigs = () => {
const configs = jsonToConfigs(inputs.Chats);
setChatConfigs(configs);
};
const syncConfigsToJson = (configs) => {
const jsonString = configsToJson(configs);
setInputs((prev) => ({
...prev,
Chats: jsonString,
}));
if (refForm.current && editMode === 'json') {
refForm.current.setValues({ Chats: jsonString });
}
};
async function onSubmit() {
try {
@@ -104,15 +168,145 @@ export default function SettingsChats(props) {
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
// 同步到可视化配置
const configs = jsonToConfigs(currentInputs.Chats || '[]');
setChatConfigs(configs);
}, [props.options]);
useEffect(() => {
if (editMode === 'visual') {
syncJsonToConfigs();
}
}, [inputs.Chats, editMode]);
useEffect(() => {
if (refForm.current && editMode === 'json') {
refForm.current.setValues(inputs);
}
}, [editMode, inputs]);
const handleAddConfig = () => {
setEditingConfig({ name: '', url: '' });
setIsEdit(false);
setModalVisible(true);
setTimeout(() => {
if (modalFormRef.current) {
modalFormRef.current.setValues({ name: '', url: '' });
}
}, 100);
};
const handleEditConfig = (config) => {
setEditingConfig({ ...config });
setIsEdit(true);
setModalVisible(true);
setTimeout(() => {
if (modalFormRef.current) {
modalFormRef.current.setValues(config);
}
}, 100);
};
const handleDeleteConfig = (id) => {
const newConfigs = chatConfigs.filter((config) => config.id !== id);
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
showSuccess(t('删除成功'));
};
const handleModalOk = () => {
if (modalFormRef.current) {
modalFormRef.current
.validate()
.then((values) => {
if (isEdit) {
const newConfigs = chatConfigs.map((config) =>
config.id === editingConfig.id
? { ...editingConfig, name: values.name, url: values.url }
: config,
);
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
} else {
const maxId =
chatConfigs.length > 0
? Math.max(...chatConfigs.map((c) => c.id))
: -1;
const newConfig = {
id: maxId + 1,
name: values.name,
url: values.url,
};
const newConfigs = [...chatConfigs, newConfig];
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
}
setModalVisible(false);
setEditingConfig(null);
showSuccess(isEdit ? t('编辑成功') : t('添加成功'));
})
.catch((error) => {
console.error('Modal form validation error:', error);
});
}
};
const handleModalCancel = () => {
setModalVisible(false);
setEditingConfig(null);
};
const filteredConfigs = chatConfigs.filter(
(config) =>
!searchText ||
config.name.toLowerCase().includes(searchText.toLowerCase()),
);
const columns = [
{
title: t('聊天应用名称'),
dataIndex: 'name',
key: 'name',
render: (text) => text || t('未命名'),
},
{
title: t('URL链接'),
dataIndex: 'url',
key: 'url',
render: (text) => (
<div style={{ maxWidth: 300, wordBreak: 'break-all' }}>{text}</div>
),
},
{
title: t('操作'),
key: 'action',
render: (_, record) => (
<Space>
<Button
type='primary'
icon={<IconEdit />}
size='small'
onClick={() => handleEditConfig(record)}
>
{t('编辑')}
</Button>
<Button
type='danger'
icon={<IconDelete />}
size='small'
onClick={() => handleDeleteConfig(record.id)}
>
{t('删除')}
</Button>
</Space>
),
},
];
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Space vertical style={{ width: '100%' }}>
<Form.Section text={t('聊天设置')}>
<Banner
type='info'
@@ -120,34 +314,141 @@ export default function SettingsChats(props) {
'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
)}
/>
<Form.TextArea
label={t('聊天配置')}
extraText={''}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
onChange={(value) =>
setInputs({
...inputs,
Chats: value,
})
}
/>
<Divider />
<div style={{ marginBottom: 16 }}>
<span style={{ marginRight: 16, fontWeight: 600 }}>
{t('编辑模式')}:
</span>
<RadioGroup
type='button'
value={editMode}
onChange={(e) => {
const newMode = e.target.value;
setEditMode(newMode);
// 确保模式切换时数据正确同步
setTimeout(() => {
if (newMode === 'json' && refForm.current) {
refForm.current.setValues(inputs);
}
}, 100);
}}
>
<Radio value='visual'>{t('可视化编辑')}</Radio>
<Radio value='json'>{t('JSON编辑')}</Radio>
</RadioGroup>
</div>
{editMode === 'visual' ? (
<div>
<Space style={{ marginBottom: 16 }}>
<Button
type='primary'
icon={<IconPlus />}
onClick={handleAddConfig}
>
{t('添加聊天配置')}
</Button>
<Input
prefix={<IconSearch />}
placeholder={t('搜索聊天应用名称')}
value={searchText}
onChange={(value) => setSearchText(value)}
style={{ width: 250 }}
showClear
/>
</Space>
<Table
columns={columns}
dataSource={filteredConfigs}
rowKey='id'
pagination={{
pageSize: 10,
showSizeChanger: false,
showQuickJumper: true,
showTotal: (total, range) =>
t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', {
total,
start: range[0],
end: range[1],
}),
}}
/>
</div>
) : (
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
>
<Form.TextArea
label={t('聊天配置')}
extraText={''}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
onChange={(value) =>
setInputs({
...inputs,
Chats: value,
})
}
/>
</Form>
)}
</Form.Section>
</Form>
<Space>
<Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
<Space>
<Button type='primary' onClick={onSubmit}>
{t('保存聊天设置')}
</Button>
</Space>
</Space>
<Modal
title={isEdit ? t('编辑聊天配置') : t('添加聊天配置')}
visible={modalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
width={600}
>
<Form getFormApi={(api) => (modalFormRef.current = api)}>
<Form.Input
field='name'
label={t('聊天应用名称')}
placeholder={t('请输入聊天应用名称')}
rules={[
{ required: true, message: t('请输入聊天应用名称') },
{ min: 1, message: t('名称不能为空') },
]}
/>
<Form.Input
field='url'
label={t('URL链接')}
placeholder={t('请输入完整的URL链接')}
rules={[{ required: true, message: t('请输入URL链接') }]}
/>
<Banner
type='info'
description={t(
'提示:链接中的{key}将被替换为API密钥{address}将被替换为服务器地址',
)}
style={{ marginTop: 16 }}
/>
</Form>
</Modal>
</Spin>
);
}