Froala Editor - Stream AI Chat Responses with OpenRouter

by Froala_marketing

HTML

<link href="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" />

<div id="editor">
  <p>Froala Editor is a lightweight WYSIWYG HTML editor written in JavaScript.</p>
  <h3>Before you start:</h3>
  <p>In the JavaScript code, replace <code>sk-or-v1-YOUR_OPENROUTER_API_KEY</code> with your own <a href="https://openrouter.ai/">OpenRouter API </a> key.</p>
  <h3>How to test:</h3>
  <p>Open AI Chat Assistant panel from the toolbar and ask it to write a long paragraph about anything in your mind. The answer appears in small pieces as the model writes it.</p>
  <p>Note: If you use the AI Assist popup, the complete answer is returned at once.</p>
</div>

<script src="https://cdn.jsdelivr.net/npm/froala-editor@5.5.0/js/froala_editor.pkgd.min.js"></script>

JavaScript

// 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:...