@alicloud/appflow-chat 0.0.4-alpha.1 → 0.0.4-alpha.3

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,216 @@
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: 200px;
45
+ color: #333;
46
+
47
+ .image-preview {
48
+ border-radius: 6px;
49
+ overflow: hidden;
50
+ cursor: pointer;
51
+ }
52
+ `;
53
+
54
+ const ImagesRow = styled.div`
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ gap: 8px;
58
+ `;
59
+
60
+ const FileCard = styled.a<{ $role: 'user' | 'bot' }>`
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 10px;
64
+ background: ${props => props.$role === 'user' ? '#eef0ff' : '#e8f4fd'};
65
+ border-radius: 8px;
66
+ padding: 10px 12px;
67
+ text-decoration: none;
68
+ color: #333;
69
+ transition: background 0.2s;
70
+ max-width: 280px;
71
+
72
+ &:hover {
73
+ background: ${props => props.$role === 'user' ? '#e2e5ff' : '#d6ecf8'};
74
+ }
75
+
76
+ .file-icon {
77
+ font-size: 28px;
78
+ flex-shrink: 0;
79
+ color: #1677ff;
80
+ }
81
+
82
+ .file-info {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 2px;
86
+ min-width: 0;
87
+ }
88
+
89
+ .file-name {
90
+ font-size: 13px;
91
+ font-weight: 500;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ color: #333;
96
+ }
97
+
98
+ .file-meta {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 6px;
102
+ }
103
+
104
+ .file-type {
105
+ font-size: 11px;
106
+ padding: 1px 4px;
107
+ border-radius: 3px;
108
+ background: rgba(0, 0, 0, 0.06);
109
+ color: #666;
110
+ text-transform: uppercase;
111
+ font-weight: 500;
112
+ }
113
+ `;
114
+
115
+ // ==================== 工具函数 ====================
116
+
117
+ /** 根据文件名获取文件扩展名 */
118
+ function getFileExtension(fileName: string): string {
119
+ const parts = fileName.split('.');
120
+ return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
121
+ }
122
+
123
+ /** 根据文件扩展名获取对应的图标组件 */
124
+ function getFileIcon(ext: string): React.ReactNode {
125
+ switch (ext) {
126
+ case 'doc':
127
+ case 'docx':
128
+ return <FileWordOutlined />;
129
+ case 'pdf':
130
+ return <FilePdfOutlined />;
131
+ case 'xls':
132
+ case 'xlsx':
133
+ case 'csv':
134
+ return <FileExcelOutlined />;
135
+ case 'txt':
136
+ case 'md':
137
+ case 'json':
138
+ return <FileTextOutlined />;
139
+ default:
140
+ return <FileOutlined />;
141
+ }
142
+ }
143
+
144
+ // ==================== 组件实现 ====================
145
+
146
+ /**
147
+ * MessageAttachments - 消息附件展示组件
148
+ *
149
+ * 在消息气泡中展示上传的图片(缩略图 + 点击预览)和文件(图标 + 文件名 + 类型标签 + 点击下载)。
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * <MessageAttachments
154
+ * role="user"
155
+ * images={['https://example.com/image.png']}
156
+ * files={[{ name: '文档.docx', url: 'https://example.com/doc.docx' }]}
157
+ * />
158
+ * ```
159
+ */
160
+ export const MessageAttachments: React.FC<MessageAttachmentsProps> = ({
161
+ role = 'user',
162
+ images,
163
+ files,
164
+ }) => {
165
+ const hasImages = images && images.length > 0;
166
+ const hasFiles = files && files.length > 0;
167
+
168
+ if (!hasImages && !hasFiles) return null;
169
+
170
+ return (
171
+ <AttachmentsArea>
172
+ {/* 图片列表 */}
173
+ {hasImages && (
174
+ <ImagesRow>
175
+ {images.map((url, index) => (
176
+ <ImageCard key={index} $role={role}>
177
+ <div className="image-preview">
178
+ <Image
179
+ src={url}
180
+ width={160}
181
+ style={{ borderRadius: 6, objectFit: 'cover' }}
182
+ preview={{ mask: '预览' }}
183
+ />
184
+ </div>
185
+ </ImageCard>
186
+ ))}
187
+ </ImagesRow>
188
+ )}
189
+
190
+ {/* 文件列表 */}
191
+ {hasFiles && files.map((file, index) => {
192
+ const ext = getFileExtension(file.name);
193
+ return (
194
+ <FileCard
195
+ key={index}
196
+ className="appflow-file-card"
197
+ $role={role}
198
+ href={file.url}
199
+ target="_blank"
200
+ rel="noopener noreferrer"
201
+ >
202
+ <span className="file-icon">{getFileIcon(ext)}</span>
203
+ <div className="file-info">
204
+ <span className="file-name">{file.name}</span>
205
+ <div className="file-meta">
206
+ {ext && <span className="file-type">{ext}</span>}
207
+ </div>
208
+ </div>
209
+ </FileCard>
210
+ );
211
+ })}
212
+ </AttachmentsArea>
213
+ );
214
+ };
215
+
216
+ export default MessageAttachments;
@@ -7,12 +7,11 @@
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';
13
14
  import { HumanVerify, HistoryCard, CustomParamSchema } from './HumanVerify';
