A minimal Node/React dashboard inspired by Paperclip
Welcome to another official UVF IT guide! In this quick 45‑minute sprint we’ll build a tiny dashboard that lets you define a goal, add a few mock agents, and see a live task list – a stripped‑down version of Paperclip’s AI‑agent manager.
You’ll use Node.js with Express, a tiny SQLite DB, and a React front‑end written in TypeScript. No paid APIs, just local storage, so you can run everything on your laptop.
Create a monorepo that keeps the backend and frontend isolated but under one version control root. This makes local development simple: you can start both servers with one npm run dev script later.
We’ll use Vite for the React TypeScript UI and plain Express for the API. All dependencies are installed locally, so the clone runs on any recent laptop without extra tooling.
mkdir ai-dashboard && cd ai-dashboard
# Initialise a git repo (optional but handy)
git init
# Create workspace folders
mkdir backend frontend
# ---------- Frontend ----------
cd frontend
npm init -y
npm install react react-dom
npm install -D @vitejs/plugin-react vite typescript
# Generate a minimal Vite config
cat > vite.config.ts <<'EOF'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3000 },
});
EOF
# Add a tsconfig for React
cat > tsconfig.json <<'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
EOF
cd ..
# ---------- Backend ----------
cd backend
npm init -y
npm install express sqlite3
npm install -D typescript @types/express @types/node
# Basic tsconfig for Node
cat > tsconfig.json <<'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"outDir": "dist"
},
"include": ["src"]
}
EOF
mkdir src
cat > src/index.ts <<'EOF'
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/health', (_req, res) => res.json({status:'ok'}));
app.listen(4000, () => console.log('Backend listening on http://localhost:4000'));
EOF
cd ..
# Install root-level tools for convenience
npm install -g concurrently
# Add a root package.json script to run both servers
cat > package.json <<'EOF'
{
"name": "ai-dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "concurrently \"npm run dev --prefix frontend\" \"npm run dev --prefix backend\"",
"frontend:dev": "vite --config frontend/vite.config.ts",
"backend:dev": "ts-node backend/src/index.ts"
}
}
EOFWe’ll store goals and agents in a single SQLite file located in the backend folder. SQLite is file‑based, requires zero configuration, and works perfectly for a prototype.
The schema is tiny: a goals table for user‑defined objectives and an agents table that references a goal. All columns are simple primitives, keeping the SQL under 25 lines.
backend/schema.sql
-- SQLite schema for the AI Agent Dashboard clone
CREATE TABLE IF NOT EXISTS goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
goal_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'idle',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (goal_id) REFERENCES goals(id) ON DELETE CASCADE
);
-- Optional: seed a demo goal and two mock agents
INSERT INTO goals (title) VALUES ('Demo Goal');
INSERT INTO agents (name, goal_id) VALUES ('Agent Alpha', 1), ('Agent Beta', 1);In this phase we spin up a tiny Express server that will be the brain of our clone. It stores goals and agents in memory, exposing simple CRUD endpoints so the front‑end can read and write data without a database. Keeping everything in RAM keeps the example lightweight while still showing the full request‑response cycle.
The routes we create mirror the Paperclip UI: you can fetch all goals, add a new goal, list agents, and assign an agent to a specific goal. Each endpoint returns JSON so the React client can update its state instantly.
backend/index.js
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = 4000;
app.use(cors());
app.use(express.json());
// In‑memory stores
let goals = [];
let agents = [{id:1, name:'Mock Agent'}];
// GET /goals – list all goals
app.get('/goals', (req, res) => {
res.json(goals);
});
// POST /goals – add a new goal
app.post('/goals', (req, res) => {
const {title, description} = req.body;
const newGoal = {id: Date.now(), title, description, agents: []};
goals.push(newGoal);
res.status(201).json(newGoal);
});
// GET /agents – list available agents
app.get('/agents', (req, res) => {
res.json(agents);
});
// POST /goals/:id/assign – assign an agent to a goal
app.post('/goals/:id/assign', (req, res) => {
const goal = goals.find(g => g.id === Number(req.params.id));
const {agentId} = req.body;
if (goal && agents.find(a => a.id === agentId)) {
goal.agents.push(agentId);
return res.json(goal);
}
res.status(404).json({error:'Goal or agent not found'});
});
app.listen(PORT, () => console.log(`🚀 Backend listening on http://localhost:${PORT}`));Now we connect a tiny Vite‑powered React app to the API we just built. The UI consists of a header, a form to create a new goal, a list that displays existing goals, and a button that attaches the mock agent to a selected goal.
We use the native fetch API for simplicity, updating local component state so the list refreshes instantly after each operation. No external state libraries are needed – just React hooks.
frontend/src/App.tsx
import { useEffect, useState } from 'react';
interface Goal {
id: number;
title: string;
description: string;
agents: number[];
}
const API = 'http://localhost:4000';
function App() {
const [goals, setGoals] = useState<Goal[]>([]);
const [title, setTitle] = useState('');
const [desc, setDesc] = useState('');
// Load goals on mount
useEffect(() => {
fetch(`${API}/goals`).then(r => r.json()).then(setGoals);
}, []);
const addGoal = async () => {
const res = await fetch(`${API}/goals`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({title, description: desc})
});
const newGoal = await res.json();
setGoals([...goals, newGoal]);
setTitle('');
setDesc('');
};
const assignAgent = async (goalId: number) => {
await fetch(`${API}/goals/${goalId}/assign`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({agentId:1})
});
setGoals(goals.map(g => g.id===goalId? {...g, agents:[...g.agents,1]}:g));
};
return (
<div style={{padding:'2rem', fontFamily:'Arial, sans-serif'}}>
<h1>AI Agent Dashboard</h1>
<div style={{marginBottom:'1rem'}}>
<input placeholder="Goal title" value={title} onChange={e=>setTitle(e.target.value)} />
<input placeholder="Description" value={desc} onChange={e=>setDesc(e.target.value)} style={{marginLeft:'0.5rem'}} />
<button onClick={addGoal} style={{marginLeft:'0.5rem'}}>Add Goal</button>
</div>
<ul>
{goals.map(g => (
<li key={g.id}>
<strong>{g.title}</strong>: {g.description}
<button onClick={()=>assignAgent(g.id)} style={{marginLeft:'0.5rem'}}>Add Mock Agent</button>
{g.agents.length>0 && <span> (Agents: {g.agents.join(',')})</span>}
</li>
))}
</ul>
</div>
);
}
export default App;With both server and client ready, we launch them side‑by‑side. The backend uses nodemon for automatic restarts on code changes, while Vite serves the React UI with hot‑module replacement.
Open http://localhost:5173 in a browser, add a goal via the form, and watch the list update instantly. Then click “Add Mock Agent” to see the agent ID appear next to the goal, confirming the full round‑trip works.
# From the project root
# Install backend deps
cd backend && npm install express cors nodemon && cd ..
# Install frontend deps
cd frontend && npm install && cd ..
# Start services (in separate terminals or using & for quick demo)
# Terminal 1: backend
cd backend && npx nodemon index.js &
# Terminal 2: frontend
cd frontend && npm run dev| Check | Command / Why it matters |
|---|---|
| Backend is listening on port 4000 | curl http://localhost:4000/goals should return [] |
| Frontend loads at http://localhost:5173 and shows the header | Open browser, see "AI Agent Dashboard" |
| Adding a goal updates the list without reload | Enter title, click Add Goal, list shows new entry |
Your minimal AI‑agent dashboard is up and running. From here you can experiment by adding more fields, persisting agents, or swapping SQLite for Postgres. Happy hacking!