yt-chat-components 1.0.0 → 1.0.2

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 (42) hide show
  1. package/.idea/modules.xml +1 -1
  2. package/.idea/sonarlint/issuestore/index.pb +3 -31
  3. package/dist/build/static/js/bundle.min.js +1 -0
  4. package/package.json +76 -76
  5. package/public/index.html +108 -108
  6. package/src/YtChatView/chatWidget/chatWindow/chatMessage/index.tsx +464 -464
  7. package/src/YtChatView/chatWidget/chatWindow/controllers/index.ts +249 -249
  8. package/src/YtChatView/chatWidget/chatWindow/index.module.css +196 -196
  9. package/src/YtChatView/chatWidget/chatWindow/index.tsx +1054 -1041
  10. package/src/YtChatView/chatWidget/chatWindow/types/chatWidget/index.ts +50 -50
  11. package/src/YtChatView/chatWidget/index.tsx +2586 -2586
  12. package/src/YtChatView/logoBtn/index.css +3 -3
  13. package/src/YtChatView/logoBtn/index.jsx +103 -103
  14. package/src/YtChatView/logoSplitBtn/index.css +3 -3
  15. package/src/YtChatView/logoSplitBtn/index.jsx +105 -105
  16. package/src/YtChatView/mobileChat/index.jsx +944 -848
  17. package/src/YtChatView/mobileChat/index.module.css +253 -253
  18. package/src/YtChatView/previewDialog/index.jsx +600 -600
  19. package/src/YtChatView/previewDialog/index.module.css +253 -253
  20. package/src/chatWidget/chatWindow/index.tsx +426 -426
  21. package/src/chatWidget/index.tsx +2193 -2193
  22. package/src/index.tsx +10 -10
  23. package/webpack.config.js +50 -50
  24. package/.idea/sonarlint/issuestore/3/6/364385cedcce4c06de1901392ffeeac0caef0f3c +0 -0
  25. package/.idea/sonarlint/issuestore/3/9/39129446b425a1d640160c068e4194e96639eedf +0 -0
  26. package/.idea/sonarlint/issuestore/4/a/4a2f33951ce07c1ff7184f91877aa13db05d3785 +0 -0
  27. package/.idea/sonarlint/issuestore/4/a/4a7b99bdbee5792679d347b6474463bf5e14b66d +0 -0
  28. package/.idea/sonarlint/issuestore/4/b/4b015aa5428c4d4c3d672893ec23f5fe3969f9be +0 -0
  29. package/.idea/sonarlint/issuestore/4/b/4b6989b8ccae808ebc45d02230d336ea53800365 +0 -0
  30. package/.idea/sonarlint/issuestore/6/1/61ebb9fd6e8cf9082658121d5d81e297791dacd0 +0 -0
  31. package/.idea/sonarlint/issuestore/6/c/6c024c1d0ad64656b9d4b0695ec3c49c0454addf +0 -0
  32. package/.idea/sonarlint/issuestore/6/e/6e75fc1c07c3a427a86fc213ca9479caaaff00ea +0 -0
  33. package/.idea/sonarlint/issuestore/8/d/8d6123af13a140f93e06299fff7ea23c547e9ec8 +0 -0
  34. package/.idea/sonarlint/issuestore/c/c/cc2352788140b6778ac06df4b33f50b390d2d8be +0 -0
  35. package/.idea/sonarlint/issuestore/d/5/d5595158cc48f9bf3e51b06f6e6805a8fd2d6262 +0 -0
  36. package/.idea/sonarlint/issuestore/d/7/d747cbed4201192dfa83a1a51345b020a050b647 +0 -0
  37. package/.idea/sonarlint/issuestore/d/9/d938938695d447dadda115e28781c6541f53fc4f +0 -0
  38. package/build/static/js/bundle.min.js +0 -2
  39. package/build/static/js/bundle.min.js.LICENSE.txt +0 -132
  40. /package/.idea/{langflow-embedded-chat.iml → langflow-embedded-chat-clone.iml} +0 -0
  41. /package/.idea/sonarlint/issuestore/{0/f/0f8c0c92cf798431ebb931ff6e997b1af86ecee5 → 7/0/7030d0b2f71b999ff89a343de08c414af32fc93a} +0 -0
  42. /package/.idea/sonarlint/issuestore/{2/7/27e69cb561aeea20c1afbdd32d260dd60b89a81b → 9/c/9cfff9a6d27bd6c255aa751213163c7901fb8ce7} +0 -0
@@ -1,1041 +1,1054 @@
1
- // @ts-nocheck
2
- import { extractMessageFromOutput } from './utils';
3
- import React, { useEffect, useRef, useState } from 'react';
4
- import {ChatMessageType, embedAppExtend, InputValueType, MessageType} from './types/chatWidget';
5
- import ChatMessage from './chatMessage';
6
- import { fetchUploadFile, getChatHistory, sendMessage } from './controllers';
7
- import ChatMessagePlaceholder from './chatPlaceholder/index.tsx';
8
- // import './index.module.css'
9
- import closexPng from '../../../assets/aicenter/closex.png';
10
- import upFilePng from '../../../assets/aicenter/upfile.png';
11
- import fileuploadPng from '../../../assets/aicenter/fileupload.png';
12
- import sendmessagePng from '../../../assets/aicenter/sendmessage.png';
13
- import stopmessagePng from '../../../assets/aicenter/stopmessage.png';
14
- import toRightPng from '../../../assets/aicenter/toRight.png';
15
- import toLeftPng from '../../../assets/aicenter/toLeft.png';
16
- import typePdfPng from '../../../assets/aicenter/type-pdf.png';
17
- import typeWordPng from '../../../assets/aicenter/type-word.png';
18
- import typeExcelPng from '../../../assets/aicenter/type-excel.png';
19
- import typeMarkdownPng from '../../../assets/aicenter/type-markdown.png';
20
- import typeTextPng from '../../../assets/aicenter/type-text.png';
21
- import typeMobiPng from '../../../assets/aicenter/type-mobi.png';
22
- import typeRPubPng from '../../../assets/aicenter/type-rpub.png';
23
- import soundWavePng from '../../../assets/aicenter/sound-wave.gif';
24
- import luyinPng from '../../../assets/aicenter/luyin.png';
25
- import { RightOutlined } from '@ant-design/icons';
26
- import { Image, message as messageTip, Tooltip, Typography } from 'antd';
27
- import { isEmpty, isFunction } from 'lodash';
28
- import btn_answer from '../../../assets/aicenter/btn_answer.png';
29
-
30
-
31
- let mediaRecorder = null; // 语音对象,用于录音
32
- let recognition = null; // 语音识别对象
33
- const delayMessageList = []
34
-
35
- export default function ChatWindow({
36
- tags,
37
- getHistoryList,
38
- userInfo,
39
- clearMessage,
40
- api_key,
41
- flowId,
42
- hostUrl,
43
- updateLastMessage,
44
- messages,
45
- output_type,
46
- input_type,
47
- output_component,
48
- bot_message_style,
49
- send_icon_style,
50
- user_message_style,
51
- chat_window_style,
52
- error_message_style,
53
- placeholder_sending,
54
- send_button_style = { paddingTop: '6px' },
55
- online = true,
56
- open,
57
- online_message = '在线',
58
- offline_message = '离线',
59
- window_title = 'AI对话',
60
- placeholder,
61
- input_style,
62
- input_container_style,
63
- addMessage,
64
- position,
65
- triggerRef,
66
- width = '900',
67
- height = '300',
68
- tweaks,
69
- sessionId,
70
- additional_headers,
71
- setDropDownList = () => {},
72
- dropDownList = [],
73
- baseConfig = {},
74
- isShowVoiceButton = true,
75
- isShowUploadButton,
76
- dropManUrl = '',
77
- modalWidth,
78
- isMobile = false,
79
- isShowChatHeader = true,
80
- }: {
81
- tags: [];
82
- getHistoryList: Function;
83
- userInfo: object;
84
- clearMessage: Function;
85
- api_key?: string;
86
- output_type: string;
87
- input_type: string;
88
- output_component?: string;
89
- bot_message_style?: React.CSSProperties;
90
- send_icon_style?: React.CSSProperties;
91
- user_message_style?: React.CSSProperties;
92
- chat_window_style?: React.CSSProperties;
93
- error_message_style?: React.CSSProperties;
94
- send_button_style?: React.CSSProperties;
95
- online?: boolean;
96
- open: boolean;
97
- online_message?: string;
98
- placeholder_sending?: string;
99
- offline_message?: string;
100
- window_title?: string;
101
- placeholder?: string;
102
- input_style?: React.CSSProperties;
103
- input_container_style?: React.CSSProperties;
104
- tweaks?: { [key: string]: any };
105
- flowId: string;
106
- hostUrl: string;
107
- updateLastMessage: Function;
108
- messages: ChatMessageType[];
109
- addMessage: Function;
110
- position?: string;
111
- triggerRef: React.RefObject<HTMLButtonElement>;
112
- width?: number;
113
- height?: string;
114
- sessionId: string;
115
- additional_headers?: { [key: string]: string };
116
- baseConfig: object;
117
- isShowVoiceButton: boolean;
118
- isShowUploadButton: boolean;
119
- dropManUrl: string;
120
- modalWidth: number;
121
- isMobile: boolean;
122
- isShowChatHeader: boolean;
123
- }) {
124
- const [value, setValue] = useState<string>('');
125
- const ref = useRef<HTMLDivElement>(null);
126
- const lastMessage = useRef<HTMLDivElement>(null);
127
- const [windowPosition, setWindowPosition] = useState({ left: '0', top: '0' });
128
- const inputRef = useRef<HTMLInputElement>(null); /* User input Ref */
129
- /* Initial listener for loss of focus that refocuses User input after a small delay */
130
- const [nowAIContent, setNowAIContent] = useState<string>('');
131
- const [sendingMessage, setSendingMessage] = useState(false);
132
- const abortControllerRef = useRef(new AbortController());
133
- const [fileList, setFileList] = useState<
134
- { file: File; fileUrl: string; fileType: string; fileId: string }[]
135
- >([]);
136
- const scrollContainerRef = useRef(null);
137
- const [showLeftArrow, setShowLeftArrow] = useState(false); // 控制左侧箭头显示
138
- const [showRightArrow, setShowRightArrow] = useState(false); // 控制右侧箭头显示
139
- const isStream = true;//是否流式输出(手动开关)
140
- const [recordState, setRecordState] = useState(false); // 录音状态。true为正在录音,false为停止录音
141
- const [tagList, setTagList] = useState([]); // 问题标签列表
142
- const {isTitleSideIcon, logoWidth, agentUrl} = baseConfig;
143
- const inputContainerRef = useRef(null);
144
- const [inputContainerHeight,setInputContainerHeight] = useState('50px')
145
- const contentRef = useRef(nowAIContent);
146
-
147
- let voiceChunks = []; // 临时存储录制的语音片段
148
- // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
149
- const handleScroll = () => {
150
- if (scrollContainerRef.current) {
151
- const { scrollLeft, clientWidth, scrollWidth } = scrollContainerRef.current;
152
-
153
- // 判断内容是否超出容器
154
- const isContentOverflowed = scrollWidth > clientWidth;
155
-
156
- // 判断是否滚动到最左侧
157
- setShowLeftArrow(isContentOverflowed && scrollLeft > 0);
158
-
159
- // 判断是否滚动到最右侧
160
- setShowRightArrow(isContentOverflowed && scrollLeft + clientWidth < scrollWidth);
161
- }
162
- };
163
-
164
- // 处理延时队列,因为后端返回的是 token token form token token.....会导致form渲染在token前面,所以用这个方式
165
- // 当处理token的时候,form的信息放到这里,然后使用 delayMessageTimer 处理这个延时队列
166
- // 处理逻辑是:收到 token 的时候,重置timer,由于timer一直被重置所以不会处理 delayMessageList,直到 1.5内没有新 token 才会处理延时队列
167
- const handleDelayMessage = () => {
168
- if (delayMessageList.length > 0) {
169
- const message = delayMessageList.shift();
170
- addMessage(message);
171
- handleDelayMessage();
172
- }
173
- }
174
-
175
- const delayMessageTimer = (whenTimerExistsDoNothing: boolean = false) => {
176
- if(window.delayMessageTimer)
177
- if(whenTimerExistsDoNothing){
178
- return
179
- }
180
- clearTimeout(window.delayMessageTimer);
181
- window.delayMessageTimer = null
182
- if(!window.delayMessageTimer){
183
- window.delayMessageTimer = setTimeout(handleDelayMessage, 1500);
184
- }
185
- }
186
-
187
- // 流式输出消息,实时显示(token为流式输出内容,end为结束输出,整体输出一次)
188
- const handleMessageContent = (event, data) => {
189
- // console.error("event, data",event, data)
190
- if (event == 'add_message' && data['sender'] == 'Machine') {
191
- getHistoryList();
192
- }
193
- else if (event == 'token') {
194
- setNowAIContent((prevState) => {
195
- const newValue = prevState + data['chunk'];
196
- contentRef.current = newValue
197
- return newValue
198
- });
199
- if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
200
-
201
- // 处理延时队列
202
- delayMessageTimer();
203
- }
204
- else if (event == 'form') {
205
- // 这里添加到延时队列,直接处理可能会把form渲染到token上面
206
- delayMessageList.push({
207
- message: "",
208
- isSend: false,
209
- rawInfo: data,
210
- type: MessageType.form
211
- })
212
- delayMessageTimer(true)
213
- }
214
- else if (event == 'end') {
215
- const res = {
216
- data: data['result'],
217
- };
218
- if (false&&
219
- res.data &&
220
- res.data.outputs &&
221
- Object.keys(res.data.outputs).length > 0 &&
222
- res.data.outputs[0].outputs &&
223
- res.data.outputs[0].outputs.length > 0
224
- ) {
225
- const flowOutputs: Array<any> = res.data.outputs[0].outputs;
226
- if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
227
- Object.values(
228
- flowOutputs.find((e) => e.component_id === output_component).outputs,
229
- ).forEach((output: any) => {
230
- addMessage({
231
- message: extractMessageFromOutput(output),
232
- isSend: false,
233
- rawInfo: output.message,
234
- });
235
- });
236
- } else if (flowOutputs.length === 1) {
237
- Object.values(flowOutputs[0].outputs).forEach((output: any) => {
238
- addMessage({
239
- message: extractMessageFromOutput(output),
240
- isSend: false,
241
- rawInfo: output.message,
242
- });
243
- });
244
- } else {
245
- flowOutputs
246
- .sort((a, b) => {
247
- // Get the earliest timestamp from each flowOutput's outputs
248
- const aTimestamp = Math.min(
249
- ...Object.values(a.outputs).map((output: any) =>
250
- Date.parse(output.message?.timestamp),
251
- ),
252
- );
253
- const bTimestamp = Math.min(
254
- ...Object.values(b.outputs).map((output: any) =>
255
- Date.parse(output.message?.timestamp),
256
- ),
257
- );
258
- return aTimestamp - bTimestamp; // Sort descending (newest first)
259
- })
260
- .forEach((flowOutput) => {
261
- Object.values(flowOutput.outputs).forEach((output: any) => {
262
- addMessage({
263
- message: extractMessageFromOutput(output),
264
- isSend: false,
265
- rawInfo: output.message,
266
- });
267
- });
268
- });
269
- }
270
- }
271
- const output = {
272
- message:contentRef.current,
273
- type:'text'
274
- };
275
- addMessage({
276
- message: extractMessageFromOutput(output),
277
- isSend: false,
278
- rawInfo: output.message,
279
- });
280
- setSendingMessage(false);
281
- }
282
- };
283
-
284
- const sendMessageNoStream = (res) => {
285
- if (
286
- res.data &&
287
- res.data.outputs &&
288
- Object.keys(res.data.outputs).length > 0 &&
289
- res.data.outputs[0].outputs &&
290
- res.data.outputs[0].outputs.length > 0
291
- ) {
292
- const flowOutputs: Array<any> = res.data.outputs[0].outputs;
293
- if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
294
- Object.values(flowOutputs.find((e) => e.component_id === output_component).outputs).forEach(
295
- (output: any) => {
296
- addMessage({
297
- message: extractMessageFromOutput(output),
298
- isSend: false,
299
- });
300
- },
301
- );
302
- } else if (flowOutputs.length === 1) {
303
- Object.values(flowOutputs[0].outputs).forEach((output: any) => {
304
- addMessage({
305
- message: extractMessageFromOutput(output),
306
- isSend: false,
307
- });
308
- });
309
- } else {
310
- flowOutputs
311
- .sort((a, b) => {
312
- // Get the earliest timestamp from each flowOutput's outputs
313
- const aTimestamp = Math.min(
314
- ...Object.values(a.outputs).map((output: any) =>
315
- Date.parse(output.message?.timestamp),
316
- ),
317
- );
318
- const bTimestamp = Math.min(
319
- ...Object.values(b.outputs).map((output: any) =>
320
- Date.parse(output.message?.timestamp),
321
- ),
322
- );
323
- return aTimestamp - bTimestamp; // Sort descending (newest first)
324
- })
325
- .forEach((flowOutput) => {
326
- Object.values(flowOutput.outputs).forEach((output: any) => {
327
- addMessage({
328
- message: extractMessageFromOutput(output),
329
- isSend: false,
330
- });
331
- });
332
- });
333
- }
334
- }
335
- setSendingMessage(false);
336
- };
337
-
338
- // 点击send发送按钮,进行消息提交发送逻辑;userMessage为传入的文本消息,优先级比input中value高
339
- const handleSendMessage = (userMessage, callback = () => {}, input_value_type: string = InputValueType.text)=> {
340
- let message = '';
341
- if (value && value.trim() !== '') {
342
- message = value;
343
- setDropDownList(undefined);
344
- }
345
- // 有传入消息,则优先使用传入的消息
346
- if (userMessage && userMessage?.trim() !== '') {
347
- message = userMessage;
348
- }
349
-
350
- if (message && message.trim() !== '') {
351
- if(input_value_type === InputValueType.text){
352
- addMessage({
353
- message: message,
354
- isSend: true,
355
- rawInfo: { files: fileList.map((fileItem) => fileItem.fileUrl) },
356
- });
357
- setValue('');
358
- setSendingMessage(true);
359
- }
360
-
361
- let userInfoClone = userInfo;
362
- if(isEmpty(userInfoClone.code)) {
363
- userInfoClone.code = sessionId;
364
- }
365
- const code = userInfoClone['code'] ? userInfoClone['code'] : ""
366
- let embedAppExtend: embedAppExtend = {
367
- operator_id: code,
368
- upload_file_path_list: [],
369
- http_extend: {
370
- bd:{
371
- code: code,
372
- },
373
- body: {
374
- code: code,
375
- },
376
- },
377
- };
378
- if (fileList.length > 0) {
379
- fileList.forEach((fileItem) => {
380
- if (fileItem.fileType === 'image' || fileItem.fileType === 'file') {
381
- embedAppExtend['upload_file_path_list'].push(fileItem.fileUrl);
382
- }
383
- });
384
- }
385
- setFileList([]);
386
- handleScroll();
387
- sendMessage(
388
- input_value_type,
389
- embedAppExtend,
390
- isStream,
391
- handleMessageContent,
392
- abortControllerRef.current.signal,
393
- hostUrl,
394
- flowId,
395
- message,
396
- input_type,
397
- output_type,
398
- sessionId,
399
- output_component,
400
- tweaks,
401
- api_key,
402
- additional_headers,
403
- )
404
- .then((res) => {
405
- // 非流式输出
406
- if(!isStream){
407
- sendMessageNoStream(res);
408
- }
409
- getHistoryList();
410
- setSendingMessage(false);
411
- setNowAIContent('');
412
- })
413
- .catch((e) => {
414
- if (e.name !== 'AbortError') {
415
- messageTip.error('网络请求错误,请重试');
416
- addMessage({
417
- message: '网络错误',
418
- isSend: false,
419
- });
420
- }
421
- setSendingMessage(false);
422
- setNowAIContent('');
423
- })
424
- .finally(() => {
425
- if (isFunction(callback)) {
426
- callback();
427
- }
428
- });
429
- setDropDownList(undefined);
430
- }
431
- }
432
-
433
- /**
434
- * 获取文件类型
435
- * @param file File 对象
436
- */
437
- const getFileType = (file) => {
438
- if (!(file instanceof File)) {
439
- throw new Error('Input must be a File object');
440
- }
441
-
442
- const mimeType = file.type;
443
-
444
- // 判断是否为图片
445
- if (mimeType.startsWith('image/')) {
446
- return 'image';
447
- }
448
-
449
- // 判断是否为音频
450
- if (mimeType.startsWith('audio/')) {
451
- return 'voice';
452
- }
453
-
454
- // 默认返回 file
455
- return 'file';
456
- };
457
-
458
- /**
459
- * 根据文件URL获取文件类型
460
- * @param url
461
- */
462
- const getFileTypeByUrl = (url) => {
463
- if (!url) {
464
- return 'file';
465
- }
466
- switch (url.split('.').pop().toLowerCase()) {
467
- case 'jpg':
468
- case 'jpeg':
469
- case 'png':
470
- case 'gif':
471
- return 'image';
472
- case 'pdf':
473
- return 'pdf';
474
- case 'doc':
475
- case 'docx':
476
- return 'word';
477
- case 'xls':
478
- case 'xlsx':
479
- return 'excel';
480
- case 'md':
481
- return 'markdown';
482
- case 'txt':
483
- return 'txt';
484
- case 'mobi':
485
- return 'mobi';
486
- case 'rpub':
487
- return 'rpub';
488
- default:
489
- return 'file';
490
- }
491
- };
492
-
493
- /**
494
- * 获取易于阅读的文件大小(将字节转成KB、MB等)
495
- * @param value 文件字节大小,file.size
496
- */
497
- const getFileSize = (value) => {
498
- if (null == value || value == '') {
499
- return '0 Bytes';
500
- }
501
- var unitArr = new Array('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
502
- var index = 0;
503
- var srcsize = parseFloat(value);
504
- index = Math.floor(Math.log(srcsize) / Math.log(1024));
505
- var size = srcsize / Math.pow(1024, index);
506
- size = size.toFixed(2); //保留的小数位数
507
- return size + unitArr[index];
508
- };
509
-
510
- /**
511
- * 删除文件列表中的文件
512
- * @param item 点击的File对象
513
- * @param index 索引
514
- */
515
- const removeFile = (item, index) => {
516
- setFileList(fileList.filter((item, i) => i !== index));
517
- handleScroll();
518
- };
519
-
520
- /**
521
- * 上传文件
522
- */
523
- const uploadFile = () => {
524
- if (sendingMessage) {
525
- return;
526
- }
527
- const input = document.createElement('input');
528
- input.type = 'file';
529
- const acceptFileType = [
530
- '.png',
531
- '.jpg',
532
- '.jpeg',
533
- // '.pdf',
534
- // '.doc',
535
- // '.docx',
536
- // '.xls',
537
- // '.xlsx',
538
- // '.txt',
539
- // '.md',
540
- // '.mobi',
541
- // '.rpub',
542
- ];
543
- input.accept = acceptFileType.join(',');
544
- input.multiple = true; // 允许多选文件
545
- input.onchange = (event) => {
546
- const files = event.target.files;
547
- if (files && files.length > 0) {
548
- Array.from(files).forEach((file) => {
549
- if (acceptFileType.includes(`.${file.name.split('.').pop().toLowerCase()}`)) {
550
- fetchUploadFile(hostUrl, file, flowId, api_key)
551
- .then((res) => {
552
- const { data = {} } = res.data;
553
- const {
554
- id,
555
- name,
556
- save_name,
557
- url,
558
- extension,
559
- } = data;
560
- const filePreItem = {
561
- file,
562
- fileType: getFileType(file),
563
- fileUrl: url,
564
- fileId: id,
565
- };
566
- setFileList((prevFileList) => [...prevFileList, filePreItem]);
567
- handleScroll();
568
- })
569
- .catch((e) => {
570
- messageTip.error('上传失败,请重试!');
571
- });
572
- } else {
573
- messageTip.error('已过滤不支持的文件类型');
574
- }
575
- });
576
- }
577
- };
578
- input.click();
579
- };
580
-
581
- // 向右滚动方法
582
- const handleScrollRight = (direction) => {
583
- if (direction == 'right') {
584
- if (scrollContainerRef.current) {
585
- scrollContainerRef.current.scrollLeft += 200;
586
- }
587
- } else if (direction == 'left') {
588
- if (scrollContainerRef.current) {
589
- scrollContainerRef.current.scrollLeft -= 200;
590
- }
591
- }
592
- };
593
-
594
- /**
595
- * 语音实时识别
596
- */
597
- const startRecord = ()=>{
598
- if (!('webkitSpeechRecognition' in window)) {
599
- alert('您的浏览器不支持语音识别功能')
600
- return;
601
- }
602
- if(recordState){
603
- recognition.stop();
604
- setRecordState(false);
605
- }else{
606
- recognition = new webkitSpeechRecognition();
607
- recognition.continuous = true;
608
- recognition.interimResults = true;
609
- recognition.lang = 'zh-CN';
610
-
611
- recognition.onresult = (event) => {
612
- const transcript = Array.from(event.results)
613
- .map(result => result[0].transcript)
614
- .join('');
615
- setValue(transcript)
616
- };
617
-
618
- recognition.onerror = (event) => {
619
- if(event.error === 'not-allowed'){
620
- messageTip.error("录音权限被拒绝,请允许录音权限后重试")
621
- }
622
- console.error('语音识别错误:', event);
623
- };
624
- recognition.start()
625
- setRecordState(true);
626
- }
627
- }
628
-
629
- /**
630
- * 输出消息时,滚动到底部
631
- */
632
- useEffect(() => {
633
- if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
634
- }, [messages]);
635
-
636
- /* Refocus the User input whenever a new response is returned from the LLM */
637
-
638
- useEffect(() => {
639
- // after a slight delay
640
- setTagList(tags);
641
- setTimeout(() => {
642
- // inputRef.current?.focus();
643
- }, 100);
644
- }, [messages, open, tags]);
645
-
646
- /**
647
- * 当获取历史记录时(sessindId变化时),清空消息,并添加历史记录
648
- */
649
- useEffect(() => {
650
- const fetchChatHistory = async () => {
651
- try {
652
- let userInfoClone = userInfo;
653
- if(isEmpty(userInfoClone.code)) {
654
- userInfoClone.code = sessionId;
655
- }
656
- const res = await getChatHistory(hostUrl, flowId, sessionId, userInfoClone['code']);
657
- const chatHistory = res.data?.length ? res.data : [];
658
-
659
- clearMessage();
660
- chatHistory.forEach((data) => {
661
- addMessage({
662
- message: extractMessageFromOutput({
663
- message: data,
664
- type: data['category'],
665
- }),
666
- isSend: data['sender'] === 'User',
667
- rawInfo: data,
668
- });
669
- });
670
- } catch (error) {
671
- console.error("Failed to fetch chat history:", error);
672
- }
673
- };
674
-
675
- fetchChatHistory();
676
-
677
- return () => {
678
- abortControllerRef.current.abort('disconnect');
679
- // abortControllerRef是一次性的,中止之后要初始化,否则fetch请求会被中止导致请求无响应
680
- abortControllerRef.current = new AbortController();
681
- };
682
- }, [sessionId]);
683
-
684
- /**
685
- * 开场白展示优化
686
- * 1、如果是全屏模式:则横向展示三项开场白
687
- * 2、如果是弹窗模式,则当弹窗宽度大于【x】时横向展示三项开场白,当小于等于【x】时则纵向展示三项开场白
688
- * */
689
- const judgeDropClass = () => {
690
- if (modalWidth === undefined) {
691
- if(isEmpty(dropManUrl)){
692
- return 'cl-drop-down';
693
- }
694
- return 'cl-drop-screen';
695
- }
696
- if (modalWidth > 1800) {
697
- return 'cl-drop-man';
698
- }
699
- return 'cl-drop-horizontal'
700
- }
701
-
702
- const renderTipsInfo = () => {
703
- if (isMobile && !isEmpty(dropDownList)){
704
- return (
705
- <div className={'cl-tips-wrapper'}>
706
- <img className="drop-man-img" src={dropManUrl}/>
707
- <div className="drop-down-title"><strong>Hi,</strong><br/><strong>欢迎使用{window_title}!</strong><br/><div style={{fontSize:'14px'}}>您可以这样问我:</div></div>
708
- <div className={'cl-drop-down-mobile'}>
709
- <div className="drop-down-list-mobile">
710
- {dropDownList.map(({backgroundImg, title}) => (
711
- <div
712
- className="drop-down-item-card-mobile"
713
- key={title}
714
- onClick={() => {
715
- setDropDownList(undefined);
716
- handleSendMessage(title);
717
- }}>
718
- <Typography.Paragraph
719
- className="drop-down-item-title-mobile"
720
- ellipsis={{
721
- rows: 2,
722
- tooltip: `“${title}”`,
723
- }}>{'“' + title + '”'}
724
- </Typography.Paragraph>
725
- <img className="drop-down-item-bottom-img-mobile" src={backgroundImg}/>
726
- {/*</div>*/}
727
- </div>
728
- ))}
729
- </div>
730
- </div>
731
- </div>
732
- )
733
- }
734
- if (!isEmpty(dropDownList)){
735
- if (isEmpty(dropManUrl)){
736
- return (
737
- <div className='cl-drop-down'>
738
- <div className="drop-down-title">Hi,ssss欢迎使用{window_title},您可以这样问我:</div>
739
- <div className="drop-down-list">
740
- {dropDownList.map(({backgroundImg, title}) => (
741
- <div className="drop-down-item-card" key={title}>
742
- <Typography.Paragraph
743
- className="drop-down-item-title"
744
- ellipsis={{
745
- rows: 2,
746
- tooltip: `“${title}”`,
747
- }}>{'“' + title + '”'}
748
- </Typography.Paragraph>
749
- <div className="drop-down-item-bottom">
750
- <div
751
- className="drop-down-item-bottom-button"
752
- onClick={() => {
753
- setDropDownList(undefined);
754
- handleSendMessage(title);
755
- }}
756
- >
757
- <img src={btn_answer}/>
758
- {/*<RightOutlined style={{ height: '0.8rem', width: '0.4rem' }} />*/}
759
- </div>
760
- <img className="drop-down-item-bottom-img" src={backgroundImg}/>
761
- </div>
762
- </div>
763
- ))}
764
- </div>
765
- </div>
766
- )
767
- } else {
768
- return (
769
- <div className={judgeDropClass()}>
770
- <img className="drop-man-img" src={dropManUrl}/>
771
- <div className='cl-drop-down'>
772
- <div className="drop-down-title">Hi,欢迎使用{window_title},您可以这样问我:</div>
773
- <div className="drop-down-list">
774
- {dropDownList.map(({backgroundImg, title}) => (
775
- <div
776
- className="drop-down-item-card"
777
- key={title}
778
- onClick={() => {
779
- setDropDownList(undefined);
780
- handleSendMessage(title);
781
- }}>
782
- <Typography.Paragraph
783
- className="drop-down-item-title"
784
- ellipsis={{
785
- rows: 2,
786
- tooltip: `“${title}”`,
787
- }}>{'“' + title + '”'}
788
- </Typography.Paragraph>
789
- <div className="drop-down-item-bottom">
790
- <div
791
- className="drop-down-item-bottom-button"
792
- onClick={() => {
793
- setDropDownList(undefined);
794
- handleSendMessage(title);
795
- }}
796
- >
797
- <img src={btn_answer}/>
798
- {/*<RightOutlined style={{ height: '0.8rem', width: '0.4rem' }} />*/}
799
- </div>
800
- <img className="drop-down-item-bottom-img" src={backgroundImg}/>
801
- </div>
802
- </div>
803
- ))}
804
- </div>
805
- </div>
806
- </div>
807
- )
808
- }
809
- }
810
- return <></>
811
- }
812
-
813
- return (
814
- <div
815
- style={{...chat_window_style, width: width, height: "100%"}}
816
- ref={ref}
817
- className="cl-window"
818
- >
819
- <div className="cl-middle-container">
820
- <div className="cl-header" style={isMobile ? {position: 'absolute', top: 20, left: 0, paddingLeft: '1.6rem', paddingRight: '1.6rem'} : {}}>
821
- {
822
- isShowChatHeader && (
823
- <div className="header-title">
824
- {
825
- isTitleSideIcon ?
826
- <img className="p_logoImg" style={{width: logoWidth, height: 'auto', marginRight: 10}}
827
- src={agentUrl}/> :
828
- <span className="diamond"/>
829
- }
830
- {window_title}
831
- </div>
832
- )
833
- }
834
- <div className="cl-header-subtitle"/>
835
- {renderTipsInfo()}
836
- </div>
837
- <div
838
- className="cl-messages_container"
839
- // style={{ maxWidth: '100%', minHeight:'300px', height:'700px', paddingBottom: '56px' }}
840
- >
841
- {messages.map((message, index) => (
842
- <ChatMessage
843
- bot_message_style={bot_message_style}
844
- user_message_style={user_message_style}
845
- error_message_style={error_message_style}
846
- key={index}
847
- host_url={hostUrl}
848
- message={message}
849
- isSend={message.isSend}
850
- error={message.error}
851
- type={message.type}
852
- rawInfo={message.rawInfo}
853
- handleSendMessage={handleSendMessage}
854
- />
855
- ))}
856
- {sendingMessage && (nowAIContent?
857
- <ChatMessage
858
- bot_message_style={bot_message_style}
859
- user_message_style={user_message_style}
860
- error_message_style={error_message_style}
861
- key={1688574569}
862
- host_url={hostUrl}
863
- message={{ message: nowAIContent}}
864
- isSend={false}
865
- handleSendMessage={handleSendMessage}
866
- />:<ChatMessagePlaceholder bot_message_style={bot_message_style} />
867
- )}
868
- <div ref={lastMessage}></div>
869
- </div>
870
- <div
871
- className="w_tagListClass"
872
- style={{
873
- // bottom: fileList.length > 0 ? '130px' : '80px',
874
- display: tagList?.length > 0 ? '' : 'none',
875
- zIndex: tagList?.length > 0 ? '2' : '-1',
876
- }}
877
- >
878
- {tagList.map((item, index) => (
879
- <div
880
- key={index}
881
- className="w_tagItemBox"
882
- onClick={() => {
883
- handleSendMessage(item?.name);
884
- setFileList([]);
885
- }}
886
- >
887
- <div>
888
- <img
889
- style={item?.img ? {} : { display: 'none' }}
890
- src={'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/'+ item?.img}
891
- className="w_tagImgh"
892
- alt="Image"
893
- />
894
- </div>
895
- <div className="w_tagItemText">{item?.name}</div>
896
- </div>
897
- ))}
898
- </div>
899
- <div style={input_container_style} className="cl-input_container">
900
- <div className="w_file_preview" ref={scrollContainerRef} onScroll={handleScroll}>
901
- <div className="w_toLeftBox" style={{display: showLeftArrow ? 'flex' : 'none'}}>
902
- <div className="w_toLeft" onClick={() => handleScrollRight('left')}>
903
- <img src={toLeftPng}/>
904
- </div>
905
- </div>
906
- <div className="w_toRightBox" style={{display: showRightArrow ? 'flex' : 'none'}}>
907
- <div className="w_toRight" onClick={() => handleScrollRight('right')}>
908
- <img src={toRightPng}/>
909
- </div>
910
- </div>
911
- {fileList.map((item, index) => {
912
- return (
913
- <div key={item.filePath} className="w_fileBox">
914
- {item.fileType == 'image' && (
915
- <Image
916
- height={40}
917
- width={40}
918
- src={item.fileUrl}
919
- className="w_upImg"
920
- preview={{
921
- mask: <span className="custom-mask"></span>,
922
- }}
923
- />
924
- )}
925
- {getFileTypeByUrl(item.file.name) == 'pdf' && (
926
- <img style={{width: 40, height: 40}} src={typePdfPng}/>
927
- )}
928
- {getFileTypeByUrl(item.file.name) == 'word' && (
929
- <img style={{width: 40, height: 40}} src={typeWordPng}/>
930
- )}
931
- {getFileTypeByUrl(item.file.name) == 'excel' && (
932
- <img style={{width: 40, height: 40}} src={typeExcelPng}/>
933
- )}
934
- {getFileTypeByUrl(item.file.name) == 'markdown' && (
935
- <img style={{width: 40, height: 40}} src={typeMarkdownPng}/>
936
- )}
937
- {getFileTypeByUrl(item.file.name) == 'txt' && (
938
- <img style={{width: 40, height: 40}} src={typeTextPng}/>
939
- )}
940
- {getFileTypeByUrl(item.file.name) == 'mobi' && (
941
- <img style={{width: 40, height: 40}} src={typeMobiPng}/>
942
- )}
943
- {getFileTypeByUrl(item.file.name) == 'rpub' && (
944
- <img style={{width: 40, height: 40}} src={typeRPubPng}/>
945
- )}
946
- {getFileTypeByUrl(item.file.name) == 'file' && (
947
- <img style={{width: 40, height: 40}} src={upFilePng}/>
948
- )}
949
- <div className="w_fileInfoBox">
950
- <div className="w_fileInfoFileName">{item.file.name}</div>
951
- <div className="w_fileInfoMeta">{`${item.fileType} · ${getFileSize(
952
- item.file.size,
953
- )}`}</div>
954
- </div>
955
- <div
956
- className="w_fileRemove"
957
- onClick={() => {
958
- removeFile(item, index);
959
- }}
960
- >
961
- <img src={closexPng}/>
962
- </div>
963
- </div>
964
- );
965
- })}
966
- </div>
967
- <div className="w_inputBox" style={{height: inputContainerHeight}}>
968
- <textarea
969
- onFocus={()=>setInputContainerHeight('120px')}
970
- onBlur={()=>setInputContainerHeight('50px')}
971
- value={value}
972
- onChange={(e) => setValue(e.target.value)}
973
- onKeyDown={(e) => {
974
- if (e.key === 'Enter' && !e.shiftKey) handleSendMessage();
975
- }}
976
- disabled={sendingMessage}
977
- placeholder={
978
- sendingMessage
979
- ? placeholder_sending || '思考中...'
980
- : placeholder || '请输入您的问题...'
981
- }
982
- style={{...input_style,resize:"none"}}
983
- ref={inputRef}
984
- className="cl-input-element"
985
- />
986
- {
987
- isShowVoiceButton &&
988
- <Tooltip title={recordState ? "点击结束录音" : "点击开始录音"}>
989
- <div
990
- className="w_send_voice_box"
991
- style={sendingMessage ? { cursor: 'not-allowed' } : {}}
992
- onClick={startRecord}
993
- >
994
- <img src={recordState ? soundWavePng : luyinPng} style={{ width: 23 }} className={recordState ? "w_recordIng" : ''}></img>
995
- </div>
996
- </Tooltip>
997
- }
998
- {/*<Tooltip title="支持PDF / Word / Excel / Markdown / txt / mobi / rpub">*/}
999
- {
1000
- isShowUploadButton &&
1001
- <Tooltip title="支持图片格式">
1002
- <div
1003
- className="w_send_file_box"
1004
- style={sendingMessage ? { cursor: 'not-allowed' } : {}}
1005
- onClick={uploadFile}
1006
- >
1007
- <img src={fileuploadPng} style={{ width: 23 }}></img>
1008
- </div>
1009
- </Tooltip>
1010
- }
1011
- <Tooltip title={sendingMessage?'点击暂停':'点击发送'}>
1012
- <button
1013
- style={{ ...(sendingMessage ? { cursor: 'pointer' } : {}), padding: '0 13px', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center' }}
1014
- onClick={() => {
1015
- if(sendingMessage){
1016
- const output = {
1017
- message:nowAIContent,
1018
- type:'text'
1019
- };
1020
- addMessage({
1021
- message: extractMessageFromOutput(output),
1022
- isSend: false,
1023
- rawInfo: output.message,
1024
- });
1025
- abortControllerRef.current.abort('disconnect');
1026
- abortControllerRef.current = new AbortController();
1027
- }else{
1028
- handleSendMessage()
1029
- }
1030
- }}
1031
- >
1032
-
1033
- <img src={sendingMessage ? stopmessagePng : sendmessagePng} style={{width: 55}}/>
1034
- </button>
1035
- </Tooltip>
1036
- </div>
1037
- </div>
1038
- </div>
1039
- </div>
1040
- );
1041
- }
1
+ // @ts-nocheck
2
+ import { extractMessageFromOutput } from './utils';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import {ChatMessageType, embedAppExtend, InputValueType, MessageType} from './types/chatWidget';
5
+ import ChatMessage from './chatMessage';
6
+ import { fetchUploadFile, getChatHistory, sendMessage } from './controllers';
7
+ import ChatMessagePlaceholder from './chatPlaceholder/index.tsx';
8
+ // import './index.module.css'
9
+ import closexPng from '../../../assets/aicenter/closex.png';
10
+ import upFilePng from '../../../assets/aicenter/upfile.png';
11
+ import fileuploadPng from '../../../assets/aicenter/fileupload.png';
12
+ import sendmessagePng from '../../../assets/aicenter/sendmessage.png';
13
+ import stopmessagePng from '../../../assets/aicenter/stopmessage.png';
14
+ import toRightPng from '../../../assets/aicenter/toRight.png';
15
+ import toLeftPng from '../../../assets/aicenter/toLeft.png';
16
+ import typePdfPng from '../../../assets/aicenter/type-pdf.png';
17
+ import typeWordPng from '../../../assets/aicenter/type-word.png';
18
+ import typeExcelPng from '../../../assets/aicenter/type-excel.png';
19
+ import typeMarkdownPng from '../../../assets/aicenter/type-markdown.png';
20
+ import typeTextPng from '../../../assets/aicenter/type-text.png';
21
+ import typeMobiPng from '../../../assets/aicenter/type-mobi.png';
22
+ import typeRPubPng from '../../../assets/aicenter/type-rpub.png';
23
+ import soundWavePng from '../../../assets/aicenter/sound-wave.gif';
24
+ import luyinPng from '../../../assets/aicenter/luyin.png';
25
+ import { RightOutlined } from '@ant-design/icons';
26
+ import { Image, message as messageTip, Tooltip, Typography } from 'antd';
27
+ import { isEmpty, isFunction } from 'lodash';
28
+ import btn_answer from '../../../assets/aicenter/btn_answer.png';
29
+
30
+
31
+ let mediaRecorder = null; // 语音对象,用于录音
32
+ let recognition = null; // 语音识别对象
33
+ const delayMessageList = []
34
+
35
+ export default function ChatWindow({
36
+ tags,
37
+ getHistoryList,
38
+ userInfo,
39
+ clearMessage,
40
+ api_key,
41
+ flowId,
42
+ hostUrl,
43
+ updateLastMessage,
44
+ messages,
45
+ output_type,
46
+ input_type,
47
+ output_component,
48
+ bot_message_style,
49
+ send_icon_style,
50
+ user_message_style,
51
+ chat_window_style,
52
+ error_message_style,
53
+ placeholder_sending,
54
+ send_button_style = { paddingTop: '6px' },
55
+ online = true,
56
+ open,
57
+ online_message = '在线',
58
+ offline_message = '离线',
59
+ window_title = 'AI对话',
60
+ placeholder,
61
+ input_style,
62
+ input_container_style,
63
+ addMessage,
64
+ position,
65
+ triggerRef,
66
+ width = '900',
67
+ height = '300',
68
+ tweaks,
69
+ sessionId,
70
+ additional_headers,
71
+ setDropDownList = () => {},
72
+ dropDownList = [],
73
+ baseConfig = {},
74
+ isShowVoiceButton = true,
75
+ isShowUploadButton,
76
+ dropManUrl = '',
77
+ modalWidth,
78
+ isMobile = false,
79
+ isShowChatHeader = true,
80
+ }: {
81
+ tags: [];
82
+ getHistoryList: Function;
83
+ userInfo: object;
84
+ clearMessage: Function;
85
+ api_key?: string;
86
+ output_type: string;
87
+ input_type: string;
88
+ output_component?: string;
89
+ bot_message_style?: React.CSSProperties;
90
+ send_icon_style?: React.CSSProperties;
91
+ user_message_style?: React.CSSProperties;
92
+ chat_window_style?: React.CSSProperties;
93
+ error_message_style?: React.CSSProperties;
94
+ send_button_style?: React.CSSProperties;
95
+ online?: boolean;
96
+ open: boolean;
97
+ online_message?: string;
98
+ placeholder_sending?: string;
99
+ offline_message?: string;
100
+ window_title?: string;
101
+ placeholder?: string;
102
+ input_style?: React.CSSProperties;
103
+ input_container_style?: React.CSSProperties;
104
+ tweaks?: { [key: string]: any };
105
+ flowId: string;
106
+ hostUrl: string;
107
+ updateLastMessage: Function;
108
+ messages: ChatMessageType[];
109
+ addMessage: Function;
110
+ position?: string;
111
+ triggerRef: React.RefObject<HTMLButtonElement>;
112
+ width?: number;
113
+ height?: string;
114
+ sessionId: string;
115
+ additional_headers?: { [key: string]: string };
116
+ baseConfig: object;
117
+ isShowVoiceButton: boolean;
118
+ isShowUploadButton: boolean;
119
+ dropManUrl: string;
120
+ modalWidth: number;
121
+ isMobile: boolean;
122
+ isShowChatHeader: boolean;
123
+ }) {
124
+ const [value, setValue] = useState<string>('');
125
+ const ref = useRef<HTMLDivElement>(null);
126
+ const lastMessage = useRef<HTMLDivElement>(null);
127
+ const [windowPosition, setWindowPosition] = useState({ left: '0', top: '0' });
128
+ const inputRef = useRef<HTMLInputElement>(null); /* User input Ref */
129
+ /* Initial listener for loss of focus that refocuses User input after a small delay */
130
+ const [nowAIContent, setNowAIContent] = useState<string>('');
131
+ const [sendingMessage, setSendingMessage] = useState(false);
132
+ const abortControllerRef = useRef(new AbortController());
133
+ const [fileList, setFileList] = useState<
134
+ { file: File; fileUrl: string; fileType: string; fileId: string }[]
135
+ >([]);
136
+ const scrollContainerRef = useRef(null);
137
+ const [showLeftArrow, setShowLeftArrow] = useState(false); // 控制左侧箭头显示
138
+ const [showRightArrow, setShowRightArrow] = useState(false); // 控制右侧箭头显示
139
+ const isStream = true;//是否流式输出(手动开关)
140
+ const [recordState, setRecordState] = useState(false); // 录音状态。true为正在录音,false为停止录音
141
+ const [tagList, setTagList] = useState([]); // 问题标签列表
142
+ const {isTitleSideIcon, logoWidth, agentUrl} = baseConfig;
143
+ const inputContainerRef = useRef(null);
144
+ const [inputContainerHeight,setInputContainerHeight] = useState('50px')
145
+ const contentRef = useRef(nowAIContent);
146
+
147
+ let voiceChunks = []; // 临时存储录制的语音片段
148
+ // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
149
+ const handleScroll = () => {
150
+ if (scrollContainerRef.current) {
151
+ const { scrollLeft, clientWidth, scrollWidth } = scrollContainerRef.current;
152
+
153
+ // 判断内容是否超出容器
154
+ const isContentOverflowed = scrollWidth > clientWidth;
155
+
156
+ // 判断是否滚动到最左侧
157
+ setShowLeftArrow(isContentOverflowed && scrollLeft > 0);
158
+
159
+ // 判断是否滚动到最右侧
160
+ setShowRightArrow(isContentOverflowed && scrollLeft + clientWidth < scrollWidth);
161
+ }
162
+ };
163
+
164
+ // 处理延时队列,因为后端返回的是 token token form token token.....会导致form渲染在token前面,所以用这个方式
165
+ // 当处理token的时候,form的信息放到这里,然后使用 delayMessageTimer 处理这个延时队列
166
+ // 处理逻辑是:收到 token 的时候,重置timer,由于timer一直被重置所以不会处理 delayMessageList,直到 1.5内没有新 token 才会处理延时队列
167
+ const handleDelayMessage = () => {
168
+ if (delayMessageList.length > 0) {
169
+ const message = delayMessageList.shift();
170
+ addMessage(message);
171
+ handleDelayMessage();
172
+ }
173
+ }
174
+
175
+ const delayMessageTimer = () => {
176
+ if(window.delayMessageTimer) {
177
+ clearTimeout(window.delayMessageTimer);
178
+ window.delayMessageTimer = null
179
+ }
180
+
181
+ if(!window.delayMessageTimer){
182
+ window.delayMessageTimer = setTimeout(handleDelayMessage, 1500);
183
+ }
184
+ }
185
+
186
+ // 流式输出消息,实时显示(token为流式输出内容,end为结束输出,整体输出一次)
187
+ const handleMessageContent = (event, data) => {
188
+ // console.error("event, data",event, data)
189
+ if (event == 'add_message' && data['sender'] == 'Machine') {
190
+ getHistoryList();
191
+ }
192
+ else if (event == 'token') {
193
+ setNowAIContent((prevState) => {
194
+ const newValue = prevState + data['chunk'];
195
+ contentRef.current = newValue
196
+ return newValue
197
+ });
198
+ if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
199
+
200
+ // 处理延时队列
201
+ delayMessageTimer();
202
+ }
203
+ else if (event == 'form') {
204
+ // 这里添加到延时队列,直接处理可能会把form渲染到token上面
205
+ delayMessageList.push({
206
+ message: "",
207
+ isSend: false,
208
+ rawInfo: data,
209
+ type: MessageType.form
210
+ })
211
+ delayMessageTimer()
212
+ }
213
+ else if (event == 'end') {
214
+ const res = {
215
+ data: data['result'],
216
+ };
217
+ if (false&&
218
+ res.data &&
219
+ res.data.outputs &&
220
+ Object.keys(res.data.outputs).length > 0 &&
221
+ res.data.outputs[0].outputs &&
222
+ res.data.outputs[0].outputs.length > 0
223
+ ) {
224
+ const flowOutputs: Array<any> = res.data.outputs[0].outputs;
225
+ if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
226
+ Object.values(
227
+ flowOutputs.find((e) => e.component_id === output_component).outputs,
228
+ ).forEach((output: any) => {
229
+ addMessage({
230
+ message: extractMessageFromOutput(output),
231
+ isSend: false,
232
+ rawInfo: output.message,
233
+ });
234
+ });
235
+ } else if (flowOutputs.length === 1) {
236
+ Object.values(flowOutputs[0].outputs).forEach((output: any) => {
237
+ addMessage({
238
+ message: extractMessageFromOutput(output),
239
+ isSend: false,
240
+ rawInfo: output.message,
241
+ });
242
+ });
243
+ } else {
244
+ flowOutputs
245
+ .sort((a, b) => {
246
+ // Get the earliest timestamp from each flowOutput's outputs
247
+ const aTimestamp = Math.min(
248
+ ...Object.values(a.outputs).map((output: any) =>
249
+ Date.parse(output.message?.timestamp),
250
+ ),
251
+ );
252
+ const bTimestamp = Math.min(
253
+ ...Object.values(b.outputs).map((output: any) =>
254
+ Date.parse(output.message?.timestamp),
255
+ ),
256
+ );
257
+ return aTimestamp - bTimestamp; // Sort descending (newest first)
258
+ })
259
+ .forEach((flowOutput) => {
260
+ Object.values(flowOutput.outputs).forEach((output: any) => {
261
+ addMessage({
262
+ message: extractMessageFromOutput(output),
263
+ isSend: false,
264
+ rawInfo: output.message,
265
+ });
266
+ });
267
+ });
268
+ }
269
+ }
270
+ const output = {
271
+ message:contentRef.current,
272
+ type:'text'
273
+ };
274
+ addMessage({
275
+ message: extractMessageFromOutput(output),
276
+ isSend: false,
277
+ rawInfo: output.message,
278
+ });
279
+ setSendingMessage(false);
280
+ }
281
+ };
282
+
283
+ const sendMessageNoStream = (res) => {
284
+ if (
285
+ res.data &&
286
+ res.data.outputs &&
287
+ Object.keys(res.data.outputs).length > 0 &&
288
+ res.data.outputs[0].outputs &&
289
+ res.data.outputs[0].outputs.length > 0
290
+ ) {
291
+ const flowOutputs: Array<any> = res.data.outputs[0].outputs;
292
+ if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
293
+ Object.values(flowOutputs.find((e) => e.component_id === output_component).outputs).forEach(
294
+ (output: any) => {
295
+ addMessage({
296
+ message: extractMessageFromOutput(output),
297
+ isSend: false,
298
+ });
299
+ },
300
+ );
301
+ } else if (flowOutputs.length === 1) {
302
+ Object.values(flowOutputs[0].outputs).forEach((output: any) => {
303
+ addMessage({
304
+ message: extractMessageFromOutput(output),
305
+ isSend: false,
306
+ });
307
+ });
308
+ } else {
309
+ flowOutputs
310
+ .sort((a, b) => {
311
+ // Get the earliest timestamp from each flowOutput's outputs
312
+ const aTimestamp = Math.min(
313
+ ...Object.values(a.outputs).map((output: any) =>
314
+ Date.parse(output.message?.timestamp),
315
+ ),
316
+ );
317
+ const bTimestamp = Math.min(
318
+ ...Object.values(b.outputs).map((output: any) =>
319
+ Date.parse(output.message?.timestamp),
320
+ ),
321
+ );
322
+ return aTimestamp - bTimestamp; // Sort descending (newest first)
323
+ })
324
+ .forEach((flowOutput) => {
325
+ Object.values(flowOutput.outputs).forEach((output: any) => {
326
+ addMessage({
327
+ message: extractMessageFromOutput(output),
328
+ isSend: false,
329
+ });
330
+ });
331
+ });
332
+ }
333
+ }
334
+ setSendingMessage(false);
335
+ };
336
+
337
+ // 点击send发送按钮,进行消息提交发送逻辑;userMessage为传入的文本消息,优先级比input中value高
338
+ const handleSendMessage = (userMessage, callback = () => {}, input_value_type: string = InputValueType.text)=> {
339
+ let message = '';
340
+ if (value && value.trim() !== '') {
341
+ message = value;
342
+ setDropDownList(undefined);
343
+ }
344
+ // 有传入消息,则优先使用传入的消息
345
+ if (userMessage && userMessage?.trim() !== '') {
346
+ message = userMessage;
347
+ }
348
+
349
+ if (message && message.trim() !== '') {
350
+ if(input_value_type === InputValueType.text){
351
+ addMessage({
352
+ message: message,
353
+ isSend: true,
354
+ rawInfo: { files: fileList.map((fileItem) => fileItem.fileUrl) },
355
+ });
356
+ setValue('');
357
+ setSendingMessage(true);
358
+ }
359
+
360
+ let userInfoClone = userInfo;
361
+ if(isEmpty(userInfoClone.code)) {
362
+ userInfoClone.code = sessionId;
363
+ }
364
+ const code = userInfoClone['code'] ? userInfoClone['code'] : ""
365
+ let embedAppExtend: embedAppExtend = {
366
+ operator_id: code,
367
+ upload_file_path_list: [],
368
+ http_extend: {
369
+ bd:{
370
+ code: code,
371
+ },
372
+ body: {
373
+ code: code,
374
+ },
375
+ },
376
+ };
377
+ if (fileList.length > 0) {
378
+ fileList.forEach((fileItem) => {
379
+ if (fileItem.fileType === 'image' || fileItem.fileType === 'file') {
380
+ embedAppExtend['upload_file_path_list'].push(fileItem.fileUrl);
381
+ }
382
+ });
383
+ }
384
+ setFileList([]);
385
+ handleScroll();
386
+ sendMessage(
387
+ input_value_type,
388
+ embedAppExtend,
389
+ isStream,
390
+ handleMessageContent,
391
+ abortControllerRef.current.signal,
392
+ hostUrl,
393
+ flowId,
394
+ message,
395
+ input_type,
396
+ output_type,
397
+ sessionId,
398
+ output_component,
399
+ tweaks,
400
+ api_key,
401
+ additional_headers,
402
+ )
403
+ .then((res) => {
404
+ // 非流式输出
405
+ if(!isStream){
406
+ sendMessageNoStream(res);
407
+ }
408
+ getHistoryList();
409
+ setSendingMessage(false);
410
+ setNowAIContent('');
411
+ })
412
+ .catch((e) => {
413
+ if (e.name !== 'AbortError') {
414
+ messageTip.error('网络请求错误,请重试');
415
+ addMessage({
416
+ message: '网络错误',
417
+ isSend: false,
418
+ });
419
+ }
420
+ setSendingMessage(false);
421
+ setNowAIContent('');
422
+ })
423
+ .finally(() => {
424
+ if (isFunction(callback)) {
425
+ callback();
426
+ }
427
+ });
428
+ setDropDownList(undefined);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * 获取文件类型
434
+ * @param file File 对象
435
+ */
436
+ const getFileType = (file) => {
437
+ if (!(file instanceof File)) {
438
+ throw new Error('Input must be a File object');
439
+ }
440
+
441
+ const mimeType = file.type;
442
+
443
+ // 判断是否为图片
444
+ if (mimeType.startsWith('image/')) {
445
+ return 'image';
446
+ }
447
+
448
+ // 判断是否为音频
449
+ if (mimeType.startsWith('audio/')) {
450
+ return 'voice';
451
+ }
452
+
453
+ // 默认返回 file
454
+ return 'file';
455
+ };
456
+
457
+ /**
458
+ * 根据文件URL获取文件类型
459
+ * @param url
460
+ */
461
+ const getFileTypeByUrl = (url) => {
462
+ if (!url) {
463
+ return 'file';
464
+ }
465
+ switch (url.split('.').pop().toLowerCase()) {
466
+ case 'jpg':
467
+ case 'jpeg':
468
+ case 'png':
469
+ case 'gif':
470
+ return 'image';
471
+ case 'pdf':
472
+ return 'pdf';
473
+ case 'doc':
474
+ case 'docx':
475
+ return 'word';
476
+ case 'xls':
477
+ case 'xlsx':
478
+ return 'excel';
479
+ case 'md':
480
+ return 'markdown';
481
+ case 'txt':
482
+ return 'txt';
483
+ case 'mobi':
484
+ return 'mobi';
485
+ case 'rpub':
486
+ return 'rpub';
487
+ default:
488
+ return 'file';
489
+ }
490
+ };
491
+
492
+ /**
493
+ * 获取易于阅读的文件大小(将字节转成KB、MB等)
494
+ * @param value 文件字节大小,file.size
495
+ */
496
+ const getFileSize = (value) => {
497
+ if (null == value || value == '') {
498
+ return '0 Bytes';
499
+ }
500
+ var unitArr = new Array('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
501
+ var index = 0;
502
+ var srcsize = parseFloat(value);
503
+ index = Math.floor(Math.log(srcsize) / Math.log(1024));
504
+ var size = srcsize / Math.pow(1024, index);
505
+ size = size.toFixed(2); //保留的小数位数
506
+ return size + unitArr[index];
507
+ };
508
+
509
+ /**
510
+ * 删除文件列表中的文件
511
+ * @param item 点击的File对象
512
+ * @param index 索引
513
+ */
514
+ const removeFile = (item, index) => {
515
+ setFileList(fileList.filter((item, i) => i !== index));
516
+ handleScroll();
517
+ };
518
+
519
+ /**
520
+ * 上传文件
521
+ */
522
+ const uploadFile = () => {
523
+ if (sendingMessage) {
524
+ return;
525
+ }
526
+ const input = document.createElement('input');
527
+ input.type = 'file';
528
+ const acceptFileType = [
529
+ '.png',
530
+ '.jpg',
531
+ '.jpeg',
532
+ // '.pdf',
533
+ // '.doc',
534
+ // '.docx',
535
+ // '.xls',
536
+ // '.xlsx',
537
+ // '.txt',
538
+ // '.md',
539
+ // '.mobi',
540
+ // '.rpub',
541
+ ];
542
+ input.accept = acceptFileType.join(',');
543
+ input.multiple = true; // 允许多选文件
544
+ input.onchange = (event) => {
545
+ const files = event.target.files;
546
+ if (files && files.length > 0) {
547
+ Array.from(files).forEach((file) => {
548
+ if (acceptFileType.includes(`.${file.name.split('.').pop().toLowerCase()}`)) {
549
+ fetchUploadFile(hostUrl, file, flowId, api_key)
550
+ .then((res) => {
551
+ const { data = {} } = res.data;
552
+ const {
553
+ id,
554
+ name,
555
+ save_name,
556
+ url,
557
+ extension,
558
+ } = data;
559
+ const filePreItem = {
560
+ file,
561
+ fileType: getFileType(file),
562
+ fileUrl: url,
563
+ fileId: id,
564
+ };
565
+ setFileList((prevFileList) => [...prevFileList, filePreItem]);
566
+ handleScroll();
567
+ })
568
+ .catch((e) => {
569
+ messageTip.error('上传失败,请重试!');
570
+ });
571
+ } else {
572
+ messageTip.error('已过滤不支持的文件类型');
573
+ }
574
+ });
575
+ }
576
+ };
577
+ input.click();
578
+ };
579
+
580
+ // 向右滚动方法
581
+ const handleScrollRight = (direction) => {
582
+ if (direction == 'right') {
583
+ if (scrollContainerRef.current) {
584
+ scrollContainerRef.current.scrollLeft += 200;
585
+ }
586
+ } else if (direction == 'left') {
587
+ if (scrollContainerRef.current) {
588
+ scrollContainerRef.current.scrollLeft -= 200;
589
+ }
590
+ }
591
+ };
592
+
593
+ /**
594
+ * 语音实时识别
595
+ */
596
+ const startRecord = ()=>{
597
+ if (!('webkitSpeechRecognition' in window)) {
598
+ alert('您的浏览器不支持语音识别功能')
599
+ return;
600
+ }
601
+ if(recordState){
602
+ recognition.stop();
603
+ setRecordState(false);
604
+ }else{
605
+ recognition = new webkitSpeechRecognition();
606
+ recognition.continuous = true;
607
+ recognition.interimResults = true;
608
+ recognition.lang = 'zh-CN';
609
+
610
+ recognition.onresult = (event) => {
611
+ const transcript = Array.from(event.results)
612
+ .map(result => result[0].transcript)
613
+ .join('');
614
+ setValue(transcript)
615
+ };
616
+
617
+ recognition.onerror = (event) => {
618
+ if(event.error === 'not-allowed'){
619
+ messageTip.error("录音权限被拒绝,请允许录音权限后重试")
620
+ }
621
+ console.error('语音识别错误:', event);
622
+ };
623
+ recognition.start()
624
+ setRecordState(true);
625
+ }
626
+ }
627
+
628
+ /**
629
+ * 输出消息时,滚动到底部
630
+ */
631
+ useEffect(() => {
632
+ if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
633
+ }, [messages]);
634
+
635
+ /* Refocus the User input whenever a new response is returned from the LLM */
636
+
637
+ useEffect(() => {
638
+ // after a slight delay
639
+ setTagList(tags);
640
+ setTimeout(() => {
641
+ // inputRef.current?.focus();
642
+ }, 100);
643
+ }, [messages, open, tags]);
644
+
645
+ /**
646
+ * 当获取历史记录时(sessindId变化时),清空消息,并添加历史记录
647
+ */
648
+ useEffect(() => {
649
+ const fetchChatHistory = async () => {
650
+ try {
651
+ let userInfoClone = userInfo;
652
+ if(isEmpty(userInfoClone.code)) {
653
+ userInfoClone.code = sessionId;
654
+ }
655
+ const res = await getChatHistory(hostUrl, flowId, sessionId, userInfoClone['code']);
656
+ const chatHistory = res.data?.length ? res.data : [];
657
+
658
+ clearMessage();
659
+ chatHistory.forEach((data) => {
660
+ addMessage({
661
+ message: extractMessageFromOutput({
662
+ message: data,
663
+ type: data['category'],
664
+ }),
665
+ isSend: data['sender'] === 'User',
666
+ rawInfo: data,
667
+ });
668
+ });
669
+ } catch (error) {
670
+ console.error("Failed to fetch chat history:", error);
671
+ }
672
+ };
673
+
674
+ fetchChatHistory();
675
+
676
+ return () => {
677
+ abortControllerRef.current.abort('disconnect');
678
+ // abortControllerRef是一次性的,中止之后要初始化,否则fetch请求会被中止导致请求无响应
679
+ abortControllerRef.current = new AbortController();
680
+ };
681
+ }, [sessionId]);
682
+
683
+ /**
684
+ * 开场白展示优化
685
+ * 1、如果是全屏模式:则横向展示三项开场白
686
+ * 2、如果是弹窗模式,则当弹窗宽度大于【x】时横向展示三项开场白,当小于等于【x】时则纵向展示三项开场白
687
+ * */
688
+ const judgeDropClass = () => {
689
+ if (modalWidth === undefined) {
690
+ if(isEmpty(dropManUrl)){
691
+ return 'cl-drop-down';
692
+ }
693
+ return 'cl-drop-screen';
694
+ }
695
+ if (modalWidth > 1800) {
696
+ return 'cl-drop-man';
697
+ }
698
+ return 'cl-drop-horizontal'
699
+ }
700
+
701
+ const renderTipsInfo = () => {
702
+ if (isMobile && !isEmpty(dropDownList)){
703
+ return (
704
+ <div className={'cl-tips-wrapper'}>
705
+ <img className="drop-man-img" src={dropManUrl}/>
706
+ <div className="drop-down-title"><strong>Hi,</strong><br/><strong>欢迎使用{window_title}!</strong><br/><div style={{fontSize:'14px'}}>您可以这样问我:</div></div>
707
+ <div className={'cl-drop-down-mobile'}>
708
+ <div className="drop-down-list-mobile">
709
+ {dropDownList.map(({backgroundImg, title}) => (
710
+ <div
711
+ className="drop-down-item-card-mobile"
712
+ key={title}
713
+ onClick={() => {
714
+ setDropDownList(undefined);
715
+ handleSendMessage(title);
716
+ }}>
717
+ <Typography.Paragraph
718
+ className="drop-down-item-title-mobile"
719
+ ellipsis={{
720
+ rows: 2,
721
+ tooltip: `“${title}”`,
722
+ }}>{'“' + title + '”'}
723
+ </Typography.Paragraph>
724
+ <img className="drop-down-item-bottom-img-mobile" src={backgroundImg}/>
725
+ {/*</div>*/}
726
+ </div>
727
+ ))}
728
+ </div>
729
+ </div>
730
+ </div>
731
+ )
732
+ }
733
+ if (!isEmpty(dropDownList)){
734
+ if (isEmpty(dropManUrl)){
735
+ return (
736
+ <div className='cl-drop-down'>
737
+ <div className="drop-down-title">Hi,ssss欢迎使用{window_title},您可以这样问我:</div>
738
+ <div className="drop-down-list">
739
+ {dropDownList.map(({backgroundImg, title}) => (
740
+ <div className="drop-down-item-card" key={title}>
741
+ <Typography.Paragraph
742
+ className="drop-down-item-title"
743
+ ellipsis={{
744
+ rows: 2,
745
+ tooltip: `“${title}”`,
746
+ }}>{'“' + title + '”'}
747
+ </Typography.Paragraph>
748
+ <div className="drop-down-item-bottom">
749
+ <div
750
+ className="drop-down-item-bottom-button"
751
+ onClick={() => {
752
+ setDropDownList(undefined);
753
+ handleSendMessage(title);
754
+ }}
755
+ >
756
+ <img src={btn_answer}/>
757
+ {/*<RightOutlined style={{ height: '0.8rem', width: '0.4rem' }} />*/}
758
+ </div>
759
+ <img className="drop-down-item-bottom-img" src={backgroundImg}/>
760
+ </div>
761
+ </div>
762
+ ))}
763
+ </div>
764
+ </div>
765
+ )
766
+ } else {
767
+ return (
768
+ <div className={judgeDropClass()}>
769
+ <img className="drop-man-img" src={dropManUrl}/>
770
+ <div className='cl-drop-down'>
771
+ <div className="drop-down-title">Hi,欢迎使用{window_title},您可以这样问我:</div>
772
+ <div className="drop-down-list">
773
+ {dropDownList.map(({backgroundImg, title}) => (
774
+ <div
775
+ className="drop-down-item-card"
776
+ key={title}
777
+ onClick={() => {
778
+ setDropDownList(undefined);
779
+ handleSendMessage(title);
780
+ }}>
781
+ <Typography.Paragraph
782
+ className="drop-down-item-title"
783
+ ellipsis={{
784
+ rows: 2,
785
+ tooltip: `“${title}”`,
786
+ }}>{'“' + title + '”'}
787
+ </Typography.Paragraph>
788
+ <div className="drop-down-item-bottom">
789
+ <div
790
+ className="drop-down-item-bottom-button"
791
+ onClick={() => {
792
+ setDropDownList(undefined);
793
+ handleSendMessage(title);
794
+ }}
795
+ >
796
+ <img src={btn_answer}/>
797
+ {/*<RightOutlined style={{ height: '0.8rem', width: '0.4rem' }} />*/}
798
+ </div>
799
+ <img className="drop-down-item-bottom-img" src={backgroundImg}/>
800
+ </div>
801
+ </div>
802
+ ))}
803
+ </div>
804
+ </div>
805
+ </div>
806
+ )
807
+ }
808
+ }
809
+ return <></>
810
+ }
811
+
812
+ return (
813
+ <div
814
+ style={{...chat_window_style, width: width, height: "100%"}}
815
+ ref={ref}
816
+ className="cl-window"
817
+ >
818
+ <div className="cl-middle-container">
819
+ <div className="cl-header" style={isMobile ? {position: 'absolute', top: 20, left: 0, paddingLeft: '1.6rem', paddingRight: '1.6rem'} : {}}>
820
+ {
821
+ isShowChatHeader && (
822
+ <div className="header-title">
823
+ {
824
+ isTitleSideIcon ?
825
+ <img className="p_logoImg" style={{width: logoWidth, height: 'auto', marginRight: 10}}
826
+ src={agentUrl}/> :
827
+ <span className="diamond"/>
828
+ }
829
+ {window_title}
830
+ </div>
831
+ )
832
+ }
833
+ <div className="cl-header-subtitle"/>
834
+ {renderTipsInfo()}
835
+ </div>
836
+ <div
837
+ className="cl-messages_container"
838
+ style={isMobile ? {paddingLeft: '0', paddingRight: '0'} :{}}
839
+ // style={{ maxWidth: '100%', minHeight:'300px', height:'700px', paddingBottom: '56px' }}
840
+ >
841
+ {messages.map((message, index) => (
842
+ <ChatMessage
843
+ bot_message_style={bot_message_style}
844
+ user_message_style={user_message_style}
845
+ error_message_style={error_message_style}
846
+ key={index}
847
+ host_url={hostUrl}
848
+ message={message}
849
+ isSend={message.isSend}
850
+ error={message.error}
851
+ type={message.type}
852
+ rawInfo={message.rawInfo}
853
+ handleSendMessage={handleSendMessage}
854
+ />
855
+ ))}
856
+ {sendingMessage && (nowAIContent?
857
+ <ChatMessage
858
+ bot_message_style={bot_message_style}
859
+ user_message_style={user_message_style}
860
+ error_message_style={error_message_style}
861
+ key={1688574569}
862
+ host_url={hostUrl}
863
+ message={{ message: nowAIContent}}
864
+ isSend={false}
865
+ handleSendMessage={handleSendMessage}
866
+ />:<ChatMessagePlaceholder bot_message_style={bot_message_style} />
867
+ )}
868
+ <div ref={lastMessage}></div>
869
+ </div>
870
+ <div
871
+ className="w_tagListClass"
872
+ style={isMobile ?
873
+ {
874
+ display: tagList?.length > 0 ? '' : 'none',
875
+ zIndex: tagList?.length > 0 ? '2' : '-1',
876
+ maxWidth: 'calc(100vw - 3.2rem)',
877
+ flexWrap: "nowrap",
878
+ overflowX: "auto",
879
+ WebkitOverflowScrolling: 'touch',
880
+ scrollbarWidth: "none",
881
+ overflowY: "hidden",
882
+ background: 'transparent',
883
+ }
884
+ :
885
+ {
886
+ // bottom: fileList.length > 0 ? '130px' : '80px',
887
+ display: tagList?.length > 0 ? '' : 'none',
888
+ zIndex: tagList?.length > 0 ? '2' : '-1',
889
+ }}
890
+ >
891
+ {tagList.map((item, index) => (
892
+ <div
893
+ key={index}
894
+ className="w_tagItemBox"
895
+ onClick={() => {
896
+ handleSendMessage(item?.name);
897
+ setFileList([]);
898
+ }}
899
+ >
900
+ <div>
901
+ <img
902
+ style={item?.img ? {} : { display: 'none' }}
903
+ src={'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/'+ item?.img}
904
+ className="w_tagImgh"
905
+ alt="Image"
906
+ />
907
+ </div>
908
+ <div className="w_tagItemText" style={isMobile ? {wordBreak: "keep-all"} : {}}>{item?.name}</div>
909
+ </div>
910
+ ))}
911
+ </div>
912
+ <div style={input_container_style} className="cl-input_container">
913
+ <div className="w_file_preview" ref={scrollContainerRef} onScroll={handleScroll}>
914
+ <div className="w_toLeftBox" style={{display: showLeftArrow ? 'flex' : 'none'}}>
915
+ <div className="w_toLeft" onClick={() => handleScrollRight('left')}>
916
+ <img src={toLeftPng}/>
917
+ </div>
918
+ </div>
919
+ <div className="w_toRightBox" style={{display: showRightArrow ? 'flex' : 'none'}}>
920
+ <div className="w_toRight" onClick={() => handleScrollRight('right')}>
921
+ <img src={toRightPng}/>
922
+ </div>
923
+ </div>
924
+ {fileList.map((item, index) => {
925
+ return (
926
+ <div key={item.filePath} className="w_fileBox">
927
+ {item.fileType == 'image' && (
928
+ <Image
929
+ height={40}
930
+ width={40}
931
+ src={item.fileUrl}
932
+ className="w_upImg"
933
+ preview={{
934
+ mask: <span className="custom-mask"></span>,
935
+ }}
936
+ />
937
+ )}
938
+ {getFileTypeByUrl(item.file.name) == 'pdf' && (
939
+ <img style={{width: 40, height: 40}} src={typePdfPng}/>
940
+ )}
941
+ {getFileTypeByUrl(item.file.name) == 'word' && (
942
+ <img style={{width: 40, height: 40}} src={typeWordPng}/>
943
+ )}
944
+ {getFileTypeByUrl(item.file.name) == 'excel' && (
945
+ <img style={{width: 40, height: 40}} src={typeExcelPng}/>
946
+ )}
947
+ {getFileTypeByUrl(item.file.name) == 'markdown' && (
948
+ <img style={{width: 40, height: 40}} src={typeMarkdownPng}/>
949
+ )}
950
+ {getFileTypeByUrl(item.file.name) == 'txt' && (
951
+ <img style={{width: 40, height: 40}} src={typeTextPng}/>
952
+ )}
953
+ {getFileTypeByUrl(item.file.name) == 'mobi' && (
954
+ <img style={{width: 40, height: 40}} src={typeMobiPng}/>
955
+ )}
956
+ {getFileTypeByUrl(item.file.name) == 'rpub' && (
957
+ <img style={{width: 40, height: 40}} src={typeRPubPng}/>
958
+ )}
959
+ {getFileTypeByUrl(item.file.name) == 'file' && (
960
+ <img style={{width: 40, height: 40}} src={upFilePng}/>
961
+ )}
962
+ <div className="w_fileInfoBox">
963
+ <div className="w_fileInfoFileName">{item.file.name}</div>
964
+ <div className="w_fileInfoMeta">{`${item.fileType} · ${getFileSize(
965
+ item.file.size,
966
+ )}`}</div>
967
+ </div>
968
+ <div
969
+ className="w_fileRemove"
970
+ onClick={() => {
971
+ removeFile(item, index);
972
+ }}
973
+ >
974
+ <img src={closexPng}/>
975
+ </div>
976
+ </div>
977
+ );
978
+ })}
979
+ </div>
980
+ <div className="w_inputBox" style={{height: inputContainerHeight}}>
981
+ <textarea
982
+ onFocus={()=>setInputContainerHeight('120px')}
983
+ onBlur={()=>setInputContainerHeight('50px')}
984
+ value={value}
985
+ onChange={(e) => setValue(e.target.value)}
986
+ onKeyDown={(e) => {
987
+ if (e.key === 'Enter' && !e.shiftKey) handleSendMessage();
988
+ }}
989
+ disabled={sendingMessage}
990
+ placeholder={
991
+ sendingMessage
992
+ ? placeholder_sending || '思考中...'
993
+ : placeholder || '请输入您的问题...'
994
+ }
995
+ style={{...input_style,resize:"none"}}
996
+ ref={inputRef}
997
+ className="cl-input-element"
998
+ />
999
+ {
1000
+ isShowVoiceButton &&
1001
+ <Tooltip title={recordState ? "点击结束录音" : "点击开始录音"}>
1002
+ <div
1003
+ className="w_send_voice_box"
1004
+ style={sendingMessage ? { cursor: 'not-allowed' } : {}}
1005
+ onClick={startRecord}
1006
+ >
1007
+ <img src={recordState ? soundWavePng : luyinPng} style={{ width: 23 }} className={recordState ? "w_recordIng" : ''}></img>
1008
+ </div>
1009
+ </Tooltip>
1010
+ }
1011
+ {/*<Tooltip title="支持PDF / Word / Excel / Markdown / txt / mobi / rpub">*/}
1012
+ {
1013
+ isShowUploadButton &&
1014
+ <Tooltip title="支持图片格式">
1015
+ <div
1016
+ className="w_send_file_box"
1017
+ style={sendingMessage ? { cursor: 'not-allowed' } : {}}
1018
+ onClick={uploadFile}
1019
+ >
1020
+ <img src={fileuploadPng} style={{ width: 23 }}></img>
1021
+ </div>
1022
+ </Tooltip>
1023
+ }
1024
+ <Tooltip title={sendingMessage?'点击暂停':'点击发送'}>
1025
+ <button
1026
+ style={{ ...(sendingMessage ? { cursor: 'pointer' } : {}), padding: '0 13px', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center' }}
1027
+ onClick={() => {
1028
+ if(sendingMessage){
1029
+ const output = {
1030
+ message:nowAIContent,
1031
+ type:'text'
1032
+ };
1033
+ addMessage({
1034
+ message: extractMessageFromOutput(output),
1035
+ isSend: false,
1036
+ rawInfo: output.message,
1037
+ });
1038
+ abortControllerRef.current.abort('disconnect');
1039
+ abortControllerRef.current = new AbortController();
1040
+ }else{
1041
+ handleSendMessage()
1042
+ }
1043
+ }}
1044
+ >
1045
+
1046
+ <img src={sendingMessage ? stopmessagePng : sendmessagePng} style={{width: 55}}/>
1047
+ </button>
1048
+ </Tooltip>
1049
+ </div>
1050
+ </div>
1051
+ </div>
1052
+ </div>
1053
+ );
1054
+ }