Streaming responses
Add real-time streaming to see AI responses as they're generated.
What you'll build
A chat interface that shows AI responses word-by-word as they arrive, with a streaming indicator and the ability to stop generation mid-stream.
Step 1 — Use stream() instead of send()
The useConversation hook provides two methods for sending messages: stream() delivers tokens in real time, while send() waits for the complete response. Use stream() for interactive chat.
import { useConversation } from "@arlopass/react";
function Chat() {
const { messages, stream, isStreaming } = useConversation();
async function handleSend(text: string) {
// stream() sends the message and streams the response token-by-token.
// The response is automatically appended to the messages array when done.
await stream(text);
}
}
Step 2 — Show streamingContent
While a response is being generated, streamingContent holds the partial text. Render it below the completed messages so the user sees the AI "typing" in real time.
import { useConversation } from "@arlopass/react";
function Chat() {
const { messages, stream, streamingContent, isStreaming } = useConversation();
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
{isStreaming && streamingContent && (
<div style={{ opacity: 0.7 }}>
<strong>assistant:</strong> {streamingContent}
</div>
)}
</div>
);
}
Step 3 — Track streaming state
isStreaming is true while a response is being generated. Use it to disable inputs and show loading indicators.
function ChatInput({ onSend }: { onSend: (text: string) => void }) {
const { isStreaming } = useConversation();
return (
<div>
<input placeholder="Type a message..." disabled={isStreaming} />
<button disabled={isStreaming}>
{isStreaming ? "Generating..." : "Send"}
</button>
</div>
);
}
Step 4 — Add a stop button
Call stop() to abort the current stream. The partial response is preserved in the messages array — nothing is lost.
import { useConversation } from "@arlopass/react";
function ChatControls() {
const { isStreaming, stop } = useConversation();
return (
<div>
{isStreaming && <button onClick={() => stop()}>⏹ Stop generating</button>}
</div>
);
}
Step 5 — Handle the onDelta callback
For custom token processing (word counting, syntax highlighting, etc.), subscribe to "stream" events. Each delta gives you the latest chunk of text.
import { useConversation } from "@arlopass/react";
import { useRef } from "react";
function Chat() {
const wordCount = useRef(0);
const { messages, stream, subscribe } = useConversation();
function handleSend(text: string) {
wordCount.current = 0;
const unsub = subscribe("stream", (delta: string) => {
const words = delta.split(/\s+/).filter(Boolean);
wordCount.current += words.length;
console.log("Words so far:", wordCount.current);
});
stream(text).finally(unsub);
}
}
Complete example
Here’s a full streaming chat app with a send/stop toggle button:
import { useState } from "react";
import { ArlopassProvider, ChatReadyGate, useConversation } from "@arlopass/react";
function Chat() {
const { messages, streamingContent, isStreaming, stream, stop } = useConversation({
systemPrompt: "You are a helpful assistant.",
});
const [input, setInput] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || isStreaming) return;
const text = input;
setInput("");
await stream(text);
}
return (
<div style={{ maxWidth: 600, margin: "0 auto" }}>
<div style={{ minHeight: 300, padding: 16 }}>
{messages.map((msg) => (
<div key={msg.id} style={{ padding: "8px 0" }}>
<strong>{msg.role === "user" ? "You" : "AI"}:</strong> {msg.content}
</div>
))}
{isStreaming && streamingContent && (
<div style={{ padding: "8px 0", opacity: 0.7, fontStyle: "italic" }}>
<strong>AI:</strong> {streamingContent}
</div>
)}
</div>
<form onSubmit={handleSubmit} style={{ display: "flex", gap: 8 }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isStreaming}
style={{ flex: 1, padding: 8 }}
/>
{isStreaming ? (
<button type="button" onClick={() => stop()}>
Stop
</button>
) : (
<button type="submit" disabled={!input.trim()}>
Send
</button>
)}
</form>
</div>
);
}
export default function App() {
return (
<ArlopassProvider appId="streaming-demo">
<ChatReadyGate
connecting={<p>Connecting...</p>}
noProvider={<p>Select a provider in the Arlopass extension.</p>}
error={(err) => <p>Error: {err.message}</p>}
>
<Chat />
</ChatReadyGate>
</ArlopassProvider>
);
}
React SDK vs Web SDK
The React SDK manages streaming state automatically. Here’s the comparison:
React SDK
import { useConversation } from "@arlopass/react";
const { messages, stream, streamingContent, isStreaming, stop } = useConversation();
// Stream a message — UI updates automatically
await stream("Explain React hooks");
// Stop mid-stream
stop();
Web SDK
import { ArlopassClient, ConversationManager } from "@arlopass/web-sdk";
const client = new ArlopassClient({ transport: window.arlopass });
await client.connect({ appId: "streaming-demo" });
const convo = new ConversationManager({ client });
// Stream token-by-token
for await (const event of convo.stream("Explain React hooks")) {
if (event.type === "delta") {
document.getElementById("output")!.textContent += event.content;
}
}
// To abort: call convo.stop()