-
Notifications
You must be signed in to change notification settings - Fork 46
ait: add react token streaming examples #3268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -449,6 +449,31 @@ channel.subscribe(message -> { | |
| } | ||
| }); | ||
| ``` | ||
| ```react | ||
| const [responses, setResponses] = useState(new Map()); | ||
|
|
||
| // Subscribe to live messages | ||
| useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => { | ||
| setResponses((prev) => { | ||
| const next = new Map(prev); | ||
| switch (message.action) { | ||
| case 'message.create': | ||
| // New response started | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| case 'message.append': | ||
| // Append token to existing response | ||
| next.set(message.serial, (next.get(message.serial) || '') + message.data); | ||
| break; | ||
| case 'message.update': | ||
| // Replace entire response content | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| } | ||
| return next; | ||
| }); | ||
| }); | ||
| ``` | ||
| </Code> | ||
|
|
||
| ## Client hydration <a id="hydration"/> | ||
|
|
@@ -549,6 +574,31 @@ channel.subscribe(message -> { | |
| } | ||
| }); | ||
| ``` | ||
| ```react | ||
| // Set rewind via ChannelProvider options={{ params: { rewind: '2m' } }} | ||
|
|
||
| const [responses, setResponses] = useState(new Map()); | ||
|
|
||
| // Receive both recent historical (via rewind) and live messages | ||
| useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => { | ||
| setResponses((prev) => { | ||
| const next = new Map(prev); | ||
| switch (message.action) { | ||
| case 'message.create': | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| case 'message.append': | ||
| const current = next.get(message.serial) || ''; | ||
| next.set(message.serial, current + message.data); | ||
| break; | ||
| case 'message.update': | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| } | ||
| return next; | ||
| }); | ||
| }); | ||
| ``` | ||
| </Code> | ||
|
|
||
| Rewind supports two formats: | ||
|
|
@@ -678,6 +728,46 @@ while (page != null) { | |
| page = page.hasNext() ? page.next() : null; | ||
| } | ||
| ``` | ||
| ```react | ||
| const [responses, setResponses] = useState(new Map()); | ||
| const hydrated = useRef(false); | ||
|
|
||
| // Subscribe to live messages and get the history function | ||
| const { history } = useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => { | ||
| setResponses((prev) => { | ||
| const next = new Map(prev); | ||
| switch (message.action) { | ||
| case 'message.create': | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| case 'message.append': | ||
| next.set(message.serial, (next.get(message.serial) || '') + message.data); | ||
| break; | ||
| case 'message.update': | ||
| next.set(message.serial, message.data); | ||
| break; | ||
| } | ||
| return next; | ||
| }); | ||
| }); | ||
|
|
||
| // Fetch history on mount | ||
| useEffect(() => { | ||
| if (hydrated.current) return; | ||
| hydrated.current = true; | ||
|
|
||
| (async () => { | ||
| let page = await history({ untilAttach: true }); | ||
| while (page) { | ||
| for (const message of page.items) { | ||
| // message.data contains the full concatenated text | ||
| setResponses((prev) => new Map(prev).set(message.serial, message.data)); | ||
| } | ||
| page = page.hasNext() ? await page.next() : null; | ||
| } | ||
| })(); | ||
| }, [history]); | ||
| ``` | ||
| </Code> | ||
|
|
||
| ### Hydrating an in-progress response <a id="in-progress-response"/> | ||
|
|
@@ -911,6 +1001,37 @@ channel.subscribe(message -> { | |
| } | ||
| }); | ||
| ``` | ||
| ```react | ||
| // Set rewind via ChannelProvider options={{ params: { rewind: '2m' } }} | ||
|
|
||
| const [inProgressResponses, setInProgressResponses] = useState(new Map()); | ||
|
|
||
| // Receive both recent historical and live messages | ||
| useChannel('ai:responses', (message) => { | ||
| const responseId = message.extras?.headers?.responseId; | ||
|
|
||
| if (!responseId) return; | ||
|
|
||
| // Skip messages for responses already loaded from database | ||
| if (completedResponses.has(responseId)) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this react snippet is missing the example where |
||
|
|
||
| setInProgressResponses((prev) => { | ||
| const next = new Map(prev); | ||
| switch (message.action) { | ||
| case 'message.create': | ||
| next.set(responseId, message.data); | ||
| break; | ||
| case 'message.append': | ||
| next.set(responseId, (next.get(responseId) || '') + message.data); | ||
| break; | ||
| case 'message.update': | ||
| next.set(responseId, message.data); | ||
| break; | ||
| } | ||
| return next; | ||
| }); | ||
| }); | ||
| ``` | ||
| </Code> | ||
|
|
||
| <Aside data-type="note"> | ||
|
|
@@ -1118,6 +1239,53 @@ while (page != null) { | |
| page = page.hasNext() ? page.next() : null; | ||
| } | ||
| ``` | ||
| ```react | ||
| const [inProgressResponses, setInProgressResponses] = useState(new Map()); | ||
| const hydrated = useRef(false); | ||
|
|
||
| // Subscribe to live messages and get the history function | ||
| const { history } = useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => { | ||
| const responseId = message.extras?.headers?.responseId; | ||
|
|
||
| if (!responseId) return; | ||
| if (completedResponses.has(responseId)) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same, not clear where |
||
|
|
||
| setInProgressResponses((prev) => { | ||
| const next = new Map(prev); | ||
| switch (message.action) { | ||
| case 'message.create': | ||
| next.set(responseId, message.data); | ||
| break; | ||
| case 'message.append': | ||
| next.set(responseId, (next.get(responseId) || '') + message.data); | ||
| break; | ||
| case 'message.update': | ||
| next.set(responseId, message.data); | ||
| break; | ||
| } | ||
| return next; | ||
| }); | ||
| }); | ||
|
|
||
| // Fetch history from the last completed response until attachment | ||
| useEffect(() => { | ||
| if (hydrated.current) return; | ||
| hydrated.current = true; | ||
|
|
||
| (async () => { | ||
| let page = await history({ untilAttach: true, start: latestTimestamp }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not clear where |
||
| while (page) { | ||
| for (const message of page.items) { | ||
| const responseId = message.extras?.headers?.responseId; | ||
| if (responseId) { | ||
| setInProgressResponses((prev) => new Map(prev).set(responseId, message.data)); | ||
| } | ||
| } | ||
| page = page.hasNext() ? await page.next() : null; | ||
| } | ||
| })(); | ||
| }, [history]); | ||
| ``` | ||
| </Code> | ||
|
|
||
| <Aside data-type="note"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT about adding quick React setup context for useChannel examples?
The JS/Python/Java examples don't show full realtime client setup either, but that's fine, in those languages, channel configuration is visible inline:
realtime.channels.get('ai:channel', { params: { rewind: '2m' } })- makes it clear how the channel name, namespace prefix, and options all come together.React splits this across a component hierarchy (AblyProvider -> ChannelProvider -> useChannel), so a reader seeing only
useChannel('ai:channel', callback)doesn't see where channel options like rewind are configured or that the component must be nested inside specific providers, like ChannelProvider withchannelName="ai:channel".This rewind example addresses this with a comment, but that's easy to miss and doesn't convey the structural requirement - that your component tree needs to be shaped in a specific way for the hook to work at all.
How about adding a short
<If lang="react">block right after the "Subscribing to token streams" heading (message-per-response) and "Streaming patterns" heading (message-per-token), before the user sees the first React code example: