@alicloud/appflow-chat 0.0.3 → 0.0.4-alpha.10
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/dist/appflow-chat.cjs.js +383 -164
- package/dist/appflow-chat.esm.js +12015 -11276
- package/dist/types/index.d.ts +135 -2
- package/package.json +3 -2
- package/src/App.tsx +193 -0
- package/src/components/AudioPlayer.tsx +246 -0
- package/src/components/ChatSender.tsx +679 -0
- package/src/components/MessageAttachments.tsx +227 -0
- package/src/components/MessageBubble.tsx +66 -40
- package/src/index.ts +3 -0
- package/src/main.tsx +9 -0
- package/src/markdown/index.tsx +21 -2
- package/src/services/ChatService.ts +168 -101
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageAttachments - 消息附件展示组件
|
|
3
|
+
* 用于在消息气泡中展示上传的图片和文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import styled from 'styled-components';
|
|
8
|
+
import { Image } from 'antd';
|
|
9
|
+
import {
|
|
10
|
+
FileWordOutlined,
|
|
11
|
+
FilePdfOutlined,
|
|
12
|
+
FileExcelOutlined,
|
|
13
|
+
FileTextOutlined,
|
|
14
|
+
FileOutlined,
|
|
15
|
+
} from '@ant-design/icons';
|
|
16
|
+
|
|
17
|
+
// ==================== 类型定义 ====================
|
|
18
|
+
|
|
19
|
+
export interface MessageAttachmentsProps {
|
|
20
|
+
/** 消息角色,影响卡片配色 */
|
|
21
|
+
role?: 'user' | 'bot';
|
|
22
|
+
/** 图片URL列表 */
|
|
23
|
+
images?: string[];
|
|
24
|
+
/** 文件列表 */
|
|
25
|
+
files?: { name: string; url: string }[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ==================== 样式组件 ====================
|
|
29
|
+
|
|
30
|
+
const AttachmentsArea = styled.div`
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 8px;
|
|
34
|
+
margin-top: 4px;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const ImageCard = styled.div<{ $role: 'user' | 'bot' }>`
|
|
38
|
+
background: ${props => props.$role === 'user' ? '#eef0ff' : '#e8f4fd'};
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
padding: 8px;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 6px;
|
|
44
|
+
max-width: 180px;
|
|
45
|
+
color: #333;
|
|
46
|
+
|
|
47
|
+
.image-preview {
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
|
|
52
|
+
.ant-image {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
img {
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
height: auto;
|
|
59
|
+
display: block;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const ImagesRow = styled.div`
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-wrap: wrap;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const FileCard = styled.a<{ $role: 'user' | 'bot' }>`
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 10px;
|
|
74
|
+
background: ${props => props.$role === 'user' ? '#eef0ff' : '#e8f4fd'};
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 10px 12px;
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
color: #333;
|
|
79
|
+
transition: background 0.2s;
|
|
80
|
+
max-width: 100%;
|
|
81
|
+
box-sizing: border-box;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
background: ${props => props.$role === 'user' ? '#e2e5ff' : '#d6ecf8'};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.file-icon {
|
|
88
|
+
font-size: 28px;
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
color: #1677ff;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.file-info {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 2px;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.file-name {
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
text-overflow: ellipsis;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
color: #333;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.file-meta {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 6px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.file-type {
|
|
116
|
+
font-size: 11px;
|
|
117
|
+
padding: 1px 4px;
|
|
118
|
+
border-radius: 3px;
|
|
119
|
+
background: rgba(0, 0, 0, 0.06);
|
|
120
|
+
color: #666;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
// ==================== 工具函数 ====================
|
|
127
|
+
|
|
128
|
+
/** 根据文件名获取文件扩展名 */
|
|
129
|
+
function getFileExtension(fileName: string): string {
|
|
130
|
+
const parts = fileName.split('.');
|
|
131
|
+
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 根据文件扩展名获取对应的图标组件 */
|
|
135
|
+
function getFileIcon(ext: string): React.ReactNode {
|
|
136
|
+
switch (ext) {
|
|
137
|
+
case 'doc':
|
|
138
|
+
case 'docx':
|
|
139
|
+
return <FileWordOutlined />;
|
|
140
|
+
case 'pdf':
|
|
141
|
+
return <FilePdfOutlined />;
|
|
142
|
+
case 'xls':
|
|
143
|
+
case 'xlsx':
|
|
144
|
+
case 'csv':
|
|
145
|
+
return <FileExcelOutlined />;
|
|
146
|
+
case 'txt':
|
|
147
|
+
case 'md':
|
|
148
|
+
case 'json':
|
|
149
|
+
return <FileTextOutlined />;
|
|
150
|
+
default:
|
|
151
|
+
return <FileOutlined />;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ==================== 组件实现 ====================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* MessageAttachments - 消息附件展示组件
|
|
159
|
+
*
|
|
160
|
+
* 在消息气泡中展示上传的图片(缩略图 + 点击预览)和文件(图标 + 文件名 + 类型标签 + 点击下载)。
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```tsx
|
|
164
|
+
* <MessageAttachments
|
|
165
|
+
* role="user"
|
|
166
|
+
* images={['https://example.com/image.png']}
|
|
167
|
+
* files={[{ name: '文档.docx', url: 'https://example.com/doc.docx' }]}
|
|
168
|
+
* />
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export const MessageAttachments: React.FC<MessageAttachmentsProps> = ({
|
|
172
|
+
role = 'user',
|
|
173
|
+
images,
|
|
174
|
+
files,
|
|
175
|
+
}) => {
|
|
176
|
+
const hasImages = images && images.length > 0;
|
|
177
|
+
const hasFiles = files && files.length > 0;
|
|
178
|
+
|
|
179
|
+
if (!hasImages && !hasFiles) return null;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<AttachmentsArea>
|
|
183
|
+
{/* 图片列表 */}
|
|
184
|
+
{hasImages && (
|
|
185
|
+
<ImagesRow>
|
|
186
|
+
{images.map((url, index) => (
|
|
187
|
+
<ImageCard key={index} $role={role}>
|
|
188
|
+
<div className="image-preview">
|
|
189
|
+
<Image
|
|
190
|
+
src={url}
|
|
191
|
+
width={160}
|
|
192
|
+
style={{ borderRadius: 6, objectFit: 'cover' }}
|
|
193
|
+
preview={{ mask: '预览' }}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</ImageCard>
|
|
197
|
+
))}
|
|
198
|
+
</ImagesRow>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* 文件列表 */}
|
|
202
|
+
{hasFiles && files.map((file, index) => {
|
|
203
|
+
const ext = getFileExtension(file.name);
|
|
204
|
+
return (
|
|
205
|
+
<FileCard
|
|
206
|
+
key={index}
|
|
207
|
+
className="appflow-file-card"
|
|
208
|
+
$role={role}
|
|
209
|
+
href={file.url}
|
|
210
|
+
target="_blank"
|
|
211
|
+
rel="noopener noreferrer"
|
|
212
|
+
>
|
|
213
|
+
<span className="file-icon">{getFileIcon(ext)}</span>
|
|
214
|
+
<div className="file-info">
|
|
215
|
+
<span className="file-name">{file.name}</span>
|
|
216
|
+
<div className="file-meta">
|
|
217
|
+
{ext && <span className="file-type">{ext}</span>}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</FileCard>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</AttachmentsArea>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export default MessageAttachments;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
8
8
|
import styled from 'styled-components';
|
|
9
9
|
import { Modal, Image, Space } from 'antd';
|
|
10
|
+
import { MessageAttachments } from './MessageAttachments';
|
|
11
|
+
import { AudioPlayer } from './AudioPlayer';
|
|
10
12
|
import { loadEchartsScript } from '@/utils/loadEcharts';
|
|
11
13
|
import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
12
14
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
@@ -47,6 +49,12 @@ export interface MessageBubbleProps {
|
|
|
47
49
|
status?: 'Running' | 'Success' | 'Error';
|
|
48
50
|
/** 参考资料列表 */
|
|
49
51
|
references?: DocReferenceItem[];
|
|
52
|
+
/** 图片URL列表(用户消息中上传的图片) */
|
|
53
|
+
images?: string[];
|
|
54
|
+
/** 文件列表(用户消息中上传的文件) */
|
|
55
|
+
files?: { name: string; url: string }[];
|
|
56
|
+
/** 语音消息URL */
|
|
57
|
+
audio?: string;
|
|
50
58
|
/** 自定义类名 */
|
|
51
59
|
className?: string;
|
|
52
60
|
/** 自定义样式 */
|
|
@@ -80,11 +88,10 @@ const StyledContainer = styled.div<{ $role: 'user' | 'bot' }>`
|
|
|
80
88
|
const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
|
|
81
89
|
padding: 12px 16px;
|
|
82
90
|
border-radius: 12px;
|
|
83
|
-
background: ${props => props.$role === 'user'
|
|
84
|
-
|
|
85
|
-
: 'rgba(205, 208, 220, 0.15)'};
|
|
86
|
-
color: ${props => props.$role === 'user' ? '#fff' : '#333'};
|
|
91
|
+
background: ${props => props.$role === 'user' ? '#e5effe' : 'rgba(205, 208, 220, 0.15)'};
|
|
92
|
+
color: '#333';
|
|
87
93
|
word-break: break-word;
|
|
94
|
+
overflow: hidden;
|
|
88
95
|
|
|
89
96
|
/* 样式隔离 */
|
|
90
97
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
@@ -178,7 +185,7 @@ const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
|
|
|
178
185
|
border-radius: 4px;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
a {
|
|
188
|
+
a:not(.appflow-file-card) {
|
|
182
189
|
color: ${props => props.$role === 'user' ? '#fff' : '#1890ff'};
|
|
183
190
|
text-decoration: underline;
|
|
184
191
|
}
|
|
@@ -261,6 +268,9 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
261
268
|
role = 'bot',
|
|
262
269
|
status = 'Success',
|
|
263
270
|
references = [],
|
|
271
|
+
images,
|
|
272
|
+
files,
|
|
273
|
+
audio,
|
|
264
274
|
className,
|
|
265
275
|
style,
|
|
266
276
|
onReferenceClick,
|
|
@@ -334,6 +344,9 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
334
344
|
const handleReferenceClick = onReferenceClick || defaultReferenceClick;
|
|
335
345
|
const handleWebSearchClick = onWebSearchClick || defaultWebSearchClick;
|
|
336
346
|
|
|
347
|
+
// 纯语音消息:跳过 StyledBubble 包裹,直接渲染 AudioPlayer
|
|
348
|
+
const isAudioOnly = !!(audio && !content);
|
|
349
|
+
|
|
337
350
|
return (
|
|
338
351
|
<>
|
|
339
352
|
<StyledContainer
|
|
@@ -341,42 +354,55 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
341
354
|
className={`appflow-sdk-message-bubble ${className || ''}`}
|
|
342
355
|
style={style}
|
|
343
356
|
>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
{references && references.length > 0 && status === 'Success' && (
|
|
368
|
-
<ReferencesContainer>
|
|
369
|
-
<DocReferences
|
|
370
|
-
items={references}
|
|
371
|
-
status={status}
|
|
372
|
-
onItemClick={handleReferenceClick}
|
|
373
|
-
onWebSearchClick={handleWebSearchClick}
|
|
357
|
+
{isAudioOnly ? (
|
|
358
|
+
/* 纯语音消息:直接渲染播放器,无气泡包裹 */
|
|
359
|
+
<AudioPlayer src={audio} role={role} />
|
|
360
|
+
) : (
|
|
361
|
+
<StyledBubble $role={role}>
|
|
362
|
+
{/* 使用核心组件渲染内容 */}
|
|
363
|
+
<BubbleContent
|
|
364
|
+
content={content}
|
|
365
|
+
status={status}
|
|
366
|
+
role={role}
|
|
367
|
+
>
|
|
368
|
+
{/* HumanVerify事件 - 人工审核表单 */}
|
|
369
|
+
{eventType === 'humanVerify' && humanVerifyData && (
|
|
370
|
+
<HumanVerify
|
|
371
|
+
data={humanVerifyData}
|
|
372
|
+
onSubmit={onHumanVerifySubmit}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* HistoryCard 事件 - 历史对话中的审核卡片 */}
|
|
377
|
+
{eventType === 'historyCard' && historyCardData && (
|
|
378
|
+
<HistoryCard
|
|
379
|
+
data={historyCardData}
|
|
374
380
|
/>
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* 参考资料 */}
|
|
384
|
+
{references && references.length > 0 && status === 'Success' && (
|
|
385
|
+
<ReferencesContainer>
|
|
386
|
+
<DocReferences
|
|
387
|
+
items={references}
|
|
388
|
+
status={status}
|
|
389
|
+
onItemClick={handleReferenceClick}
|
|
390
|
+
onWebSearchClick={handleWebSearchClick}
|
|
391
|
+
/>
|
|
392
|
+
</ReferencesContainer>
|
|
393
|
+
)}
|
|
394
|
+
</BubbleContent>
|
|
395
|
+
|
|
396
|
+
{/* 附件展示区域:图片和文件 */}
|
|
397
|
+
<MessageAttachments
|
|
398
|
+
role={role}
|
|
399
|
+
images={images}
|
|
400
|
+
files={files}
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
{contextHolder}
|
|
404
|
+
</StyledBubble>
|
|
405
|
+
)}
|
|
380
406
|
</StyledContainer>
|
|
381
407
|
|
|
382
408
|
{/* 默认的网页搜索抽屉(仅在用户未传入onWebSearchClick时使用) */}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type {
|
|
|
20
20
|
ChatStream,
|
|
21
21
|
HistoryMessage,
|
|
22
22
|
ChatSession,
|
|
23
|
+
UploadResult,
|
|
23
24
|
} from './services/ChatService';
|
|
24
25
|
|
|
25
26
|
// ==================== UI 组件导出(简化接口,包含默认交互) ====================
|
|
@@ -28,6 +29,7 @@ export { MessageBubble } from './components/MessageBubble';
|
|
|
28
29
|
export { RichMessageBubble } from './components/RichMessageBubble';
|
|
29
30
|
export { DocReferences } from './components/DocReferences';
|
|
30
31
|
export { WebSearchPanel } from './components/WebSearchPanel';
|
|
32
|
+
export { ChatSender } from './components/ChatSender';
|
|
31
33
|
|
|
32
34
|
// ==================== UI 组件类型导出 ====================
|
|
33
35
|
export type { MarkdownRendererProps } from './components/MarkdownRenderer';
|
|
@@ -40,6 +42,7 @@ export type {
|
|
|
40
42
|
export type { RichMessageBubbleProps } from './components/RichMessageBubble';
|
|
41
43
|
export type { DocReferencesProps, DocReferenceItem } from './components/DocReferences';
|
|
42
44
|
export type { WebSearchPanelProps, WebSearchItem } from './components/WebSearchPanel';
|
|
45
|
+
export type { ChatSenderProps, ChatSenderSubmitData, ChatAttachment } from './components/ChatSender';
|
|
43
46
|
|
|
44
47
|
// ==================== Core 组件导出(纯展示组件,供高级定制) ====================
|
|
45
48
|
export { BubbleContent } from './core/BubbleContent';
|
package/src/main.tsx
ADDED
package/src/markdown/index.tsx
CHANGED
|
@@ -53,9 +53,28 @@ export const MarkdownView: React.FC<MarkdownViewProps> = ({
|
|
|
53
53
|
/>
|
|
54
54
|
);
|
|
55
55
|
},
|
|
56
|
-
code: ({ node, className, ...props }: any) => {
|
|
56
|
+
code: ({ node, inline, className, ...props }: any) => {
|
|
57
|
+
// 判断是否为行内代码
|
|
58
|
+
// 行内代码没有 className,且 inline 为 true
|
|
59
|
+
const isInline = inline || (!className && !node?.properties?.className);
|
|
60
|
+
|
|
61
|
+
if (isInline) {
|
|
62
|
+
// 行内代码 - 使用简单的 <code> 标签样式
|
|
63
|
+
return (
|
|
64
|
+
<code style={{
|
|
65
|
+
backgroundColor: 'rgba(175, 184, 193, 0.2)',
|
|
66
|
+
padding: '0.2em 0.4em',
|
|
67
|
+
borderRadius: '3px',
|
|
68
|
+
fontSize: '85%',
|
|
69
|
+
fontFamily: 'Consolas, Monaco, "Andale Mono", monospace',
|
|
70
|
+
}}>
|
|
71
|
+
{props.children}
|
|
72
|
+
</code>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
// 元信息中解析语言类型
|
|
58
|
-
const languageType = /language-(\w+)/.exec(node?.properties?.className || '')?.[0];
|
|
77
|
+
const languageType = /language-(\w+)/.exec(node?.properties?.className || className || '')?.[0];
|
|
59
78
|
const langFromMeta = node?.position?.start?.line === 2 ? 'language-file' : null;
|
|
60
79
|
const finalLang = languageType || langFromMeta;
|
|
61
80
|
|