New Froala Editor v5.5.0 is here – Learn More
Froala Documentation
- Installation Guides
- Browser Support
- Languages Support
- Shortcuts
- Activation
- Examples
- Customize the Editor
- Use-cases
- Plugins
- APIs
- Development Frameworks
- Server Integrations
- Server SDKs
- Migration Guides
- Changelog
- Tutorials
Stream AI Chat Responses with OpenRouter
Try the Streaming AI Chat Demo
Streaming shows an AI answer word by word as the model writes it, instead of making users wait for the full reply. This example connects Froala’s AI Chat to OpenRouter with one JavaScript function, aiAssistRequest. It sends each piece through the onChunk callback, so your rich text editor feels as responsive as a chat app.
HTML
<div id="editor">
</div> JAVASCRIPT
<script>
// OpenRouter settings. Replace the key with your own.
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
const OPENROUTER_API_KEY = 'sk-or-v1-YOUR_OPENROUTER_API_KEY';
const OPENROUTER_MODEL = 'openai/gpt-4o-mini';
// Reads a streamed response one line at a time.
// OpenRouter sends each piece of the answer as a "data: {...}" line.
async function readSSELines(response, onLine) {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep the last, possibly incomplete, line for the next read.
lines.forEach(onLine);
}
if (buffer) onLine(buffer); // Process anything left after the stream closes.
}
// Froala calls this function for every AI Chat and AI Assist request.
// AI Chat passes onChunk, so the answer streams in piece by piece.
// AI Assist leaves onChunk out, so the full answer comes back at once.
async function openRouterRequestHandler(request, signal, onChunk) {
const streaming = typeof onChunk === 'function';
const response = await fetch(OPENROUTER_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENROUTER_API_KEY}`
},
body: JSON.stringify({
model: OPENROUTER_MODEL,
messages: [{ role: 'user', content: request.prompt }],
stream: streaming
}),
signal // Lets the Cancel button end the request.
});
if (!response.ok) {
throw new Error(`OpenRouter API error ${response.status}`);
}
// One-shot mode: the full answer arrives in a single JSON response.
if (!streaming) {
const data = await response.json();
return (data.choices?.[0]?.message?.content || '').trim();
}
// Streaming mode: pass each new piece to onChunk as it arrives.
let answer = '';
await readSSELines(response, (line) => {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) return; // Skips keep-alive comments.
const payload = trimmed.slice(5).trim();
if (payload === '[DONE]') return; // OpenRouter's end-of-stream marker.
let chunk;
try {
chunk = JSON.parse(payload);
} catch (err) {
console.warn('OpenRouter stream: skipped an unreadable line', err);
return;
}
// Errors that happen mid-stream arrive as a data line with an "error" field.
if (chunk.error) {
throw new Error(`OpenRouter stream error: ${chunk.error.message}`);
}
const text = chunk.choices?.[0]?.delta?.content;
if (text) {
answer += text;
onChunk(text);
}
});
return answer.trim();
}
new FroalaEditor('#editor', {
toolbarButtons: ['aiChatAssistant', 'aiAssist', '|', 'bold', 'italic', 'formatUL', '|', 'undo', 'redo'],
heightMin: 300,
aiSupplementalTermsAccepted: true, // Required for every AI feature.
aiChatStreamResponse: true, // The default. Shown here for clarity.
aiAssistRequest: openRouterRequestHandler
});
</script> In this example
You write one function, aiAssistRequest, and Froala uses it for both AI features in the toolbar.
- AI Chat calls it with an
onChunkcallback and shows the answer in small pieces as they arrive. - AI Assist calls it once and inserts the complete answer.
- The
signalargument connects the chat's Cancel button to your request. - OpenRouter gives you access to models from many AI providers through one API key.
How it works
How a streamed answer reaches the chat panel
- The user asks a question in AI Chat
- Froala calls your handler
aiAssistRequest - OpenRouter streams the answer in small pieces
stream: true - Your handler passes each piece to Froala
onChunk(text) - Your handler returns the full answer
return answer
Froala picks the mode for you
AI Chat streams by default because aiChatStreamResponse is true. In this mode, Froala calls your handler with a third argument, onChunk. AI Assist calls the same handler with two arguments.
Checking typeof onChunk === 'function' tells your code which path to take. One function then serves both features, which keeps your setup in one place.
OpenRouter sends the answer in small pieces
With stream: true, OpenRouter replies with server-sent events. Each event is a line that starts with data: and holds a small JSON object with the next piece of text.
The handler reads three kinds of lines.
- Lines that start with
:are keep-alive comments that hold the connection open. The handler skips them. data: [DONE]marks the end of the answer.- A
data:line with anerrorfield reports a problem mid-stream. The handler throws an error, and the chat panel shows a Retry option.
readSSELines keeps every line whole
The network delivers data in chunks, and a chunk can end in the middle of a line. readSSELines holds the unfinished part in a buffer and joins it with the next chunk. Every line reaches JSON.parse complete, so the stream reads reliably on slow connections too.
Return the complete answer
Each onChunk(text) call adds text to the chat message. When the stream ends, the handler returns the full answer, and Froala uses it as the final version of the message. This keeps the message and the chat history in sync.
Cancel works through signal
Froala creates an AbortSignal for every request. Passing it to fetch lets the chat's Cancel button close the connection to OpenRouter. Froala then clears the unfinished reply.
Good to know
- This example calls OpenRouter from the browser, so anyone who opens the page can read your API key. Use it for prototypes, demos, and model testing. For production, keep the key on your server and follow the Custom Backend example.
- Froala AI features stay inactive until you set
aiSupplementalTermsAcceptedtotrue. Setting it confirms that you accept the Froala AI Supplemental Terms. - Froala's prompt asks the model to answer in HTML, so answers keep their formatting when users insert them into the editor.
Related AI examples
- Direct OpenAI API integration
Call OpenAI from the browser with a short handler. - Custom backend with validation and event hooks
The production pattern with your key kept on the server. - Standard endpoint with custom formats
Connect to an existing API using options only.
Want to see AI Chat in action? Try it in our overview demo.
Related Plugin:
AI Assist Whats on this page hide