SettingsChats.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useRef } from 'react';
  16. import {
  17. Banner,
  18. Button,
  19. Form,
  20. Space,
  21. Spin,
  22. RadioGroup,
  23. Radio,
  24. Table,
  25. Modal,
  26. Input,
  27. Divider,
  28. } from '@douyinfe/semi-ui';
  29. import {
  30. IconPlus,
  31. IconEdit,
  32. IconDelete,
  33. IconSearch,
  34. } from '@douyinfe/semi-icons';
  35. import {
  36. compareObjects,
  37. API,
  38. showError,
  39. showSuccess,
  40. showWarning,
  41. verifyJSON,
  42. } from '../../../helpers';
  43. import { useTranslation } from 'react-i18next';
  44. export default function SettingsChats(props) {
  45. const { t } = useTranslation();
  46. const [loading, setLoading] = useState(false);
  47. const [inputs, setInputs] = useState({
  48. Chats: '[]',
  49. });
  50. const refForm = useRef();
  51. const [inputsRow, setInputsRow] = useState(inputs);
  52. const [editMode, setEditMode] = useState('json');
  53. const [chatConfigs, setChatConfigs] = useState([]);
  54. const [modalVisible, setModalVisible] = useState(false);
  55. const [editingConfig, setEditingConfig] = useState(null);
  56. const [isEdit, setIsEdit] = useState(false);
  57. const [searchText, setSearchText] = useState('');
  58. const modalFormRef = useRef();
  59. const jsonToConfigs = (jsonString) => {
  60. try {
  61. const configs = JSON.parse(jsonString);
  62. return Array.isArray(configs)
  63. ? configs.map((config, index) => ({
  64. id: index,
  65. name: Object.keys(config)[0] || '',
  66. url: Object.values(config)[0] || '',
  67. }))
  68. : [];
  69. } catch (error) {
  70. console.error('JSON parse error:', error);
  71. return [];
  72. }
  73. };
  74. const configsToJson = (configs) => {
  75. const jsonArray = configs.map((config) => ({
  76. [config.name]: config.url,
  77. }));
  78. return JSON.stringify(jsonArray, null, 2);
  79. };
  80. const syncJsonToConfigs = () => {
  81. const configs = jsonToConfigs(inputs.Chats);
  82. setChatConfigs(configs);
  83. };
  84. const syncConfigsToJson = (configs) => {
  85. const jsonString = configsToJson(configs);
  86. setInputs((prev) => ({
  87. ...prev,
  88. Chats: jsonString,
  89. }));
  90. if (refForm.current && editMode === 'json') {
  91. refForm.current.setValues({ Chats: jsonString });
  92. }
  93. };
  94. async function onSubmit() {
  95. try {
  96. console.log('Starting validation...');
  97. await refForm.current
  98. .validate()
  99. .then(() => {
  100. console.log('Validation passed');
  101. const updateArray = compareObjects(inputs, inputsRow);
  102. if (!updateArray.length)
  103. return showWarning(t('你似乎并没有修改什么'));
  104. const requestQueue = updateArray.map((item) => {
  105. let value = '';
  106. if (typeof inputs[item.key] === 'boolean') {
  107. value = String(inputs[item.key]);
  108. } else {
  109. value = inputs[item.key];
  110. }
  111. return API.put('/api/option/', {
  112. key: item.key,
  113. value,
  114. });
  115. });
  116. setLoading(true);
  117. Promise.all(requestQueue)
  118. .then((res) => {
  119. if (requestQueue.length === 1) {
  120. if (res.includes(undefined)) return;
  121. } else if (requestQueue.length > 1) {
  122. if (res.includes(undefined))
  123. return showError(t('部分保存失败,请重试'));
  124. }
  125. showSuccess(t('保存成功'));
  126. props.refresh();
  127. })
  128. .catch(() => {
  129. showError(t('保存失败,请重试'));
  130. })
  131. .finally(() => {
  132. setLoading(false);
  133. });
  134. })
  135. .catch((error) => {
  136. console.error('Validation failed:', error);
  137. showError(t('请检查输入'));
  138. });
  139. } catch (error) {
  140. showError(t('请检查输入'));
  141. console.error(error);
  142. }
  143. }
  144. useEffect(() => {
  145. const currentInputs = {};
  146. for (let key in props.options) {
  147. if (Object.keys(inputs).includes(key)) {
  148. if (key === 'Chats') {
  149. const obj = JSON.parse(props.options[key]);
  150. currentInputs[key] = JSON.stringify(obj, null, 2);
  151. } else {
  152. currentInputs[key] = props.options[key];
  153. }
  154. }
  155. }
  156. setInputs(currentInputs);
  157. setInputsRow(structuredClone(currentInputs));
  158. refForm.current.setValues(currentInputs);
  159. // 同步到可视化配置
  160. const configs = jsonToConfigs(currentInputs.Chats || '[]');
  161. setChatConfigs(configs);
  162. }, [props.options]);
  163. useEffect(() => {
  164. if (editMode === 'visual') {
  165. syncJsonToConfigs();
  166. }
  167. }, [inputs.Chats, editMode]);
  168. useEffect(() => {
  169. if (refForm.current && editMode === 'json') {
  170. refForm.current.setValues(inputs);
  171. }
  172. }, [editMode, inputs]);
  173. const handleAddConfig = () => {
  174. setEditingConfig({ name: '', url: '' });
  175. setIsEdit(false);
  176. setModalVisible(true);
  177. setTimeout(() => {
  178. if (modalFormRef.current) {
  179. modalFormRef.current.setValues({ name: '', url: '' });
  180. }
  181. }, 100);
  182. };
  183. const handleEditConfig = (config) => {
  184. setEditingConfig({ ...config });
  185. setIsEdit(true);
  186. setModalVisible(true);
  187. setTimeout(() => {
  188. if (modalFormRef.current) {
  189. modalFormRef.current.setValues(config);
  190. }
  191. }, 100);
  192. };
  193. const handleDeleteConfig = (id) => {
  194. const newConfigs = chatConfigs.filter((config) => config.id !== id);
  195. setChatConfigs(newConfigs);
  196. syncConfigsToJson(newConfigs);
  197. showSuccess(t('删除成功'));
  198. };
  199. const handleModalOk = () => {
  200. if (modalFormRef.current) {
  201. modalFormRef.current
  202. .validate()
  203. .then((values) => {
  204. if (isEdit) {
  205. const newConfigs = chatConfigs.map((config) =>
  206. config.id === editingConfig.id
  207. ? { ...editingConfig, name: values.name, url: values.url }
  208. : config,
  209. );
  210. setChatConfigs(newConfigs);
  211. syncConfigsToJson(newConfigs);
  212. } else {
  213. const maxId =
  214. chatConfigs.length > 0
  215. ? Math.max(...chatConfigs.map((c) => c.id))
  216. : -1;
  217. const newConfig = {
  218. id: maxId + 1,
  219. name: values.name,
  220. url: values.url,
  221. };
  222. const newConfigs = [...chatConfigs, newConfig];
  223. setChatConfigs(newConfigs);
  224. syncConfigsToJson(newConfigs);
  225. }
  226. setModalVisible(false);
  227. setEditingConfig(null);
  228. showSuccess(isEdit ? t('编辑成功') : t('添加成功'));
  229. })
  230. .catch((error) => {
  231. console.error('Modal form validation error:', error);
  232. });
  233. }
  234. };
  235. const handleModalCancel = () => {
  236. setModalVisible(false);
  237. setEditingConfig(null);
  238. };
  239. const filteredConfigs = chatConfigs.filter(
  240. (config) =>
  241. !searchText ||
  242. config.name.toLowerCase().includes(searchText.toLowerCase()),
  243. );
  244. const columns = [
  245. {
  246. title: t('聊天应用名称'),
  247. dataIndex: 'name',
  248. key: 'name',
  249. render: (text) => text || t('未命名'),
  250. },
  251. {
  252. title: t('URL链接'),
  253. dataIndex: 'url',
  254. key: 'url',
  255. render: (text) => (
  256. <div style={{ maxWidth: 300, wordBreak: 'break-all' }}>{text}</div>
  257. ),
  258. },
  259. {
  260. title: t('操作'),
  261. key: 'action',
  262. render: (_, record) => (
  263. <Space>
  264. <Button
  265. type='primary'
  266. icon={<IconEdit />}
  267. size='small'
  268. onClick={() => handleEditConfig(record)}
  269. >
  270. {t('编辑')}
  271. </Button>
  272. <Button
  273. type='danger'
  274. icon={<IconDelete />}
  275. size='small'
  276. onClick={() => handleDeleteConfig(record.id)}
  277. >
  278. {t('删除')}
  279. </Button>
  280. </Space>
  281. ),
  282. },
  283. ];
  284. return (
  285. <Spin spinning={loading}>
  286. <Space vertical style={{ width: '100%' }}>
  287. <Form.Section text={t('聊天设置')}>
  288. <Banner
  289. type='info'
  290. description={t(
  291. '链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
  292. )}
  293. />
  294. <Divider />
  295. <div style={{ marginBottom: 16 }}>
  296. <span style={{ marginRight: 16, fontWeight: 600 }}>
  297. {t('编辑模式')}:
  298. </span>
  299. <RadioGroup
  300. type='button'
  301. value={editMode}
  302. onChange={(e) => {
  303. const newMode = e.target.value;
  304. setEditMode(newMode);
  305. // 确保模式切换时数据正确同步
  306. setTimeout(() => {
  307. if (newMode === 'json' && refForm.current) {
  308. refForm.current.setValues(inputs);
  309. }
  310. }, 100);
  311. }}
  312. >
  313. <Radio value='visual'>{t('可视化编辑')}</Radio>
  314. <Radio value='json'>{t('JSON编辑')}</Radio>
  315. </RadioGroup>
  316. </div>
  317. {editMode === 'visual' ? (
  318. <div>
  319. <Space style={{ marginBottom: 16 }}>
  320. <Button
  321. type='primary'
  322. icon={<IconPlus />}
  323. onClick={handleAddConfig}
  324. >
  325. {t('添加聊天配置')}
  326. </Button>
  327. <Input
  328. prefix={<IconSearch />}
  329. placeholder={t('搜索聊天应用名称')}
  330. value={searchText}
  331. onChange={(value) => setSearchText(value)}
  332. style={{ width: 250 }}
  333. showClear
  334. />
  335. </Space>
  336. <Table
  337. columns={columns}
  338. dataSource={filteredConfigs}
  339. rowKey='id'
  340. pagination={{
  341. pageSize: 10,
  342. showSizeChanger: false,
  343. showQuickJumper: true,
  344. showTotal: (total, range) =>
  345. t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', {
  346. total,
  347. start: range[0],
  348. end: range[1],
  349. }),
  350. }}
  351. />
  352. </div>
  353. ) : (
  354. <Form
  355. values={inputs}
  356. getFormApi={(formAPI) => (refForm.current = formAPI)}
  357. >
  358. <Form.TextArea
  359. label={t('聊天配置')}
  360. extraText={''}
  361. placeholder={t('为一个 JSON 文本')}
  362. field={'Chats'}
  363. autosize={{ minRows: 6, maxRows: 12 }}
  364. trigger='blur'
  365. stopValidateWithError
  366. rules={[
  367. {
  368. validator: (rule, value) => {
  369. return verifyJSON(value);
  370. },
  371. message: t('不是合法的 JSON 字符串'),
  372. },
  373. ]}
  374. onChange={(value) =>
  375. setInputs({
  376. ...inputs,
  377. Chats: value,
  378. })
  379. }
  380. />
  381. </Form>
  382. )}
  383. </Form.Section>
  384. <Space>
  385. <Button type='primary' onClick={onSubmit}>
  386. {t('保存聊天设置')}
  387. </Button>
  388. </Space>
  389. </Space>
  390. <Modal
  391. title={isEdit ? t('编辑聊天配置') : t('添加聊天配置')}
  392. visible={modalVisible}
  393. onOk={handleModalOk}
  394. onCancel={handleModalCancel}
  395. width={600}
  396. >
  397. <Form getFormApi={(api) => (modalFormRef.current = api)}>
  398. <Form.Input
  399. field='name'
  400. label={t('聊天应用名称')}
  401. placeholder={t('请输入聊天应用名称')}
  402. rules={[
  403. { required: true, message: t('请输入聊天应用名称') },
  404. { min: 1, message: t('名称不能为空') },
  405. ]}
  406. />
  407. <Form.Input
  408. field='url'
  409. label={t('URL链接')}
  410. placeholder={t('请输入完整的URL链接')}
  411. rules={[{ required: true, message: t('请输入URL链接') }]}
  412. />
  413. <Banner
  414. type='info'
  415. description={t(
  416. '提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址',
  417. )}
  418. style={{ marginTop: 16 }}
  419. />
  420. </Form>
  421. </Modal>
  422. </Spin>
  423. );
  424. }