A quick‑start TypeScript/Node sandbox inspired by OpenBot
Welcome to another official UVF IT build guide. In the next 45 minutes you’ll spin up a tiny desktop‑style AI workspace that mimics OpenBot’s core idea: a local server, a SQLite store, and a tiny web UI for chatting with a single AI agent.
We’ll use plain TypeScript, Express, and the free OpenAI‑compatible endpoint at https://api.openai.com (you can swap in Ollama later). No paid services, no native binaries – just a browser‑based UI that runs locally.
Initialize a new npm workspace with TypeScript and install Express and better‑sqlite3. This gives us a clean TypeScript project that can compile to modern ES modules and provides the web server and lightweight SQLite engine we need.
mkdir minimal-ai-clone && cd minimal-ai-clone
npm init -y
npm i express better-sqlite3
npm i -D typescript @types/express ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module esnext --moduleResolution node --target es2022
mkdir src && touch src/index.tsCreate a tiny DB with a single table to persist chat history between reloads. The helper functions hide raw SQL so the rest of the app can just call addMessage and getMessages.
src/db.ts
import Database from 'better-sqlite3';
// Open (or create) a local SQLite file named chat.db
const db = new Database('chat.db');
// Ensure the messages table exists – id, role (user/assistant), content, timestamp
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
/** Return all stored messages ordered by creation time */
export function getMessages(): {role: string; content: string}[] {
const stmt = db.prepare('SELECT role, content FROM messages ORDER BY created_at ASC');
return stmt.all();
}
/** Insert a new message into the DB */
export function addMessage(role: 'user' | 'assistant', content: string): void {
const stmt = db.prepare('INSERT INTO messages (role, content) VALUES (?, ?)');
stmt.run(role, content);
}In this step we spin up an Express server that serves a tiny static HTML page and a JSON endpoint for chatting. The /chat route receives a user message, forwards it to the OpenAI Completion API, stores the reply in an in‑memory array, and returns the updated conversation. Keeping the data in memory is enough for a minimal clone and lets us see persistence across page reloads while the server runs.
src/server.ts
import express from 'express';
import cors from 'cors';
import fetch from 'node-fetch';
import path from 'path';
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, '../../public')));
// simple in‑memory store for the conversation
let conversation: {role: string; content: string}[] = [];
app.post('/chat', async (req, res) => {
const userMsg = req.body.message;
if (!userMsg) return res.status(400).json({error: 'Message required'});
// add user message
conversation.push({role: 'user', content: userMsg});
// call OpenAI API (replace with your key)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({model: 'gpt-3.5-turbo', messages: conversation}),
});
const data = await response.json();
const aiReply = data.choices[0].message.content.trim();
conversation.push({role: 'assistant', content: aiReply});
res.json({conversation});
});
app.listen(PORT, () => console.log(`🚀 http://localhost:${PORT}`));Now that the server code is ready, we launch it with ts-node. The script compiles the TypeScript on the fly, so no separate build step is required. After the server prints its address, open the same URL in a browser to see the chat UI.
# make sure dependencies are installed first
npm install express cors node-fetch @types/express @types/node-fetch ts-node typescript
# run the server (ensure OPENAI_API_KEY is set in your environment)
OPENAI_API_KEY=YOUR_KEY npx ts-node src/server.tsWith the UI loaded, type a message and hit Send – the server will call OpenAI, store both sides of the exchange, and render the full conversation. Refresh the page; because the conversation lives in the server’s memory, it will still be displayed. To experiment further, duplicate the conversation array logic into a second variable (e.g., agent2) and expose another /chat2 endpoint, giving you a simple multi‑agent prototype.
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mini AI Chat</title>
<style>
body {font-family:Arial, sans-serif; margin:20px;}
#log {border:1px solid #ccc; padding:10px; height:300px; overflow:auto;}
#msg {width:80%;}
</style>
</head>
<body>
<h2>🗨️ Minimal AI Desktop Workspace</h2>
<div id="log"></div>
<input id="msg" placeholder="Type a message..." />
<button onclick="send()">Send</button>
<script>
async function loadLog() {
const res = await fetch('/chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({message:''})});
const data = await res.json();
render(data.conversation || []);
}
function render(conv) {
const log = document.getElementById('log');
log.innerHTML = conv.map(m=>`<b>${m.role}:</b> ${m.content}`).join('<br/>');
}
async function send() {
const input = document.getElementById('msg');
const msg = input.value.trim();
if (!msg) return;
const res = await fetch('/chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({message:msg})});
const data = await res.json();
render(data.conversation);
input.value='';
}
loadLog();
</script>
</body>
</html>| Check | Command / Why it matters |
|---|---|
| Server starts without errors | OPENAI_API_KEY=YOUR_KEY npx ts-node src/server.ts → should print ‘🚀 http://localhost:3000’ |
| Chat UI loads in browser | Open http://localhost:3000 in a web browser – the page with input box appears |
| Message persists after refresh | Send a message, refresh the page, and the same conversation remains displayed |
You now have a functional local AI workspace. Experiment by swapping the OpenAI endpoint for a local Ollama server, add multiple agents with separate tables, or integrate file‑upload routes to approach the full OpenBot feature set.