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. 12
First Local Chatbot

12. UI Polish

Chapter 12 of 15 · 20 min
KEY INSIGHT

A chatbot that is technically correct but visually broken feels broken to users. The typing indicator and auto-scroll take 15 minutes and double the perceived quality.

Four improvements that make the chatbot usable:

Auto-scroll. After each token, scroll the chat div to the bottom:

function scrollToBottom() {
  const chat = document.getElementById("chat");
  chat.scrollTop = chat.scrollHeight;
}

Markdown rendering. Use a lightweight regex replacer for bold, italic, inline code, and code blocks—no external library needed:

function renderMarkdown(text) {
  return text
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(/\*(.+?)\*/g, "<em>$1</em>")
    .replace(/`([^`]+)`/g, "<code>$1</code>")
    .replace(/```([\s\S]+?)```/g, "<pre><code>$1</code></pre>");
}

Typing indicator. Show "..." while waiting for the first token:

let typingIndicator = null;
async function sendMessage() {
  typingIndicator = document.createElement("div");
  typingIndicator.textContent = "Model is thinking...";
  chat.appendChild(typingIndicator);
  // ... stream response ...
  if (typingIndicator) typingIndicator.remove();
}

Clear button. Add a clear-history button that POSTs to /sessions/{sessionId}/clear and reloads the chat div.

Failure mode: The markdown regex will corrupt text that contains ** as part of a URL. Use a proper tokenizer for production.

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.

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

Add a token counter showing estimated prompt+response tokens (count characters / 4 as a rough proxy) after each response completes.

← Chapter 11
Error Handling
Chapter 13 →
LAN Deployment