yt-chat-components 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.idea/sonarlint/issuestore/7/0/7030d0b2f71b999ff89a343de08c414af32fc93a +0 -0
  2. package/.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d +0 -0
  3. package/.idea/sonarlint/issuestore/9/c/9cfff9a6d27bd6c255aa751213163c7901fb8ce7 +0 -0
  4. package/.idea/sonarlint/issuestore/index.pb +7 -0
  5. package/build/asset-manifest.json +3 -3
  6. package/build/index.html +1 -1
  7. package/build/static/css/main.8ee59d98.css +2 -0
  8. package/build/static/css/main.8ee59d98.css.map +1 -0
  9. package/build/static/js/main.371ede49.js.map +1 -1
  10. package/package.json +80 -79
  11. package/public/index.html +108 -108
  12. package/src/YtChatView/chatWidget/chatWindow/callInterface/index.module.css +50 -50
  13. package/src/YtChatView/chatWidget/chatWindow/callInterface/index.tsx +549 -549
  14. package/src/YtChatView/chatWidget/chatWindow/callInterface/style.ts +44 -44
  15. package/src/YtChatView/chatWidget/chatWindow/chatMessage/index.tsx +501 -501
  16. package/src/YtChatView/chatWidget/chatWindow/chatPlaceholder/index.tsx +23 -23
  17. package/src/YtChatView/chatWidget/chatWindow/controllers/index.ts +249 -249
  18. package/src/YtChatView/chatWidget/chatWindow/index.module.css +196 -196
  19. package/src/YtChatView/chatWidget/chatWindow/index.tsx +1182 -1186
  20. package/src/YtChatView/chatWidget/chatWindow/types/chatWidget/index.ts +50 -50
  21. package/src/YtChatView/chatWidget/index.tsx +2598 -2596
  22. package/src/YtChatView/logoBtn/index.css +3 -3
  23. package/src/YtChatView/logoBtn/index.jsx +103 -103
  24. package/src/YtChatView/logoSplitBtn/index.css +3 -3
  25. package/src/YtChatView/logoSplitBtn/index.jsx +105 -105
  26. package/src/YtChatView/mobileChat/index.jsx +945 -945
  27. package/src/YtChatView/mobileChat/index.module.css +253 -253
  28. package/src/YtChatView/previewDialog/index.jsx +601 -601
  29. package/src/YtChatView/previewDialog/index.module.css +253 -253
  30. package/src/chatWidget/chatWindow/index.tsx +426 -426
  31. package/src/chatWidget/index.tsx +2195 -2195
  32. package/src/index.tsx +10 -10
  33. package/webpack.config.js +50 -50
  34. package/build/static/css/main.e41b943a.css +0 -2
  35. package/build/static/css/main.e41b943a.css.map +0 -1
