From d87923144ac493f415fb7e4bf8af7d24d4f72150 Mon Sep 17 00:00:00 2001 From: owenpearson Date: Thu, 12 Mar 2026 10:52:30 +0000 Subject: [PATCH] ait: add react token streaming examples --- src/data/languages/languageData.ts | 1 + .../token-streaming/message-per-response.mdx | 168 +++++++++++++++++ .../token-streaming/message-per-token.mdx | 178 ++++++++++++++++++ 3 files changed, 347 insertions(+) diff --git a/src/data/languages/languageData.ts b/src/data/languages/languageData.ts index 10710cf4a0..dda7e90139 100644 --- a/src/data/languages/languageData.ts +++ b/src/data/languages/languageData.ts @@ -44,6 +44,7 @@ export default { }, aiTransport: { javascript: '2.19', + react: '2.19', java: '1.6', python: '3.1', swift: '1.2', diff --git a/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx b/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx index b8e6ea6026..d41f168b84 100644 --- a/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx +++ b/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx @@ -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; + }); +}); +``` ## Client 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; + }); +}); +``` 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]); +``` ### Hydrating an 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; + + 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; + }); +}); +```