@unif/react-native-chat 0.1.0

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/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@unif/react-native-chat",
3
+ "version": "0.1.0",
4
+ "description": "UNIF React Native Chat — AI 聊天 UI 组件(Bubble、Sender、Conversations 等)",
5
+ "main": "./src/index.tsx",
6
+ "types": "./src/index.tsx",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./src/index.tsx",
11
+ "default": "./src/index.tsx"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "!**/__tests__",
19
+ "!**/__mocks__",
20
+ "!**/.*"
21
+ ],
22
+ "scripts": {
23
+ "build": "bob build",
24
+ "clean": "del-cli lib",
25
+ "typecheck": "tsc"
26
+ },
27
+ "keywords": ["react-native", "chat", "ui", "bubble", "sender"],
28
+ "author": "qq382724935 <liulijun@pec.com.cn>",
29
+ "license": "MIT",
30
+ "publishConfig": {
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "*",
35
+ "react-native": "*"
36
+ },
37
+ "react-native-builder-bob": {
38
+ "source": "src",
39
+ "output": "lib",
40
+ "targets": [
41
+ ["module", { "esm": true }],
42
+ ["typescript", { "project": "tsconfig.build.json" }]
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Bubble — 消息气泡容器
3
+ *
4
+ * 根据 role 自动镜像布局:
5
+ * - user → 右对齐浅色气泡 + 不对称圆角
6
+ * - assistant → 左侧 Avatar + 名称 + 内容
7
+ */
8
+
9
+ import React from 'react';
10
+ import {
11
+ View,
12
+ Text,
13
+ StyleSheet,
14
+ type ViewStyle,
15
+ type TextStyle,
16
+ } from 'react-native';
17
+ import {chatTokens} from '../theme/tokens';
18
+
19
+ export interface BubbleSemanticStyles {
20
+ root?: ViewStyle;
21
+ avatar?: ViewStyle;
22
+ name?: TextStyle;
23
+ content?: ViewStyle;
24
+ footer?: ViewStyle;
25
+ }
26
+
27
+ export interface BubbleProps {
28
+ role: 'user' | 'assistant' | 'system';
29
+ avatar?: React.ReactNode;
30
+ name?: string;
31
+ children: React.ReactNode;
32
+ footer?: React.ReactNode;
33
+ style?: ViewStyle;
34
+ styles?: Partial<BubbleSemanticStyles>;
35
+ testID?: string;
36
+ }
37
+
38
+ const Bubble: React.FC<BubbleProps> = ({
39
+ role,
40
+ avatar,
41
+ name,
42
+ children,
43
+ footer,
44
+ style,
45
+ styles: semanticStyles,
46
+ testID,
47
+ }) => {
48
+ if (role === 'user') {
49
+ return (
50
+ <View
51
+ style={[defaultStyles.userRow, semanticStyles?.root, style]}
52
+ testID={testID ?? 'bubble-user'}>
53
+ <View style={defaultStyles.userBubble}>
54
+ {typeof children === 'string' ? (
55
+ <Text style={defaultStyles.userText}>{children}</Text>
56
+ ) : (
57
+ children
58
+ )}
59
+ </View>
60
+ {footer && (
61
+ <View style={[defaultStyles.footer, semanticStyles?.footer]}>
62
+ {footer}
63
+ </View>
64
+ )}
65
+ </View>
66
+ );
67
+ }
68
+
69
+ // assistant / system
70
+ return (
71
+ <View
72
+ style={[defaultStyles.assistantRow, semanticStyles?.root, style]}
73
+ testID={testID ?? 'bubble-assistant'}>
74
+ <View style={defaultStyles.headerRow}>
75
+ {avatar && (
76
+ <View style={[defaultStyles.avatarContainer, semanticStyles?.avatar]}>
77
+ {avatar}
78
+ </View>
79
+ )}
80
+ {name && (
81
+ <Text style={[defaultStyles.assistantName, semanticStyles?.name]}>
82
+ {name}
83
+ </Text>
84
+ )}
85
+ </View>
86
+ <View style={[defaultStyles.contentArea, semanticStyles?.content]}>
87
+ {children}
88
+ </View>
89
+ {footer && (
90
+ <View style={[defaultStyles.footer, semanticStyles?.footer]}>
91
+ {footer}
92
+ </View>
93
+ )}
94
+ <View style={defaultStyles.separator} />
95
+ </View>
96
+ );
97
+ };
98
+
99
+ const defaultStyles = StyleSheet.create({
100
+ // Assistant
101
+ assistantRow: {
102
+ paddingHorizontal: chatTokens.spaceMd,
103
+ paddingVertical: chatTokens.spaceSm,
104
+ marginBottom: 12,
105
+ },
106
+ headerRow: {
107
+ flexDirection: 'row',
108
+ alignItems: 'center',
109
+ marginBottom: 6,
110
+ },
111
+ avatarContainer: {
112
+ marginRight: 8,
113
+ },
114
+ assistantName: {
115
+ fontSize: 13,
116
+ fontWeight: '500',
117
+ color: chatTokens.colorTextSecondary,
118
+ },
119
+ contentArea: {
120
+ paddingLeft: 36,
121
+ },
122
+ separator: {
123
+ marginTop: 12,
124
+ marginLeft: 36,
125
+ height: StyleSheet.hairlineWidth,
126
+ backgroundColor: chatTokens.colorBorder,
127
+ opacity: 0.5,
128
+ },
129
+ footer: {
130
+ paddingLeft: 36,
131
+ marginTop: 4,
132
+ },
133
+
134
+ // User
135
+ userRow: {
136
+ flexDirection: 'row',
137
+ justifyContent: 'flex-end',
138
+ flexWrap: 'wrap',
139
+ paddingHorizontal: chatTokens.spaceMd,
140
+ paddingVertical: chatTokens.spaceSm,
141
+ marginBottom: chatTokens.spaceSm,
142
+ },
143
+ userBubble: {
144
+ backgroundColor: chatTokens.colorBgUserMsg,
145
+ borderTopLeftRadius: 20,
146
+ borderTopRightRadius: 20,
147
+ borderBottomLeftRadius: 20,
148
+ borderBottomRightRadius: 6,
149
+ paddingHorizontal: chatTokens.spaceMd,
150
+ paddingVertical: chatTokens.space,
151
+ maxWidth: '75%',
152
+ },
153
+ userText: {
154
+ fontSize: chatTokens.fontSize,
155
+ color: chatTokens.colorText,
156
+ lineHeight: chatTokens.lineHeight,
157
+ },
158
+ });
159
+
160
+ export default React.memo(Bubble);
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Bubble.List — 消息列表
3
+ * 封装 inverted FlatList,自动滚动到底部
4
+ */
5
+
6
+ import React, {useCallback} from 'react';
7
+ import {FlatList, View, StyleSheet, type ViewStyle} from 'react-native';
8
+
9
+ export interface BubbleListProps<T = unknown> {
10
+ items: T[];
11
+ renderBubble: (item: T) => React.ReactNode;
12
+ keyExtractor?: (item: T) => string;
13
+ onEndReached?: () => void;
14
+ header?: React.ReactNode;
15
+ footer?: React.ReactNode;
16
+ style?: ViewStyle;
17
+ testID?: string;
18
+ }
19
+
20
+ function BubbleListInner<T>(props: BubbleListProps<T>) {
21
+ const {
22
+ items,
23
+ renderBubble,
24
+ keyExtractor,
25
+ onEndReached,
26
+ header,
27
+ footer,
28
+ style,
29
+ testID = 'bubble-list',
30
+ } = props;
31
+
32
+ const renderItem = useCallback(
33
+ ({item}: {item: T}) => <>{renderBubble(item)}</>,
34
+ [renderBubble]
35
+ );
36
+
37
+ const defaultKeyExtractor = useCallback(
38
+ (item: T, index: number) => {
39
+ if (keyExtractor) return keyExtractor(item);
40
+ const record = item as Record<string, unknown>;
41
+ return (record['id'] as string) ?? String(index);
42
+ },
43
+ [keyExtractor]
44
+ );
45
+
46
+ if (items.length === 0 && header) {
47
+ return (
48
+ <View style={[defaultStyles.container, style]} testID={testID}>
49
+ {header}
50
+ </View>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <FlatList
56
+ data={items}
57
+ renderItem={renderItem}
58
+ keyExtractor={defaultKeyExtractor}
59
+ inverted
60
+ style={[defaultStyles.list, style]}
61
+ contentContainerStyle={defaultStyles.contentContainer}
62
+ onEndReached={onEndReached}
63
+ onEndReachedThreshold={0.3}
64
+ ListFooterComponent={footer ? <>{footer}</> : null}
65
+ showsVerticalScrollIndicator={false}
66
+ testID={testID}
67
+ />
68
+ );
69
+ }
70
+
71
+ const defaultStyles = StyleSheet.create({
72
+ container: {
73
+ flex: 1,
74
+ },
75
+ list: {
76
+ flex: 1,
77
+ },
78
+ contentContainer: {
79
+ paddingVertical: 8,
80
+ },
81
+ });
82
+
83
+ const BubbleList = React.memo(BubbleListInner) as typeof BubbleListInner;
84
+ export default BubbleList;
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: Bubble 消息气泡
3
+ nav:
4
+ title: 组件
5
+ path: /components
6
+ ---
7
+
8
+ # Bubble 消息气泡
9
+
10
+ 消息气泡容器,根据 `role` 自动镜像布局。
11
+
12
+ ## 何时使用
13
+
14
+ - 展示聊天消息(用户消息、AI 回复、系统消息)
15
+ - 需要根据角色自动区分左右布局
16
+
17
+ ## 代码示例
18
+
19
+ ### 基本用法
20
+
21
+ ```tsx
22
+ import { Bubble } from '@unif/react-native-chat';
23
+
24
+ // 用户消息(右对齐)
25
+ <Bubble role="user">你好,请帮我查询订单</Bubble>
26
+
27
+ // AI 回复(左对齐)
28
+ <Bubble role="assistant" name="小U" avatar={<Avatar name="AI" />}>
29
+ 好的,正在为您查询...
30
+ </Bubble>
31
+ ```
32
+
33
+ ### 自定义底部区域
34
+
35
+ ```tsx
36
+ <Bubble role="assistant" footer={<Text>14:30</Text>}>
37
+ 这是消息内容
38
+ </Bubble>
39
+ ```
40
+
41
+ ### 语义样式覆盖
42
+
43
+ ```tsx
44
+ <Bubble
45
+ role="user"
46
+ styles={{
47
+ root: { paddingHorizontal: 20 },
48
+ content: { backgroundColor: '#E8F0FE' },
49
+ }}
50
+ >
51
+ 自定义样式的消息
52
+ </Bubble>
53
+ ```
54
+
55
+ ## API
56
+
57
+ ### BubbleProps
58
+
59
+ | 属性 | 说明 | 类型 | 默认值 |
60
+ |------|------|------|--------|
61
+ | role | 消息角色 | `'user' \| 'assistant' \| 'system'` | - |
62
+ | avatar | 头像(assistant/system 角色显示) | `ReactNode` | - |
63
+ | name | 发送者名称 | `string` | - |
64
+ | children | 消息内容 | `ReactNode` | - |
65
+ | footer | 底部区域(时间戳、操作等) | `ReactNode` | - |
66
+ | style | 外层容器样式 | `ViewStyle` | - |
67
+ | styles | 语义样式覆盖 | `Partial<BubbleSemanticStyles>` | - |
68
+ | testID | 测试标识 | `string` | - |
69
+
70
+ ### BubbleSemanticStyles
71
+
72
+ | 属性 | 说明 | 类型 |
73
+ |------|------|------|
74
+ | root | 外层容器 | `ViewStyle` |
75
+ | avatar | 头像容器 | `ViewStyle` |
76
+ | name | 名称文字 | `TextStyle` |
77
+ | content | 内容区域 | `ViewStyle` |
78
+ | footer | 底部区域 | `ViewStyle` |
79
+
80
+ ## Bubble.List 消息列表
81
+
82
+ 封装 inverted FlatList,消息从底部向上排列,支持泛型。
83
+
84
+ ### 基本用法
85
+
86
+ ```tsx
87
+ import { Bubble } from '@unif/react-native-chat';
88
+
89
+ <Bubble.List
90
+ items={messages}
91
+ header={<Welcome />}
92
+ renderBubble={(msg) => (
93
+ <Bubble role={msg.message.role}>
94
+ {msg.message.text}
95
+ </Bubble>
96
+ )}
97
+ />
98
+ ```
99
+
100
+ ### BubbleListProps
101
+
102
+ | 属性 | 说明 | 类型 | 默认值 |
103
+ |------|------|------|--------|
104
+ | items | 消息数据 | `T[]` | - |
105
+ | renderBubble | 渲染单条消息 | `(item: T) => ReactNode` | - |
106
+ | keyExtractor | 自定义 key 提取 | `(item: T) => string` | 使用 `item.id` |
107
+ | onEndReached | 滚动到底部回调(加载更多) | `() => void` | - |
108
+ | header | 列表为空时的顶部占位 | `ReactNode` | - |
109
+ | footer | 列表底部内容 | `ReactNode` | - |
110
+ | style | 容器样式 | `ViewStyle` | - |
111
+ | testID | 测试标识 | `string` | `'bubble-list'` |
@@ -0,0 +1,76 @@
1
+ /**
2
+ * CardWrapper — 通用卡片路由容器
3
+ * registry 通过 props 注入(而非全局 import),更灵活
4
+ */
5
+
6
+ import React from 'react';
7
+ import {View, Text, StyleSheet, type ViewStyle} from 'react-native';
8
+ import {chatTokens} from '../theme/tokens';
9
+
10
+ export type CardComponentType = React.ComponentType<{
11
+ data: Record<string, unknown>;
12
+ actions: string[];
13
+ }>;
14
+
15
+ export interface CardWrapperProps {
16
+ cardType: string;
17
+ data: Record<string, unknown>;
18
+ actions: string[];
19
+ registry?: Record<string, CardComponentType>;
20
+ fallback?: React.ReactNode;
21
+ style?: ViewStyle;
22
+ testID?: string;
23
+ }
24
+
25
+ const CardWrapper: React.FC<CardWrapperProps> = ({
26
+ cardType,
27
+ data,
28
+ actions,
29
+ registry = {},
30
+ fallback,
31
+ style,
32
+ testID,
33
+ }) => {
34
+ const CardComponent = registry[cardType];
35
+
36
+ if (!CardComponent) {
37
+ return (
38
+ <View style={[defaultStyles.container, style]} testID={testID}>
39
+ {fallback ?? (
40
+ <Text style={defaultStyles.fallback}>
41
+ 不支持的卡片类型: {cardType}
42
+ </Text>
43
+ )}
44
+ </View>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <View
50
+ style={[defaultStyles.container, style]}
51
+ testID={testID ?? `card-${cardType}`}>
52
+ <CardComponent data={data} actions={actions} />
53
+ </View>
54
+ );
55
+ };
56
+
57
+ const defaultStyles = StyleSheet.create({
58
+ container: {
59
+ backgroundColor: chatTokens.colorBgElevated,
60
+ borderRadius: 12,
61
+ borderWidth: 1,
62
+ borderColor: chatTokens.colorBorder,
63
+ marginLeft: 8,
64
+ marginRight: 40,
65
+ marginBottom: 4,
66
+ overflow: 'hidden',
67
+ maxWidth: '85%',
68
+ },
69
+ fallback: {
70
+ padding: 12,
71
+ fontSize: 13,
72
+ color: chatTokens.colorTextSecondary,
73
+ },
74
+ });
75
+
76
+ export default React.memo(CardWrapper);
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: CardWrapper 卡片容器
3
+ nav:
4
+ title: 组件
5
+ path: /components
6
+ ---
7
+
8
+ # CardWrapper 卡片容器
9
+
10
+ 通用卡片路由容器,通过 `registry` props 注入实现卡片类型到组件的映射。
11
+
12
+ ## 何时使用
13
+
14
+ - AI 回复中包含结构化卡片内容(产品列表、表单、图表等)
15
+ - 需要根据 `cardType` 动态渲染不同业务组件
16
+
17
+ ## 代码示例
18
+
19
+ ### 基本用法
20
+
21
+ ```tsx
22
+ import { CardWrapper, type CardComponentType } from '@unif/react-native-chat';
23
+
24
+ // 定义业务卡片
25
+ const ProductCard: CardComponentType = ({ data, actions }) => (
26
+ <View>
27
+ <Text>{data.productName as string}</Text>
28
+ <Text>{data.price as string}</Text>
29
+ </View>
30
+ );
31
+
32
+ // 注册卡片
33
+ const registry = {
34
+ product_list: ProductCard,
35
+ };
36
+
37
+ <CardWrapper
38
+ cardType="product_list"
39
+ data={{ productName: '基金A', price: '1.2345' }}
40
+ actions={['buy', 'detail']}
41
+ registry={registry}
42
+ />
43
+ ```
44
+
45
+ ### 自定义 fallback
46
+
47
+ ```tsx
48
+ <CardWrapper
49
+ cardType="unknown_type"
50
+ data={{}}
51
+ actions={[]}
52
+ registry={registry}
53
+ fallback={<Text>暂不支持此类型</Text>}
54
+ />
55
+ ```
56
+
57
+ ## API
58
+
59
+ ### CardWrapperProps
60
+
61
+ | 属性 | 说明 | 类型 | 默认值 |
62
+ |------|------|------|--------|
63
+ | cardType | 卡片类型标识 | `string` | - |
64
+ | data | 卡片数据 | `Record<string, unknown>` | - |
65
+ | actions | 可用操作列表 | `string[]` | - |
66
+ | registry | 卡片注册表 | `Record<string, CardComponentType>` | `{}` |
67
+ | fallback | 未知卡片类型的 fallback | `ReactNode` | 默认错误提示 |
68
+ | style | 容器样式 | `ViewStyle` | - |
69
+ | testID | 测试标识 | `string` | - |
70
+
71
+ ### CardComponentType
72
+
73
+ ```typescript
74
+ type CardComponentType = React.ComponentType<{
75
+ data: Record<string, unknown>;
76
+ actions: string[];
77
+ }>;
78
+ ```