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.
Files changed (2) hide show
  1. package/README.md +159 -101
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,32 @@
1
- # aix
1
+ <img src="https://github.com/vercel/aix/blob/main/Aix.png?raw=true"
2
+ alt="aix" width="1600" height="900" />
2
3
 
3
- Primitives for building beautiful AI chat apps with React Native.
4
+ # AIX
4
5
 
5
- > aix is currently in alpha preview. The API will change, and it is not yet feature complete.
6
+ UI Primitives for building AI apps in React Native.
6
7
 
7
- We're rewriting the engine that powers the chat experience in the v0 mobile app with a focus on native feel.
8
+ ## Features
8
9
 
9
- aix is a native module with UIKit with Nitro Modules.
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 rebuild.
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
- To add a floating composer which lets content scroll under it, you can use the `AixFooter` and `KeyboardStickyView` from `react-native-keyboard-controller`:
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 style={{ flex: 1 }}>
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
- <KeyboardStickyView offset={{ opened: 0, closed: -bottomInsetPadding }}>
73
- <AixFooter style={{ position: 'absolute', inset: 0, top: 'auto'}}>
74
- <Composer />
75
- </AixFooter>
76
- </KeyboardStickyView>
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
- ## TODOs
113
+ ### Send a message
83
114
 
84
- - [ ] Android support
85
- - [ ] LegendList support
86
- - [ ] FlashList support
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 scrolling for chat interfaces.
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 | Type | Default | Description |
97
- |------|------|---------|-------------|
98
- | `shouldStartAtEnd` | `boolean` | - | Whether the scroll view should start scrolled to the end of the content. |
99
- | `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. |
100
- | `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. |
101
- | `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. |
102
- | `additionalContentInsets` | `object` | - | Additional content insets applied when keyboard is open or closed. Shape: `{ top?: { whenKeyboardOpen, whenKeyboardClosed }, bottom?: { whenKeyboardOpen, whenKeyboardClosed } }` |
103
- | `additionalScrollIndicatorInsets` | `object` | - | Additional insets for the scroll indicator, added to existing safe area insets. Applied to `verticalScrollIndicatorInsets` on iOS. |
104
- | `mainScrollViewID` | `string` | - | The `nativeID` of the scroll view to use. If provided, will search for a scroll view with this `accessibilityIdentifier`. |
105
- | `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. |
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 | Parameters | Description |
122
- |--------|------------|-------------|
123
- | `scrollToEnd` | `animated?: boolean` | Scrolls to the end of the content. |
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 and dimensions to the parent `Aix` component.
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 | Type | Required | Description |
135
- |------|------|----------|-------------|
136
- | `index` | `number` | Yes | The index of this cell in the message list. |
137
- | `isLast` | `boolean` | Yes | Whether this cell is the last item in the list. Used for scroll positioning and animations. |
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 underneath it. The footer's height is automatically tracked for proper scroll offset calculations.
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 `AixFooter`. Apply padding to a child view instead.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aix",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "author": "Fernando Rojo",
5
5
  "repository": {
6
6
  "type": "git",