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;
+ });
+});
+```