@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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Conversations — 会话列表
3
+ *
4
+ * SectionList + 日期分组(今天/昨天/更早)
5
+ * 基于 ListItem 模式渲染
6
+ */
7
+
8
+ import React, {useCallback, useMemo} from 'react';
9
+ import {
10
+ View,
11
+ Text,
12
+ TouchableOpacity,
13
+ Alert,
14
+ SectionList,
15
+ StyleSheet,
16
+ type ViewStyle,
17
+ type TextStyle,
18
+ } from 'react-native';
19
+ import {chatTokens} from '../theme/tokens';
20
+
21
+ export interface ConversationItem {
22
+ id: string;
23
+ title: string;
24
+ lastMessage?: string;
25
+ timestamp: number;
26
+ messageCount?: number;
27
+ }
28
+
29
+ export interface ConversationsSemanticStyles {
30
+ root?: ViewStyle;
31
+ newButton?: ViewStyle;
32
+ sectionHeader?: TextStyle;
33
+ item?: ViewStyle;
34
+ itemActive?: ViewStyle;
35
+ }
36
+
37
+ export interface ConversationsProps {
38
+ items: ConversationItem[];
39
+ activeId?: string;
40
+ onSelect: (id: string) => void;
41
+ onDelete?: (id: string) => void;
42
+ onNew?: () => void;
43
+ groupByDate?: boolean;
44
+ header?: React.ReactNode;
45
+ style?: ViewStyle;
46
+ styles?: Partial<ConversationsSemanticStyles>;
47
+ testID?: string;
48
+ }
49
+
50
+ function groupByDate(sessions: ConversationItem[]) {
51
+ const now = new Date();
52
+ const todayStart = new Date(
53
+ now.getFullYear(),
54
+ now.getMonth(),
55
+ now.getDate()
56
+ ).getTime();
57
+ const yesterdayStart = todayStart - 86400000;
58
+
59
+ const groups: { title: string; data: ConversationItem[] }[] = [];
60
+ const today: ConversationItem[] = [];
61
+ const yesterday: ConversationItem[] = [];
62
+ const earlier: ConversationItem[] = [];
63
+
64
+ for (const s of sessions) {
65
+ if (s.timestamp >= todayStart) {
66
+ today.push(s);
67
+ } else if (s.timestamp >= yesterdayStart) {
68
+ yesterday.push(s);
69
+ } else {
70
+ earlier.push(s);
71
+ }
72
+ }
73
+
74
+ if (today.length > 0) groups.push({ title: '今天', data: today });
75
+ if (yesterday.length > 0) groups.push({ title: '昨天', data: yesterday });
76
+ if (earlier.length > 0) groups.push({ title: '更早', data: earlier });
77
+
78
+ return groups;
79
+ }
80
+
81
+ const Conversations: React.FC<ConversationsProps> = ({
82
+ items,
83
+ activeId,
84
+ onSelect,
85
+ onDelete,
86
+ onNew,
87
+ groupByDate: shouldGroup = true,
88
+ header,
89
+ style,
90
+ styles: semanticStyles,
91
+ testID = 'conversations',
92
+ }) => {
93
+ const sections = useMemo(() => {
94
+ if (shouldGroup) {
95
+ return groupByDate(items);
96
+ }
97
+ return [{ title: '', data: items }];
98
+ }, [items, shouldGroup]);
99
+
100
+ const handleDelete = useCallback(
101
+ (item: ConversationItem) => {
102
+ if (!onDelete) return;
103
+ Alert.alert('删除会话', `确定删除"${item.title}"?`, [
104
+ { text: '取消', style: 'cancel' },
105
+ {
106
+ text: '删除',
107
+ style: 'destructive',
108
+ onPress: () => onDelete(item.id),
109
+ },
110
+ ]);
111
+ },
112
+ [onDelete]
113
+ );
114
+
115
+ const renderItem = useCallback(
116
+ ({ item }: { item: ConversationItem }) => {
117
+ const isActive = item.id === activeId;
118
+ return (
119
+ <TouchableOpacity
120
+ style={[
121
+ defaultStyles.sessionItem,
122
+ semanticStyles?.item,
123
+ isActive && defaultStyles.sessionItemActive,
124
+ isActive && semanticStyles?.itemActive,
125
+ ]}
126
+ onPress={() => onSelect(item.id)}
127
+ onLongPress={() => handleDelete(item)}
128
+ activeOpacity={0.7}
129
+ testID={`${testID}-item-${item.id}`}>
130
+ <Text style={defaultStyles.sessionTitle} numberOfLines={1}>
131
+ {item.title}
132
+ </Text>
133
+ </TouchableOpacity>
134
+ );
135
+ },
136
+ [activeId, onSelect, handleDelete, semanticStyles, testID]
137
+ );
138
+
139
+ const renderSectionHeader = useCallback(
140
+ ({ section }: { section: { title: string } }) => {
141
+ if (!section.title) return null;
142
+ return (
143
+ <Text
144
+ style={[
145
+ defaultStyles.sectionHeader,
146
+ semanticStyles?.sectionHeader,
147
+ ]}>
148
+ {section.title}
149
+ </Text>
150
+ );
151
+ },
152
+ [semanticStyles]
153
+ );
154
+
155
+ return (
156
+ <View
157
+ style={[defaultStyles.container, semanticStyles?.root, style]}
158
+ testID={testID}>
159
+ {header}
160
+
161
+ {onNew && (
162
+ <View style={defaultStyles.newButtonWrapper}>
163
+ <TouchableOpacity
164
+ style={[defaultStyles.newButton, semanticStyles?.newButton]}
165
+ onPress={onNew}
166
+ testID={`${testID}-new`}>
167
+ <Text style={defaultStyles.newButtonText}>+ 新建会话</Text>
168
+ </TouchableOpacity>
169
+ </View>
170
+ )}
171
+
172
+ <SectionList
173
+ sections={sections}
174
+ keyExtractor={(item) => item.id}
175
+ renderItem={renderItem}
176
+ renderSectionHeader={renderSectionHeader}
177
+ style={defaultStyles.list}
178
+ showsVerticalScrollIndicator={false}
179
+ stickySectionHeadersEnabled={false}
180
+ />
181
+ </View>
182
+ );
183
+ };
184
+
185
+ const defaultStyles = StyleSheet.create({
186
+ container: {
187
+ flex: 1,
188
+ },
189
+ newButtonWrapper: {
190
+ alignItems: 'center',
191
+ paddingVertical: 12,
192
+ paddingHorizontal: 20,
193
+ },
194
+ newButton: {
195
+ flexDirection: 'row',
196
+ alignItems: 'center',
197
+ justifyContent: 'center',
198
+ width: '80%',
199
+ paddingVertical: 10,
200
+ borderRadius: chatTokens.radiusFull,
201
+ borderWidth: 1,
202
+ borderColor: chatTokens.colorPrimary,
203
+ },
204
+ newButtonText: {
205
+ fontSize: 14,
206
+ fontWeight: '500',
207
+ color: chatTokens.colorPrimary,
208
+ },
209
+ sectionHeader: {
210
+ fontSize: 12,
211
+ color: chatTokens.colorTextSecondary,
212
+ paddingHorizontal: 20,
213
+ paddingTop: 16,
214
+ paddingBottom: 6,
215
+ },
216
+ sessionItem: {
217
+ flexDirection: 'row',
218
+ alignItems: 'center',
219
+ paddingHorizontal: 20,
220
+ paddingVertical: 12,
221
+ },
222
+ sessionItemActive: {
223
+ backgroundColor: '#F3F4F6',
224
+ },
225
+ sessionTitle: {
226
+ flex: 1,
227
+ fontSize: 14,
228
+ fontWeight: '500',
229
+ color: chatTokens.colorText,
230
+ },
231
+ list: {
232
+ flex: 1,
233
+ },
234
+ });
235
+
236
+ export default React.memo(Conversations);
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Conversations 会话列表
3
+ nav:
4
+ title: 组件
5
+ path: /components
6
+ ---
7
+
8
+ # Conversations 会话列表
9
+
10
+ 按日期分组的会话列表组件,支持新建、选中、删除操作。
11
+
12
+ ## 何时使用
13
+
14
+ - 展示聊天历史会话列表
15
+ - 需要按日期分组(今天/昨天/更早)
16
+ - 侧边栏会话管理
17
+
18
+ ## 代码示例
19
+
20
+ ### 基本用法
21
+
22
+ ```tsx
23
+ import { Conversations } from '@unif/react-native-chat';
24
+
25
+ <Conversations
26
+ items={sessions}
27
+ activeId={currentSessionId}
28
+ onSelect={(id) => switchSession(id)}
29
+ onDelete={(id) => deleteSession(id)}
30
+ onNew={() => createNewSession()}
31
+ />
32
+ ```
33
+
34
+ ### 不分组
35
+
36
+ ```tsx
37
+ <Conversations
38
+ items={sessions}
39
+ activeId={currentSessionId}
40
+ onSelect={handleSelect}
41
+ groupByDate={false}
42
+ />
43
+ ```
44
+
45
+ ### 自定义头部
46
+
47
+ ```tsx
48
+ <Conversations
49
+ items={sessions}
50
+ onSelect={handleSelect}
51
+ header={<DrawerHeader />}
52
+ />
53
+ ```
54
+
55
+ ## API
56
+
57
+ ### ConversationsProps
58
+
59
+ | 属性 | 说明 | 类型 | 默认值 |
60
+ |------|------|------|--------|
61
+ | items | 会话列表数据 | `ConversationItem[]` | - |
62
+ | activeId | 当前选中会话 ID | `string` | - |
63
+ | onSelect | 选中会话回调 | `(id: string) => void` | - |
64
+ | onDelete | 删除会话回调(长按触发) | `(id: string) => void` | - |
65
+ | onNew | 新建会话回调 | `() => void` | - |
66
+ | groupByDate | 按日期分组 | `boolean` | `true` |
67
+ | header | 列表顶部内容 | `ReactNode` | - |
68
+ | style | 容器样式 | `ViewStyle` | - |
69
+ | styles | 语义样式 | `Partial<ConversationsSemanticStyles>` | - |
70
+ | testID | 测试标识 | `string` | `'conversations'` |
71
+
72
+ ### ConversationItem
73
+
74
+ | 属性 | 说明 | 类型 |
75
+ |------|------|------|
76
+ | id | 唯一标识 | `string` |
77
+ | title | 会话标题 | `string` |
78
+ | lastMessage | 最后一条消息 | `string` |
79
+ | timestamp | 时间戳(毫秒) | `number` |
80
+ | messageCount | 消息数量 | `number` |
81
+
82
+ ### ConversationsSemanticStyles
83
+
84
+ | 属性 | 说明 | 类型 |
85
+ |------|------|------|
86
+ | root | 外层容器 | `ViewStyle` |
87
+ | newButton | 新建按钮 | `ViewStyle` |
88
+ | sectionHeader | 分组标题 | `TextStyle` |
89
+ | item | 会话项 | `ViewStyle` |
90
+ | itemActive | 选中态会话项 | `ViewStyle` |
package/src/index.tsx ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @unif/react-native-chat
3
+ * AI 聊天 UI 组件库 — Bubble、Sender、Conversations、Prompts、Welcome、CardWrapper
4
+ */
5
+
6
+ // Bubble
7
+ export {default as Bubble} from './bubble/Bubble';
8
+ export type {BubbleProps, BubbleSemanticStyles} from './bubble/Bubble';
9
+ export {default as BubbleList} from './bubble/BubbleList';
10
+ export type {BubbleListProps} from './bubble/BubbleList';
11
+
12
+ // Sender
13
+ export {default as Sender} from './sender/Sender';
14
+ export type {SenderProps, SenderSemanticStyles, ActionSheetOption} from './sender/Sender';
15
+
16
+ // Conversations
17
+ export {default as Conversations} from './conversations/Conversations';
18
+ export type {
19
+ ConversationsProps,
20
+ ConversationsSemanticStyles,
21
+ ConversationItem,
22
+ } from './conversations/Conversations';
23
+
24
+ // Prompts
25
+ export {default as Prompts} from './prompts/Prompts';
26
+ export type {PromptsProps, PromptsSemanticStyles, PromptItem} from './prompts/Prompts';
27
+
28
+ // Welcome
29
+ export {default as Welcome} from './welcome/Welcome';
30
+ export type {
31
+ WelcomeProps,
32
+ WelcomeSemanticStyles,
33
+ WelcomeQuickStart,
34
+ } from './welcome/Welcome';
35
+
36
+ // CardWrapper
37
+ export {default as CardWrapper} from './card-wrapper/CardWrapper';
38
+ export type {CardWrapperProps, CardComponentType} from './card-wrapper/CardWrapper';
39
+
40
+ // Theme
41
+ export {chatTokens} from './theme/tokens';
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Prompts — 建议提示
3
+ * 水平 ScrollView + Chip 列表
4
+ */
5
+
6
+ import React from 'react';
7
+ import {
8
+ View,
9
+ Text,
10
+ ScrollView,
11
+ TouchableOpacity,
12
+ StyleSheet,
13
+ type ViewStyle,
14
+ type TextStyle,
15
+ } from 'react-native';
16
+ import {chatTokens} from '../theme/tokens';
17
+
18
+ export interface PromptItem {
19
+ id: string;
20
+ label: string;
21
+ }
22
+
23
+ export interface PromptsSemanticStyles {
24
+ root?: ViewStyle;
25
+ chip?: ViewStyle;
26
+ chipText?: TextStyle;
27
+ }
28
+
29
+ export interface PromptsProps {
30
+ items: PromptItem[];
31
+ onSelect: (item: PromptItem) => void;
32
+ disabled?: boolean;
33
+ wrap?: boolean;
34
+ style?: ViewStyle;
35
+ styles?: Partial<PromptsSemanticStyles>;
36
+ testID?: string;
37
+ }
38
+
39
+ const Prompts: React.FC<PromptsProps> = ({
40
+ items,
41
+ onSelect,
42
+ disabled = false,
43
+ wrap = false,
44
+ style,
45
+ styles: semanticStyles,
46
+ testID = 'prompts',
47
+ }) => {
48
+ if (items.length === 0) return null;
49
+
50
+ const renderChips = () =>
51
+ items.map((item) => (
52
+ <TouchableOpacity
53
+ key={item.id}
54
+ style={[defaultStyles.chip, semanticStyles?.chip]}
55
+ onPress={() => !disabled && onSelect(item)}
56
+ activeOpacity={disabled ? 1 : 0.7}
57
+ disabled={disabled}
58
+ testID={`${testID}-${item.id}`}>
59
+ <Text
60
+ style={[
61
+ defaultStyles.chipText,
62
+ semanticStyles?.chipText,
63
+ disabled && defaultStyles.chipTextDisabled,
64
+ ]}
65
+ numberOfLines={1}>
66
+ {item.label}
67
+ </Text>
68
+ </TouchableOpacity>
69
+ ));
70
+
71
+ if (wrap) {
72
+ return (
73
+ <View
74
+ style={[defaultStyles.container, defaultStyles.wrapContainer, semanticStyles?.root, style]}
75
+ testID={testID}>
76
+ {renderChips()}
77
+ </View>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <View style={[defaultStyles.container, semanticStyles?.root, style]} testID={testID}>
83
+ <ScrollView
84
+ horizontal
85
+ showsHorizontalScrollIndicator={false}
86
+ contentContainerStyle={defaultStyles.scrollContent}>
87
+ {renderChips()}
88
+ </ScrollView>
89
+ </View>
90
+ );
91
+ };
92
+
93
+ const defaultStyles = StyleSheet.create({
94
+ container: {
95
+ paddingVertical: 8,
96
+ backgroundColor: 'transparent',
97
+ },
98
+ scrollContent: {
99
+ paddingHorizontal: 12,
100
+ gap: 8,
101
+ },
102
+ wrapContainer: {
103
+ flexDirection: 'row',
104
+ flexWrap: 'wrap',
105
+ paddingHorizontal: 12,
106
+ gap: 8,
107
+ },
108
+ chip: {
109
+ backgroundColor: '#F3F4F6',
110
+ borderRadius: 16,
111
+ paddingHorizontal: 14,
112
+ paddingVertical: 8,
113
+ },
114
+ chipText: {
115
+ fontSize: 13,
116
+ color: chatTokens.colorText,
117
+ },
118
+ chipTextDisabled: {
119
+ color: chatTokens.colorTextPlaceholder,
120
+ },
121
+ });
122
+
123
+ export default React.memo(Prompts);
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Prompts 建议提示
3
+ nav:
4
+ title: 组件
5
+ path: /components
6
+ ---
7
+
8
+ # Prompts 建议提示
9
+
10
+ 水平滚动的建议提示 Chip 列表,可选换行模式。
11
+
12
+ ## 何时使用
13
+
14
+ - AI 回复后展示后续建议问题
15
+ - 欢迎页展示快捷提问入口
16
+
17
+ ## 代码示例
18
+
19
+ ### 水平滚动
20
+
21
+ ```tsx
22
+ import { Prompts } from '@unif/react-native-chat';
23
+
24
+ <Prompts
25
+ items={[
26
+ { id: '1', label: '查询余额' },
27
+ { id: '2', label: '转账汇款' },
28
+ { id: '3', label: '理财推荐' },
29
+ ]}
30
+ onSelect={(item) => console.log(item.label)}
31
+ />
32
+ ```
33
+
34
+ ### 换行模式
35
+
36
+ ```tsx
37
+ <Prompts
38
+ items={suggestions}
39
+ onSelect={handleSelect}
40
+ wrap={true}
41
+ />
42
+ ```
43
+
44
+ ### 禁用状态
45
+
46
+ ```tsx
47
+ <Prompts
48
+ items={suggestions}
49
+ onSelect={handleSelect}
50
+ disabled={isRequesting}
51
+ />
52
+ ```
53
+
54
+ ## API
55
+
56
+ ### PromptsProps
57
+
58
+ | 属性 | 说明 | 类型 | 默认值 |
59
+ |------|------|------|--------|
60
+ | items | 建议项列表 | `PromptItem[]` | - |
61
+ | onSelect | 选中回调 | `(item: PromptItem) => void` | - |
62
+ | disabled | 禁用状态 | `boolean` | `false` |
63
+ | wrap | 换行模式(FlexWrap) | `boolean` | `false` |
64
+ | style | 容器样式 | `ViewStyle` | - |
65
+ | styles | 语义样式 | `Partial<PromptsSemanticStyles>` | - |
66
+ | testID | 测试标识 | `string` | `'prompts'` |
67
+
68
+ ### PromptItem
69
+
70
+ | 属性 | 说明 | 类型 |
71
+ |------|------|------|
72
+ | id | 唯一标识 | `string` |
73
+ | label | 显示文字 | `string` |
74
+
75
+ ### PromptsSemanticStyles
76
+
77
+ | 属性 | 说明 | 类型 |
78
+ |------|------|------|
79
+ | root | 外层容器 | `ViewStyle` |
80
+ | chip | Chip 样式 | `ViewStyle` |
81
+ | chipText | Chip 文字 | `TextStyle` |