CustomRequestEditor.js 5.9 KB

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