yt-chat-components 1.2.2 → 1.2.4

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