CozeWorkflowDemo.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Button, Form, Spinner, Card, Container, Row, Col, Alert } from 'react-bootstrap';
  3. import { toast } from 'react-toastify';
  4. import {
  5. textToSpeech,
  6. speechToText,
  7. translateText,
  8. summarizeText,
  9. cozeService
  10. } from '../utils/cozeExample';
  11. /**
  12. * Coze工作流演示组件
  13. */
  14. const CozeWorkflowDemo = () => {
  15. // 状态管理
  16. const [loading, setLoading] = useState(false);
  17. const [audioUrl, setAudioUrl] = useState(null);
  18. const [inputText, setInputText] = useState('');
  19. const [outputText, setOutputText] = useState('');
  20. const [targetLanguage, setTargetLanguage] = useState('en');
  21. const [audioFile, setAudioFile] = useState(null);
  22. const [errorMessage, setErrorMessage] = useState('');
  23. const [workflowId, setWorkflowId] = useState('');
  24. const [activeTab, setActiveTab] = useState('tts'); // tts, stt, translate, summarize
  25. const audioRef = useRef(null);
  26. useEffect(() => {
  27. // 在组件卸载时清理音频URL
  28. return () => {
  29. if (audioUrl) {
  30. URL.revokeObjectURL(audioUrl);
  31. }
  32. };
  33. }, [audioUrl]);
  34. // 处理文本转语音
  35. const handleTextToSpeech = async () => {
  36. if (!inputText.trim()) {
  37. setErrorMessage('请输入要转换的文本');
  38. return;
  39. }
  40. setLoading(true);
  41. setErrorMessage('');
  42. try {
  43. if (audioUrl) {
  44. URL.revokeObjectURL(audioUrl);
  45. setAudioUrl(null);
  46. }
  47. const response = await textToSpeech(inputText, {
  48. voice: 'female', // 可以是不同的声音选项
  49. speed: 1.0 // 语速
  50. });
  51. // 处理流式响应
  52. if (response && response.audio) {
  53. // 假设返回的是Base64格式的音频数据
  54. const blob = base64ToBlob(response.audio, 'audio/mp3');
  55. const url = URL.createObjectURL(blob);
  56. setAudioUrl(url);
  57. // 设置输出文本为成功消息
  58. setOutputText('语音合成成功,可点击播放按钮收听');
  59. } else {
  60. throw new Error('未接收到有效的音频数据');
  61. }
  62. } catch (error) {
  63. console.error('文本转语音失败:', error);
  64. setErrorMessage(`文本转语音失败: ${error.message}`);
  65. toast.error(`文本转语音失败: ${error.message}`);
  66. } finally {
  67. setLoading(false);
  68. }
  69. };
  70. // 处理语音转文本
  71. const handleSpeechToText = async () => {
  72. if (!audioFile) {
  73. setErrorMessage('请先选择音频文件');
  74. return;
  75. }
  76. setLoading(true);
  77. setErrorMessage('');
  78. try {
  79. const response = await speechToText(audioFile);
  80. if (response && response.text) {
  81. setOutputText(response.text);
  82. } else {
  83. throw new Error('未接收到有效的识别结果');
  84. }
  85. } catch (error) {
  86. console.error('语音识别失败:', error);
  87. setErrorMessage(`语音识别失败: ${error.message}`);
  88. toast.error(`语音识别失败: ${error.message}`);
  89. } finally {
  90. setLoading(false);
  91. }
  92. };
  93. // 处理文本翻译
  94. const handleTextTranslation = async () => {
  95. if (!inputText.trim()) {
  96. setErrorMessage('请输入要翻译的文本');
  97. return;
  98. }
  99. setLoading(true);
  100. setErrorMessage('');
  101. try {
  102. const response = await translateText(inputText, targetLanguage);
  103. if (response && response.translated_text) {
  104. setOutputText(response.translated_text);
  105. } else {
  106. throw new Error('未接收到有效的翻译结果');
  107. }
  108. } catch (error) {
  109. console.error('文本翻译失败:', error);
  110. setErrorMessage(`文本翻译失败: ${error.message}`);
  111. toast.error(`文本翻译失败: ${error.message}`);
  112. } finally {
  113. setLoading(false);
  114. }
  115. };
  116. // 处理文本摘要
  117. const handleTextSummarization = async () => {
  118. if (!inputText.trim()) {
  119. setErrorMessage('请输入要摘要的文本');
  120. return;
  121. }
  122. setLoading(true);
  123. setErrorMessage('');
  124. try {
  125. const response = await summarizeText(inputText);
  126. if (response && response.summary) {
  127. setOutputText(response.summary);
  128. } else {
  129. throw new Error('未接收到有效的摘要结果');
  130. }
  131. } catch (error) {
  132. console.error('文本摘要失败:', error);
  133. setErrorMessage(`文本摘要失败: ${error.message}`);
  134. toast.error(`文本摘要失败: ${error.message}`);
  135. } finally {
  136. setLoading(false);
  137. }
  138. };
  139. // 直接调用工作流
  140. const handleRunWorkflow = async () => {
  141. if (!workflowId.trim()) {
  142. setErrorMessage('请输入工作流ID');
  143. return;
  144. }
  145. if (!inputText.trim()) {
  146. setErrorMessage('请输入参数');
  147. return;
  148. }
  149. setLoading(true);
  150. setErrorMessage('');
  151. try {
  152. // 解析JSON参数
  153. let parameters = {};
  154. try {
  155. parameters = JSON.parse(inputText);
  156. } catch (e) {
  157. // 如果不是有效的JSON,则作为text参数传递
  158. parameters = { text: inputText };
  159. }
  160. const response = await cozeService.runWorkflow(workflowId, parameters);
  161. // 在实际应用中,您可能需要根据工作流的不同输出格式进行不同的处理
  162. setOutputText(JSON.stringify(response, null, 2));
  163. } catch (error) {
  164. console.error('运行工作流失败:', error);
  165. setErrorMessage(`运行工作流失败: ${error.message}`);
  166. toast.error(`运行工作流失败: ${error.message}`);
  167. } finally {
  168. setLoading(false);
  169. }
  170. };
  171. // 处理音频文件选择
  172. const handleFileChange = (e) => {
  173. const file = e.target.files[0];
  174. if (file) {
  175. setAudioFile(file);
  176. // 创建本地URL以便预览
  177. if (audioUrl) {
  178. URL.revokeObjectURL(audioUrl);
  179. }
  180. const url = URL.createObjectURL(file);
  181. setAudioUrl(url);
  182. }
  183. };
  184. // 辅助函数:将Base64字符串转换为Blob对象
  185. const base64ToBlob = (base64, mimeType) => {
  186. const byteString = atob(base64);
  187. const ab = new ArrayBuffer(byteString.length);
  188. const ia = new Uint8Array(ab);
  189. for (let i = 0; i < byteString.length; i++) {
  190. ia[i] = byteString.charCodeAt(i);
  191. }
  192. return new Blob([ab], { type: mimeType });
  193. };
  194. // 渲染不同的功能面板
  195. const renderPanel = () => {
  196. switch (activeTab) {
  197. case 'tts':
  198. return (
  199. <>
  200. <Form.Group className="mb-3">
  201. <Form.Label>输入文本</Form.Label>
  202. <Form.Control
  203. as="textarea"
  204. rows={5}
  205. value={inputText}
  206. onChange={(e) => setInputText(e.target.value)}
  207. placeholder="请输入要转换为语音的文本..."
  208. />
  209. </Form.Group>
  210. <Button
  211. variant="primary"
  212. onClick={handleTextToSpeech}
  213. disabled={loading || !inputText.trim()}
  214. >
  215. {loading ? (
  216. <>
  217. <Spinner size="sm" animation="border" className="me-2" />
  218. 处理中...
  219. </>
  220. ) : '转换为语音'}
  221. </Button>
  222. {audioUrl && (
  223. <div className="mt-3">
  224. <audio ref={audioRef} src={audioUrl} controls className="w-100" />
  225. </div>
  226. )}
  227. </>
  228. );
  229. case 'stt':
  230. return (
  231. <>
  232. <Form.Group className="mb-3">
  233. <Form.Label>选择音频文件</Form.Label>
  234. <Form.Control
  235. type="file"
  236. accept="audio/*"
  237. onChange={handleFileChange}
  238. />
  239. <Form.Text className="text-muted">
  240. 支持常见的音频格式,如MP3、WAV等
  241. </Form.Text>
  242. </Form.Group>
  243. {audioUrl && (
  244. <div className="mb-3">
  245. <audio src={audioUrl} controls className="w-100" />
  246. </div>
  247. )}
  248. <Button
  249. variant="primary"
  250. onClick={handleSpeechToText}
  251. disabled={loading || !audioFile}
  252. >
  253. {loading ? (
  254. <>
  255. <Spinner size="sm" animation="border" className="me-2" />
  256. 识别中...
  257. </>
  258. ) : '识别语音'}
  259. </Button>
  260. {outputText && (
  261. <div className="mt-3">
  262. <Form.Group>
  263. <Form.Label>识别结果</Form.Label>
  264. <Form.Control
  265. as="textarea"
  266. rows={5}
  267. value={outputText}
  268. readOnly
  269. />
  270. </Form.Group>
  271. </div>
  272. )}
  273. </>
  274. );
  275. case 'translate':
  276. return (
  277. <>
  278. <Form.Group className="mb-3">
  279. <Form.Label>输入文本</Form.Label>
  280. <Form.Control
  281. as="textarea"
  282. rows={5}
  283. value={inputText}
  284. onChange={(e) => setInputText(e.target.value)}
  285. placeholder="请输入要翻译的文本..."
  286. />
  287. </Form.Group>
  288. <Form.Group className="mb-3">
  289. <Form.Label>目标语言</Form.Label>
  290. <Form.Select
  291. value={targetLanguage}
  292. onChange={(e) => setTargetLanguage(e.target.value)}
  293. >
  294. <option value="en">英语</option>
  295. <option value="zh">中文</option>
  296. <option value="ja">日语</option>
  297. <option value="ko">韩语</option>
  298. <option value="fr">法语</option>
  299. <option value="de">德语</option>
  300. <option value="es">西班牙语</option>
  301. <option value="ru">俄语</option>
  302. </Form.Select>
  303. </Form.Group>
  304. <Button
  305. variant="primary"
  306. onClick={handleTextTranslation}
  307. disabled={loading || !inputText.trim()}
  308. >
  309. {loading ? (
  310. <>
  311. <Spinner size="sm" animation="border" className="me-2" />
  312. 翻译中...
  313. </>
  314. ) : '翻译文本'}
  315. </Button>
  316. {outputText && (
  317. <div className="mt-3">
  318. <Form.Group>
  319. <Form.Label>翻译结果</Form.Label>
  320. <Form.Control
  321. as="textarea"
  322. rows={5}
  323. value={outputText}
  324. readOnly
  325. />
  326. </Form.Group>
  327. </div>
  328. )}
  329. </>
  330. );
  331. case 'summarize':
  332. return (
  333. <>
  334. <Form.Group className="mb-3">
  335. <Form.Label>输入文本</Form.Label>
  336. <Form.Control
  337. as="textarea"
  338. rows={5}
  339. value={inputText}
  340. onChange={(e) => setInputText(e.target.value)}
  341. placeholder="请输入要摘要的文本..."
  342. />
  343. </Form.Group>
  344. <Button
  345. variant="primary"
  346. onClick={handleTextSummarization}
  347. disabled={loading || !inputText.trim()}
  348. >
  349. {loading ? (
  350. <>
  351. <Spinner size="sm" animation="border" className="me-2" />
  352. 摘要中...
  353. </>
  354. ) : '生成摘要'}
  355. </Button>
  356. {outputText && (
  357. <div className="mt-3">
  358. <Form.Group>
  359. <Form.Label>摘要结果</Form.Label>
  360. <Form.Control
  361. as="textarea"
  362. rows={5}
  363. value={outputText}
  364. readOnly
  365. />
  366. </Form.Group>
  367. </div>
  368. )}
  369. </>
  370. );
  371. case 'custom':
  372. return (
  373. <>
  374. <Form.Group className="mb-3">
  375. <Form.Label>工作流ID</Form.Label>
  376. <Form.Control
  377. type="text"
  378. value={workflowId}
  379. onChange={(e) => setWorkflowId(e.target.value)}
  380. placeholder="请输入Coze工作流ID..."
  381. />
  382. </Form.Group>
  383. <Form.Group className="mb-3">
  384. <Form.Label>参数 (JSON格式或文本)</Form.Label>
  385. <Form.Control
  386. as="textarea"
  387. rows={5}
  388. value={inputText}
  389. onChange={(e) => setInputText(e.target.value)}
  390. placeholder='{"key": "value"} 或直接输入文本'
  391. />
  392. </Form.Group>
  393. <Button
  394. variant="primary"
  395. onClick={handleRunWorkflow}
  396. disabled={loading || !workflowId.trim() || !inputText.trim()}
  397. >
  398. {loading ? (
  399. <>
  400. <Spinner size="sm" animation="border" className="me-2" />
  401. 处理中...
  402. </>
  403. ) : '运行工作流'}
  404. </Button>
  405. {outputText && (
  406. <div className="mt-3">
  407. <Form.Group>
  408. <Form.Label>输出结果</Form.Label>
  409. <Form.Control
  410. as="textarea"
  411. rows={5}
  412. value={outputText}
  413. readOnly
  414. />
  415. </Form.Group>
  416. </div>
  417. )}
  418. </>
  419. );
  420. default:
  421. return null;
  422. }
  423. };
  424. return (
  425. <Container className="my-4">
  426. <h2 className="mb-4 text-center">Coze工作流演示</h2>
  427. {errorMessage && (
  428. <Alert variant="danger" className="mb-3" onClose={() => setErrorMessage('')} dismissible>
  429. {errorMessage}
  430. </Alert>
  431. )}
  432. <Card>
  433. <Card.Header>
  434. <Nav variant="tabs" className="flex-row">
  435. <Nav.Item>
  436. <Nav.Link
  437. active={activeTab === 'tts'}
  438. onClick={() => setActiveTab('tts')}
  439. >
  440. 文本转语音
  441. </Nav.Link>
  442. </Nav.Item>
  443. <Nav.Item>
  444. <Nav.Link
  445. active={activeTab === 'stt'}
  446. onClick={() => setActiveTab('stt')}
  447. >
  448. 语音识别
  449. </Nav.Link>
  450. </Nav.Item>
  451. <Nav.Item>
  452. <Nav.Link
  453. active={activeTab === 'translate'}
  454. onClick={() => setActiveTab('translate')}
  455. >
  456. 文本翻译
  457. </Nav.Link>
  458. </Nav.Item>
  459. <Nav.Item>
  460. <Nav.Link
  461. active={activeTab === 'summarize'}
  462. onClick={() => setActiveTab('summarize')}
  463. >
  464. 文本摘要
  465. </Nav.Link>
  466. </Nav.Item>
  467. <Nav.Item>
  468. <Nav.Link
  469. active={activeTab === 'custom'}
  470. onClick={() => setActiveTab('custom')}
  471. >
  472. 自定义工作流
  473. </Nav.Link>
  474. </Nav.Item>
  475. </Nav>
  476. </Card.Header>
  477. <Card.Body>
  478. {renderPanel()}
  479. </Card.Body>
  480. </Card>
  481. </Container>
  482. );
  483. };
  484. export default CozeWorkflowDemo;