CustomRequestEditor.js 6.4 KB

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