14
- import { A2UISurface } from './A2UIRenderer';
15
- import type { A2UIMessage } from './A2UIRenderer';
16
15
  import { BubbleContent } from '@/core';
17
16
 
18
17
  /** HumanVerify 提交数据类型 */
@@ -49,6 +48,10 @@ export interface MessageBubbleProps {
49
48
  status?: 'Running' | 'Success' | 'Error';
50
49
  /** 参考资料列表 */
51
50
  references?: DocReferenceItem[];
51
+ /** 图片URL列表(用户消息中上传的图片) */
52
+ images?: string[];
53
+ /** 文件列表(用户消息中上传的文件) */
54
+ files?: { name: string; url: string }[];
52
55
  /** 自定义类名 */
53
56
  className?: string;
54
57
  /** 自定义样式 */
@@ -60,21 +63,14 @@ export interface MessageBubbleProps {
60
63
 
61
64
  // ==================== HumanVerify相关Props ====================
62
65
 
63
- /** 事件类型(用于特殊消息如 humanVerify、historyCard、a2ui) */
64
- eventType?: 'humanVerify' | 'historyCard' | 'a2ui';
66
+ /** 事件类型(用于特殊消息如 humanVerify、historyCard) */
67
+ eventType?: 'humanVerify' | 'historyCard';
65
68
  /** HumanVerify 相关数据 */
66
69
  humanVerifyData?: HumanVerifyData;
67
70
  /** HistoryCard 相关数据(历史对话中的审核卡片) */
68
71
  historyCardData?: HistoryCardData;
69
72
  /** HumanVerify 提交回调 */
70
73
  onHumanVerifySubmit?: (data: HumanVerifySubmitData) => void;
71
-
72
- // ==================== A2UI相关Props ====================
73
-
74
- /** A2UI 消息数组(Agent 生成的声明式 UI 描述) */
75
- a2uiMessages?: A2UIMessage[];
76
- /** A2UI 用户交互回调 */
77
- onA2UIAction?: (action: any) => void;
78
74
  }
79
75
 
80
76
  // 样式隔离容器
@@ -89,10 +85,8 @@ const StyledContainer = styled.div<{ $role: 'user' | 'bot' }>`
89
85
  const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
90
86
  padding: 12px 16px;
91
87
  border-radius: 12px;
92
- background: ${props => props.$role === 'user'
93
- ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
94
- : 'rgba(205, 208, 220, 0.15)'};
95
- color: ${props => props.$role === 'user' ? '#fff' : '#333'};
88
+ background: ${props => props.$role === 'user' ? '#e5effe' : 'rgba(205, 208, 220, 0.15)'};
89
+ color: '#333';
96
90
  word-break: break-word;
97
91
 
98
92
  /* 样式隔离 */
@@ -187,7 +181,7 @@ const StyledBubble = styled.div<{ $role: 'user' | 'bot' }>`
187
181
  border-radius: 4px;
188
182
  }
189
183
 
190
- a {
184
+ a:not(.appflow-file-card) {
191
185
  color: ${props => props.$role === 'user' ? '#fff' : '#1890ff'};
192
186
  text-decoration: underline;
193
187
  }
@@ -270,6 +264,8 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
270
264
  role = 'bot',
271
265
  status = 'Success',
272
266
  references = [],
267
+ images,
268
+ files,
273
269
  className,
274
270
  style,
275
271
  onReferenceClick,
@@ -279,9 +275,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
279
275
  humanVerifyData,
280
276
  historyCardData,
281
277
  onHumanVerifySubmit,
282
- // A2UI 相关 props
283
- a2uiMessages,
284
- onA2UIAction,
285
278
  }) => {
286
279
  const [modal, contextHolder] = Modal.useModal();
287
280
 
@@ -374,14 +367,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
374
367
  data={historyCardData}
375
368
  />
376
369
  )}
