yt-chat-components 0.1.0

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 (89) hide show
  1. package/.idea/langflow-embedded-chat.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/sonarlint/issuestore/0/f/0f8c0c92cf798431ebb931ff6e997b1af86ecee5 +0 -0
  4. package/.idea/sonarlint/issuestore/3/9/39129446b425a1d640160c068e4194e96639eedf +0 -0
  5. package/.idea/sonarlint/issuestore/4/a/4a2f33951ce07c1ff7184f91877aa13db05d3785 +0 -0
  6. package/.idea/sonarlint/issuestore/4/a/4a7b99bdbee5792679d347b6474463bf5e14b66d +0 -0
  7. package/.idea/sonarlint/issuestore/4/b/4b6989b8ccae808ebc45d02230d336ea53800365 +0 -0
  8. package/.idea/sonarlint/issuestore/6/c/6c024c1d0ad64656b9d4b0695ec3c49c0454addf +0 -0
  9. package/.idea/sonarlint/issuestore/8/d/8d6123af13a140f93e06299fff7ea23c547e9ec8 +0 -0
  10. package/.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d +0 -0
  11. package/.idea/sonarlint/issuestore/d/9/d938938695d447dadda115e28781c6541f53fc4f +0 -0
  12. package/.idea/sonarlint/issuestore/index.pb +19 -0
  13. package/.idea/vcs.xml +6 -0
  14. package/README.md +274 -0
  15. package/build/asset-manifest.json +16 -0
  16. package/build/index.html +1 -0
  17. package/build/static/css/main.6f7c593d.css +2 -0
  18. package/build/static/css/main.6f7c593d.css.map +1 -0
  19. package/build/static/js/bundle.min.js +2 -0
  20. package/build/static/js/bundle.min.js.LICENSE.txt +124 -0
  21. package/build/static/js/main.cb252095.js +3 -0
  22. package/build/static/js/main.cb252095.js.LICENSE.txt +134 -0
  23. package/build/static/js/main.cb252095.js.map +1 -0
  24. package/build/static/media/aiavatar.74bafa995cce4c01b804.png +0 -0
  25. package/build/static/media/history-list-empty.1eb65b1550aef4e8c8a4.png +0 -0
  26. package/build/static/media/moreBg.9fc998472925cecd89f2.png +0 -0
  27. package/package.json +75 -0
  28. package/public/index.html +47 -0
  29. package/src/YtChatView/chatWidget/chatWindow/chatMessage/index.module.css +86 -0
  30. package/src/YtChatView/chatWidget/chatWindow/chatMessage/index.tsx +211 -0
  31. package/src/YtChatView/chatWidget/chatWindow/chatPlaceholder/index.module.css +9 -0
  32. package/src/YtChatView/chatWidget/chatWindow/chatPlaceholder/index.tsx +23 -0
  33. package/src/YtChatView/chatWidget/chatWindow/controllers/index.ts +236 -0
  34. package/src/YtChatView/chatWidget/chatWindow/index.module.css +197 -0
  35. package/src/YtChatView/chatWidget/chatWindow/index.tsx +791 -0
  36. package/src/YtChatView/chatWidget/chatWindow/types/chatWidget/index.ts +37 -0
  37. package/src/YtChatView/chatWidget/chatWindow/utils.ts +75 -0
  38. package/src/YtChatView/chatWidget/index.tsx +2289 -0
  39. package/src/YtChatView/logoBtn/index.css +4 -0
  40. package/src/YtChatView/logoBtn/index.jsx +65 -0
  41. package/src/YtChatView/logoSplitBtn/index.css +4 -0
  42. package/src/YtChatView/logoSplitBtn/index.jsx +67 -0
  43. package/src/YtChatView/previewDialog/index.jsx +431 -0
  44. package/src/YtChatView/previewDialog/index.module.css +144 -0
  45. package/src/assets/aicenter/add.png +0 -0
  46. package/src/assets/aicenter/aiavatar.png +0 -0
  47. package/src/assets/aicenter/aicenterbg.png +0 -0
  48. package/src/assets/aicenter/aicenterbgdark.png +0 -0
  49. package/src/assets/aicenter/close.png +0 -0
  50. package/src/assets/aicenter/closex.png +0 -0
  51. package/src/assets/aicenter/copy.png +0 -0
  52. package/src/assets/aicenter/file.png +0 -0
  53. package/src/assets/aicenter/fileupload.png +0 -0
  54. package/src/assets/aicenter/history-list-empty.png +0 -0
  55. package/src/assets/aicenter/history.png +0 -0
  56. package/src/assets/aicenter/luyin.png +0 -0
  57. package/src/assets/aicenter/moreAi.png +0 -0
  58. package/src/assets/aicenter/moreBg.png +0 -0
  59. package/src/assets/aicenter/play-run.gif +0 -0
  60. package/src/assets/aicenter/play.png +0 -0
  61. package/src/assets/aicenter/send-img.png +0 -0
  62. package/src/assets/aicenter/send-question-black.png +0 -0
  63. package/src/assets/aicenter/send-question.png +0 -0
  64. package/src/assets/aicenter/sendmessage.png +0 -0
  65. package/src/assets/aicenter/sound-wave.gif +0 -0
  66. package/src/assets/aicenter/toLeft.png +0 -0
  67. package/src/assets/aicenter/toRight.png +0 -0
  68. package/src/assets/aicenter/type-excel.png +0 -0
  69. package/src/assets/aicenter/type-markdown.png +0 -0
  70. package/src/assets/aicenter/type-mobi.png +0 -0
  71. package/src/assets/aicenter/type-pdf.png +0 -0
  72. package/src/assets/aicenter/type-rpub.png +0 -0
  73. package/src/assets/aicenter/type-text.png +0 -0
  74. package/src/assets/aicenter/type-word.png +0 -0
  75. package/src/assets/aicenter/upfile.png +0 -0
  76. package/src/chatPlaceholder/index.tsx +18 -0
  77. package/src/chatWidget/chatTrigger/index.tsx +15 -0
  78. package/src/chatWidget/chatWindow/chatMessage/index.tsx +42 -0
  79. package/src/chatWidget/chatWindow/index.tsx +426 -0
  80. package/src/chatWidget/index.tsx +2195 -0
  81. package/src/chatWidget/utils.ts +76 -0
  82. package/src/controllers/index.ts +205 -0
  83. package/src/index.tsx +60 -0
  84. package/src/react-app-env.d.ts +1 -0
  85. package/src/reportWebVitals.ts +15 -0
  86. package/src/setupTests.ts +5 -0
  87. package/src/types/chatWidget/index.ts +13 -0
  88. package/tsconfig.json +26 -0
  89. package/webpack.config.js +51 -0
