Simple OpenMausBot‑style Chatbot Playground with TypeScript

A minimal TypeScript/Node clone you can build in 45 minutes

Welcome to another official UVF IT guide! In this tutorial we’ll craft a tiny chatbot playground inspired by OpenMausBot, using only Node.js, TypeScript, and SQLite – all free and open‑source.

The result is a local web UI where you can type a message, the server echoes it back with a fake “AI” response, and the conversation is persisted. It’s a stripped‑down version of the real product, perfect for learning.

Before you start

Clone Architecture
LOCAL STACK fetch /messages store/retrieve HTML + Vanilla JS UI Node / Express API SQLite DB
1

Phase 1: Scaffold the project

Create a fresh npm workspace with TypeScript and Express dependencies.

mkdir openmausbot-clone && cd openmausbot-clone
npm init -y
npm install express sqlite3
npm install -D typescript @types/express @types/node ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module commonjs --target es2022
mkdir src && echo "" > src/index.ts
2

Phase 2: Define the data model

We store each chat message (author, text, timestamp) in a SQLite table.

src/db.ts

import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';

let db: Database<sqlite3.Database, sqlite3.Statement>;

export async function initDb() {
  db = await open({
    filename: './chat.db',
    driver: sqlite3.Database,
  });
  await db.exec(`CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    author TEXT NOT NULL,
    text TEXT NOT NULL,
    ts INTEGER NOT NULL
  );`);
}

export async function addMessage(author: string, text: string) {
  const ts = Date.now();
  await db.run('INSERT INTO messages (author, text, ts) VALUES (?,?,?)', author, text, ts);
}

export async function getMessages() {
  return db.all<any[]>('SELECT author, text, ts FROM messages ORDER BY ts ASC');
}
3

Phase 3: Build the API & fake AI

In this step we spin up a tiny Express server that keeps chat history in memory. Two routes are enough: a GET /messages endpoint that returns the whole conversation array, and a POST /chat endpoint that receives the user's text, pushes it onto the history, and replies with a dummy AI message. The dummy reply simply echoes the user's input prefixed with an emoji, letting us verify the round‑trip without any external model.

Having the server in TypeScript gives us type safety while staying lightweight. All data lives in a plain JavaScript array, so you can see the flow of messages directly in the console or via the UI you’ll add next.

src/server.ts

import express, { Request, Response } from 'express';
import cors from 'cors';

// In‑memory conversation store
interface Message { role: 'user' | 'bot'; content: string; }
const history: Message[] = [];

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// Return full chat history
app.get('/messages', (req: Request, res: Response) => {
  res.json(history);
});

// Accept user message and return a fake AI reply
app.post('/chat', (req: Request, res: Response) => {
  const userText: string = req.body.text ?? '';
  if (!userText) return res.status(400).json({ error: 'No text provided' });

  // Record user message
  history.push({ role: 'user', content: userText });

  // Generate dummy bot reply
  const botReply = `🤖 Echo: ${userText}`;
  history.push({ role: 'bot', content: botReply });

  res.json({ reply: botReply });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
4

Phase 4: Quick UI to test

Now we add a single HTML page that talks to the API we just created. The page contains a textarea for the user, a button to send, and a simple list that shows each message as it arrives. All the logic lives in a tiny inline script that uses the Fetch API – no bundlers, no React, just plain browser JavaScript.

When you click Send, the script POSTs the text to /chat, appends the returned bot line, and then refreshes the conversation by GETting /messages. This gives you an instant, interactive playground to confirm the server works.

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>OpenMausBot Playground</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 2rem; }
    #log { border: 1px solid #ccc; padding: 1rem; height: 300px; overflow-y: auto; }
    .msg { margin: 0.3rem 0; }
    .user { color: blue; }
    .bot { color: green; }
  </style>
</head>
<body>
  <h1>Chat Playground</h1>
  <div id="log"></div>
  <textarea id="input" rows="3" style="width:100%"></textarea><br/>
  <button id="send">Send</button>

  <script>
    const log = document.getElementById('log');
    const input = document.getElementById('input');
    const sendBtn = document.getElementById('send');

    async function refresh() {
      const res = await fetch('/messages');
      const msgs = await res.json();
      log.innerHTML = '';
      msgs.forEach(m => {
        const div = document.createElement('div');
        div.className = 'msg ' + (m.role === 'user' ? 'user' : 'bot');
        div.textContent = `${m.role === 'user' ? 'You' : 'Bot'}: ${m.content}`;
        log.appendChild(div);
      });
      log.scrollTop = log.scrollHeight;
    }

    sendBtn.onclick = async () => {
      const text = input.value.trim();
      if (!text) return;
      await fetch('/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text })
      });
      input.value = '';
      await refresh();
    };

    // Initial load
    refresh();
  </script>
</body>
</html>
5

Phase 5: Run & verify locally

With the server and UI in place, start the TypeScript runtime using ts-node-dev. This watches for changes, recompiles on the fly, and serves the static public folder so the HTML page loads at http://localhost:3000. Open that address in a browser, type a message, and watch the echo reply appear.

If you prefer a separate port for the UI you can run a simple static file server, but serving it from Express keeps the setup minimal and requires only one terminal command.

# Install dev dependencies if you haven't yet
npm install express cors ts-node-dev @types/express @types/cors

# Start the server in watch mode
npx ts-node-dev src/server.ts

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsYou see Server running on http://localhost:3000 in the terminal after running the launch command
UI loads at http://localhost:3000Browser displays the chat playground page with a textarea and Send button
Sending a message displays both User and Bot linesAfter typing and clicking Send, the conversation log shows You: <your text> followed by Bot: 🤖 Echo: <your text>

What’s next?

Swap the placeholder echo with a real LLM (e.g., Ollama or a local HuggingFace model) by calling its HTTP API in the /chat route. You can also persist the SQLite file instead of :memory: and add multiple agents for a richer experience.