@@ -1,501 +1,501 @@
1
- // @ts-nocheck
2
- import Markdown from 'react-markdown';
3
- import EChartsReact from 'echarts-for-react';
4
- import {ChatMessageType, InputValueType, MessageType} from '../types/chatWidget';
5
- import remarkGfm from 'remark-gfm';
6
- import rehypeMathjax from 'rehype-mathjax';
7
- // import './index.module.css';
8
- import upFilePng from '../../../../assets/aicenter/upfile.png';
9
- import {Form, Image, Input, message as messageTip, Select, DatePicker, Button, Spin, Skeleton} from 'antd';
10
- import React, {useState, useRef} from 'react';
11
- import typePdfPng from '../../../../assets/aicenter/type-pdf.png';
12
- import typeWordPng from '../../../../assets/aicenter/type-word.png';
13
- import typeExcelPng from '../../../../assets/aicenter/type-excel.png';
14
- import typeMarkdownPng from '../../../../assets/aicenter/type-markdown.png';
15
- import typeTextPng from '../../../../assets/aicenter/type-text.png';
16
- import typeMobiPng from '../../../../assets/aicenter/type-mobi.png';
17
- import typeRPubPng from '../../../../assets/aicenter/type-rpub.png';
18
- import playPng from '../../../../assets/aicenter/play.png';
19
- import playRunGif from '../../../../assets/aicenter/play-run.gif';
20
- import copyPng from '../../../../assets/aicenter/copy.png';
21
-
22
- let speechSynth = window.speechSynthesis;
23
- let utterance = null;
24
-
25
- export default function ChatMessage({
26
- type = MessageType.text,
27
- rawInfo,
28
- message,
29
- isSend,
30
- error,
31
- host_url,
32
- user_message_style,
33
- bot_message_style,
34
- error_message_style,
35
- handleSendMessage
36
- }: ChatMessageType) {
37
- const parseFileName = (
38
- text: string,
39
- ): { fileName: string | null; fileType: string | null; isImg: boolean } => {
40
- // 检查输入是否有效
41
- if (!text || typeof text !== 'string') {
42
- return { fileName: null, fileType: null };
43
- }
44
-
45
- // 找到最后一个反斜杠的位置
46
- const lastBackslashIndex = text.lastIndexOf('\\');
47
- if (lastBackslashIndex === -1) {
48
- return { fileName: null, fileType: null }; // 如果没有找到反斜杠,返回空结果
49
- }
50
-
51
- // 截取反斜杠后面的部分
52
- const fileNameWithExtension = text.substring(lastBackslashIndex + 1);
53
-
54
- // 分割文件名和文件类型
55
- const parts = fileNameWithExtension.split('.');
56
- const fileName = parts.slice(0, -1).join('.'); // 文件名(不含扩展名)
57
- const fileType = parts.length > 1 ? parts.pop() : null; // 文件类型
58
-
59
- const isImg = fileType && ['jpg', 'jpeg', 'png', 'gif'].includes(fileType.toLowerCase());
60
- // 返回结果对象
61
- return {
62
- fileName: fileName,
63
- fileType: fileType,
64
- isImg,
65
- };
66
- };
67
-
68
- const [isPlay, setIsPlay] = useState(false) // 是否正在播放文字
69
- const [isSubmittingForm, setIsSubmittingForm] = useState(false) // 正在提交form
70
- const [isShowFormBtns, setIsShowFormBtns] = useState(true) // 正在提交form
71
- const formRef = useRef(null);
72
-
73
- /**
74
- * 根据文件URL获取文件类型
75
- * @param url
76
- */
77
- const getFileTypeByUrl = (url) => {
78
- if (!url) {
79
- return 'file';
80
- }
81
- switch (url.split('.').pop().toLowerCase()) {
82
- case 'jpg':
83
- case 'jpeg':
84
- case 'png':
85
- case 'gif':
86
- return 'image';
87
- case 'pdf':
88
- return 'pdf';
89
- case 'doc':
90
- case 'docx':
91
- return 'word';
92
- case 'xls':
93
- case 'xlsx':
94
- return 'excel';
95
- case 'md':
96
- return 'markdown';
97
- case 'txt':
98
- return 'txt';
99
- case 'mobi':
100
- return 'mobi';
101
- case 'rpub':
102
- return 'rpub';
103
- default:
104
- return 'file';
105
- }
106
- };
107
-
108
- /**
109
- * 播放文字
110
- * @param text 文字
111
- */
112
- const playVoice = (text)=>{
113
- if (isPlay) {
114
- speechSynth.cancel();
115
- setIsPlay(false)
116
- }else{
117
- if (text) {
118
- utterance = new SpeechSynthesisUtterance(text);
119
- const voices = speechSynth.getVoices();
120
- utterance.voice = voices[61];
121
- utterance.rate = 1;
122
- utterance.pitch = 1;
123
- speechSynth.speak(utterance);
124
- setIsPlay(true)
125
- }
126
- }
127
- }
128
-
129
- /**
130
- * 复制文字
131
- * @param text 文字
132
- */
133
- const copyText = async (text) => {
134
- if(text){
135
- await navigator.clipboard.writeText(text);
136
- messageTip.info('复制成功')
137
- }else{
138
- messageTip.error("消息框没有文字")
139
- }
140
- }
141
-
142
- const renderUserMessage = () => {
143
- return (
144
- <div style={user_message_style} className="msg_userMessageBox">
145
- <div style={user_message_style} className="cl-user_message">
146
- {message.message}
147
- </div>
148
- <div className="msg_messageImgBox">
149
- {message.rawInfo?.files.map((item, index) => {
150
- return (
151
- <div key={item} className="msg_fileBox">
152
- {parseFileName(item).isImg && (
153
- <Image
154
- height={40}
155
- width={40}
156
- className="msg_messageImg"
157
- src={`${host_url}/api/v1/files/images/${item}`}
158
- alt={item}
159
- preview={{
160
- mask: <span className="custom-mask"></span>,
161
- }}
162
- />
163
- )}
164
- {getFileTypeByUrl(item) == 'pdf' && (
165
- <img style={{ width: 40, height: 40 }} src={typePdfPng} />
166
- )}
167
- {getFileTypeByUrl(item) == 'excel' && (
168
- <img style={{ width: 40, height: 40 }} src={typeExcelPng} />
169
- )}
170
- {getFileTypeByUrl(item) == 'markdown' && (
171
- <img style={{ width: 40, height: 40 }} src={typeMarkdownPng} />
172
- )}
173
- {getFileTypeByUrl(item) == 'txt' && (
174
- <img style={{ width: 40, height: 40 }} src={typeTextPng} />
175
- )}
176
- {getFileTypeByUrl(item) == 'word' && (
177
- <img style={{ width: 40, height: 40 }} src={typeWordPng} />
178
- )}
179
- {getFileTypeByUrl(item) == 'mobi' && (
180
- <img style={{ width: 40, height: 40 }} src={typeMobiPng} />
181
- )}
182
- {getFileTypeByUrl(item) == 'rpub' && (
183
- <img style={{ width: 40, height: 40 }} src={typeRPubPng} />
184
- )}
185
- {getFileTypeByUrl(item) == 'file' && (
186
- <img style={{ width: 40, height: 40 }} src={upFilePng} />
187
- )}
188
- <div className="msg_fileInfoBox">
189
- <div className="msg_fileInfoFileName">{parseFileName(item).fileName}</div>
190
- </div>
191
- </div>
192
- );
193
- })}
194
- </div>
195
- </div>
196
- )
197
- }
198
-
199
- const renderErrorMessage = () => {
200
- return (
201
- <div style={error_message_style} className={'cl-error_message'}>
202
- {message.message}
203
- </div>
204
- )
205
- }
206
- const renderLoading = (type:MessageType, height:number) => {
207
- const skeletonContainerId = `skeleton-container-${Math.random().toString(36).substring(2, 9)}`;
208
-
209
- return (
210
- <div
211
- id={skeletonContainerId}
212
- style={{minWidth: 500, width: '100%', height, display: 'flex', justifyContent: 'center', alignItems: 'center'}}
213
- >
214
- {
215
- // 其他类型暂不处理
216
- type == MessageType.echart && (
217
- <Skeleton.Image active={true} style={{width: '100%', height: '100%'}}/>
218
- )
219
- }
220
- <style jsx>{`
221
- #${skeletonContainerId} .ant-skeleton.ant-skeleton-element {
222
- width: 100% !important;
223
- height: 100% !important;
224
- }
225
- `}</style>
226
- </div>
227
- )
228
- }
229
-
230
- // 自定义渲染器
231
- const EchartRender = React.useMemo(() => ({
232
- code({ node, inline, className, children, ...props }) {
233
- // 检查是否是代码块,并且语言为 'echart'
234
- if (className && className.includes('language-echart')) {
235
- try {
236
- // 解析代码块内容为 JSON 配置
237
- // console.log(" --- 1", children.toString().trim())
238
- const chartOptions = JSON.parse(children.toString().trim());
239
- // console.log(" --- 2", chartOptions)
240
-
241
- // 渲染 ECharts 组件
242
- return <EChartsReact option={chartOptions} style={{minWidth: 500, width: '100%', height: 300, backgroundColor: '#fff' }} />;
243
- } catch (error) {
244
- return renderLoading(MessageType.echart, 300);
245
- }
246
- }
247
- // 默认渲染其他代码块
248
- return <code className={className} {...props}>{children}</code>;
249
- },
250
- }), []); // 空依赖数组,只创建一次
251
-
252
- const renderBotTextMessage = () => {
253
- return (
254
- <div>
255
- <div style={bot_message_style} className={'cl-bot_message'}>
256
- <Markdown
257
- className={'markdown-body prose flex flex-col word-break-break-word'}
258
- remarkPlugins={[remarkGfm]}
259
- rehypePlugins={[rehypeMathjax]}
260
- components={EchartRender}
261
- >
262
- {message.message}
263
- </Markdown>
264
- </div>
265
- <div className="msg_operateBox">
266
- <img src={isPlay?playRunGif:playPng} onClick={()=>playVoice(message.message)} />
267
- <img src={copyPng} onClick={()=>copyText(message.message)} />
268
- </div>
269
- </div>
270
- )
271
- }
272
-
273
- const renderBotFormMessage = () => {
274
- const { form_config } = rawInfo;
275
-
276
- // 处理表单提交
277
- const handleSubmit = (values) => {
278
- console.log('表单提交数据:', values);
279
- if (handleSendMessage) {
280
- setIsSubmittingForm(true);
281
- // TODO 提交的数据需要多一些
282
- handleSendMessage(JSON.stringify(values), () => {
283
- messageTip.success('表单提交成功');
284
- setIsSubmittingForm(false);
285
- setIsShowFormBtns(false);
286
- }, InputValueType.form);
287
- }
288
- };
289
-
290
- // 处理表单取消
291
- const handleCancel = () => {
292
- if (formRef.current) {
293
- formRef.current.resetFields();
294
- }
295
- if (handleSendMessage) {
296
- setIsSubmittingForm(true);
297
- handleSendMessage('USER CANCELED', () => {
298
- messageTip.success('取消成功');
299
- setIsSubmittingForm(false);
300
- setIsShowFormBtns(false);
301
- }, InputValueType.form);
302
- }
303
- };
304
-
305
- return (
306
- <div className="form-container">
307
- <div className="form-title">{form_config?.name || '表单'}</div>
308
- <Form
309
- ref={formRef}
310
- layout="vertical"
311
- onFinish={handleSubmit}
312
- className="dynamic-form"
313
- >
314
- {form_config?.config?.map((item, index) => {
315
- const { key, name, type, options } = item;
316
- // 根据不同的表单项类型渲染不同的组件
317
- switch (type) {
318
- case 'text':
319
- return (
320
- <Form.Item
321
- key={index}
322
- label={name}
323
- name={key}
324
- rules={[{ required: true, message: `请输入${name}` }]}
325
- >
326
- <Input placeholder={`请输入${name}`} />
327
- </Form.Item>
328
- );
329
- case 'date':
330
- return (
331
- <Form.Item
332
- key={index}
333
- label={name}
334
- name={key}
335
- rules={[{ required: true, message: `请选择${name}` }]}
336
- >
337
- <DatePicker style={{ width: '100%' }} placeholder={`请选择${name}`} />
338
- </Form.Item>
339
- );
340
- case 'select':
341
- return (
342
- <Form.Item
343
- key={index}
344
- label={name}
345
- name={key}
346
- rules={[{ required: true, message: `请选择${name}` }]}
347
- >
348
- <Select
349
- placeholder={`请选择${name}`}
350
- style={{ width: '100%' }}
351
- >
352
- {options?.map((option, optIndex) => (
353
- <Select.Option key={optIndex} value={option}>
354
- {option}
355
- </Select.Option>
356
- ))}
357
- </Select>
358
- </Form.Item>
359
- );
360
- default:
361
- return (
362
- <Form.Item
363
- key={index}
364
- label={name}
365
- name={name}
366
- >
367
- <Input placeholder={`请输入${name}`} />
368
- </Form.Item>
369
- );
370
- }
371
- })}
372
-
373
- {
374
- isShowFormBtns && (
375
- <div className="form-buttons">
376
- <Form.Item>
377
- <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
378
- <Button
379
- loading={isSubmittingForm}
380
- onClick={handleCancel}
381
- >
382
- 取消
383
- </Button>
384
- <Button
385
- loading={isSubmittingForm}
386
- type="primary"
387
- htmlType="submit"
388
- style={{ backgroundColor: '#8064f6' }}
389
- >
390
- 提交
391
- </Button>
392
- </div>
393
- </Form.Item>
394
- </div>
395
- )
396
- }
397
- </Form>
398
-
399
- <style jsx>{`
400
- .form-container {
401
- width: 100%;
402
- padding: 16px;
403
- border-radius: 8px;
404
- background-color:rgb(244, 244, 244);
405
- }
406
-
407
- .form-title {
408
- font-size: 18px;
409
- font-weight: bold;
410
- margin-bottom: 16px;
411
- color: #333;
412
- }
413
-
414
- .dynamic-form {
415
- width: 100%;
416
- }
417
-
418
- .form-select {
419
- width: 100%;
420
- height: 32px;
421
- border: 1px solid #d9d9d9;
422
- border-radius: 4px;
423
- padding: 4px 11px;
424
- }
425
-
426
- .form-buttons {
427
- display: flex;
428
- justify-content: flex-end;
429
- margin-top: 20px;
430
- gap: 12px;
431
- }
432
-
433
- .cancel-button {
434
- padding: 6px 16px;
435
- background-color: #f0f0f0;
436
- border: 1px solid #d9d9d9;
437
- border-radius: 4px;
438
- cursor: pointer;
439
- }
440
-
441
- .submit-button {
442
- padding: 6px 16px;
443
- background-color: #8064f6;
444
- color: white;
445
- border: none;
446
- border-radius: 4px;
447
- cursor: pointer;
448
- }
449
-
450
- /* 移动端适配 */
451
- @media (max-width: 768px) {
452
- .form-container {
453
- padding: 12px;
454
- }
455
-
456
- .form-buttons {
457
- flex-direction: column;
458
- gap: 8px;
459
- }
460
-
461
- .cancel-button,
462
- .submit-button {
463
- width: 100%;
464
- padding: 8px 0;
465
- }
466
- }
467
- `}</style>
468
- </div>
469
- );
470
- };
471
-
472
- const renderBotMessage = () => {
473
- switch (type) {
474
- case MessageType.form:
475
- return renderBotFormMessage()
476
- case MessageType.text:
477
- default:
478
- return renderBotTextMessage()
479
- }
480
- }
481
-
482
- const render = () => {
483
- if (isSend){
484
- return renderUserMessage()
485
- }else {
486
- if (error){
487
- return renderErrorMessage()
488
- }else{
489
- return renderBotMessage()
490
- }
491
- }
492
- }
493
-
494
- return (
495
- <div className={'cl-chat-message ' + (isSend ? ' cl-justify-end' : ' cl-justify-start')}>
496
- {
497
- render()
498
- }
499
- </div>
500
- );
501
- }
1
+ // @ts-nocheck
2
+ import Markdown from 'react-markdown';
3
+ import EChartsReact from 'echarts-for-react';
4
+ import {ChatMessageType, InputValueType, MessageType} from '../types/chatWidget';
5
+ import remarkGfm from 'remark-gfm';
6
+ import rehypeMathjax from 'rehype-mathjax';
7
+ // import './index.module.css';
8
+ import upFilePng from '../../../../assets/aicenter/upfile.png';
9
+ import {Form, Image, Input, message as messageTip, Select, DatePicker, Button, Spin, Skeleton} from 'antd';
10
+ import React, {useState, useRef} from 'react';
11
+ import typePdfPng from '../../../../assets/aicenter/type-pdf.png';
12
+ import typeWordPng from '../../../../assets/aicenter/type-word.png';
13
+ import typeExcelPng from '../../../../assets/aicenter/type-excel.png';
14
+ import typeMarkdownPng from '../../../../assets/aicenter/type-markdown.png';
15
+ import typeTextPng from '../../../../assets/aicenter/type-text.png';
16
+ import typeMobiPng from '../../../../assets/aicenter/type-mobi.png';
17
+ import typeRPubPng from '../../../../assets/aicenter/type-rpub.png';
18
+ import playPng from '../../../../assets/aicenter/play.png';
19
+ import playRunGif from '../../../../assets/aicenter/play-run.gif';
20
+ import copyPng from '../../../../assets/aicenter/copy.png';
21
+
22
+ let speechSynth = window.speechSynthesis;
23
+ let utterance = null;
24
+
25
+ export default function ChatMessage({
26
+ type = MessageType.text,
27
+ rawInfo,
28
+ message,
29
+ isSend,
30
+ error,
31
+ host_url,
32
+ user_message_style,
33
+ bot_message_style,
34
+ error_message_style,
35
+ handleSendMessage
36
+ }: ChatMessageType) {
37
+ const parseFileName = (
38
+ text: string,
39
+ ): { fileName: string | null; fileType: string | null; isImg: boolean } => {
40
+ // 检查输入是否有效
41
+ if (!text || typeof text !== 'string') {
42
+ return { fileName: null, fileType: null };
43
+ }
44
+
45
+ // 找到最后一个反斜杠的位置
46
+ const lastBackslashIndex = text.lastIndexOf('\\');
47
+ if (lastBackslashIndex === -1) {
48
+ return { fileName: null, fileType: null }; // 如果没有找到反斜杠,返回空结果
49
+ }
50
+
51
+ // 截取反斜杠后面的部分
52
+ const fileNameWithExtension = text.substring(lastBackslashIndex + 1);
53
+
54
+ // 分割文件名和文件类型
55
+ const parts = fileNameWithExtension.split('.');
56
+ const fileName = parts.slice(0, -1).join('.'); // 文件名(不含扩展名)
57
+ const fileType = parts.length > 1 ? parts.pop() : null; // 文件类型
58
+
59
+ const isImg = fileType && ['jpg', 'jpeg', 'png', 'gif'].includes(fileType.toLowerCase());
60
+ // 返回结果对象
61
+ return {
62
+ fileName: fileName,
63
+ fileType: fileType,
64
+ isImg,
65
+ };
66
+ };
67
+
68
+ const [isPlay, setIsPlay] = useState(false) // 是否正在播放文字
69
+ const [isSubmittingForm, setIsSubmittingForm] = useState(false) // 正在提交form
70
+ const [isShowFormBtns, setIsShowFormBtns] = useState(true) // 正在提交form
71
+ const formRef = useRef(null);
72
+
73
+ /**
74
+ * 根据文件URL获取文件类型
75
+ * @param url
76
+ */
77
+ const getFileTypeByUrl = (url) => {
78
+ if (!url) {
79
+ return 'file';
80
+ }
81
+ switch (url.split('.').pop().toLowerCase()) {
82
+ case 'jpg':
83
+ case 'jpeg':
84
+ case 'png':
85
+ case 'gif':
86
+ return 'image';
87
+ case 'pdf':
88
+ return 'pdf';
89
+ case 'doc':
90
+ case 'docx':
91
+ return 'word';
92
+ case 'xls':
93
+ case 'xlsx':
94
+ return 'excel';
95
+ case 'md':
96
+ return 'markdown';
97
+ case 'txt':
98
+ return 'txt';
99
+ case 'mobi':
100
+ return 'mobi';
101
+ case 'rpub':
102
+ return 'rpub';
103
+ default:
104
+ return 'file';
105
+ }
106
+ };
107
+
108
+ /**
109
+ * 播放文字
110
+ * @param text 文字
111
+ */
112
+ const playVoice = (text)=>{
113
+ if (isPlay) {
114
+ speechSynth.cancel();
115
+ setIsPlay(false)
116
+ }else{
117
+ if (text) {
118
+ utterance = new SpeechSynthesisUtterance(text);
119
+ const voices = speechSynth.getVoices();
120
+ utterance.voice = voices[61];
121
+ utterance.rate = 1;
122
+ utterance.pitch = 1;
123
+ speechSynth.speak(utterance);
124
+ setIsPlay(true)
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 复制文字
131
+ * @param text 文字
132
+ */
133
+ const copyText = async (text) => {
134
+ if(text){
135
+ await navigator.clipboard.writeText(text);
136
+ messageTip.info('复制成功')
137
+ }else{
138
+ messageTip.error("消息框没有文字")
139
+ }
140
+ }
141
+
142
+ const renderUserMessage = () => {
143
+ return (
144
+ <div style={user_message_style} className="msg_userMessageBox">
145
+ <div style={user_message_style} className="cl-user_message">
146
+ {message.message}
147
+ </div>
148
+ <div className="msg_messageImgBox">
149
+ {message.rawInfo?.files.map((item, index) => {
150
+ return (
151
+ <div key={item} className="msg_fileBox">
152
+ {parseFileName(item).isImg && (
153
+ <Image
154
+ height={40}
155
+ width={40}
156
+ className="msg_messageImg"
157
+ src={`${host_url}/api/v1/files/images/${item}`}
158
+ alt={item}
159
+ preview={{
160
+ mask: <span className="custom-mask"></span>,
161
+ }}
162
+ />
163
+ )}
164
+ {getFileTypeByUrl(item) == 'pdf' && (
165
+ <img style={{ width: 40, height: 40 }} src={typePdfPng} />
166
+ )}
167
+ {getFileTypeByUrl(item) == 'excel' && (
168
+ <img style={{ width: 40, height: 40 }} src={typeExcelPng} />
169
+ )}
170
+ {getFileTypeByUrl(item) == 'markdown' && (
171
+ <img style={{ width: 40, height: 40 }} src={typeMarkdownPng} />
172
+ )}
173
+ {getFileTypeByUrl(item) == 'txt' && (
174
+ <img style={{ width: 40, height: 40 }} src={typeTextPng} />
175
+ )}
176
+ {getFileTypeByUrl(item) == 'word' && (
177
+ <img style={{ width: 40, height: 40 }} src={typeWordPng} />
178
+ )}
179
+ {getFileTypeByUrl(item) == 'mobi' && (
180
+ <img style={{ width: 40, height: 40 }} src={typeMobiPng} />
181
+ )}
182
+ {getFileTypeByUrl(item) == 'rpub' && (
183
+ <img style={{ width: 40, height: 40 }} src={typeRPubPng} />
184
+ )}
185
+ {getFileTypeByUrl(item) == 'file' && (
186
+ <img style={{ width: 40, height: 40 }} src={upFilePng} />
187
+ )}
188
+ <div className="msg_fileInfoBox">
189
+ <div className="msg_fileInfoFileName">{parseFileName(item).fileName}</div>
190
+ </div>
191
+ </div>
192
+ );
193
+ })}
194
+ </div>
195
+ </div>
196
+ )
197
+ }
198
+
199
+ const renderErrorMessage = () => {
200
+ return (
201
+ <div style={error_message_style} className={'cl-error_message'}>
202
+ {message.message}
203
+ </div>
204
+ )
205
+ }
206
+ const renderLoading = (type:MessageType, height:number) => {
207
+ const skeletonContainerId = `skeleton-container-${Math.random().toString(36).substring(2, 9)}`;
208
+
209
+ return (
210
+ <div
211
+ id={skeletonContainerId}
212
+ style={{minWidth: 500, width: '100%', height, display: 'flex', justifyContent: 'center', alignItems: 'center'}}
213
+ >
214
+ {
215
+ // 其他类型暂不处理
216
+ type == MessageType.echart && (
217
+ <Skeleton.Image active={true} style={{width: '100%', height: '100%'}}/>
218
+ )
219
+ }
220
+ <style jsx>{`
221
+ #${skeletonContainerId} .ant-skeleton.ant-skeleton-element {
222
+ width: 100% !important;
223
+ height: 100% !important;
224
+ }
225
+ `}</style>
226
+ </div>
227
+ )
228
+ }
229
+
230
+ // 自定义渲染器
231
+ const EchartRender = React.useMemo(() => ({
232
+ code({ node, inline, className, children, ...props }) {
233
+ // 检查是否是代码块,并且语言为 'echart'
234
+ if (className && className.includes('language-echart')) {
235
+ try {
236
+ // 解析代码块内容为 JSON 配置
237
+ // console.log(" --- 1", children.toString().trim())
238
+ const chartOptions = JSON.parse(children.toString().trim());
239
+ // console.log(" --- 2", chartOptions)
240
+
241
+ // 渲染 ECharts 组件
242
+ return <EChartsReact option={chartOptions} style={{minWidth: 500, width: '100%', height: 300, backgroundColor: '#fff' }} />;
243
+ } catch (error) {
244
+ return renderLoading(MessageType.echart, 300);
245
+ }
246
+ }
247
+ // 默认渲染其他代码块
248
+ return <code className={className} {...props}>{children}</code>;
249
+ },
250
+ }), []); // 空依赖数组,只创建一次
251
+
252
+ const renderBotTextMessage = () => {
253
+ return (
254
+ <div>
255
+ <div style={bot_message_style} className={'cl-bot_message'}>
256
+ <Markdown
257
+ className={'markdown-body prose flex flex-col word-break-break-word'}
258
+ remarkPlugins={[remarkGfm]}
259
+ rehypePlugins={[rehypeMathjax]}
260
+ components={EchartRender}
261
+ >
262
+ {message.message}
263
+ </Markdown>
264
+ </div>
265
+ <div className="msg_operateBox">
266
+ <img src={isPlay?playRunGif:playPng} onClick={()=>playVoice(message.message)} />
267
+ <img src={copyPng} onClick={()=>copyText(message.message)} />
268
+ </div>
269
+ </div>
270
+ )
271
+ }
272
+
273
+ const renderBotFormMessage = () => {
274
+ const { form_config } = rawInfo;
275
+
276
+ // 处理表单提交
277
+ const handleSubmit = (values) => {
278
+ console.log('表单提交数据:', values);
279
+ if (handleSendMessage) {
280
+ setIsSubmittingForm(true);
281
+ // TODO 提交的数据需要多一些
282
+ handleSendMessage(JSON.stringify(values), () => {
283
+ messageTip.success('表单提交成功');
284
+ setIsSubmittingForm(false);
285
+ setIsShowFormBtns(false);
286
+ }, InputValueType.form);
287
+ }
288
+ };
289
+
290
+ // 处理表单取消
291
+ const handleCancel = () => {
292
+ if (formRef.current) {
293
+ formRef.current.resetFields();
294
+ }
295
+ if (handleSendMessage) {
296
+ setIsSubmittingForm(true);
297
+ handleSendMessage('USER CANCELED', () => {
298
+ messageTip.success('取消成功');
299
+ setIsSubmittingForm(false);
300
+ setIsShowFormBtns(false);
301
+ }, InputValueType.form);
302
+ }
303
+ };
304
+
305
+ return (
306
+ <div className="form-container">
307
+ <div className="form-title">{form_config?.name || '表单'}</div>
308
+ <Form
309
+ ref={formRef}
310
+ layout="vertical"
311
+ onFinish={handleSubmit}
312
+ className="dynamic-form"
313
+ >
314
+ {form_config?.config?.map((item, index) => {
315
+ const { key, name, type, options } = item;
316
+ // 根据不同的表单项类型渲染不同的组件
317
+ switch (type) {
318
+ case 'text':
319
+ return (
320
+ <Form.Item
321
+ key={index}
322
+ label={name}
323
+ name={key}
324
+ rules={[{ required: true, message: `请输入${name}` }]}
325
+ >
326
+ <Input placeholder={`请输入${name}`} />
327
+ </Form.Item>
328
+ );
329
+ case 'date':
330
+ return (
331
+ <Form.Item
332
+ key={index}
333
+ label={name}
334
+ name={key}
335
+ rules={[{ required: true, message: `请选择${name}` }]}
336
+ >
337
+ <DatePicker style={{ width: '100%' }} placeholder={`请选择${name}`} />
338
+ </Form.Item>
339
+ );
340
+ case 'select':
341
+ return (
342
+ <Form.Item
343
+ key={index}
344
+ label={name}
345
+ name={key}
346
+ rules={[{ required: true, message: `请选择${name}` }]}
347
+ >
348
+ <Select
349
+ placeholder={`请选择${name}`}
350
+ style={{ width: '100%' }}
351
+ >
352
+ {options?.map((option, optIndex) => (
353
+ <Select.Option key={optIndex} value={option}>
354
+ {option}
355
+ </Select.Option>
356
+ ))}
357
+ </Select>
358
+ </Form.Item>
359
+ );
360
+ default:
361
+ return (
362
+ <Form.Item
363
+ key={index}
364
+ label={name}
365
+ name={name}
366
+ >
367
+ <Input placeholder={`请输入${name}`} />
368
+ </Form.Item>
369
+ );
370
+ }
371
+ })}
372
+
373
+ {
374
+ isShowFormBtns && (
375
+ <div className="form-buttons">
376
+ <Form.Item>
377
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
378
+ <Button
379
+ loading={isSubmittingForm}
380
+ onClick={handleCancel}
381
+ >
382
+ 取消
383
+ </Button>
384
+ <Button
385
+ loading={isSubmittingForm}
386
+ type="primary"
387
+ htmlType="submit"
388
+ style={{ backgroundColor: '#8064f6' }}
389
+ >
390
+ 提交
391
+ </Button>
392
+ </div>
393
+ </Form.Item>
394
+ </div>
395
+ )
396
+ }
397
+ </Form>
398
+
399
+ <style jsx>{`
400
+ .form-container {
401
+ width: 100%;
402
+ padding: 16px;
403
+ border-radius: 8px;
404
+ background-color:rgb(244, 244, 244);
405
+ }
406
+
407
+ .form-title {
408
+ font-size: 18px;
409
+ font-weight: bold;
410
+ margin-bottom: 16px;
411
+ color: #333;
412
+ }
413
+
414
+ .dynamic-form {
415
+ width: 100%;
416
+ }
417
+
418
+ .form-select {
419
+ width: 100%;
420
+ height: 32px;
421
+ border: 1px solid #d9d9d9;
422
+ border-radius: 4px;
423
+ padding: 4px 11px;
424
+ }
425
+
426
+ .form-buttons {
427
+ display: flex;
428
+ justify-content: flex-end;
429
+ margin-top: 20px;
430
+ gap: 12px;
431
+ }
432
+
433
+ .cancel-button {
434
+ padding: 6px 16px;
435
+ background-color: #f0f0f0;
436
+ border: 1px solid #d9d9d9;
437
+ border-radius: 4px;
438
+ cursor: pointer;
439
+ }
440
+
441
+ .submit-button {
442
+ padding: 6px 16px;
443
+ background-color: #8064f6;
444
+ color: white;
445
+ border: none;
446
+ border-radius: 4px;
447
+ cursor: pointer;
448
+ }
449
+
450
+ /* 移动端适配 */
451
+ @media (max-width: 768px) {
452
+ .form-container {
453
+ padding: 12px;
454
+ }
455
+
456
+ .form-buttons {
457
+ flex-direction: column;
458
+ gap: 8px;
459
+ }
460
+
461
+ .cancel-button,
462
+ .submit-button {
463
+ width: 100%;
464
+ padding: 8px 0;
465
+ }
466
+ }
467
+ `}</style>
468
+ </div>
469
+ );
470
+ };
471
+
472
+ const renderBotMessage = () => {
473
+ switch (type) {
474
+ case MessageType.form:
475
+ return renderBotFormMessage()
476
+ case MessageType.text:
477
+ default:
478
+ return renderBotTextMessage()
479
+ }
480
+ }
481
+
482
+ const render = () => {
483
+ if (isSend){
484
+ return renderUserMessage()
485
+ }else {
486
+ if (error){
487
+ return renderErrorMessage()
488
+ }else{
489
+ return renderBotMessage()
490
+ }
491
+ }
492
+ }
493
+
494
+ return (
495
+ <div className={'cl-chat-message ' + (isSend ? ' cl-justify-end' : ' cl-justify-start')}>
496
+ {
497
+ render()
498
+ }
499
+ </div>
500
+ );
501
+ }