فهرست منبع

Merge pull request #1947 from HynoR/feat/newedit

feat: Add visual editing mode for chat configurations
Seefs 5 ماه پیش
والد
کامیت
9e6828653b
1فایلهای تغییر یافته به همراه386 افزوده شده و 32 حذف شده
  1. 386 32
      web/src/pages/Setting/Chat/SettingsChats.jsx

+ 386 - 32
web/src/pages/Setting/Chat/SettingsChats.jsx

@@ -18,7 +18,26 @@ For commercial licensing, please contact support@quantumnous.com
 */
 */
 
 
 import React, { useEffect, useState, useRef } from 'react';
 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,
+  IconSaveStroked,
+} from '@douyinfe/semi-icons';
 import {
 import {
   compareObjects,
   compareObjects,
   API,
   API,
@@ -37,6 +56,52 @@ export default function SettingsChats(props) {
   });
   });
   const refForm = useRef();
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
   const [inputsRow, setInputsRow] = useState(inputs);
+  const [editMode, setEditMode] = useState('visual');
+  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() {
   async function onSubmit() {
     try {
     try {
@@ -103,16 +168,184 @@ export default function SettingsChats(props) {
     }
     }
     setInputs(currentInputs);
     setInputs(currentInputs);
     setInputsRow(structuredClone(currentInputs));
     setInputsRow(structuredClone(currentInputs));
-    refForm.current.setValues(currentInputs);
+    if (refForm.current) {
+      refForm.current.setValues(currentInputs);
+    }
+
+    // 同步到可视化配置
+    const configs = jsonToConfigs(currentInputs.Chats || '[]');
+    setChatConfigs(configs);
   }, [props.options]);
   }, [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) => {
+          // 检查名称是否重复
+          const isDuplicate = chatConfigs.some(
+            (config) =>
+              config.name === values.name &&
+              (!isEdit || config.id !== editingConfig.id)
+          );
+
+          if (isDuplicate) {
+            showError(t('聊天应用名称已存在,请使用其他名称'));
+            return;
+          }
+
+          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 highlightKeywords = (text) => {
+    if (!text) return text;
+
+    const parts = text.split(/(\{address\}|\{key\})/g);
+    return parts.map((part, index) => {
+      if (part === '{address}') {
+        return (
+          <span key={index} style={{ color: '#0077cc', fontWeight: 600 }}>
+            {part}
+          </span>
+        );
+      } else if (part === '{key}') {
+        return (
+          <span key={index} style={{ color: '#ff6b35', fontWeight: 600 }}>
+            {part}
+          </span>
+        );
+      }
+      return part;
+    });
+  };
+
+  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' }}>
+          {highlightKeywords(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 (
   return (
     <Spin spinning={loading}>
     <Spin spinning={loading}>
-      <Form
-        values={inputs}
-        getFormApi={(formAPI) => (refForm.current = formAPI)}
-        style={{ marginBottom: 15 }}
-      >
+      <Space vertical style={{ width: '100%' }}>
         <Form.Section text={t('聊天设置')}>
         <Form.Section text={t('聊天设置')}>
           <Banner
           <Banner
             type='info'
             type='info'
@@ -120,34 +353,155 @@ export default function SettingsChats(props) {
               '链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
               '链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
             )}
             )}
           />
           />
-          <Form.TextArea
-            label={t('聊天配置')}
-            extraText={''}
-            placeholder={t('为一个 JSON 文本')}
-            field={'Chats'}
-            autosize={{ minRows: 6, maxRows: 12 }}
-            trigger='blur'
-            stopValidateWithError
+
+          <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>
+                <Button
+                  type='primary'
+                  theme='solid'
+                  icon={<IconSaveStroked />}
+                  onClick={onSubmit}
+                >
+                  {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>
+
+        {editMode === 'json' && (
+          <Space>
+            <Button
+              type='primary'
+              icon={<IconSaveStroked />}
+              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={[
             rules={[
-              {
-                validator: (rule, value) => {
-                  return verifyJSON(value);
-                },
-                message: t('不是合法的 JSON 字符串'),
-              },
+              { required: true, message: t('请输入聊天应用名称') },
+              { min: 1, message: t('名称不能为空') },
             ]}
             ]}
-            onChange={(value) =>
-              setInputs({
-                ...inputs,
-                Chats: value,
-              })
-            }
           />
           />
-        </Form.Section>
-      </Form>
-      <Space>
-        <Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
-      </Space>
+          <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>
     </Spin>
   );
   );
 }
 }