@@ -0,0 +1,791 @@
1
+ // @ts-nocheck
2
+ import { extractMessageFromOutput } from './utils';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { ChatMessageType, embedAppExtend } 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 toRightPng from '../../../assets/aicenter/toRight.png';
14
+ import toLeftPng from '../../../assets/aicenter/toLeft.png';
15
+ import typePdfPng from '../../../assets/aicenter/type-pdf.png';
16
+ import typeWordPng from '../../../assets/aicenter/type-word.png';
17
+ import typeExcelPng from '../../../assets/aicenter/type-excel.png';
18
+ import typeMarkdownPng from '../../../assets/aicenter/type-markdown.png';
19
+ import typeTextPng from '../../../assets/aicenter/type-text.png';
20
+ import typeMobiPng from '../../../assets/aicenter/type-mobi.png';
21
+ import typeRPubPng from '../../../assets/aicenter/type-rpub.png';
22
+ import soundWavePng from '../../../assets/aicenter/sound-wave.gif';
23
+ import luyinPng from '../../../assets/aicenter/luyin.png';
24
+ import { RightOutlined } from '@ant-design/icons';
25
+ import { Image, message as messageTip, Tooltip } from 'antd';
26
+ import { isEmpty, isFunction } from 'lodash';
27
+ import {MoreHorizontal} from "lucide-react";
28
+
29
+
30
+ let mediaRecorder = null; // 语音对象,用于录音
31
+ let recognition = null; // 语音识别对象
32
+
33
+ export default function ChatWindow({
34
+ tags,
35
+ getHistoryList,
36
+ userInfo,
37
+ clearMessage,
38
+ api_key,
39
+ flowId,
40
+ hostUrl,
41
+ updateLastMessage,
42
+ messages,
43
+ output_type,
44
+ input_type,
45
+ output_component,
46
+ bot_message_style,
47
+ send_icon_style,
48
+ user_message_style,
49
+ chat_window_style,
50
+ error_message_style,
51
+ placeholder_sending,
52
+ send_button_style = { paddingTop: '6px' },
53
+ online = true,
54
+ open,
55
+ online_message = '在线',
56
+ offline_message = '离线',
57
+ window_title = 'AI对话',
58
+ placeholder,
59
+ input_style,
60
+ input_container_style,
61
+ addMessage,
62
+ position,
63
+ triggerRef,
64
+ width = '900',
65
+ height = '300',
66
+ tweaks,
67
+ sessionId,
68
+ additional_headers,
69
+ setDropDownList = () => {},
70
+ dropDownList = [],
71
+ }: {
72
+ tags: [];
73
+ getHistoryList: Function;
74
+ userInfo: object;
75
+ clearMessage: Function;
76
+ api_key?: string;
77
+ output_type: string;
78
+ input_type: string;
79
+ output_component?: string;
80
+ bot_message_style?: React.CSSProperties;
81
+ send_icon_style?: React.CSSProperties;
82
+ user_message_style?: React.CSSProperties;
83
+ chat_window_style?: React.CSSProperties;
84
+ error_message_style?: React.CSSProperties;
85
+ send_button_style?: React.CSSProperties;
86
+ online?: boolean;
87
+ open: boolean;
88
+ online_message?: string;
89
+ placeholder_sending?: string;
90
+ offline_message?: string;
91
+ window_title?: string;
92
+ placeholder?: string;
93
+ input_style?: React.CSSProperties;
94
+ input_container_style?: React.CSSProperties;
95
+ tweaks?: { [key: string]: any };
96
+ flowId: string;
97
+ hostUrl: string;
98
+ updateLastMessage: Function;
99
+ messages: ChatMessageType[];
100
+ addMessage: Function;
101
+ position?: string;
102
+ triggerRef: React.RefObject<HTMLButtonElement>;
103
+ width?: number;
104
+ height?: string;
105
+ sessionId: string;
106
+ additional_headers?: { [key: string]: string };
107
+ }) {
108
+ const [value, setValue] = useState<string>('');
109
+ const ref = useRef<HTMLDivElement>(null);
110
+ const lastMessage = useRef<HTMLDivElement>(null);
111
+ const [windowPosition, setWindowPosition] = useState({ left: '0', top: '0' });
112
+ const inputRef = useRef<HTMLInputElement>(null); /* User input Ref */
113
+ /* Initial listener for loss of focus that refocuses User input after a small delay */
114
+ const [nowAIContent, setNowAIContent] = useState<string>('');
115
+ const [sendingMessage, setSendingMessage] = useState(false);
116
+ const abortControllerRef = useRef(new AbortController());
117
+ const [fileList, setFileList] = useState<
118
+ { file: File; fileUrl: string; fileType: string; fileId: string }[]
119
+ >([]);
120
+ const scrollContainerRef = useRef(null);
121
+ const [showLeftArrow, setShowLeftArrow] = useState(false); // 控制左侧箭头显示
122
+ const [showRightArrow, setShowRightArrow] = useState(false); // 控制右侧箭头显示
123
+ const isStreamOutput = false;//是否流式输出(手动开关)
124
+ const [recordState, setRecordState] = useState(false); // 录音状态。true为正在录音,false为停止录音
125
+ let voiceChunks = []; // 临时存储录制的语音片段
126
+ // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
127
+ const handleScroll = () => {
128
+ if (scrollContainerRef.current) {
129
+ const { scrollLeft, clientWidth, scrollWidth } = scrollContainerRef.current;
130
+
131
+ // 判断内容是否超出容器
132
+ const isContentOverflowed = scrollWidth > clientWidth;
133
+
134
+ // 判断是否滚动到最左侧
135
+ setShowLeftArrow(isContentOverflowed && scrollLeft > 0);
136
+
137
+ // 判断是否滚动到最右侧
138
+ setShowRightArrow(isContentOverflowed && scrollLeft + clientWidth < scrollWidth);
139
+ }
140
+ };
141
+
142
+ // 流式输出消息,实时显示(token为流式输出内容,end为结束输出,整体输出一次)
143
+ const handleMessageContent = (event, data) => {
144
+ if (event == 'add_message' && data['sender'] == 'Machine') {
145
+ getHistoryList();
146
+ } else if (event == 'token') {
147
+ setNowAIContent((prevState) => prevState + data['chunk']);
148
+ if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
149
+ } else if (event == 'end') {
150
+ const res = {
151
+ data: data['result'],
152
+ };
153
+ if (
154
+ res.data &&
155
+ res.data.outputs &&
156
+ Object.keys(res.data.outputs).length > 0 &&
157
+ res.data.outputs[0].outputs &&
158
+ res.data.outputs[0].outputs.length > 0
159
+ ) {
160
+ const flowOutputs: Array<any> = res.data.outputs[0].outputs;
161
+ if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
162
+ Object.values(
163
+ flowOutputs.find((e) => e.component_id === output_component).outputs,
164
+ ).forEach((output: any) => {
165
+ addMessage({
166
+ message: extractMessageFromOutput(output),
167
+ isSend: false,
168
+ rawInfo: output.message,
169
+ });
170
+ });
171
+ } else if (flowOutputs.length === 1) {
172
+ Object.values(flowOutputs[0].outputs).forEach((output: any) => {
173
+ addMessage({
174
+ message: extractMessageFromOutput(output),
175
+ isSend: false,
176
+ rawInfo: output.message,
177
+ });
178
+ });
179
+ } else {
180
+ flowOutputs
181
+ .sort((a, b) => {
182
+ // Get the earliest timestamp from each flowOutput's outputs
183
+ const aTimestamp = Math.min(
184
+ ...Object.values(a.outputs).map((output: any) =>
185
+ Date.parse(output.message?.timestamp),
186
+ ),
187
+ );
188
+ const bTimestamp = Math.min(
189
+ ...Object.values(b.outputs).map((output: any) =>
190
+ Date.parse(output.message?.timestamp),
191
+ ),
192
+ );
193
+ return aTimestamp - bTimestamp; // Sort descending (newest first)
194
+ })
195
+ .forEach((flowOutput) => {
196
+ Object.values(flowOutput.outputs).forEach((output: any) => {
197
+ addMessage({
198
+ message: extractMessageFromOutput(output),
199
+ isSend: false,
200
+ rawInfo: output.message,
201
+ });
202
+ });
203
+ });
204
+ }
205
+ }
206
+ setSendingMessage(false);
207
+ }
208
+ };
209
+
210
+ const sendMessageNoStream = (res) => {
211
+ if (
212
+ res.data &&
213
+ res.data.outputs &&
214
+ Object.keys(res.data.outputs).length > 0 &&
215
+ res.data.outputs[0].outputs &&
216
+ res.data.outputs[0].outputs.length > 0
217
+ ) {
218
+ const flowOutputs: Array<any> = res.data.outputs[0].outputs;
219
+ if (output_component && flowOutputs.map((e) => e.component_id).includes(output_component)) {
220
+ Object.values(flowOutputs.find((e) => e.component_id === output_component).outputs).forEach(
221
+ (output: any) => {
222
+ addMessage({
223
+ message: extractMessageFromOutput(output),
224
+ isSend: false,
225
+ });
226
+ },
227
+ );
228
+ } else if (flowOutputs.length === 1) {
229
+ Object.values(flowOutputs[0].outputs).forEach((output: any) => {
230
+ addMessage({
231
+ message: extractMessageFromOutput(output),
232
+ isSend: false,
233
+ });
234
+ });
235
+ } else {
236
+ flowOutputs
237
+ .sort((a, b) => {
238
+ // Get the earliest timestamp from each flowOutput's outputs
239
+ const aTimestamp = Math.min(
240
+ ...Object.values(a.outputs).map((output: any) =>
241
+ Date.parse(output.message?.timestamp),
242
+ ),
243
+ );
244
+ const bTimestamp = Math.min(
245
+ ...Object.values(b.outputs).map((output: any) =>
246
+ Date.parse(output.message?.timestamp),
247
+ ),
248
+ );
249
+ return aTimestamp - bTimestamp; // Sort descending (newest first)
250
+ })
251
+ .forEach((flowOutput) => {
252
+ Object.values(flowOutput.outputs).forEach((output: any) => {
253
+ addMessage({
254
+ message: extractMessageFromOutput(output),
255
+ isSend: false,
256
+ });
257
+ });
258
+ });
259
+ }
260
+ }
261
+ setSendingMessage(false);
262
+ };
263
+
264
+ // 点击send发送按钮,进行消息提交发送逻辑;userMessage为传入的文本消息,优先级比input中value高
265
+ function handleClick(userMessage, callback = () => {}) {
266
+ let message = '';
267
+ if (value && value.trim() !== '') {
268
+ message = value;
269
+ setDropDownList(undefined);
270
+ }
271
+ // 有传入消息,则优先使用传入的消息
272
+ if (userMessage && userMessage?.trim() !== '') {
273
+ message = userMessage;
274
+ }
275
+
276
+ if (message && message.trim() !== '') {
277
+ addMessage({
278
+ message: message,
279
+ isSend: true,
280
+ rawInfo: { files: fileList.map((fileItem) => fileItem.fileUrl) },
281
+ });
282
+ setSendingMessage(true);
283
+ setValue('');
284
+ const code = userInfo['code'] ? userInfo['code'] : ""
285
+ let embedAppExtend: embedAppExtend = {
286
+ operator_id: code,
287
+ upload_file_path_list: [],
288
+ http_extend: {
289
+ bd:{
290
+ code: code,
291
+ },
292
+ body: {
293
+ code: code,
294
+ },
295
+ },
296
+ };
297
+ if (fileList.length > 0) {
298
+ fileList.forEach((fileItem) => {
299
+ if (fileItem.fileType === 'image' || fileItem.fileType === 'file') {
300
+ embedAppExtend['upload_file_path_list'].push(fileItem.fileUrl);
301
+ }
302
+ });
303
+ }
304
+ setFileList([]);
305
+ handleScroll();
306
+ sendMessage(
307
+ embedAppExtend,
308
+ isStreamOutput,
309
+ handleMessageContent,
310
+ abortControllerRef.current.signal,
311
+ hostUrl,
312
+ flowId,
313
+ message,
314
+ input_type,
315
+ output_type,
316
+ sessionId,
317
+ output_component,
318
+ tweaks,
319
+ api_key,
320
+ additional_headers,
321
+ )
322
+ .then((res) => {
323
+ // 非流式输出
324
+ if(!isStreamOutput){
325
+ sendMessageNoStream(res);
326
+ }
327
+ getHistoryList();
328
+ setSendingMessage(false);
329
+ setNowAIContent('');
330
+ })
331
+ .catch((e) => {
332
+ if (e.name !== 'AbortError') {
333
+ messageTip.error('网络请求错误,请重试');
334
+ addMessage({
335
+ message: '网络错误',
336
+ isSend: false,
337
+ });
338
+ }
339
+ setSendingMessage(false);
340
+ setNowAIContent('');
341
+ })
342
+ .finally(() => {
343
+ if (isFunction(callback)) {
344
+ callback();
345
+ }
346
+ });
347
+ setDropDownList(undefined);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * 获取文件类型
353
+ * @param file File 对象
354
+ */
355
+ const getFileType = (file) => {
356
+ if (!(file instanceof File)) {
357
+ throw new Error('Input must be a File object');
358
+ }
359
+
360
+ const mimeType = file.type;
361
+
362
+ // 判断是否为图片
363
+ if (mimeType.startsWith('image/')) {
364
+ return 'image';
365
+ }
366
+
367
+ // 判断是否为音频
368
+ if (mimeType.startsWith('audio/')) {
369
+ return 'voice';
370
+ }
371
+
372
+ // 默认返回 file
373
+ return 'file';
374
+ };
375
+
376
+ /**
377
+ * 根据文件URL获取文件类型
378
+ * @param url
379
+ */
380
+ const getFileTypeByUrl = (url) => {
381
+ if (!url) {
382
+ return 'file';
383
+ }
384
+ switch (url.split('.').pop().toLowerCase()) {
385
+ case 'jpg':
386
+ case 'jpeg':
387
+ case 'png':
388
+ case 'gif':
389
+ return 'image';
390
+ case 'pdf':
391
+ return 'pdf';
392
+ case 'doc':
393
+ case 'docx':
394
+ return 'word';
395
+ case 'xls':
396
+ case 'xlsx':
397
+ return 'excel';
398
+ case 'md':
399
+ return 'markdown';
400
+ case 'txt':
401
+ return 'txt';
402
+ case 'mobi':
403
+ return 'mobi';
404
+ case 'rpub':
405
+ return 'rpub';
406
+ default:
407
+ return 'file';
408
+ }
409
+ };
410
+
411
+ /**
412
+ * 获取易于阅读的文件大小(将字节转成KB、MB等)
413
+ * @param value 文件字节大小,file.size
414
+ */
415
+ const getFileSize = (value) => {
416
+ if (null == value || value == '') {
417
+ return '0 Bytes';
418
+ }
419
+ var unitArr = new Array('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
420
+ var index = 0;
421
+ var srcsize = parseFloat(value);
422
+ index = Math.floor(Math.log(srcsize) / Math.log(1024));
423
+ var size = srcsize / Math.pow(1024, index);
424
+ size = size.toFixed(2); //保留的小数位数
425
+ return size + unitArr[index];
426
+ };
427
+
428
+ /**
429
+ * 删除文件列表中的文件
430
+ * @param item 点击的File对象
431
+ * @param index 索引
432
+ */
433
+ const removeFile = (item, index) => {
434
+ setFileList(fileList.filter((item, i) => i !== index));
435
+ handleScroll();
436
+ };
437
+
438
+ /**
439
+ * 上传文件
440
+ */
441
+ const uploadFile = () => {
442
+ if (sendingMessage) {
443
+ return;
444
+ }
445
+ const input = document.createElement('input');
446
+ input.type = 'file';
447
+ const acceptFileType = [
448
+ '.png',
449
+ '.jpg',
450
+ '.jpeg',
451
+ // '.pdf',
452
+ // '.doc',
453
+ // '.docx',
454
+ // '.xls',
455
+ // '.xlsx',
456
+ // '.txt',
457
+ // '.md',
458
+ // '.mobi',
459
+ // '.rpub',
460
+ ];
461
+ input.accept = acceptFileType.join(',');
462
+ input.multiple = true; // 允许多选文件
463
+ input.onchange = (event) => {
464
+ const files = event.target.files;
465
+ if (files && files.length > 0) {
466
+ Array.from(files).forEach((file) => {
467
+ if (acceptFileType.includes(`.${file.name.split('.').pop().toLowerCase()}`)) {
468
+ fetchUploadFile(hostUrl, file, flowId, api_key)
469
+ .then((res) => {
470
+ const { data = {} } = res.data;
471
+ const {
472
+ id,
473
+ name,
474
+ save_name,
475
+ url,
476
+ extension,
477
+ } = data;
478
+ const filePreItem = {
479
+ file,
480
+ fileType: getFileType(file),
481
+ fileUrl: url,
482
+ fileId: id,
483
+ };
484
+ setFileList((prevFileList) => [...prevFileList, filePreItem]);
485
+ handleScroll();
486
+ })
487
+ .catch((e) => {
488
+ messageTip.error('上传失败,请重试!');
489
+ });
490
+ } else {
491
+ messageTip.error('已过滤不支持的文件类型');
492
+ }
493
+ });
494
+ }
495
+ };
496
+ input.click();
497
+ };
498
+
499
+ // 向右滚动方法
500
+ const handleScrollRight = (direction) => {
501
+ if (direction == 'right') {
502
+ if (scrollContainerRef.current) {
503
+ scrollContainerRef.current.scrollLeft += 200;
504
+ }
505
+ } else if (direction == 'left') {
506
+ if (scrollContainerRef.current) {
507
+ scrollContainerRef.current.scrollLeft -= 200;
508
+ }
509
+ }
510
+ };
511
+
512
+ /**
513
+ * 语音实时识别
514
+ */
515
+ const startRecord = ()=>{
516
+ if (!('webkitSpeechRecognition' in window)) {
517
+ alert('您的浏览器不支持语音识别功能')
518
+ return;
519
+ }
520
+ if(recordState){
521
+ recognition.stop();
522
+ setRecordState(false);
523
+ }else{
524
+ recognition = new webkitSpeechRecognition();
525
+ recognition.continuous = true;
526
+ recognition.interimResults = true;
527
+ recognition.lang = 'zh-CN';
528
+
529
+ recognition.onresult = (event) => {
530
+ const transcript = Array.from(event.results)
531
+ .map(result => result[0].transcript)
532
+ .join('');
533
+ setValue(transcript)
534
+ };
535
+
536
+ recognition.onerror = (event) => {
537
+ if(event.error === 'not-allowed'){
538
+ messageTip.error("录音权限被拒绝,请允许录音权限后重试")
539
+ }
540
+ console.error('语音识别错误:', event);
541
+ };
542
+ recognition.start()
543
+ setRecordState(true);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * 输出消息时,滚动到底部
549
+ */
550
+ useEffect(() => {
551
+ if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
552
+ }, [messages]);
553
+
554
+ /* Refocus the User input whenever a new response is returned from the LLM */
555
+
556
+ useEffect(() => {
557
+ // after a slight delay
558
+ setTimeout(() => {
559
+ inputRef.current?.focus();
560
+ }, 100);
561
+ }, [messages, open]);
562
+
563
+ /**
564
+ * 当获取历史记录时(sessindId变化时),清空消息,并添加历史记录
565
+ */
566
+ useEffect(() => {
567
+ const fetchChatHistory = async () => {
568
+ try {
569
+ const res = await getChatHistory(hostUrl, flowId, sessionId, userInfo['code']);
570
+ const chatHistory = res.data?.length ? res.data : [];
571
+
572
+ clearMessage();
573
+ chatHistory.forEach((data) => {
574
+ addMessage({
575
+ message: extractMessageFromOutput({
576
+ message: data,
577
+ type: data['category'],
578
+ }),
579
+ isSend: data['sender'] === 'User',
580
+ rawInfo: data,
581
+ });
582
+ });
583
+ } catch (error) {
584
+ console.error("Failed to fetch chat history:", error);
585
+ }
586
+ };
587
+
588
+ fetchChatHistory();
589
+
590
+ return () => {
591
+ abortControllerRef.current.abort();
592
+ };
593
+ }, [sessionId]);
594
+
595
+
596
+ return (
597
+ <div
598
+ style={{ ...chat_window_style, width: width, height: "100%" }}
599
+ ref={ref}
600
+ className="cl-window"
601
+ >
602
+ <div className="cl-header">
603
+ {window_title}
604
+ <div className="cl-header-subtitle">
605
+ {online ? (
606
+ <>
607
+ <div className="cl-online-message"></div>
608
+ {online_message}
609
+ </>
610
+ ) : (
611
+ <>
612
+ <div className="cl-offline-message"></div>
613
+ {offline_message}
614
+ </>
615
+ )}
616
+ </div>
617
+ {!isEmpty(dropDownList) && (
618
+ <div className="cl-drop-down">
619
+ <div className="drop-down-title">Hi,欢迎使用{window_title},您可以这样问我:</div>
620
+ <div className="drop-down-list">
621
+ {dropDownList.map(({ backgroundImg, title }) => (
622
+ <div className="drop-down-item-card" key={title}>
623
+ <div className="drop-down-item-title">“{title}”</div>
624
+ <div className="drop-down-item-bottom">
625
+ <div
626
+ className="drop-down-item-bottom-button"
627
+ onClick={() => {
628
+ setDropDownList(undefined);
629
+ handleClick(title);
630
+ }}
631
+ >
632
+ 去提问&nbsp;
633
+ <RightOutlined style={{ height: '0.8rem', width: '0.4rem' }} />
634
+ </div>
635
+ <img className="drop-down-item-bottom-img" src={backgroundImg} />
636
+ </div>
637
+ </div>
638
+ ))}
639
+ </div>
640
+ </div>
641
+ )}
642
+ &nbsp;
643
+ </div>
644
+ <div
645
+ className="cl-messages_container"
646
+ // style={{ maxWidth: '100%', minHeight:'300px', height:'700px', paddingBottom: '56px' }}
647
+ >
648
+ {messages.map((message, index) => (
649
+ <ChatMessage
650
+ bot_message_style={bot_message_style}
651
+ user_message_style={user_message_style}
652
+ error_message_style={error_message_style}
653
+ key={index}
654
+ host_url={hostUrl}
655
+ message={message}
656
+ isSend={message.isSend}
657
+ error={message.error}
658
+ />
659
+ ))}
660
+ {sendingMessage && (nowAIContent?
661
+ <ChatMessage
662
+ bot_message_style={bot_message_style}
663
+ user_message_style={user_message_style}
664
+ error_message_style={error_message_style}
665
+ key={1688574569}
666
+ host_url={hostUrl}
667
+ message={{ message: nowAIContent}}
668
+ isSend={false}
669
+ />:<ChatMessagePlaceholder bot_message_style={bot_message_style} />
670
+ )}
671
+ <div ref={lastMessage}></div>
672
+ </div>
673
+
674
+ <div style={input_container_style} className="cl-input_container">
675
+ <div className="w_file_preview" ref={scrollContainerRef} onScroll={handleScroll}>
676
+ <div className="w_toLeftBox" style={{ display: showLeftArrow ? 'flex' : 'none' }}>
677
+ <div className="w_toLeft" onClick={() => handleScrollRight('left')}>
678
+ <img src={toLeftPng} />
679
+ </div>
680
+ </div>
681
+ <div className="w_toRightBox" style={{ display: showRightArrow ? 'flex' : 'none' }}>
682
+ <div className="w_toRight" onClick={() => handleScrollRight('right')}>
683
+ <img src={toRightPng} />
684
+ </div>
685
+ </div>
686
+ {fileList.map((item, index) => {
687
+ return (
688
+ <div key={item.filePath} className="w_fileBox">
689
+ {item.fileType == 'image' && (
690
+ <Image
691
+ height={40}
692
+ width={40}
693
+ src={item.fileUrl}
694
+ className="w_upImg"
695
+ preview={{
696
+ mask: <span className="custom-mask"></span>,
697
+ }}
698
+ />
699
+ )}
700
+ {getFileTypeByUrl(item.file.name) == 'pdf' && (
701
+ <img style={{ width: 40, height: 40 }} src={typePdfPng} />
702
+ )}
703
+ {getFileTypeByUrl(item.file.name) == 'word' && (
704
+ <img style={{ width: 40, height: 40 }} src={typeWordPng} />
705
+ )}
706
+ {getFileTypeByUrl(item.file.name) == 'excel' && (
707
+ <img style={{ width: 40, height: 40 }} src={typeExcelPng} />
708
+ )}
709
+ {getFileTypeByUrl(item.file.name) == 'markdown' && (
710
+ <img style={{ width: 40, height: 40 }} src={typeMarkdownPng} />
711
+ )}
712
+ {getFileTypeByUrl(item.file.name) == 'txt' && (
713
+ <img style={{ width: 40, height: 40 }} src={typeTextPng} />
714
+ )}
715
+ {getFileTypeByUrl(item.file.name) == 'mobi' && (
716
+ <img style={{ width: 40, height: 40 }} src={typeMobiPng} />
717
+ )}
718
+ {getFileTypeByUrl(item.file.name) == 'rpub' && (
719
+ <img style={{ width: 40, height: 40 }} src={typeRPubPng} />
720
+ )}
721
+ {getFileTypeByUrl(item.file.name) == 'file' && (
722
+ <img style={{ width: 40, height: 40 }} src={upFilePng} />
723
+ )}
724
+ <div className="w_fileInfoBox">
725
+ <div className="w_fileInfoFileName">{item.file.name}</div>
726
+ <div className="w_fileInfoMeta">{`${item.fileType} · ${getFileSize(
727
+ item.file.size,
728
+ )}`}</div>
729
+ </div>
730
+ <div
731
+ className="w_fileRemove"
732
+ onClick={() => {
733
+ removeFile(item, index);
734
+ }}
735
+ >
736
+ <img src={closexPng} />
737
+ </div>
738
+ </div>
739
+ );
740
+ })}
741
+ </div>
742
+ <div className="w_inputBox">
743
+ <textarea
744
+ value={value}
745
+ onChange={(e) => setValue(e.target.value)}
746
+ onKeyDown={(e) => {
747
+ if (e.key === 'Enter' && !e.shiftKey) handleClick();
748
+ }}
749
+ disabled={sendingMessage}
750
+ placeholder={
751
+ sendingMessage
752
+ ? placeholder_sending || '思考中...'
753
+ : placeholder || '请输入您的问题...'
754
+ }
755
+ style={{...input_style,resize:"none"}}
756
+ ref={inputRef}
757
+ className="cl-input-element"
758
+ />
759
+ <Tooltip title={recordState ? "点击结束录音" : "点击开始录音"}>
760
+ <div
761
+ className="w_send_voice_box"
762
+ style={sendingMessage ? { cursor: 'not-allowed' } : {}}
763
+ onClick={startRecord}
764
+ >
765
+ <img src={recordState ? soundWavePng : luyinPng} style={{ width: 23 }} className={recordState ? "w_recordIng" : ''}></img>
766
+ </div>
767
+ </Tooltip>
768
+ {/*<Tooltip title="支持PDF / Word / Excel / Markdown / txt / mobi / rpub">*/}
769
+ <Tooltip title="支持图片格式">
770
+ <div
771
+ className="w_send_file_box"
772
+ style={sendingMessage ? { cursor: 'not-allowed' } : {}}
773
+ onClick={uploadFile}
774
+ >
775
+ <img src={fileuploadPng} style={{ width: 23 }}></img>
776
+ </div>
777
+ </Tooltip>
778
+ <button
779
+ disabled={sendingMessage}
780
+ style={{ ...(sendingMessage ? { cursor: 'not-allowed' } : {}), padding: '0 13px', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center' }}
781
+ onClick={() => {
782
+ handleClick('');
783
+ }}
784
+ >
785
+ <img src={sendmessagePng} style={{ width: 55 }}></img>
786
+ </button>
787
+ </div>
788
+ </div>
789
+ </div>
790
+ );
791
+ }