RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Eruo Fredoline
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /Courses
  5. /First Local Chatbot
  6. /Ch. 6
First Local Chatbot

06. JavaScript EventSource

Chapter 6 of 15 · 15 min
KEY INSIGHT

`fetch` + `ReadableStream` gives you full control over streaming with POST requests, which `EventSource` cannot do.

The browser receives streamed responses via EventSource, which only works with GET requests. Since the chatbot needs to send a POST body (the message), we use a two-step pattern: POST the message to create the stream, then read the stream via a separate mechanism. For SSE, the POST route itself returns the stream, so EventSource works—but you must pass the message as URL query parameters or use a hidden fetch + manual parsing.

The cleanest approach uses a fetch + ReadableStream reader in JavaScript, bypassing EventSource for POST-initiated streams:

async function sendMessage(message) {
  const response = await fetch(`/chat?model=${encodeURIComponent(selectedModel)}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages: conversationHistory }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let partial = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    partial += decoder.decode(value, { stream: true });
    // SSE format: data: {...}\n\n
    const lines = partial.split("\n");
    partial = lines.pop() ?? "";
    for (const line of lines) {
      if (line.startsWith("data: ")) {
        try {
          const data = JSON.parse(line.slice(6));
          if (data.message?.content) {
            appendToken(data.message.content);
          }
        } catch (e) { /* ignore parse errors on partial JSON */ }
      }
    }
  }
}

A failure mode: if the Ollama response is not valid SSE format (e.g., a JSON parse error from Ollama), the JSON.parse throws. Wrap it in try/catch and log to console.

Local verification checkpoint

Run the smallest example from this chapter in a local workspace and record the package version, runtime, data path, and observed output. If the result depends on model size, vector count, CPU/GPU backend, or available memory, note that constraint beside the exercise so the lesson remains reproducible.

EXERCISE

Replace the hardcoded appendToken with a function that appends the token to a <span> element and auto-scrolls the chat div to the bottom.

← Chapter 5
HTML Frontend Basics
Chapter 7 →
Conversation Memory