/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; import { Banner, Button, Dropdown, Form, Space, Spin, RadioGroup, Radio, Table, Modal, Input, Divider, } from '@douyinfe/semi-ui'; import { IconPlus, IconEdit, IconDelete, IconSearch, IconSaveStroked, IconBolt, } from '@douyinfe/semi-icons'; import { compareObjects, API, showError, showSuccess, showWarning, verifyJSON, } from '../../../helpers'; import { useTranslation } from 'react-i18next'; export default function SettingsChats(props) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [inputs, setInputs] = useState({ Chats: '[]', }); const refForm = useRef(); 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 BUILTIN_TEMPLATES = [ { name: 'Cherry Studio', url: 'cherrystudio://providers/api-keys?v=1&data={cherryConfig}' }, { name: '流畅阅读', url: 'fluentread' }, { name: 'CC Switch', url: 'ccswitch' }, { name: 'Lobe Chat', url: 'https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"{key}","baseURL":"{address}/v1"}}}' }, { name: 'AI as Workspace', url: 'https://aiaw.app/set-provider?provider={"type":"openai","settings":{"apiKey":"{key}","baseURL":"{address}/v1","compatibility":"strict"}}' }, { name: 'AMA 问天', url: 'ama://set-api-key?server={address}&key={key}' }, { name: 'OpenCat', url: 'opencat://team/join?domain={address}&token={key}' }, ]; const addTemplates = (templates) => { const existingNames = new Set(chatConfigs.map((c) => c.name)); const toAdd = templates.filter((tpl) => !existingNames.has(tpl.name)); if (toAdd.length === 0) { showWarning(t('所选模板已存在')); return; } let maxId = chatConfigs.length > 0 ? Math.max(...chatConfigs.map((c) => c.id)) : -1; const newItems = toAdd.map((tpl) => ({ id: ++maxId, name: tpl.name, url: tpl.url, })); const newConfigs = [...chatConfigs, ...newItems]; setChatConfigs(newConfigs); syncConfigsToJson(newConfigs); showSuccess(t('已添加 {{count}} 个模板', { count: toAdd.length })); }; 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 { if (editMode === 'json' && refForm.current) { try { await refForm.current.validate(); } catch (error) { console.error('Validation failed:', error); showError(t('请检查输入')); return; } } const updateArray = compareObjects(inputs, inputsRow); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); const requestQueue = updateArray.map((item) => { let value = ''; if (typeof inputs[item.key] === 'boolean') { value = String(inputs[item.key]); } else { value = inputs[item.key]; } return API.put('/api/option/', { key: item.key, value, }); }); setLoading(true); try { const res = await Promise.all(requestQueue); if (res.includes(undefined)) { if (requestQueue.length > 1) { showError(t('部分保存失败,请重试')); } return; } showSuccess(t('保存成功')); props.refresh(); } catch { showError(t('保存失败,请重试')); } finally { setLoading(false); } } catch (error) { showError(t('请检查输入')); console.error(error); } } useEffect(() => { const currentInputs = {}; for (let key in props.options) { if (Object.keys(inputs).includes(key)) { if (key === 'Chats') { const obj = JSON.parse(props.options[key]); currentInputs[key] = JSON.stringify(obj, null, 2); } else { currentInputs[key] = props.options[key]; } } } setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); if (refForm.current) { 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) => { // 检查名称是否重复 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 ( {part} ); } else if (part === '{key}') { return ( {part} ); } return part; }); }; const columns = [ { title: t('聊天应用名称'), dataIndex: 'name', key: 'name', render: (text) => text || t('未命名'), }, { title: t('URL链接'), dataIndex: 'url', key: 'url', render: (text) => (
{highlightKeywords(text)}
), }, { title: t('操作'), key: 'action', render: (_, record) => ( ), }, ]; return (
{t('编辑模式')}: { const newMode = e.target.value; setEditMode(newMode); // 确保模式切换时数据正确同步 setTimeout(() => { if (newMode === 'json' && refForm.current) { refForm.current.setValues(inputs); } }, 100); }} > {t('可视化编辑')} {t('JSON编辑')}
{editMode === 'visual' ? (
({ node: 'item', key: String(idx), name: tpl.name, onClick: () => addTemplates([tpl]), })), { node: 'divider', key: 'divider' }, { node: 'item', key: 'all', name: t('全部填入'), onClick: () => addTemplates(BUILTIN_TEMPLATES), }, ]} > } placeholder={t('搜索聊天应用名称')} value={searchText} onChange={(value) => setSearchText(value)} style={{ width: 250 }} showClear /> t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', { total, start: range[0], end: range[1], }), }} /> ) : ( (refForm.current = formAPI)} > { return verifyJSON(value); }, message: t('不是合法的 JSON 字符串'), }, ]} onChange={(value) => setInputs({ ...inputs, Chats: value, }) } /> )} {editMode === 'json' && ( )}
(modalFormRef.current = api)}>
); }