Minimal AI Desktop Workspace Clone

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.

Before you begin…

Minimal Clone Architecture
LOCAL RUNTIME EXTERNAL SERVICES HTTP SQL REST Node/Express Server TypeScript + SQLite Browser UI HTML/JS SQLite DB OpenAI‑compatible API cloud
1

Phase 1: Scaffold the project

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.ts
2

Phase 2: Data model – SQLite store for messages

Create 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);
}
3

Phase 3: API & UI – simple chat endpoint and static page

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}`));
4

Phase 4: Run locally

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.ts
5

Phase 5: Verify & extend

With 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>

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsOPENAI_API_KEY=YOUR_KEY npx ts-node src/server.ts → should print ‘🚀 http://localhost:3000’
Chat UI loads in browserOpen http://localhost:3000 in a web browser – the page with input box appears
Message persists after refreshSend a message, refresh the page, and the same conversation remains displayed

What’s next?

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.