Add Cortex Portal — web visualization for knowledge graph
Express API server wrapping existing store/graph core with REST endpoints for nodes, edges, graph, and search. React + Vite portal with React Flow for interactive graph visualization, Tailwind CSS styling, and full CRUD UI (sidebar, node panel, add/link modals, search bar, toast notifications).
This commit is contained in:
29
src/server/index.ts
Normal file
29
src/server/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import routes from './routes';
|
||||
import { closeDb } from '../core/db';
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '3100');
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use('/api', routes);
|
||||
|
||||
// Serve portal static files in production
|
||||
const portalDist = path.join(__dirname, '../../portal/dist');
|
||||
app.use(express.static(portalDist));
|
||||
app.get('{*path}', (_req, res) => {
|
||||
res.sendFile(path.join(portalDist, 'index.html'));
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Cortex Portal running at http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
closeDb();
|
||||
server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
136
src/server/routes.ts
Normal file
136
src/server/routes.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { addNode, getNode, listNodes, updateNode, removeNode, addEdge, removeEdge, query } from '../core/store';
|
||||
import { getConnections, buildTree } from '../core/graph';
|
||||
import { getDb } from '../core/db';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function param(req: Request, name: string): string {
|
||||
const v = req.params[name];
|
||||
return Array.isArray(v) ? v[0] : v;
|
||||
}
|
||||
|
||||
// List nodes
|
||||
router.get('/nodes', (req: Request, res: Response) => {
|
||||
try {
|
||||
const options: any = {};
|
||||
if (req.query.kind) options.kind = req.query.kind as string;
|
||||
if (req.query.status) options.status = req.query.status as string;
|
||||
if (req.query.tags) options.tags = (req.query.tags as string).split(',');
|
||||
if (req.query.limit) options.limit = parseInt(req.query.limit as string);
|
||||
if (req.query.includeStale === 'true') options.includeStale = true;
|
||||
const nodes = listNodes(options);
|
||||
res.json(nodes.map(n => ({ ...n, embedding: undefined })));
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single node + connections
|
||||
router.get('/nodes/:id', (req: Request, res: Response) => {
|
||||
try {
|
||||
const node = getNode(param(req, 'id'));
|
||||
if (!node) return res.status(404).json({ error: 'Node not found' });
|
||||
const connections = getConnections(param(req, 'id'));
|
||||
res.json({ ...node, embedding: undefined, connections });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add node
|
||||
router.post('/nodes', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const node = await addNode(req.body);
|
||||
res.status(201).json({ ...node, embedding: undefined });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update node
|
||||
router.patch('/nodes/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const node = await updateNode(param(req, 'id'), req.body);
|
||||
if (!node) return res.status(404).json({ error: 'Node not found' });
|
||||
res.json({ ...node, embedding: undefined });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete node
|
||||
router.delete('/nodes/:id', (req: Request, res: Response) => {
|
||||
try {
|
||||
const hard = req.query.hard === 'true';
|
||||
const ok = removeNode(param(req, 'id'), hard);
|
||||
if (!ok) return res.status(404).json({ error: 'Node not found' });
|
||||
res.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add edge
|
||||
router.post('/edges', (req: Request, res: Response) => {
|
||||
try {
|
||||
const { fromId, toId, type, metadata } = req.body;
|
||||
const edge = addEdge(fromId, toId, type, metadata);
|
||||
res.status(201).json(edge);
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete edge
|
||||
router.delete('/edges/:id', (req: Request, res: Response) => {
|
||||
try {
|
||||
const ok = removeEdge(param(req, 'id'));
|
||||
if (!ok) return res.status(404).json({ error: 'Edge not found' });
|
||||
res.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Graph — returns nodes + edges for React Flow
|
||||
router.get('/graph', (_req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const nodes = (db.prepare('SELECT * FROM nodes WHERE is_stale = 0').all() as any[]).map(row => ({
|
||||
id: row.id,
|
||||
kind: row.kind,
|
||||
title: row.title,
|
||||
content: row.content,
|
||||
status: row.status,
|
||||
tags: JSON.parse(row.tags || '[]'),
|
||||
metadata: JSON.parse(row.metadata || '{}'),
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
const edges = (db.prepare('SELECT * FROM edges').all() as any[]).map(row => ({
|
||||
id: row.id,
|
||||
fromId: row.from_id,
|
||||
toId: row.to_id,
|
||||
type: row.type,
|
||||
metadata: JSON.parse(row.metadata || '{}'),
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
res.json({ nodes, edges });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Search
|
||||
router.post('/search', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { text, options } = req.body;
|
||||
const results = await query(text, options || {});
|
||||
res.json(results.map(r => ({ ...r, node: { ...r.node, embedding: undefined } })));
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user