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:
2026-02-02 17:01:29 +01:00
parent 21107443a7
commit 08c26754a8
25 changed files with 4566 additions and 1 deletions

44
portal/src/api.ts Normal file
View File

@@ -0,0 +1,44 @@
import type { CortexNode, CortexEdge, GraphData, NodeWithConnections, SearchResult, NodeKind, EdgeType } from './types';
const BASE = '/api';
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...init,
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `HTTP ${res.status}`);
}
return res.json();
}
export const api = {
listNodes: (params?: { kind?: string; status?: string; tags?: string }) =>
request<CortexNode[]>(`/nodes?${new URLSearchParams(params as any || {})}`),
getNode: (id: string) =>
request<NodeWithConnections>(`/nodes/${id}`),
addNode: (body: { kind: NodeKind; title: string; content?: string; status?: string; tags?: string[] }) =>
request<CortexNode>('/nodes', { method: 'POST', body: JSON.stringify(body) }),
updateNode: (id: string, body: Record<string, any>) =>
request<CortexNode>(`/nodes/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
deleteNode: (id: string, hard = false) =>
request<{ ok: boolean }>(`/nodes/${id}?hard=${hard}`, { method: 'DELETE' }),
addEdge: (body: { fromId: string; toId: string; type: EdgeType }) =>
request<CortexEdge>('/edges', { method: 'POST', body: JSON.stringify(body) }),
deleteEdge: (id: string) =>
request<{ ok: boolean }>(`/edges/${id}`, { method: 'DELETE' }),
getGraph: () =>
request<GraphData>('/graph'),
search: (text: string, options?: Record<string, any>) =>
request<SearchResult[]>('/search', { method: 'POST', body: JSON.stringify({ text, options }) }),
};