377
-
378
- {/* A2UI 事件 - Agent 声明式 UI 渲染 */}
379
- {eventType === 'a2ui' && a2uiMessages && a2uiMessages.length > 0 && (
380
- <A2UISurface
381
- messages={a2uiMessages}
382
- onAction={onA2UIAction}
383
- />
384
- )}
385
370
 
386
371
  {/* 参考资料 */}
387
372
  {references && references.length > 0 && status === 'Success' && (
@@ -395,6 +380,14 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
395
380
  </ReferencesContainer>
396
381
  )}
397
382
  </BubbleContent>
383
+
384
+ {/* 附件展示区域:图片和文件 */}
385
+ <MessageAttachments
386
+ role={role}
387
+ images={images}
388
+ files={files}
389
+ />
390
+
398
391
  {contextHolder}
399
392
  </StyledBubble>
400
393
  </StyledContainer>
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export { MessageBubble } from './components/MessageBubble';
28
28
  export { RichMessageBubble } from './components/RichMessageBubble';
29
29
  export { DocReferences } from './components/DocReferences';
30
30
  export { WebSearchPanel } from './components/WebSearchPanel';
31
+ export { ChatSender } from './components/ChatSender';
31
32
 
32
33
  // ==================== UI 组件类型导出 ====================
33
34
  export type { MarkdownRendererProps } from './components/MarkdownRenderer';
@@ -40,6 +41,7 @@ export type {
40
41
  export type { RichMessageBubbleProps } from './components/RichMessageBubble';
41
42
  export type { DocReferencesProps, DocReferenceItem } from './components/DocReferences';
42
43
  export type { WebSearchPanelProps, WebSearchItem } from './components/WebSearchPanel';
44
+ export type { ChatSenderProps, ChatSenderSubmitData, ChatAttachment } from './components/ChatSender';
43
45
 
44
46
  // ==================== Core 组件导出(纯展示组件,供高级定制) ====================
45
47
  export { BubbleContent } from './core/BubbleContent';
@@ -76,12 +78,6 @@ export type {
76
78
  ValidationError,
77
79
  } from './components/HumanVerify';
78
80
 
79
- // ==================== A2UI 组件导出 ====================
80
- export { A2UISurface, A2UIStaticViewer } from './components/A2UIRenderer';
81
-
82
- // ==================== A2UI 组件类型导出 ====================
83
- export type { A2UISurfaceProps, A2UIStaticViewerProps, A2UIMessage } from './components/A2UIRenderer';
84
-
85
81
  // ==================== 工具函数导出 ====================
86
82
  export { loadEchartsScript } from './utils/loadEcharts';
87
83
  export { loadMermaidScript } from './utils/loadMermaid';
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
  };
