aix 0.3.0 → 0.3.1
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/README.md +159 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="https://github.com/vercel/aix/blob/main/Aix.png?raw=true"
|
|
2
|
+
alt="aix" width="1600" height="900" />
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
# AIX
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
UI Primitives for building AI apps in React Native.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Features
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- Start a chat scrolled to end on the first frame
|
|
11
|
+
- Animate scrolling to new messages when they send
|
|
12
|
+
- Float messages to the top of the screen with automated "blank size" handling
|
|
13
|
+
- Animate message content as it streams
|
|
14
|
+
- Keyboard handling out-of-the-box with no external dependencies
|
|
15
|
+
- Support for absolute-positioned composers
|
|
16
|
+
- Detect "is scrolled near end" for ScrollToEnd buttons
|
|
17
|
+
|
|
18
|
+
To learn about the motivation behind AIX, you can read our blog post on
|
|
19
|
+
[How we built the v0 iOS app](https://vercel.com/blog/how-we-built-the-v0-ios-app).
|
|
20
|
+
AIX is an opinionated, feature-complete, and extensible way to implement every
|
|
21
|
+
single feature mentioned in that blog post.
|
|
22
|
+
|
|
23
|
+
When building AIX, we started by copying the code from v0 into a separate
|
|
24
|
+
repository. However, as we worked to make it flexible for use cases outside of
|
|
25
|
+
our own app, we decided to rewrite it from scratch in native code. What you see
|
|
26
|
+
here is a Nitro Module which handles all business logic in UIKit. We plan on
|
|
27
|
+
adding support for Android as well and welcome contributions.
|
|
28
|
+
|
|
29
|
+
> aix is currently in alpha preview. The API may change.
|
|
10
30
|
|
|
11
31
|
## Installation
|
|
12
32
|
|
|
@@ -14,133 +34,195 @@ aix is a native module with UIKit with Nitro Modules.
|
|
|
14
34
|
npm i aix react-native-nitro-modules
|
|
15
35
|
```
|
|
16
36
|
|
|
17
|
-
Next, rebuild your native app. For Expo users, run `npx expo prebuild` and
|
|
37
|
+
Next, rebuild your native app. For Expo users, run `npx expo prebuild` and
|
|
38
|
+
rebuild.
|
|
39
|
+
|
|
40
|
+
- For a full example, see the [example app](./react-native-aix/example/App.tsx).
|
|
18
41
|
|
|
19
42
|
## Usage
|
|
20
43
|
|
|
21
44
|
Wrap your `ScrollView` with `Aix`, and wrap your messages with `AixCell`.
|
|
22
45
|
|
|
23
|
-
<details>
|
|
24
|
-
<summary>Click here to view a full example</summary>
|
|
25
|
-
</details>
|
|
26
|
-
|
|
27
46
|
```tsx
|
|
28
|
-
import { Aix, AixCell } from 'aix'
|
|
29
|
-
import { Message } from 'path/to/your/message'
|
|
30
|
-
import { Composer } from 'path/to/your/composer'
|
|
47
|
+
import { Aix, AixCell } from 'aix'
|
|
48
|
+
import { Message } from 'path/to/your/message'
|
|
49
|
+
import { Composer } from 'path/to/your/composer'
|
|
31
50
|
|
|
32
51
|
export function ChatScreen({ messages }) {
|
|
33
52
|
return (
|
|
34
|
-
<Aix style={{ flex: 1 }}>
|
|
53
|
+
<Aix style={{ flex: 1 }} shouldStartAtEnd>
|
|
35
54
|
<ScrollView>
|
|
36
55
|
{messages.map((message) => (
|
|
37
|
-
<AixCell
|
|
38
|
-
key={message.id}
|
|
39
|
-
index={index}
|
|
40
|
-
isLast={index === messages.length - 1}
|
|
41
|
-
>
|
|
56
|
+
<AixCell key={message.id} index={index} isLast={index === messages.length - 1}>
|
|
42
57
|
<Message message={message} />
|
|
43
58
|
</AixCell>
|
|
44
59
|
))}
|
|
45
60
|
</ScrollView>
|
|
46
61
|
</Aix>
|
|
47
|
-
)
|
|
62
|
+
)
|
|
48
63
|
}
|
|
49
64
|
```
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
### Add a floating composer
|
|
67
|
+
|
|
68
|
+
To add a floating composer which lets content scroll under it, you can use the
|
|
69
|
+
`AixFooter`. Pair it with `Aix.scrollOnFooterSizeUpdate` to ensure content
|
|
70
|
+
scrolls under the footer when it changes size.
|
|
52
71
|
|
|
53
72
|
```tsx
|
|
54
|
-
import { Aix, AixCell, AixFooter } from 'aix'
|
|
55
|
-
import { KeyboardStickyView } from 'react-native-keyboard-controller';
|
|
73
|
+
import { Aix, AixCell, AixFooter } from 'aix'
|
|
56
74
|
|
|
57
75
|
export function ChatScreen({ messages }) {
|
|
76
|
+
const { bottom } = useSafeAreaInsets()
|
|
77
|
+
|
|
58
78
|
return (
|
|
59
|
-
<Aix
|
|
79
|
+
<Aix
|
|
80
|
+
style={{ flex: 1 }}
|
|
81
|
+
shouldStartAtEnd
|
|
82
|
+
scrollOnFooterSizeUpdate={{
|
|
83
|
+
enabled: true,
|
|
84
|
+
scrolledToEndThreshold: 100,
|
|
85
|
+
animated: false,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
60
88
|
<ScrollView>
|
|
61
89
|
{messages.map((message) => (
|
|
62
|
-
<AixCell
|
|
63
|
-
key={message.id}
|
|
64
|
-
index={index}
|
|
65
|
-
isLast={index === messages.length - 1}
|
|
66
|
-
>
|
|
90
|
+
<AixCell key={message.id} index={index} isLast={index === messages.length - 1}>
|
|
67
91
|
<Message message={message} />
|
|
68
92
|
</AixCell>
|
|
69
93
|
))}
|
|
70
94
|
</ScrollView>
|
|
71
95
|
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
<AixFooter
|
|
97
|
+
style={{ position: 'absolute', inset: 0, top: 'auto' }}
|
|
98
|
+
stickToKeyboard={{
|
|
99
|
+
enabled: true,
|
|
100
|
+
offset: {
|
|
101
|
+
whenKeyboardOpen: 0,
|
|
102
|
+
whenKeyboardClosed: -bottom,
|
|
103
|
+
},
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<Composer />
|
|
107
|
+
</AixFooter>
|
|
77
108
|
</Aix>
|
|
78
|
-
)
|
|
109
|
+
)
|
|
79
110
|
}
|
|
80
111
|
```
|
|
81
112
|
|
|
82
|
-
|
|
113
|
+
### Send a message
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
When sending a message, you will likely want to scroll to it after it gets added
|
|
116
|
+
to the list.
|
|
117
|
+
|
|
118
|
+
Simply call `aix.current?.scrollToIndexWhenBlankSizeReady(index)` in your submit
|
|
119
|
+
handler.
|
|
120
|
+
|
|
121
|
+
The `index` you pass should correspond to the newest message in the list. For AI
|
|
122
|
+
chats, this is typically the next assistant message index.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { Keyboard } from 'react-native'
|
|
126
|
+
import { useAixRef } from 'aix'
|
|
127
|
+
|
|
128
|
+
function Chat() {
|
|
129
|
+
const aix = useAixRef()
|
|
130
|
+
|
|
131
|
+
const onSubmit = () => {
|
|
132
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
133
|
+
requestAnimationFrame(Keyboard.dismiss)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return <Aix ref={aix}>{/* ... */}</Aix>
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Add a scroll to end button
|
|
141
|
+
|
|
142
|
+
You can use `onScrolledNearEndChange` to show a "scroll to end" button when the
|
|
143
|
+
user scrolls away from the bottom:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { Aix, useAixRef } from 'aix'
|
|
147
|
+
import { useState } from 'react'
|
|
148
|
+
import { Button } from 'react-native'
|
|
149
|
+
|
|
150
|
+
function Chat() {
|
|
151
|
+
const aix = useAixRef()
|
|
152
|
+
const [isNearEnd, setIsNearEnd] = useState(false)
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Aix ref={aix} scrollEndReachedThreshold={200} onScrolledNearEndChange={setIsNearEnd}>
|
|
156
|
+
{/* ScrollView and messages... */}
|
|
157
|
+
|
|
158
|
+
{!isNearEnd && (
|
|
159
|
+
<Button onPress={() => aix.current?.scrollToEnd(true)} title='Scroll to end' />
|
|
160
|
+
)}
|
|
161
|
+
</Aix>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
87
165
|
|
|
88
166
|
## API Reference
|
|
89
167
|
|
|
90
168
|
### `Aix`
|
|
91
169
|
|
|
92
|
-
The main container component that provides keyboard-aware behavior and manages
|
|
170
|
+
The main container component that provides keyboard-aware behavior and manages
|
|
171
|
+
scrolling for chat interfaces.
|
|
93
172
|
|
|
94
173
|
#### Props
|
|
95
174
|
|
|
96
|
-
| Prop
|
|
97
|
-
|
|
98
|
-
| `shouldStartAtEnd`
|
|
99
|
-
| `scrollOnFooterSizeUpdate`
|
|
100
|
-
| `scrollEndReachedThreshold`
|
|
101
|
-
| `onScrolledNearEndChange`
|
|
102
|
-
| `additionalContentInsets`
|
|
103
|
-
| `additionalScrollIndicatorInsets` | `object`
|
|
104
|
-
| `mainScrollViewID`
|
|
105
|
-
| `penultimateCellIndex`
|
|
175
|
+
| Prop | Type | Default | Description |
|
|
176
|
+
| --------------------------------- | ------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
177
|
+
| `shouldStartAtEnd` | `boolean` | - | Whether the scroll view should start scrolled to the end of the content. |
|
|
178
|
+
| `scrollOnFooterSizeUpdate` | `object` | `{ enabled: true, scrolledToEndThreshold: 100, animated: false }` | Control the behavior of scrolling when the footer size changes. By default, changing the height of the footer will shift content up in the scroll view. |
|
|
179
|
+
| `scrollEndReachedThreshold` | `number` | `max(blankSize, 200)` | The number of pixels from the bottom of the scroll view to the end of the content that is considered "near the end". Used by `onScrolledNearEndChange` and to determine if content should shift up when keyboard opens. |
|
|
180
|
+
| `onScrolledNearEndChange` | `(isNearEnd: boolean) => void` | - | Callback fired when the scroll position transitions between "near end" and "not near end" states. Reactive to user scrolling, content size changes, parent size changes, and keyboard height changes. Uses `scrollEndReachedThreshold` to determine the threshold. |
|
|
181
|
+
| `additionalContentInsets` | `object` | - | Additional content insets applied when keyboard is open or closed. Shape: `{ top?: { whenKeyboardOpen, whenKeyboardClosed }, bottom?: { whenKeyboardOpen, whenKeyboardClosed } }` |
|
|
182
|
+
| `additionalScrollIndicatorInsets` | `object` | - | Additional insets for the scroll indicator, added to existing safe area insets. Applied to `verticalScrollIndicatorInsets` on iOS. |
|
|
183
|
+
| `mainScrollViewID` | `string` | - | The `nativeID` of the scroll view to use. If provided, will search for a scroll view with this `accessibilityIdentifier`. |
|
|
184
|
+
| `penultimateCellIndex` | `number` | - | The index of the second-to-last message (typically the last user message in AI chat apps). Used to determine which message will be scrolled into view. Useful when you have custom message types like timestamps in your list. |
|
|
106
185
|
|
|
107
186
|
#### Ref Methods
|
|
108
187
|
|
|
109
188
|
Access these methods via `useAixRef()`:
|
|
110
189
|
|
|
111
190
|
```tsx
|
|
112
|
-
const aix = useAixRef()
|
|
191
|
+
const aix = useAixRef()
|
|
113
192
|
|
|
114
193
|
// Scroll to the end of the content
|
|
115
|
-
aix.current?.scrollToEnd(animated)
|
|
194
|
+
aix.current?.scrollToEnd(animated)
|
|
116
195
|
|
|
117
196
|
// Scroll to a specific index when the blank size is ready
|
|
118
|
-
aix.current?.scrollToIndexWhenBlankSizeReady(index, animated, waitForKeyboardToEnd)
|
|
197
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(index, animated, waitForKeyboardToEnd)
|
|
119
198
|
```
|
|
120
199
|
|
|
121
|
-
| Method
|
|
122
|
-
|
|
123
|
-
| `scrollToEnd`
|
|
200
|
+
| Method | Parameters | Description |
|
|
201
|
+
| --------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
202
|
+
| `scrollToEnd` | `animated?: boolean` | Scrolls to the end of the content. |
|
|
124
203
|
| `scrollToIndexWhenBlankSizeReady` | `index: number, animated?: boolean, waitForKeyboardToEnd?: boolean` | Scrolls to a specific cell index once the blank size calculation is ready. |
|
|
125
204
|
|
|
126
205
|
---
|
|
127
206
|
|
|
128
207
|
### `AixCell`
|
|
129
208
|
|
|
130
|
-
A wrapper component for each message in the list. It communicates cell position
|
|
209
|
+
A wrapper component for each message in the list. It communicates cell position
|
|
210
|
+
and dimensions to the parent `Aix` component.
|
|
131
211
|
|
|
132
212
|
#### Props
|
|
133
213
|
|
|
134
|
-
| Prop
|
|
135
|
-
|
|
136
|
-
| `index`
|
|
137
|
-
| `isLast` | `boolean` | Yes
|
|
214
|
+
| Prop | Type | Required | Description |
|
|
215
|
+
| -------- | --------- | -------- | ------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `index` | `number` | Yes | The index of this cell in the message list. |
|
|
217
|
+
| `isLast` | `boolean` | Yes | Whether this cell is the last item in the list. Used for scroll positioning and animations. |
|
|
138
218
|
|
|
139
219
|
---
|
|
140
220
|
|
|
141
221
|
### `AixFooter`
|
|
142
222
|
|
|
143
|
-
A footer component for floating composers that allows content to scroll
|
|
223
|
+
A footer component for floating composers that allows content to scroll
|
|
224
|
+
underneath it. The footer's height is automatically tracked for proper scroll
|
|
225
|
+
offset calculations.
|
|
144
226
|
|
|
145
227
|
#### Props
|
|
146
228
|
|
|
@@ -148,7 +230,8 @@ Accepts all standard React Native `View` props.
|
|
|
148
230
|
|
|
149
231
|
#### Important Notes
|
|
150
232
|
|
|
151
|
-
- **Do not apply vertical padding** (`padding`, `paddingBottom`) directly to
|
|
233
|
+
- **Do not apply vertical padding** (`padding`, `paddingBottom`) directly to
|
|
234
|
+
`AixFooter`. Apply padding to a child view instead.
|
|
152
235
|
- Position the footer absolutely at the bottom of the `Aix` container:
|
|
153
236
|
|
|
154
237
|
```tsx
|
|
@@ -164,57 +247,32 @@ Accepts all standard React Native `View` props.
|
|
|
164
247
|
A hook that returns a ref to access imperative methods on the `Aix` component.
|
|
165
248
|
|
|
166
249
|
```tsx
|
|
167
|
-
import { useAixRef } from 'aix'
|
|
250
|
+
import { useAixRef } from 'aix'
|
|
168
251
|
|
|
169
252
|
function Chat({ messages }) {
|
|
170
|
-
const aix = useAixRef()
|
|
253
|
+
const aix = useAixRef()
|
|
171
254
|
const send = useSendMessage()
|
|
172
|
-
|
|
255
|
+
|
|
173
256
|
const handleSend = () => {
|
|
174
257
|
// Scroll to end after sending a message
|
|
175
|
-
send(message)
|
|
176
|
-
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
177
|
-
requestAnimationFrame(Keyboard.dismiss)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return <Aix ref={aix}>{/* ... */}</Aix
|
|
258
|
+
send(message)
|
|
259
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
260
|
+
requestAnimationFrame(Keyboard.dismiss)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return <Aix ref={aix}>{/* ... */}</Aix>
|
|
181
264
|
}
|
|
182
265
|
```
|
|
183
266
|
|
|
184
267
|
---
|
|
185
268
|
|
|
186
|
-
### Scroll to End Button
|
|
187
|
-
|
|
188
|
-
You can use `onScrolledNearEndChange` to show a "scroll to end" button when the user scrolls away from the bottom:
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
import { Aix, useAixRef } from 'aix';
|
|
192
|
-
import { useState } from 'react';
|
|
193
|
-
|
|
194
|
-
function Chat() {
|
|
195
|
-
const aix = useAixRef();
|
|
196
|
-
const [isNearEnd, setIsNearEnd] = useState(false);
|
|
197
|
-
|
|
198
|
-
return (
|
|
199
|
-
<Aix
|
|
200
|
-
ref={aix}
|
|
201
|
-
scrollEndReachedThreshold={200}
|
|
202
|
-
onScrolledNearEndChange={setIsNearEnd}
|
|
203
|
-
>
|
|
204
|
-
{/* ScrollView and messages... */}
|
|
205
|
-
|
|
206
|
-
{!isNearEnd && (
|
|
207
|
-
<Button onPress={() => aix.current?.scrollToEnd(true)} />
|
|
208
|
-
)}
|
|
209
|
-
</Aix>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
269
|
## TODOs
|
|
215
270
|
|
|
271
|
+
- [ ] Android support
|
|
272
|
+
- [ ] LegendList support
|
|
273
|
+
- [ ] FlashList support
|
|
216
274
|
|
|
217
275
|
## Requirements
|
|
218
276
|
|
|
219
277
|
- React Native v0.78.0 or higher
|
|
220
|
-
- Node 18.0.0 or higher
|
|
278
|
+
- Node 18.0.0 or higher
|