Building a minimal Ollama Chat in pure HTML & JavaScript

In our previous tutorial, we set up a local Ollama instance: How to Install Ollama locally and run your first model

In this tutorial, we’re going to build a super‑simple chat app using plain HTML and JavaScript. We’ll walk through how the chat logic works, how messages are sent to Ollama, and how the UI updates in real time.

You can download the sample app here:https://github.com/agentic-ai-info/simple-ollama-chat

Overview

Our minimal chat app consists of:

  • index.html — a simple UI with a message list, input field and send button
  • main.js — the actual chat logic (sending prompts, receiving responses, updating the UI)
  • server.js — a tiny Node server that serves the static files and proxies requests to Ollama

The entire app runs locally and communicates with a local Ollama instance.

The Chat Logic (main.js)

Let’s break down the important parts: At the top of the file, we grab references to the DOM elements we need:

const messagesEl = document.getElementById("messages");
const promptEl = document.getElementById("prompt");
const sendBtn = document.getElementById("sendBtn");
const modelEl = document.getElementById("model");

These give us access to:

  • the chat message container
  • the text input
  • the send button
  • the model selector

Displaying Messages

Whenever the user or the model sends a message, we append it to the chat window:

function addMessage(text, role) {
    const div = document.createElement("div");
    div.className = "msg " + role;
    div.textContent = text;
    messagesEl.appendChild(div);
    messagesEl.scrollTop = messagesEl.scrollHeight;
}

This function:

  • creates a new <div>
  • assigns it a CSS class (user, assistant, or llm)
  • inserts the message text
  • scrolls the chat window to the bottom


Sending a Message to Ollama

The core of the chat app is the sendMessage() function:

async function sendMessage() {
    const prompt = promptEl.value.trim();
    if (!prompt || isSending) return;

    const model = modelEl.value.trim() || "llama3";

    addMessage(prompt, "user");
    promptEl.value = "";
    promptEl.focus();
}

Here we

  • read the user’s input
  • prevent double‑sending
  • display the user message immediately
  • clear the input field


Then we send the request to our proxy endpoint:

const response = await fetch("/proxy/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        model: model,
        messages: [{ role: "user", content: prompt }],
        stream: false
    })
});
  • We call /proxy/api/chat instead of talking to Ollama directly (to prevent CORS problems)
  • The request body matches Ollama’s chat API format
  • stream: false keeps things simple (no streaming yet)


Once Ollama replies, we parse the JSON and extract the model’s message:

const data = await response.json();
const content = data.message?.content ?? JSON.stringify(data);
addMessage(content, "llm");

The Minimal Node Server (server.js)

Our ‘server’ does two things:

1. Serves the static files: It delivers index.html, main.js, and any CSS files to the browser.

2. Acts as a reverse proxy to Ollama: Normally the browser is not allowed to call Ollama (http://localhost:11434) directly because of CORS restrictions.

So we forward all requests under /proxy/* to Ollama:

  • stripping unnecessary browser headers
  • keeping the request clean
  • avoiding CORS issues entirely

To run the app, just call

node server.js

Conclusion

This tiny Ollama chat app is an example of how far you can get with just:

  • HTML
  • pure JavaScript
  • a minimal Node server

No frameworks, no build tools, no dependencies.

If you want to extend it, here are some natural next steps:

  • add streaming responses
  • store chat history
  • support multiple models
  • add a nicer UI

But even in its minimal form, this setup gives you a fully functional local LLM chat interface that’s easy to understand.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *