yt-chat-components 1.1.2 → 1.1.3

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