yt-chat-components 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/sonarlint/issuestore/index.pb +0 -7
- package/package.json +78 -80
- package/public/index.html +108 -108
- package/src/YtChatView/chatWidget/chatWindow/callInterface/index.module.css +50 -50
- package/src/YtChatView/chatWidget/chatWindow/callInterface/index.tsx +549 -542
- package/src/YtChatView/chatWidget/chatWindow/callInterface/style.ts +44 -44
- package/src/YtChatView/chatWidget/chatWindow/chatMessage/index.tsx +501 -501
- package/src/YtChatView/chatWidget/chatWindow/chatPlaceholder/index.tsx +23 -23
- 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 +1186 -1185
- package/src/YtChatView/chatWidget/chatWindow/types/chatWidget/index.ts +50 -50
- package/src/YtChatView/chatWidget/index.tsx +2596 -2596
- 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 +601 -601
- 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 +127 -11
- package/webpack.config.js +50 -50
- package/.idea/sonarlint/issuestore/7/0/7030d0b2f71b999ff89a343de08c414af32fc93a +0 -0
- package/.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d +0 -0
- package/.idea/sonarlint/issuestore/9/c/9cfff9a6d27bd6c255aa751213163c7901fb8ce7 +0 -0
- package/build/asset-manifest.json +0 -19
- package/build/index.html +0 -1
- package/build/static/css/main.8ee59d98.css +0 -2
- package/build/static/css/main.8ee59d98.css.map +0 -1
- package/build/static/js/main.ae39ed55.js +0 -3
- package/build/static/js/main.ae39ed55.js.LICENSE.txt +0 -181
- package/build/static/js/main.ae39ed55.js.map +0 -1
- package/build/static/media/aiavatar.74bafa995cce4c01b804.png +0 -0
- package/build/static/media/history-list-empty.1eb65b1550aef4e8c8a4.png +0 -0
- package/build/static/media/icon_history_headerbg.50747e81d01257f55346.png +0 -0
- package/build/static/media/moreAi.285e66289f838072060c.png +0 -0
- package/build/static/media/moreBg.9fc998472925cecd89f2.png +0 -0
- package/build/static/media/phone.19bc6f0d2e9eae4863ae.png +0 -0
|
@@ -1,501 +1,501 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import Markdown from 'react-markdown';
|
|
3
|
-
import EChartsReact from 'echarts-for-react';
|
|
4
|
-
import {ChatMessageType, InputValueType, MessageType} from '../types/chatWidget';
|
|
5
|
-
import remarkGfm from 'remark-gfm';
|
|
6
|
-
import rehypeMathjax from 'rehype-mathjax';
|
|
7
|
-
// import './index.module.css';
|
|
8
|
-
import upFilePng from '../../../../assets/aicenter/upfile.png';
|
|
9
|
-
import {Form, Image, Input, message as messageTip, Select, DatePicker, Button, Spin, Skeleton} from 'antd';
|
|
10
|
-
import React, {useState, useRef} from 'react';
|
|
11
|
-
import typePdfPng from '../../../../assets/aicenter/type-pdf.png';
|
|
12
|
-
import typeWordPng from '../../../../assets/aicenter/type-word.png';
|
|
13
|
-
import typeExcelPng from '../../../../assets/aicenter/type-excel.png';
|
|
14
|
-
import typeMarkdownPng from '../../../../assets/aicenter/type-markdown.png';
|
|
15
|
-
import typeTextPng from '../../../../assets/aicenter/type-text.png';
|
|
16
|
-
import typeMobiPng from '../../../../assets/aicenter/type-mobi.png';
|
|
17
|
-
import typeRPubPng from '../../../../assets/aicenter/type-rpub.png';
|
|
18
|
-
import playPng from '../../../../assets/aicenter/play.png';
|
|
19
|
-
import playRunGif from '../../../../assets/aicenter/play-run.gif';
|
|
20
|
-
import copyPng from '../../../../assets/aicenter/copy.png';
|
|
21
|
-
|
|
22
|
-
let speechSynth = window.speechSynthesis;
|
|
23
|
-
let utterance = null;
|
|
24
|
-
|
|
25
|
-
export default function ChatMessage({
|
|
26
|
-
type = MessageType.text,
|
|
27
|
-
rawInfo,
|
|
28
|
-
message,
|
|
29
|
-
isSend,
|
|
30
|
-
error,
|
|
31
|
-
host_url,
|
|
32
|
-
user_message_style,
|
|
33
|
-
bot_message_style,
|
|
34
|
-
error_message_style,
|
|
35
|
-
handleSendMessage
|
|
36
|
-
}: ChatMessageType) {
|
|
37
|
-
const parseFileName = (
|
|
38
|
-
text: string,
|
|
39
|
-
): { fileName: string | null; fileType: string | null; isImg: boolean } => {
|
|
40
|
-
// 检查输入是否有效
|
|
41
|
-
if (!text || typeof text !== 'string') {
|
|
42
|
-
return { fileName: null, fileType: null };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 找到最后一个反斜杠的位置
|
|
46
|
-
const lastBackslashIndex = text.lastIndexOf('\\');
|
|
47
|
-
if (lastBackslashIndex === -1) {
|
|
48
|
-
return { fileName: null, fileType: null }; // 如果没有找到反斜杠,返回空结果
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 截取反斜杠后面的部分
|
|
52
|
-
const fileNameWithExtension = text.substring(lastBackslashIndex + 1);
|
|
53
|
-
|
|
54
|
-
// 分割文件名和文件类型
|
|
55
|
-
const parts = fileNameWithExtension.split('.');
|
|
56
|
-
const fileName = parts.slice(0, -1).join('.'); // 文件名(不含扩展名)
|
|
57
|
-
const fileType = parts.length > 1 ? parts.pop() : null; // 文件类型
|
|
58
|
-
|
|
59
|
-
const isImg = fileType && ['jpg', 'jpeg', 'png', 'gif'].includes(fileType.toLowerCase());
|
|
60
|
-
// 返回结果对象
|
|
61
|
-
return {
|
|
62
|
-
fileName: fileName,
|
|
63
|
-
fileType: fileType,
|
|
64
|
-
isImg,
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const [isPlay, setIsPlay] = useState(false) // 是否正在播放文字
|
|
69
|
-
const [isSubmittingForm, setIsSubmittingForm] = useState(false) // 正在提交form
|
|
70
|
-
const [isShowFormBtns, setIsShowFormBtns] = useState(true) // 正在提交form
|
|
71
|
-
const formRef = useRef(null);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 根据文件URL获取文件类型
|
|
75
|
-
* @param url
|
|
76
|
-
*/
|
|
77
|
-
const getFileTypeByUrl = (url) => {
|
|
78
|
-
if (!url) {
|
|
79
|
-
return 'file';
|
|
80
|
-
}
|
|
81
|
-
switch (url.split('.').pop().toLowerCase()) {
|
|
82
|
-
case 'jpg':
|
|
83
|
-
case 'jpeg':
|
|
84
|
-
case 'png':
|
|
85
|
-
case 'gif':
|
|
86
|
-
return 'image';
|
|
87
|
-
case 'pdf':
|
|
88
|
-
return 'pdf';
|
|
89
|
-
case 'doc':
|
|
90
|
-
case 'docx':
|
|
91
|
-
return 'word';
|
|
92
|
-
case 'xls':
|
|
93
|
-
case 'xlsx':
|
|
94
|
-
return 'excel';
|
|
95
|
-
case 'md':
|
|
96
|
-
return 'markdown';
|
|
97
|
-
case 'txt':
|
|
98
|
-
return 'txt';
|
|
99
|
-
case 'mobi':
|
|
100
|
-
return 'mobi';
|
|
101
|
-
case 'rpub':
|
|
102
|
-
return 'rpub';
|
|
103
|
-
default:
|
|
104
|
-
return 'file';
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 播放文字
|
|
110
|
-
* @param text 文字
|
|
111
|
-
*/
|
|
112
|
-
const playVoice = (text)=>{
|
|
113
|
-
if (isPlay) {
|
|
114
|
-
speechSynth.cancel();
|
|
115
|
-
setIsPlay(false)
|
|
116
|
-
}else{
|
|
117
|
-
if (text) {
|
|
118
|
-
utterance = new SpeechSynthesisUtterance(text);
|
|
119
|
-
const voices = speechSynth.getVoices();
|
|
120
|
-
utterance.voice = voices[61];
|
|
121
|
-
utterance.rate = 1;
|
|
122
|
-
utterance.pitch = 1;
|
|
123
|
-
speechSynth.speak(utterance);
|
|
124
|
-
setIsPlay(true)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 复制文字
|
|
131
|
-
* @param text 文字
|
|
132
|
-
*/
|
|
133
|
-
const copyText = async (text) => {
|
|
134
|
-
if(text){
|
|
135
|
-
await navigator.clipboard.writeText(text);
|
|
136
|
-
messageTip.info('复制成功')
|
|
137
|
-
}else{
|
|
138
|
-
messageTip.error("消息框没有文字")
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const renderUserMessage = () => {
|
|
143
|
-
return (
|
|
144
|
-
<div style={user_message_style} className="msg_userMessageBox">
|
|
145
|
-
<div style={user_message_style} className="cl-user_message">
|
|
146
|
-
{message.message}
|
|
147
|
-
</div>
|
|
148
|
-
<div className="msg_messageImgBox">
|
|
149
|
-
{message.rawInfo?.files.map((item, index) => {
|
|
150
|
-
return (
|
|
151
|
-
<div key={item} className="msg_fileBox">
|
|
152
|
-
{parseFileName(item).isImg && (
|
|
153
|
-
<Image
|
|
154
|
-
height={40}
|
|
155
|
-
width={40}
|
|
156
|
-
className="msg_messageImg"
|
|
157
|
-
src={`${host_url}/api/v1/files/images/${item}`}
|
|
158
|
-
alt={item}
|
|
159
|
-
preview={{
|
|
160
|
-
mask: <span className="custom-mask"></span>,
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
)}
|
|
164
|
-
{getFileTypeByUrl(item) == 'pdf' && (
|
|
165
|
-
<img style={{ width: 40, height: 40 }} src={typePdfPng} />
|
|
166
|
-
)}
|
|
167
|
-
{getFileTypeByUrl(item) == 'excel' && (
|
|
168
|
-
<img style={{ width: 40, height: 40 }} src={typeExcelPng} />
|
|
169
|
-
)}
|
|
170
|
-
{getFileTypeByUrl(item) == 'markdown' && (
|
|
171
|
-
<img style={{ width: 40, height: 40 }} src={typeMarkdownPng} />
|
|
172
|
-
)}
|
|
173
|
-
{getFileTypeByUrl(item) == 'txt' && (
|
|
174
|
-
<img style={{ width: 40, height: 40 }} src={typeTextPng} />
|
|
175
|
-
)}
|
|
176
|
-
{getFileTypeByUrl(item) == 'word' && (
|
|
177
|
-
<img style={{ width: 40, height: 40 }} src={typeWordPng} />
|
|
178
|
-
)}
|
|
179
|
-
{getFileTypeByUrl(item) == 'mobi' && (
|
|
180
|
-
<img style={{ width: 40, height: 40 }} src={typeMobiPng} />
|
|
181
|
-
)}
|
|
182
|
-
{getFileTypeByUrl(item) == 'rpub' && (
|
|
183
|
-
<img style={{ width: 40, height: 40 }} src={typeRPubPng} />
|
|
184
|
-
)}
|
|
185
|
-
{getFileTypeByUrl(item) == 'file' && (
|
|
186
|
-
<img style={{ width: 40, height: 40 }} src={upFilePng} />
|
|
187
|
-
)}
|
|
188
|
-
<div className="msg_fileInfoBox">
|
|
189
|
-
<div className="msg_fileInfoFileName">{parseFileName(item).fileName}</div>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
})}
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const renderErrorMessage = () => {
|
|
200
|
-
return (
|
|
201
|
-
<div style={error_message_style} className={'cl-error_message'}>
|
|
202
|
-
{message.message}
|
|
203
|
-
</div>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
const renderLoading = (type:MessageType, height:number) => {
|
|
207
|
-
const skeletonContainerId = `skeleton-container-${Math.random().toString(36).substring(2, 9)}`;
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<div
|
|
211
|
-
id={skeletonContainerId}
|
|
212
|
-
style={{minWidth: 500, width: '100%', height, display: 'flex', justifyContent: 'center', alignItems: 'center'}}
|
|
213
|
-
>
|
|
214
|
-
{
|
|
215
|
-
// 其他类型暂不处理
|
|
216
|
-
type == MessageType.echart && (
|
|
217
|
-
<Skeleton.Image active={true} style={{width: '100%', height: '100%'}}/>
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
<style jsx>{`
|
|
221
|
-
#${skeletonContainerId} .ant-skeleton.ant-skeleton-element {
|
|
222
|
-
width: 100% !important;
|
|
223
|
-
height: 100% !important;
|
|
224
|
-
}
|
|
225
|
-
`}</style>
|
|
226
|
-
</div>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 自定义渲染器
|
|
231
|
-
const EchartRender = React.useMemo(() => ({
|
|
232
|
-
code({ node, inline, className, children, ...props }) {
|
|
233
|
-
// 检查是否是代码块,并且语言为 'echart'
|
|
234
|
-
if (className && className.includes('language-echart')) {
|
|
235
|
-
try {
|
|
236
|
-
// 解析代码块内容为 JSON 配置
|
|
237
|
-
// console.log(" --- 1", children.toString().trim())
|
|
238
|
-
const chartOptions = JSON.parse(children.toString().trim());
|
|
239
|
-
// console.log(" --- 2", chartOptions)
|
|
240
|
-
|
|
241
|
-
// 渲染 ECharts 组件
|
|
242
|
-
return <EChartsReact option={chartOptions} style={{minWidth: 500, width: '100%', height: 300, backgroundColor: '#fff' }} />;
|
|
243
|
-
} catch (error) {
|
|
244
|
-
return renderLoading(MessageType.echart, 300);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// 默认渲染其他代码块
|
|
248
|
-
return <code className={className} {...props}>{children}</code>;
|
|
249
|
-
},
|
|
250
|
-
}), []); // 空依赖数组,只创建一次
|
|
251
|
-
|
|
252
|
-
const renderBotTextMessage = () => {
|
|
253
|
-
return (
|
|
254
|
-
<div>
|
|
255
|
-
<div style={bot_message_style} className={'cl-bot_message'}>
|
|
256
|
-
<Markdown
|
|
257
|
-
className={'markdown-body prose flex flex-col word-break-break-word'}
|
|
258
|
-
remarkPlugins={[remarkGfm]}
|
|
259
|
-
rehypePlugins={[rehypeMathjax]}
|
|
260
|
-
components={EchartRender}
|
|
261
|
-
>
|
|
262
|
-
{message.message}
|
|
263
|
-
</Markdown>
|
|
264
|
-
</div>
|
|
265
|
-
<div className="msg_operateBox">
|
|
266
|
-
<img src={isPlay?playRunGif:playPng} onClick={()=>playVoice(message.message)} />
|
|
267
|
-
<img src={copyPng} onClick={()=>copyText(message.message)} />
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const renderBotFormMessage = () => {
|
|
274
|
-
const { form_config } = rawInfo;
|
|
275
|
-
|
|
276
|
-
// 处理表单提交
|
|
277
|
-
const handleSubmit = (values) => {
|
|
278
|
-
console.log('表单提交数据:', values);
|
|
279
|
-
if (handleSendMessage) {
|
|
280
|
-
setIsSubmittingForm(true);
|
|
281
|
-
// TODO 提交的数据需要多一些
|
|
282
|
-
handleSendMessage(JSON.stringify(values), () => {
|
|
283
|
-
messageTip.success('表单提交成功');
|
|
284
|
-
setIsSubmittingForm(false);
|
|
285
|
-
setIsShowFormBtns(false);
|
|
286
|
-
}, InputValueType.form);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
// 处理表单取消
|
|
291
|
-
const handleCancel = () => {
|
|
292
|
-
if (formRef.current) {
|
|
293
|
-
formRef.current.resetFields();
|
|
294
|
-
}
|
|
295
|
-
if (handleSendMessage) {
|
|
296
|
-
setIsSubmittingForm(true);
|
|
297
|
-
handleSendMessage('USER CANCELED', () => {
|
|
298
|
-
messageTip.success('取消成功');
|
|
299
|
-
setIsSubmittingForm(false);
|
|
300
|
-
setIsShowFormBtns(false);
|
|
301
|
-
}, InputValueType.form);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<div className="form-container">
|
|
307
|
-
<div className="form-title">{form_config?.name || '表单'}</div>
|
|
308
|
-
<Form
|
|
309
|
-
ref={formRef}
|
|
310
|
-
layout="vertical"
|
|
311
|
-
onFinish={handleSubmit}
|
|
312
|
-
className="dynamic-form"
|
|
313
|
-
>
|
|
314
|
-
{form_config?.config?.map((item, index) => {
|
|
315
|
-
const { key, name, type, options } = item;
|
|
316
|
-
// 根据不同的表单项类型渲染不同的组件
|
|
317
|
-
switch (type) {
|
|
318
|
-
case 'text':
|
|
319
|
-
return (
|
|
320
|
-
<Form.Item
|
|
321
|
-
key={index}
|
|
322
|
-
label={name}
|
|
323
|
-
name={key}
|
|
324
|
-
rules={[{ required: true, message: `请输入${name}` }]}
|
|
325
|
-
>
|
|
326
|
-
<Input placeholder={`请输入${name}`} />
|
|
327
|
-
</Form.Item>
|
|
328
|
-
);
|
|
329
|
-
case 'date':
|
|
330
|
-
return (
|
|
331
|
-
<Form.Item
|
|
332
|
-
key={index}
|
|
333
|
-
label={name}
|
|
334
|
-
name={key}
|
|
335
|
-
rules={[{ required: true, message: `请选择${name}` }]}
|
|
336
|
-
>
|
|
337
|
-
<DatePicker style={{ width: '100%' }} placeholder={`请选择${name}`} />
|
|
338
|
-
</Form.Item>
|
|
339
|
-
);
|
|
340
|
-
case 'select':
|
|
341
|
-
return (
|
|
342
|
-
<Form.Item
|
|
343
|
-
key={index}
|
|
344
|
-
label={name}
|
|
345
|
-
name={key}
|
|
346
|
-
rules={[{ required: true, message: `请选择${name}` }]}
|
|
347
|
-
>
|
|
348
|
-
<Select
|
|
349
|
-
placeholder={`请选择${name}`}
|
|
350
|
-
style={{ width: '100%' }}
|
|
351
|
-
>
|
|
352
|
-
{options?.map((option, optIndex) => (
|
|
353
|
-
<Select.Option key={optIndex} value={option}>
|
|
354
|
-
{option}
|
|
355
|
-
</Select.Option>
|
|
356
|
-
))}
|
|
357
|
-
</Select>
|
|
358
|
-
</Form.Item>
|
|
359
|
-
);
|
|
360
|
-
default:
|
|
361
|
-
return (
|
|
362
|
-
<Form.Item
|
|
363
|
-
key={index}
|
|
364
|
-
label={name}
|
|
365
|
-
name={name}
|
|
366
|
-
>
|
|
367
|
-
<Input placeholder={`请输入${name}`} />
|
|
368
|
-
</Form.Item>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
})}
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
isShowFormBtns && (
|
|
375
|
-
<div className="form-buttons">
|
|
376
|
-
<Form.Item>
|
|
377
|
-
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
|
378
|
-
<Button
|
|
379
|
-
loading={isSubmittingForm}
|
|
380
|
-
onClick={handleCancel}
|
|
381
|
-
>
|
|
382
|
-
取消
|
|
383
|
-
</Button>
|
|
384
|
-
<Button
|
|
385
|
-
loading={isSubmittingForm}
|
|
386
|
-
type="primary"
|
|
387
|
-
htmlType="submit"
|
|
388
|
-
style={{ backgroundColor: '#8064f6' }}
|
|
389
|
-
>
|
|
390
|
-
提交
|
|
391
|
-
</Button>
|
|
392
|
-
</div>
|
|
393
|
-
</Form.Item>
|
|
394
|
-
</div>
|
|
395
|
-
)
|
|
396
|
-
}
|
|
397
|
-
</Form>
|
|
398
|
-
|
|
399
|
-
<style jsx>{`
|
|
400
|
-
.form-container {
|
|
401
|
-
width: 100%;
|
|
402
|
-
padding: 16px;
|
|
403
|
-
border-radius: 8px;
|
|
404
|
-
background-color:rgb(244, 244, 244);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
.form-title {
|
|
408
|
-
font-size: 18px;
|
|
409
|
-
font-weight: bold;
|
|
410
|
-
margin-bottom: 16px;
|
|
411
|
-
color: #333;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.dynamic-form {
|
|
415
|
-
width: 100%;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
.form-select {
|
|
419
|
-
width: 100%;
|
|
420
|
-
height: 32px;
|
|
421
|
-
border: 1px solid #d9d9d9;
|
|
422
|
-
border-radius: 4px;
|
|
423
|
-
padding: 4px 11px;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.form-buttons {
|
|
427
|
-
display: flex;
|
|
428
|
-
justify-content: flex-end;
|
|
429
|
-
margin-top: 20px;
|
|
430
|
-
gap: 12px;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.cancel-button {
|
|
434
|
-
padding: 6px 16px;
|
|
435
|
-
background-color: #f0f0f0;
|
|
436
|
-
border: 1px solid #d9d9d9;
|
|
437
|
-
border-radius: 4px;
|
|
438
|
-
cursor: pointer;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.submit-button {
|
|
442
|
-
padding: 6px 16px;
|
|
443
|
-
background-color: #8064f6;
|
|
444
|
-
color: white;
|
|
445
|
-
border: none;
|
|
446
|
-
border-radius: 4px;
|
|
447
|
-
cursor: pointer;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/* 移动端适配 */
|
|
451
|
-
@media (max-width: 768px) {
|
|
452
|
-
.form-container {
|
|
453
|
-
padding: 12px;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.form-buttons {
|
|
457
|
-
flex-direction: column;
|
|
458
|
-
gap: 8px;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
.cancel-button,
|
|
462
|
-
.submit-button {
|
|
463
|
-
width: 100%;
|
|
464
|
-
padding: 8px 0;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
`}</style>
|
|
468
|
-
</div>
|
|
469
|
-
);
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
const renderBotMessage = () => {
|
|
473
|
-
switch (type) {
|
|
474
|
-
case MessageType.form:
|
|
475
|
-
return renderBotFormMessage()
|
|
476
|
-
case MessageType.text:
|
|
477
|
-
default:
|
|
478
|
-
return renderBotTextMessage()
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const render = () => {
|
|
483
|
-
if (isSend){
|
|
484
|
-
return renderUserMessage()
|
|
485
|
-
}else {
|
|
486
|
-
if (error){
|
|
487
|
-
return renderErrorMessage()
|
|
488
|
-
}else{
|
|
489
|
-
return renderBotMessage()
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return (
|
|
495
|
-
<div className={'cl-chat-message ' + (isSend ? ' cl-justify-end' : ' cl-justify-start')}>
|
|
496
|
-
{
|
|
497
|
-
render()
|
|
498
|
-
}
|
|
499
|
-
</div>
|
|
500
|
-
);
|
|
501
|
-
}
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
3
|
+
import EChartsReact from 'echarts-for-react';
|
|
4
|
+
import {ChatMessageType, InputValueType, MessageType} from '../types/chatWidget';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import rehypeMathjax from 'rehype-mathjax';
|
|
7
|
+
// import './index.module.css';
|
|
8
|
+
import upFilePng from '../../../../assets/aicenter/upfile.png';
|
|
9
|
+
import {Form, Image, Input, message as messageTip, Select, DatePicker, Button, Spin, Skeleton} from 'antd';
|
|
10
|
+
import React, {useState, useRef} from 'react';
|
|
11
|
+
import typePdfPng from '../../../../assets/aicenter/type-pdf.png';
|
|
12
|
+
import typeWordPng from '../../../../assets/aicenter/type-word.png';
|
|
13
|
+
import typeExcelPng from '../../../../assets/aicenter/type-excel.png';
|
|
14
|
+
import typeMarkdownPng from '../../../../assets/aicenter/type-markdown.png';
|
|
15
|
+
import typeTextPng from '../../../../assets/aicenter/type-text.png';
|
|
16
|
+
import typeMobiPng from '../../../../assets/aicenter/type-mobi.png';
|
|
17
|
+
import typeRPubPng from '../../../../assets/aicenter/type-rpub.png';
|
|
18
|
+
import playPng from '../../../../assets/aicenter/play.png';
|
|
19
|
+
import playRunGif from '../../../../assets/aicenter/play-run.gif';
|
|
20
|
+
import copyPng from '../../../../assets/aicenter/copy.png';
|
|
21
|
+
|
|
22
|
+
let speechSynth = window.speechSynthesis;
|
|
23
|
+
let utterance = null;
|
|
24
|
+
|
|
25
|
+
export default function ChatMessage({
|
|
26
|
+
type = MessageType.text,
|
|
27
|
+
rawInfo,
|
|
28
|
+
message,
|
|
29
|
+
isSend,
|
|
30
|
+
error,
|
|
31
|
+
host_url,
|
|
32
|
+
user_message_style,
|
|
33
|
+
bot_message_style,
|
|
34
|
+
error_message_style,
|
|
35
|
+
handleSendMessage
|
|
36
|
+
}: ChatMessageType) {
|
|
37
|
+
const parseFileName = (
|
|
38
|
+
text: string,
|
|
39
|
+
): { fileName: string | null; fileType: string | null; isImg: boolean } => {
|
|
40
|
+
// 检查输入是否有效
|
|
41
|
+
if (!text || typeof text !== 'string') {
|
|
42
|
+
return { fileName: null, fileType: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 找到最后一个反斜杠的位置
|
|
46
|
+
const lastBackslashIndex = text.lastIndexOf('\\');
|
|
47
|
+
if (lastBackslashIndex === -1) {
|
|
48
|
+
return { fileName: null, fileType: null }; // 如果没有找到反斜杠,返回空结果
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 截取反斜杠后面的部分
|
|
52
|
+
const fileNameWithExtension = text.substring(lastBackslashIndex + 1);
|
|
53
|
+
|
|
54
|
+
// 分割文件名和文件类型
|
|
55
|
+
const parts = fileNameWithExtension.split('.');
|
|
56
|
+
const fileName = parts.slice(0, -1).join('.'); // 文件名(不含扩展名)
|
|
57
|
+
const fileType = parts.length > 1 ? parts.pop() : null; // 文件类型
|
|
58
|
+
|
|
59
|
+
const isImg = fileType && ['jpg', 'jpeg', 'png', 'gif'].includes(fileType.toLowerCase());
|
|
60
|
+
// 返回结果对象
|
|
61
|
+
return {
|
|
62
|
+
fileName: fileName,
|
|
63
|
+
fileType: fileType,
|
|
64
|
+
isImg,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const [isPlay, setIsPlay] = useState(false) // 是否正在播放文字
|
|
69
|
+
const [isSubmittingForm, setIsSubmittingForm] = useState(false) // 正在提交form
|
|
70
|
+
const [isShowFormBtns, setIsShowFormBtns] = useState(true) // 正在提交form
|
|
71
|
+
const formRef = useRef(null);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 根据文件URL获取文件类型
|
|
75
|
+
* @param url
|
|
76
|
+
*/
|
|
77
|
+
const getFileTypeByUrl = (url) => {
|
|
78
|
+
if (!url) {
|
|
79
|
+
return 'file';
|
|
80
|
+
}
|
|
81
|
+
switch (url.split('.').pop().toLowerCase()) {
|
|
82
|
+
case 'jpg':
|
|
83
|
+
case 'jpeg':
|
|
84
|
+
case 'png':
|
|
85
|
+
case 'gif':
|
|
86
|
+
return 'image';
|
|
87
|
+
case 'pdf':
|
|
88
|
+
return 'pdf';
|
|
89
|
+
case 'doc':
|
|
90
|
+
case 'docx':
|
|
91
|
+
return 'word';
|
|
92
|
+
case 'xls':
|
|
93
|
+
case 'xlsx':
|
|
94
|
+
return 'excel';
|
|
95
|
+
case 'md':
|
|
96
|
+
return 'markdown';
|
|
97
|
+
case 'txt':
|
|
98
|
+
return 'txt';
|
|
99
|
+
case 'mobi':
|
|
100
|
+
return 'mobi';
|
|
101
|
+
case 'rpub':
|
|
102
|
+
return 'rpub';
|
|
103
|
+
default:
|
|
104
|
+
return 'file';
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 播放文字
|
|
110
|
+
* @param text 文字
|
|
111
|
+
*/
|
|
112
|
+
const playVoice = (text)=>{
|
|
113
|
+
if (isPlay) {
|
|
114
|
+
speechSynth.cancel();
|
|
115
|
+
setIsPlay(false)
|
|
116
|
+
}else{
|
|
117
|
+
if (text) {
|
|
118
|
+
utterance = new SpeechSynthesisUtterance(text);
|
|
119
|
+
const voices = speechSynth.getVoices();
|
|
120
|
+
utterance.voice = voices[61];
|
|
121
|
+
utterance.rate = 1;
|
|
122
|
+
utterance.pitch = 1;
|
|
123
|
+
speechSynth.speak(utterance);
|
|
124
|
+
setIsPlay(true)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 复制文字
|
|
131
|
+
* @param text 文字
|
|
132
|
+
*/
|
|
133
|
+
const copyText = async (text) => {
|
|
134
|
+
if(text){
|
|
135
|
+
await navigator.clipboard.writeText(text);
|
|
136
|
+
messageTip.info('复制成功')
|
|
137
|
+
}else{
|
|
138
|
+
messageTip.error("消息框没有文字")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const renderUserMessage = () => {
|
|
143
|
+
return (
|
|
144
|
+
<div style={user_message_style} className="msg_userMessageBox">
|
|
145
|
+
<div style={user_message_style} className="cl-user_message">
|
|
146
|
+
{message.message}
|
|
147
|
+
</div>
|
|
148
|
+
<div className="msg_messageImgBox">
|
|
149
|
+
{message.rawInfo?.files.map((item, index) => {
|
|
150
|
+
return (
|
|
151
|
+
<div key={item} className="msg_fileBox">
|
|
152
|
+
{parseFileName(item).isImg && (
|
|
153
|
+
<Image
|
|
154
|
+
height={40}
|
|
155
|
+
width={40}
|
|
156
|
+
className="msg_messageImg"
|
|
157
|
+
src={`${host_url}/api/v1/files/images/${item}`}
|
|
158
|
+
alt={item}
|
|
159
|
+
preview={{
|
|
160
|
+
mask: <span className="custom-mask"></span>,
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
{getFileTypeByUrl(item) == 'pdf' && (
|
|
165
|
+
<img style={{ width: 40, height: 40 }} src={typePdfPng} />
|
|
166
|
+
)}
|
|
167
|
+
{getFileTypeByUrl(item) == 'excel' && (
|
|
168
|
+
<img style={{ width: 40, height: 40 }} src={typeExcelPng} />
|
|
169
|
+
)}
|
|
170
|
+
{getFileTypeByUrl(item) == 'markdown' && (
|
|
171
|
+
<img style={{ width: 40, height: 40 }} src={typeMarkdownPng} />
|
|
172
|
+
)}
|
|
173
|
+
{getFileTypeByUrl(item) == 'txt' && (
|
|
174
|
+
<img style={{ width: 40, height: 40 }} src={typeTextPng} />
|
|
175
|
+
)}
|
|
176
|
+
{getFileTypeByUrl(item) == 'word' && (
|
|
177
|
+
<img style={{ width: 40, height: 40 }} src={typeWordPng} />
|
|
178
|
+
)}
|
|
179
|
+
{getFileTypeByUrl(item) == 'mobi' && (
|
|
180
|
+
<img style={{ width: 40, height: 40 }} src={typeMobiPng} />
|
|
181
|
+
)}
|
|
182
|
+
{getFileTypeByUrl(item) == 'rpub' && (
|
|
183
|
+
<img style={{ width: 40, height: 40 }} src={typeRPubPng} />
|
|
184
|
+
)}
|
|
185
|
+
{getFileTypeByUrl(item) == 'file' && (
|
|
186
|
+
<img style={{ width: 40, height: 40 }} src={upFilePng} />
|
|
187
|
+
)}
|
|
188
|
+
<div className="msg_fileInfoBox">
|
|
189
|
+
<div className="msg_fileInfoFileName">{parseFileName(item).fileName}</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const renderErrorMessage = () => {
|
|
200
|
+
return (
|
|
201
|
+
<div style={error_message_style} className={'cl-error_message'}>
|
|
202
|
+
{message.message}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
const renderLoading = (type:MessageType, height:number) => {
|
|
207
|
+
const skeletonContainerId = `skeleton-container-${Math.random().toString(36).substring(2, 9)}`;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
id={skeletonContainerId}
|
|
212
|
+
style={{minWidth: 500, width: '100%', height, display: 'flex', justifyContent: 'center', alignItems: 'center'}}
|
|
213
|
+
>
|
|
214
|
+
{
|
|
215
|
+
// 其他类型暂不处理
|
|
216
|
+
type == MessageType.echart && (
|
|
217
|
+
<Skeleton.Image active={true} style={{width: '100%', height: '100%'}}/>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
<style jsx>{`
|
|
221
|
+
#${skeletonContainerId} .ant-skeleton.ant-skeleton-element {
|
|
222
|
+
width: 100% !important;
|
|
223
|
+
height: 100% !important;
|
|
224
|
+
}
|
|
225
|
+
`}</style>
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 自定义渲染器
|
|
231
|
+
const EchartRender = React.useMemo(() => ({
|
|
232
|
+
code({ node, inline, className, children, ...props }) {
|
|
233
|
+
// 检查是否是代码块,并且语言为 'echart'
|
|
234
|
+
if (className && className.includes('language-echart')) {
|
|
235
|
+
try {
|
|
236
|
+
// 解析代码块内容为 JSON 配置
|
|
237
|
+
// console.log(" --- 1", children.toString().trim())
|
|
238
|
+
const chartOptions = JSON.parse(children.toString().trim());
|
|
239
|
+
// console.log(" --- 2", chartOptions)
|
|
240
|
+
|
|
241
|
+
// 渲染 ECharts 组件
|
|
242
|
+
return <EChartsReact option={chartOptions} style={{minWidth: 500, width: '100%', height: 300, backgroundColor: '#fff' }} />;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return renderLoading(MessageType.echart, 300);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 默认渲染其他代码块
|
|
248
|
+
return <code className={className} {...props}>{children}</code>;
|
|
249
|
+
},
|
|
250
|
+
}), []); // 空依赖数组,只创建一次
|
|
251
|
+
|
|
252
|
+
const renderBotTextMessage = () => {
|
|
253
|
+
return (
|
|
254
|
+
<div>
|
|
255
|
+
<div style={bot_message_style} className={'cl-bot_message'}>
|
|
256
|
+
<Markdown
|
|
257
|
+
className={'markdown-body prose flex flex-col word-break-break-word'}
|
|
258
|
+
remarkPlugins={[remarkGfm]}
|
|
259
|
+
rehypePlugins={[rehypeMathjax]}
|
|
260
|
+
components={EchartRender}
|
|
261
|
+
>
|
|
262
|
+
{message.message}
|
|
263
|
+
</Markdown>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="msg_operateBox">
|
|
266
|
+
<img src={isPlay?playRunGif:playPng} onClick={()=>playVoice(message.message)} />
|
|
267
|
+
<img src={copyPng} onClick={()=>copyText(message.message)} />
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const renderBotFormMessage = () => {
|
|
274
|
+
const { form_config } = rawInfo;
|
|
275
|
+
|
|
276
|
+
// 处理表单提交
|
|
277
|
+
const handleSubmit = (values) => {
|
|
278
|
+
console.log('表单提交数据:', values);
|
|
279
|
+
if (handleSendMessage) {
|
|
280
|
+
setIsSubmittingForm(true);
|
|
281
|
+
// TODO 提交的数据需要多一些
|
|
282
|
+
handleSendMessage(JSON.stringify(values), () => {
|
|
283
|
+
messageTip.success('表单提交成功');
|
|
284
|
+
setIsSubmittingForm(false);
|
|
285
|
+
setIsShowFormBtns(false);
|
|
286
|
+
}, InputValueType.form);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// 处理表单取消
|
|
291
|
+
const handleCancel = () => {
|
|
292
|
+
if (formRef.current) {
|
|
293
|
+
formRef.current.resetFields();
|
|
294
|
+
}
|
|
295
|
+
if (handleSendMessage) {
|
|
296
|
+
setIsSubmittingForm(true);
|
|
297
|
+
handleSendMessage('USER CANCELED', () => {
|
|
298
|
+
messageTip.success('取消成功');
|
|
299
|
+
setIsSubmittingForm(false);
|
|
300
|
+
setIsShowFormBtns(false);
|
|
301
|
+
}, InputValueType.form);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div className="form-container">
|
|
307
|
+
<div className="form-title">{form_config?.name || '表单'}</div>
|
|
308
|
+
<Form
|
|
309
|
+
ref={formRef}
|
|
310
|
+
layout="vertical"
|
|
311
|
+
onFinish={handleSubmit}
|
|
312
|
+
className="dynamic-form"
|
|
313
|
+
>
|
|
314
|
+
{form_config?.config?.map((item, index) => {
|
|
315
|
+
const { key, name, type, options } = item;
|
|
316
|
+
// 根据不同的表单项类型渲染不同的组件
|
|
317
|
+
switch (type) {
|
|
318
|
+
case 'text':
|
|
319
|
+
return (
|
|
320
|
+
<Form.Item
|
|
321
|
+
key={index}
|
|
322
|
+
label={name}
|
|
323
|
+
name={key}
|
|
324
|
+
rules={[{ required: true, message: `请输入${name}` }]}
|
|
325
|
+
>
|
|
326
|
+
<Input placeholder={`请输入${name}`} />
|
|
327
|
+
</Form.Item>
|
|
328
|
+
);
|
|
329
|
+
case 'date':
|
|
330
|
+
return (
|
|
331
|
+
<Form.Item
|
|
332
|
+
key={index}
|
|
333
|
+
label={name}
|
|
334
|
+
name={key}
|
|
335
|
+
rules={[{ required: true, message: `请选择${name}` }]}
|
|
336
|
+
>
|
|
337
|
+
<DatePicker style={{ width: '100%' }} placeholder={`请选择${name}`} />
|
|
338
|
+
</Form.Item>
|
|
339
|
+
);
|
|
340
|
+
case 'select':
|
|
341
|
+
return (
|
|
342
|
+
<Form.Item
|
|
343
|
+
key={index}
|
|
344
|
+
label={name}
|
|
345
|
+
name={key}
|
|
346
|
+
rules={[{ required: true, message: `请选择${name}` }]}
|
|
347
|
+
>
|
|
348
|
+
<Select
|
|
349
|
+
placeholder={`请选择${name}`}
|
|
350
|
+
style={{ width: '100%' }}
|
|
351
|
+
>
|
|
352
|
+
{options?.map((option, optIndex) => (
|
|
353
|
+
<Select.Option key={optIndex} value={option}>
|
|
354
|
+
{option}
|
|
355
|
+
</Select.Option>
|
|
356
|
+
))}
|
|
357
|
+
</Select>
|
|
358
|
+
</Form.Item>
|
|
359
|
+
);
|
|
360
|
+
default:
|
|
361
|
+
return (
|
|
362
|
+
<Form.Item
|
|
363
|
+
key={index}
|
|
364
|
+
label={name}
|
|
365
|
+
name={name}
|
|
366
|
+
>
|
|
367
|
+
<Input placeholder={`请输入${name}`} />
|
|
368
|
+
</Form.Item>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
})}
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
isShowFormBtns && (
|
|
375
|
+
<div className="form-buttons">
|
|
376
|
+
<Form.Item>
|
|
377
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
|
378
|
+
<Button
|
|
379
|
+
loading={isSubmittingForm}
|
|
380
|
+
onClick={handleCancel}
|
|
381
|
+
>
|
|
382
|
+
取消
|
|
383
|
+
</Button>
|
|
384
|
+
<Button
|
|
385
|
+
loading={isSubmittingForm}
|
|
386
|
+
type="primary"
|
|
387
|
+
htmlType="submit"
|
|
388
|
+
style={{ backgroundColor: '#8064f6' }}
|
|
389
|
+
>
|
|
390
|
+
提交
|
|
391
|
+
</Button>
|
|
392
|
+
</div>
|
|
393
|
+
</Form.Item>
|
|
394
|
+
</div>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
</Form>
|
|
398
|
+
|
|
399
|
+
<style jsx>{`
|
|
400
|
+
.form-container {
|
|
401
|
+
width: 100%;
|
|
402
|
+
padding: 16px;
|
|
403
|
+
border-radius: 8px;
|
|
404
|
+
background-color:rgb(244, 244, 244);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.form-title {
|
|
408
|
+
font-size: 18px;
|
|
409
|
+
font-weight: bold;
|
|
410
|
+
margin-bottom: 16px;
|
|
411
|
+
color: #333;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.dynamic-form {
|
|
415
|
+
width: 100%;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.form-select {
|
|
419
|
+
width: 100%;
|
|
420
|
+
height: 32px;
|
|
421
|
+
border: 1px solid #d9d9d9;
|
|
422
|
+
border-radius: 4px;
|
|
423
|
+
padding: 4px 11px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.form-buttons {
|
|
427
|
+
display: flex;
|
|
428
|
+
justify-content: flex-end;
|
|
429
|
+
margin-top: 20px;
|
|
430
|
+
gap: 12px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.cancel-button {
|
|
434
|
+
padding: 6px 16px;
|
|
435
|
+
background-color: #f0f0f0;
|
|
436
|
+
border: 1px solid #d9d9d9;
|
|
437
|
+
border-radius: 4px;
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.submit-button {
|
|
442
|
+
padding: 6px 16px;
|
|
443
|
+
background-color: #8064f6;
|
|
444
|
+
color: white;
|
|
445
|
+
border: none;
|
|
446
|
+
border-radius: 4px;
|
|
447
|
+
cursor: pointer;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* 移动端适配 */
|
|
451
|
+
@media (max-width: 768px) {
|
|
452
|
+
.form-container {
|
|
453
|
+
padding: 12px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.form-buttons {
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
gap: 8px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.cancel-button,
|
|
462
|
+
.submit-button {
|
|
463
|
+
width: 100%;
|
|
464
|
+
padding: 8px 0;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
`}</style>
|
|
468
|
+
</div>
|
|
469
|
+
);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const renderBotMessage = () => {
|
|
473
|
+
switch (type) {
|
|
474
|
+
case MessageType.form:
|
|
475
|
+
return renderBotFormMessage()
|
|
476
|
+
case MessageType.text:
|
|
477
|
+
default:
|
|
478
|
+
return renderBotTextMessage()
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const render = () => {
|
|
483
|
+
if (isSend){
|
|
484
|
+
return renderUserMessage()
|
|
485
|
+
}else {
|
|
486
|
+
if (error){
|
|
487
|
+
return renderErrorMessage()
|
|
488
|
+
}else{
|
|
489
|
+
return renderBotMessage()
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<div className={'cl-chat-message ' + (isSend ? ' cl-justify-end' : ' cl-justify-start')}>
|
|
496
|
+
{
|
|
497
|
+
render()
|
|
498
|
+
}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|