12. UI Polish
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.
Add a token counter showing estimated prompt+response tokens (count characters / 4 as a rough proxy) after each response completes.