CustomRequestEditor.jsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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, { useState, useEffect } from 'react';
  16. import {
  17. TextArea,
  18. Typography,
  19. Button,
  20. Switch,
  21. Banner,
  22. } from '@douyinfe/semi-ui';
  23. import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react';
  24. import { useTranslation } from 'react-i18next';
  25. const CustomRequestEditor = ({
  26. customRequestMode,
  27. customRequestBody,
  28. onCustomRequestModeChange,
  29. onCustomRequestBodyChange,
  30. defaultPayload,
  31. }) => {
  32. const { t } = useTranslation();
  33. const [isValid, setIsValid] = useState(true);
  34. const [errorMessage, setErrorMessage] = useState('');
  35. const [localValue, setLocalValue] = useState(customRequestBody || '');
  36. // 当切换到自定义模式时,用默认payload初始化
  37. useEffect(() => {
  38. if (
  39. customRequestMode &&
  40. (!customRequestBody || customRequestBody.trim() === '')
  41. ) {
  42. const defaultJson = defaultPayload
  43. ? JSON.stringify(defaultPayload, null, 2)
  44. : '';
  45. setLocalValue(defaultJson);
  46. onCustomRequestBodyChange(defaultJson);
  47. }
  48. }, [
  49. customRequestMode,
  50. defaultPayload,
  51. customRequestBody,
  52. onCustomRequestBodyChange,
  53. ]);
  54. // 同步外部传入的customRequestBody到本地状态
  55. useEffect(() => {
  56. if (customRequestBody !== localValue) {
  57. setLocalValue(customRequestBody || '');
  58. validateJson(customRequestBody || '');
  59. }
  60. }, [customRequestBody]);
  61. // 验证JSON格式
  62. const validateJson = (value) => {
  63. if (!value.trim()) {
  64. setIsValid(true);
  65. setErrorMessage('');
  66. return true;
  67. }
  68. try {
  69. JSON.parse(value);
  70. setIsValid(true);
  71. setErrorMessage('');
  72. return true;
  73. } catch (error) {
  74. setIsValid(false);
  75. setErrorMessage(`${t('JSON格式错误')}: ${error.message}`);
  76. return false;
  77. }
  78. };
  79. const handleValueChange = (value) => {
  80. setLocalValue(value);
  81. validateJson(value);
  82. // 始终保存用户输入,让预览逻辑处理JSON解析错误
  83. onCustomRequestBodyChange(value);
  84. };
  85. const handleModeToggle = (enabled) => {
  86. onCustomRequestModeChange(enabled);
  87. if (enabled && defaultPayload) {
  88. const defaultJson = JSON.stringify(defaultPayload, null, 2);
  89. setLocalValue(defaultJson);
  90. onCustomRequestBodyChange(defaultJson);
  91. }
  92. };
  93. const formatJson = () => {
  94. try {
  95. const parsed = JSON.parse(localValue);
  96. const formatted = JSON.stringify(parsed, null, 2);
  97. setLocalValue(formatted);
  98. onCustomRequestBodyChange(formatted);
  99. setIsValid(true);
  100. setErrorMessage('');
  101. } catch (error) {
  102. // 如果格式化失败,保持原样
  103. }
  104. };
  105. return (
  106. <div className='space-y-4'>
  107. {/* 自定义模式开关 */}
  108. <div className='flex items-center justify-between'>
  109. <div className='flex items-center gap-2'>
  110. <Code size={16} className='text-gray-500' />
  111. <Typography.Text strong className='text-sm'>
  112. {t('自定义请求体模式')}
  113. </Typography.Text>
  114. </div>
  115. <Switch
  116. checked={customRequestMode}
  117. onChange={handleModeToggle}
  118. checkedText={t('开')}
  119. uncheckedText={t('关')}
  120. size='small'
  121. />
  122. </div>
  123. {customRequestMode && (
  124. <>
  125. {/* 提示信息 */}
  126. <Banner
  127. type='warning'
  128. description={t('启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。')}
  129. icon={<AlertTriangle size={16} />}
  130. className='!rounded-lg'
  131. closeIcon={null}
  132. />
  133. {/* JSON编辑器 */}
  134. <div>
  135. <div className='flex items-center justify-between mb-2'>
  136. <Typography.Text strong className='text-sm'>
  137. {t('请求体 JSON')}
  138. </Typography.Text>
  139. <div className='flex items-center gap-2'>
  140. {isValid ? (
  141. <div className='flex items-center gap-1 text-green-600'>
  142. <Check size={14} />
  143. <Typography.Text className='text-xs'>
  144. {t('格式正确')}
  145. </Typography.Text>
  146. </div>
  147. ) : (
  148. <div className='flex items-center gap-1 text-red-600'>
  149. <X size={14} />
  150. <Typography.Text className='text-xs'>
  151. {t('格式错误')}
  152. </Typography.Text>
  153. </div>
  154. )}
  155. <Button
  156. theme='borderless'
  157. type='tertiary'
  158. size='small'
  159. icon={<Edit size={14} />}
  160. onClick={formatJson}
  161. disabled={!isValid}
  162. className='!rounded-lg'
  163. >
  164. {t('格式化')}
  165. </Button>
  166. </div>
  167. </div>
  168. <TextArea
  169. value={localValue}
  170. onChange={handleValueChange}
  171. placeholder='{"model": "gpt-4o", "messages": [...], ...}'
  172. autosize={{ minRows: 8, maxRows: 20 }}
  173. className={`custom-request-textarea !rounded-lg font-mono text-sm ${!isValid ? '!border-red-500' : ''}`}
  174. style={{
  175. fontFamily: 'Consolas, Monaco, "Courier New", monospace',
  176. lineHeight: '1.5',
  177. }}
  178. />
  179. {!isValid && errorMessage && (
  180. <Typography.Text type='danger' className='text-xs mt-1 block'>
  181. {errorMessage}
  182. </Typography.Text>
  183. )}
  184. <Typography.Text className='text-xs text-gray-500 mt-2 block'>
  185. {t('请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。')}
  186. </Typography.Text>
  187. </div>
  188. </>
  189. )}
  190. </div>
  191. );
  192. };
  193. export default CustomRequestEditor;