@alicloud/appflow-chat 0.0.4-alpha.2 → 0.0.4-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,7 @@
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';
10
11
  import { loadEchartsScript } from '@/utils/loadEcharts';
11
12
  import { DocReferences, DocReferenceItem } from './DocReferences';
12
13
  import { WebSearchPanel } from './WebSearchPanel';
@@ -47,6 +48,10 @@ export interface MessageBubbleProps {
47
48
  status?: 'Running' | 'Success' | 'Error';
48
49
  /** 参考资料列表 */
49
50
  references?: DocReferenceItem[];
51
+ /** 图片URL列表(用户消息中上传的图片) */
52
+ images?: string[];
53
+ /** 文件列表(用户消息中上传的文件) */
54
+ files?: { name: string; url: string }[];
50
55
  /** 自定义类名 */
51
56
  className?: string;
52
57
  /** 自定义样式 */
@@ -80,11 +85,10 @@ const StyledContainer = styled.div<{ $role: 'user' | 'bot' }>`
80
85
  const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
81
86
  padding: 12px 16px;
82
87
  border-radius: 12px;
83
- background: ${props => props.$role === 'user'
84
- ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
85
- : 'rgba(205, 208, 220, 0.15)'};
86
- color: ${props => props.$role === 'user' ? '#fff' : '#333'};
88
+ background: ${props => props.$role === 'user' ? '#e5effe' : 'rgba(205, 208, 220, 0.15)'};
89
+ color: '#333';
87
90
  word-break: break-word;
91
+ overflow: hidden;
88
92
 
89
93
  /* 样式隔离 */
90
94
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@@ -178,7 +182,7 @@ const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
178
182
  border-radius: 4px;
179
183
  }
180
184
 
181
- a {
185
+ a:not(.appflow-file-card) {
182
186
  color: ${props => props.$role === 'user' ? '#fff' : '#1890ff'};
183
187
  text-decoration: underline;
184
188
  }
@@ -261,6 +265,8 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
261
265
  role = 'bot',
262
266
  status = 'Success',
263
267
  references = [],
268
+ images,
269
+ files,
264
270
  className,
265
271
  style,
266
272
  onReferenceClick,
@@ -375,6 +381,14 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
375
381
  </ReferencesContainer>
376
382
  )}
377
383
  </BubbleContent>
384
+
385
+ {/* 附件展示区域:图片和文件 */}
386
+ <MessageAttachments
387
+ role={role}
388
+ images={images}
389
+ files={files}
390
+ />
391
+
378
392
  {contextHolder}
379
393
  </StyledBubble>
380
394
  </StyledContainer>
package/src/main.tsx ADDED
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -35,6 +35,7 @@ export interface ModelInfo {
35
35
  config?: {
36
36
  image?: boolean;
37
37
  file?: boolean;
38
+ audio?: boolean;
38
39
  webSearch?: boolean;
39
40
  fileConfig?: string;
40
41
  };