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