@@ -1,181 +0,0 @@
1
- /**
2
- * A2UISurface - A2UI 声明式 UI 渲染组件
3
- *
4
- * 基于 Google A2UI 协议,将 Agent 生成的声明式 JSON 消息
5
- * 渲染为原生 React 组件。支持流式增量更新和自定义组件注册。
6
- *
7
- */
8
-
9
- import React, { useEffect, useRef, useCallback } from 'react';
10
- import styled from 'styled-components';
11
- import {
12
- A2UIProvider,
13
- A2UIRenderer,
14
- A2UIViewer,
15
- useA2UI,
16
- initializeDefaultCatalog,
17
- ComponentRegistry,
18
- } from '@a2ui/react';
19
- import type {
20
- ServerToClientMessage,
21
- OnActionCallback,
22
- A2UIViewerProps,
23
- } from '@a2ui/react';
24
-
25
- // 初始化默认组件目录(全局只需执行一次)
26
- initializeDefaultCatalog();
27
-
28
- // ==================== Types ====================
29
-
30
- /** A2UI 协议消息(v0.8 格式,透传给 @a2ui/react 处理) */
31
- export type A2UIMessage = ServerToClientMessage;
32
-
33
- export interface A2UISurfaceProps {
34
- /** A2UI JSON 消息数组(来自 Agent 的响应) */
35
- messages: A2UIMessage[];
36
- /** 渲染的 Surface ID(默认 'main') */
37
- surfaceId?: string;
38
- /** 用户在 A2UI 组件上的交互回调 */
39
- onAction?: OnActionCallback;
40
- /** 自定义组件注册表(可选,默认使用内置组件) */
41
- registry?: ComponentRegistry;
42
- /** 自定义类名 */
43
- className?: string;
44
- /** 自定义样式 */
45
- style?: React.CSSProperties;
46
- }
47
-
48
- /** A2UIViewer 的 Props,用于静态 JSON 渲染场景 */
49
- export type A2UIStaticViewerProps = A2UIViewerProps;
50
-
51
- // ==================== Styled Components ====================
52
-
53
- const A2UIContainer = styled.div`
54
- margin-top: 12px;
55
- width: 100%;
56
- max-width: 100%;
57
- `;
58
-
59
- // ==================== Internal Component ====================
60
-
61
- /**
62
- * 内部组件:负责消息处理和 Surface 渲染
63
- * 必须在 A2UIProvider 内部使用,以获取 useA2UI hook 的上下文
64
- */
65
- const A2UISurfaceInner: React.FC<{
66
- messages: A2UIMessage[];
67
- surfaceId: string;
68
- registry?: ComponentRegistry;
69
- className?: string;
70
- style?: React.CSSProperties;
71
- }> = ({ messages, surfaceId, registry, className, style }) => {
72
- const { processMessages, getSurface } = useA2UI();
73
- const processedCountRef = useRef(0);
74
-
75
- // 增量处理新到达的消息(支持流式场景)
76
- useEffect(() => {
77
- if (messages.length > processedCountRef.current) {
78
- const newMessages = messages.slice(processedCountRef.current);
79
- processMessages(newMessages);
80
- processedCountRef.current = messages.length;
81
- }
82
- }, [messages, processMessages]);
83
-
84
- // 当消息被清空时重置计数器
85
- useEffect(() => {
86
- if (messages.length === 0) {
87
- processedCountRef.current = 0;
88
- }
89
- }, [messages.length]);
90
-
91
- const surface = getSurface(surfaceId);
92
-
93
- if (!surface) {
94
- return null;
95
- }
96
-
97
- return (
98
- <A2UIContainer className={className} style={style}>
99
- <A2UIRenderer
100
- surfaceId={surfaceId}
101
- registry={registry}
102
- />
103
- </A2UIContainer>
104
- );
105
- };
106
-
107
- // ==================== Exported Components ====================
108
-
109
- /**
110
- * A2UISurface - A2UI 声明式 UI 渲染组件(流式消息场景)
111
- *
112
- * 接收 Agent 发送的 A2UI 协议消息数组,增量处理并渲染对应的 Surface。
113
- * 内部封装了 A2UIProvider,可直接作为 BubbleContent 的 children 使用。
114
- *
115
- * @example
116
- * ```tsx
117
- * // 作为 BubbleContent 的 children 使用
118
- * <BubbleContent content={content} status={status}>
119
- * <A2UISurface
120
- * messages={a2uiMessages}
121
- * surfaceId="main"
122
- * onAction={(msg) => console.log('用户操作:', msg)}
123
- * />
124
- * </BubbleContent>
125
- * ```
126
- */
127
- export const A2UISurface: React.FC<A2UISurfaceProps> = ({
128
- messages,
129
- surfaceId = 'main',
130
- onAction,
131
- registry,
132
- className,
133
- style,
134
- }) => {
135
- const handleAction: OnActionCallback = useCallback((message) => {
136
- onAction?.(message);
137
- }, [onAction]);
138
-
139
- return (
140
- <A2UIProvider onAction={handleAction}>
141
- <A2UISurfaceInner
142
- messages={messages}
143
- surfaceId={surfaceId}
144
- registry={registry}
145
- className={className}
146
- style={style}
147
- />
148
- </A2UIProvider>
149
- );
150
- };
151
-
152
- /**
153
- * A2UIStaticViewer - A2UI 静态 JSON 渲染组件
154
- *
155
- * 用于直接从静态的组件定义和数据渲染 UI,无需流式消息。
156
- * 适用于已有完整 A2UI 组件树的场景。
157
- *
158
- * @example
159
- * ```tsx
160
- * const components = [
161
- * { id: 'root', component: { Card: { child: 'text' } } },
162
- * { id: 'text', component: { Text: { text: { path: '/message' } } } },
163
- * ];
164
- *
165
- * <A2UIStaticViewer
166
- * root="root"
167
- * components={components}
168
- * data={{ message: 'Hello World!' }}
169
- * onAction={(action) => console.log('Action:', action)}
170
- * />
171
- * ```
172
- */
173
- export const A2UIStaticViewer: React.FC<A2UIStaticViewerProps> = (props) => {
174
- return (
175
- <A2UIContainer>
176
- <A2UIViewer {...props} />
177
- </A2UIContainer>
178
- );
179
- };
180
-
181
- export default A2UISurface;
@@ -1 +0,0 @@
1
- export { A2UISurface, A2UIStaticViewer, type A2UISurfaceProps, type A2UIStaticViewerProps, type A2UIMessage } from './A2UIRenderer';