@@ -80,11 +95,11 @@ const DatabaseManager: Component = () => {
- setPath(e.currentTarget.value)} placeholder="/path/to/db" />
+ setPath(e.currentTarget.value)} placeholder="~/.nexus/my-db" />
-
diff --git a/nexus-explorer/src/frontend/src/components/editor/NqlEditor.css b/nexus-explorer/src/frontend/src/components/editor/NqlEditor.css
index 0c9948ef..944548ab 100644
--- a/nexus-explorer/src/frontend/src/components/editor/NqlEditor.css
+++ b/nexus-explorer/src/frontend/src/components/editor/NqlEditor.css
@@ -41,3 +41,105 @@
font-size: 13px;
white-space: pre-wrap;
}
+
+.sample-queries {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.sample-queries h3 {
+ margin: 0 0 8px 0;
+ font-size: 13px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.sample-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.sample-btn {
+ padding: 4px 10px;
+ font-size: 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.sample-btn:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.result-header {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border);
+}
+
+.result-header .success {
+ color: #22c55e;
+ font-weight: 500;
+}
+
+.result-header .error {
+ color: #ef4444;
+ font-weight: 500;
+}
+
+.sample-queries {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.sample-queries h3 {
+ margin: 0 0 8px 0;
+ font-size: 13px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.sample-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.sample-btn {
+ padding: 4px 10px;
+ font-size: 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.sample-btn:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.result-header {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border);
+}
+
+.result-header .success {
+ color: #22c55e;
+ font-weight: 500;
+}
+
+.result-header .error {
+ color: #ef4444;
+ font-weight: 500;
+}
diff --git a/nexus-explorer/src/frontend/src/components/editor/NqlEditor.tsx b/nexus-explorer/src/frontend/src/components/editor/NqlEditor.tsx
index c6ab798f..efe01fb6 100644
--- a/nexus-explorer/src/frontend/src/components/editor/NqlEditor.tsx
+++ b/nexus-explorer/src/frontend/src/components/editor/NqlEditor.tsx
@@ -1,4 +1,4 @@
-import { Component, createSignal, onMount, Show } from "solid-js";
+import { Component, createSignal, onMount, Show, For } from "solid-js";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { nqlExecute } from "../../lib/api";
@@ -6,6 +6,26 @@ import { activeDb } from "../../stores/app";
import type { NqlResult } from "../../types";
import "./NqlEditor.css";
+interface SampleQuery {
+ label: string;
+ tooltip: string;
+ query: string;
+ db?: string;
+}
+
+const sampleQueries: SampleQuery[] = [
+ { label: "All Nodes", tooltip: "Show all nodes and edges", query: "MATCH (n) RETURN n" },
+ { label: "Find People", tooltip: "Find all Person nodes", query: "MATCH (n:Person) RETURN n" },
+ { label: "Find Symptoms", tooltip: "Find all Symptom nodes", query: "MATCH (n:Symptom) RETURN n" },
+ { label: "Fatigue Triggers", tooltip: "Find edges connected to Fatigue", query: "MATCH (n:Fatigue)-[r]->(m) RETURN n, r, m" },
+ { label: "Count Nodes", tooltip: "Count total nodes", query: "COUNT nodes" },
+ { label: "Count Edges", tooltip: "Count total edges", query: "COUNT edges" },
+ { label: "Project Tasks", tooltip: "Find all Task nodes", query: "MATCH (n:Task) RETURN n" },
+ { label: "Task Dependencies", tooltip: "Find which tasks block others", query: "MATCH (n)-[r:BLOCKS]->(m) RETURN n, r, m" },
+ { label: "Team Members", tooltip: "Find all people", query: "MATCH (n:Person) RETURN n" },
+ { label: "Tools Used", tooltip: "Find all tools", query: "MATCH (n:Tool) RETURN n" },
+];
+
const NqlEditor: Component = () => {
let editorRef: HTMLDivElement | undefined;
const [query, setQuery] = createSignal("");
@@ -17,7 +37,7 @@ const NqlEditor: Component = () => {
if (!editorRef) return;
editor = new EditorView({
state: EditorState.create({
- doc: "",
+ doc: "// Select a sample query below or write your own\n// Supported: MATCH, GET, FIND, COUNT\n",
extensions: [
basicSetup,
EditorView.theme({
@@ -49,19 +69,52 @@ const NqlEditor: Component = () => {
}
};
+ const loadSample = (sample: SampleQuery) => {
+ if (editor) {
+ editor.dispatch({
+ changes: { from: 0, to: editor.state.doc.length, insert: sample.query },
+ });
+ }
+ setQuery(sample.query);
+ setResult(null);
+ };
+
return (
+
+
+
Sample Queries
+
+
+ {(sample) => (
+
+ )}
+
+
+
+
{(r) => (
-
{JSON.stringify(r(), null, 2)}
+
+
{JSON.stringify(r().data, null, 2)}
)}
diff --git a/nexus-explorer/src/frontend/src/components/graph/GraphView.css b/nexus-explorer/src/frontend/src/components/graph/GraphView.css
index 01f2d85d..73234a5d 100644
--- a/nexus-explorer/src/frontend/src/components/graph/GraphView.css
+++ b/nexus-explorer/src/frontend/src/components/graph/GraphView.css
@@ -1,5 +1,115 @@
.graph-container {
+ flex: 1;
+ min-height: 0;
+ position: relative;
+ background: var(--bg-primary);
+ overflow: hidden;
+}
+
+.graph-svg {
width: 100%;
height: 100%;
- background: var(--bg-primary);
+ display: block;
+}
+
+.node-info-panel {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: #1e293b;
+ border: 1px solid #475569;
+ border-radius: 8px;
+ padding: 12px 16px;
+ color: #f1f5f9;
+ z-index: 100;
+ min-width: 200px;
+}
+
+.node-info-panel h4 {
+ margin: 0 0 8px 0;
+ color: #3b82f6;
+}
+
+.node-info-panel p {
+ margin: 4px 0;
+ font-size: 13px;
+ color: #94a3b8;
+}
+
+.node-info-panel button {
+ margin-top: 8px;
+ padding: 4px 12px;
+ font-size: 12px;
+}
+
+.zoom-controls {
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+ display: flex;
+ gap: 4px;
+ z-index: 100;
+}
+
+.zoom-controls button {
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #1e293b;
+ border: 1px solid #475569;
+}
+
+.node-info-panel {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: #1e293b;
+ border: 1px solid #475569;
+ border-radius: 8px;
+ padding: 12px 16px;
+ color: #f1f5f9;
+ z-index: 100;
+ min-width: 200px;
+}
+
+.node-info-panel h4 {
+ margin: 0 0 8px 0;
+ color: #3b82f6;
+}
+
+.node-info-panel p {
+ margin: 4px 0;
+ font-size: 13px;
+ color: #94a3b8;
+}
+
+.node-info-panel button {
+ margin-top: 8px;
+ padding: 4px 12px;
+ font-size: 12px;
+}
+
+.zoom-controls {
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+ display: flex;
+ gap: 4px;
+ z-index: 100;
+}
+
+.zoom-controls button {
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #1e293b;
+ border: 1px solid #475569;
}
diff --git a/nexus-explorer/src/frontend/src/components/graph/GraphView.tsx b/nexus-explorer/src/frontend/src/components/graph/GraphView.tsx
index d5ac2fe8..1d7c1cec 100644
--- a/nexus-explorer/src/frontend/src/components/graph/GraphView.tsx
+++ b/nexus-explorer/src/frontend/src/components/graph/GraphView.tsx
@@ -1,51 +1,226 @@
-import { Component, onMount, onCleanup, createEffect } from "solid-js";
-import ForceGraph from "force-graph";
-import type { ForceGraphInstance } from "force-graph";
+import { Component, createEffect, createSignal, onMount, onCleanup } from "solid-js";
import { nodes, edges, activeDb } from "../../stores/app";
+import type { NodeDto, EdgeDto } from "../../types";
import "./GraphView.css";
+interface GraphNode {
+ id: number;
+ label: string;
+ x: number;
+ y: number;
+ color: string;
+ selected?: boolean;
+}
+
+const colors = [
+ "#3b82f6", "#ef4444", "#22c55e", "#f59e0b", "#8b5cf6",
+ "#ec4899", "#06b6d4", "#f97316", "#14b8a6", "#6366f1"
+];
+
const GraphView: Component = () => {
- let containerRef: HTMLDivElement | undefined;
- let graph: ForceGraphInstance | undefined;
-
- onMount(() => {
- if (!containerRef) return;
- graph = ForceGraph(containerRef)
- .backgroundColor("#0f172a")
- .nodeLabel("label")
- .nodeAutoColorBy("label")
- .nodeRelSize(6)
- .linkLabel("label")
- .linkWidth(1.5)
- .linkDirectionalParticles(1)
- .linkDirectionalParticleWidth(2)
- .onEngineStop(() => {
- graph?.zoomToFit(400, 40);
+ const [graphNodes, setGraphNodes] = createSignal
([]);
+ const [graphEdges, setGraphEdges] = createSignal([]);
+ const [selectedNode, setSelectedNode] = createSignal(null);
+ const [transform, setTransform] = createSignal({ x: 0, y: 0, k: 1 });
+ const [dragging, setDragging] = createSignal(null);
+ const [panning, setPanning] = createSignal(false);
+ const [panStart, setPanStart] = createSignal({ x: 0, y: 0 });
+ let svgRef: SVGSVGElement | undefined;
+
+ createEffect(() => {
+ if (!activeDb()) {
+ setGraphNodes([]);
+ setGraphEdges([]);
+ return;
+ }
+
+ const n = nodes();
+ const e = edges();
+ if (n.length === 0) {
+ setGraphNodes([]);
+ setGraphEdges([]);
+ return;
+ }
+
+ const nodeMap = new Map();
+ const width = 800;
+ const height = 600;
+ const cx = width / 2;
+ const cy = height / 2;
+
+ n.forEach((node: NodeDto, i: number) => {
+ const angle = (2 * Math.PI * i) / n.length;
+ const r = Math.min(width, height) * 0.3;
+ nodeMap.set(node.id, {
+ id: node.id,
+ label: node.label,
+ x: cx + r * Math.cos(angle),
+ y: cy + r * Math.sin(angle),
+ color: colors[i % colors.length],
});
- });
+ });
+
+ // Force simulation
+ for (let iter = 0; iter < 100; iter++) {
+ const arr = Array.from(nodeMap.values());
+ for (let i = 0; i < arr.length; i++) {
+ for (let j = i + 1; j < arr.length; j++) {
+ const dx = arr[j].x - arr[i].x;
+ const dy = arr[j].y - arr[i].y;
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
+ const force = 5000 / (dist * dist);
+ arr[i].x -= (dx / dist) * force;
+ arr[i].y -= (dy / dist) * force;
+ arr[j].x += (dx / dist) * force;
+ arr[j].y += (dy / dist) * force;
+ }
+ }
+ e.forEach((edge: EdgeDto) => {
+ const s = nodeMap.get(edge.from);
+ const t = nodeMap.get(edge.to);
+ if (s && t) {
+ const dx = t.x - s.x;
+ const dy = t.y - s.y;
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
+ const force = (dist - 100) * 0.01;
+ s.x += (dx / dist) * force;
+ s.y += (dy / dist) * force;
+ t.x -= (dx / dist) * force;
+ t.y -= (dy / dist) * force;
+ }
+ });
+ arr.forEach(node => {
+ node.x += (cx - node.x) * 0.01;
+ node.y += (cy - node.y) * 0.01;
+ });
+ }
- onCleanup(() => {
- graph = undefined;
+ setGraphNodes(Array.from(nodeMap.values()));
+ setGraphEdges(e);
+ setSelectedNode(null);
+ setTransform({ x: 0, y: 0, k: 1 });
});
- createEffect(() => {
- if (!graph || !activeDb()) return;
+ const handleWheel = (e: WheelEvent) => {
+ e.preventDefault();
+ const t = transform();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ const newK = Math.max(0.1, Math.min(5, t.k * delta));
+ setTransform({ ...t, k: newK });
+ };
- const graphNodes = nodes().map(n => ({
- id: n.id,
- label: n.label,
- }));
+ const handleMouseDown = (e: MouseEvent) => {
+ if (e.target === svgRef || (e.target as Element).tagName === 'rect') {
+ setPanning(true);
+ setPanStart({ x: e.clientX - transform().x, y: e.clientY - transform().y });
+ }
+ };
- const graphEdges = edges().map(e => ({
- source: e.from,
- target: e.to,
- label: e.label,
- }));
+ const handleMouseMove = (e: MouseEvent) => {
+ const dragNode = dragging();
+ if (dragNode) {
+ const t = transform();
+ const svg = svgRef!;
+ const rect = svg.getBoundingClientRect();
+ const x = (e.clientX - rect.left - t.x) / t.k;
+ const y = (e.clientY - rect.top - t.y) / t.k;
+ setGraphNodes(prev => prev.map(n => n.id === dragNode.id ? { ...n, x, y } : n));
+ } else if (panning()) {
+ const ps = panStart();
+ setTransform(prev => ({ ...prev, x: e.clientX - ps.x, y: e.clientY - ps.y }));
+ }
+ };
- graph.graphData({ nodes: graphNodes, links: graphEdges });
- });
+ const handleMouseUp = () => {
+ setDragging(null);
+ setPanning(false);
+ };
+
+ const handleNodeMouseDown = (node: GraphNode, e: MouseEvent) => {
+ e.stopPropagation();
+ setSelectedNode(node);
+ setDragging(node);
+ };
+
+ return (
+
+
+
+ {/* Node info panel */}
+ {selectedNode() && (
+
+
{selectedNode()!.label}
+
ID: {selectedNode()!.id}
+
Connections: {graphEdges().filter(e => e.from === selectedNode()!.id || e.to === selectedNode()!.id).length}
+
+
+ )}
- return
;
+ {/* Zoom controls */}
+
+
+
+
+
+
+ );
};
export default GraphView;
diff --git a/nexus-explorer/src/frontend/src/components/home/HomeView.css b/nexus-explorer/src/frontend/src/components/home/HomeView.css
new file mode 100644
index 00000000..7d121bc1
--- /dev/null
+++ b/nexus-explorer/src/frontend/src/components/home/HomeView.css
@@ -0,0 +1,202 @@
+.home-view {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-2xl);
+}
+
+.welcome-section {
+ text-align: center;
+ margin-bottom: var(--space-2xl);
+}
+
+.welcome-title {
+ font-size: 28px;
+ font-weight: 700;
+ margin-bottom: var(--space-sm);
+ background: linear-gradient(135deg, #6366f1, #06b6d4);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.welcome-subtitle {
+ color: var(--text-secondary);
+ font-size: var(--text-lg);
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.stats-row {
+ display: flex;
+ gap: var(--space-lg);
+ justify-content: center;
+ margin-bottom: var(--space-2xl);
+}
+
+.stat-card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg) var(--space-xl);
+ text-align: center;
+ min-width: 120px;
+}
+
+.stat-value {
+ display: block;
+ font-size: 24px;
+ font-weight: 700;
+ color: #6366f1;
+}
+
+.stat-label {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+}
+
+.section {
+ margin-bottom: var(--space-2xl);
+}
+
+.section-title {
+ font-size: var(--text-xl);
+ font-weight: 600;
+ margin-bottom: var(--space-sm);
+}
+
+.section-desc {
+ color: var(--text-secondary);
+ font-size: var(--text-sm);
+ margin-bottom: var(--space-lg);
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: var(--space-lg);
+}
+
+.demo-card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-xl);
+ padding: var(--space-xl);
+ transition: border-color var(--transition-fast);
+}
+
+.demo-card:hover {
+ border-color: #6366f1;
+}
+
+.demo-header {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-md);
+ margin-bottom: var(--space-md);
+}
+
+.demo-icon {
+ font-size: 32px;
+}
+
+.demo-info {
+ flex: 1;
+}
+
+.demo-title {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ margin-bottom: var(--space-xs);
+}
+
+.demo-stats {
+ display: flex;
+ gap: var(--space-xs);
+}
+
+.demo-desc {
+ color: var(--text-secondary);
+ font-size: var(--text-sm);
+ line-height: 1.5;
+ margin-bottom: var(--space-lg);
+}
+
+.demo-btn {
+ width: 100%;
+ padding: var(--space-sm) var(--space-lg);
+ background: #6366f1;
+ border: none;
+ border-radius: var(--radius-md);
+ color: white;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ cursor: pointer;
+ margin-bottom: var(--space-md);
+ transition: background var(--transition-fast);
+}
+
+.demo-btn:hover {
+ background: #4f46e5;
+}
+
+.demo-questions {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+}
+
+.demo-questions-label {
+ font-size: var(--text-xs);
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.demo-question {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: var(--space-xs) var(--space-sm);
+ color: var(--text-secondary);
+ font-size: var(--text-xs);
+ cursor: pointer;
+ text-align: left;
+ transition: all var(--transition-fast);
+}
+
+.demo-question:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ border-color: #6366f1;
+}
+
+.tips-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--space-lg);
+}
+
+.tip-card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ text-align: center;
+}
+
+.tip-icon {
+ font-size: 32px;
+ display: block;
+ margin-bottom: var(--space-md);
+}
+
+.tip-card h3 {
+ font-size: var(--text-base);
+ font-weight: 600;
+ margin-bottom: var(--space-sm);
+}
+
+.tip-card p {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
diff --git a/nexus-explorer/src/frontend/src/components/home/HomeView.tsx b/nexus-explorer/src/frontend/src/components/home/HomeView.tsx
new file mode 100644
index 00000000..79f0ef5b
--- /dev/null
+++ b/nexus-explorer/src/frontend/src/components/home/HomeView.tsx
@@ -0,0 +1,160 @@
+import { Component, For } from "solid-js";
+import { setActiveView, setActiveDb, databases, nodes, edges, setNodes, setEdges, setSchema } from "../../stores/app";
+import { nodeList, edgeList, schemaGet } from "../../lib/api";
+import "./HomeView.css";
+
+interface DemoCard {
+ id: string;
+ icon: string;
+ title: string;
+ description: string;
+ items: number;
+ connections: number;
+ questions: string[];
+}
+
+const demoCards: DemoCard[] = [
+ {
+ id: "health-patterns",
+ icon: "🏥",
+ title: "Health Patterns",
+ description: "Track symptoms, triggers, and how events affect your wellbeing. Discover patterns between sleep, stress, and how you feel.",
+ items: 11,
+ connections: 16,
+ questions: [
+ "What triggers fatigue?",
+ "Show me all symptoms",
+ "How is caffeine connected?",
+ ],
+ },
+ {
+ id: "project-management",
+ icon: "📋",
+ title: "Project Management",
+ description: "See how team members, tasks, and tools connect. Understand dependencies and who owns what in your projects.",
+ items: 13,
+ connections: 14,
+ questions: [
+ "What tasks are blocked?",
+ "Show me all team members",
+ "Which tools are being used?",
+ ],
+ },
+];
+
+const HomeView: Component = () => {
+ const loadDb = async (dbId: string) => {
+ setActiveDb(dbId);
+ const n = await nodeList(dbId);
+ setNodes(n);
+ const e = await edgeList(dbId);
+ setEdges(e);
+ const s = await schemaGet(dbId);
+ setSchema(s);
+ setActiveView("graph");
+ };
+
+ const handleQuestion = async (dbId: string, question: string) => {
+ await loadDb(dbId);
+ setActiveView("nql");
+ };
+
+ // Stats from current database
+ const currentNodes = nodes().length;
+ const currentEdges = edges().length;
+ const hasData = currentNodes > 0;
+
+ return (
+
+ {/* Welcome section */}
+
+
Welcome to SensibleDB
+
+ Explore your data through connections. Ask questions, find patterns, and generate insights — no database expertise required.
+
+
+
+ {/* Quick stats */}
+ {hasData && (
+
+
+ {currentNodes}
+ Items
+
+
+ {currentEdges}
+ Connections
+
+
+ {databases().length}
+ Databases
+
+
+ )}
+
+ {/* Demo databases */}
+
+
Try a Demo Database
+
+ Explore pre-loaded examples to see how SensibleDB works. Click a card to start exploring.
+
+
+
+ {(demo) => (
+
+
+
{demo.description}
+
+
+ Try asking:
+
+ {(q) => (
+
+ )}
+
+
+
+ )}
+
+
+
+
+ {/* Getting started tips */}
+
+
Getting Started
+
+
+
🔗
+
Explore the Graph
+
See how your data connects. Click items to see details, drag to rearrange, scroll to zoom.
+
+
+
💬
+
Ask Questions
+
Use the Chat view to ask about your data in plain English. No query language needed.
+
+
+
📊
+
Generate Reports
+
Create summaries of your data for any time period. Export and share with your team.
+
+
+
+
+ );
+};
+
+export default HomeView;
diff --git a/nexus-explorer/src/frontend/src/components/report/ReportView.css b/nexus-explorer/src/frontend/src/components/report/ReportView.css
new file mode 100644
index 00000000..14221a65
--- /dev/null
+++ b/nexus-explorer/src/frontend/src/components/report/ReportView.css
@@ -0,0 +1,158 @@
+.report-view {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-2xl);
+}
+
+.report-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-xl);
+}
+
+.report-header h1 {
+ font-size: var(--text-xl);
+ font-weight: 600;
+}
+
+.report-controls {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: center;
+}
+
+.metric-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: var(--space-lg);
+ margin-bottom: var(--space-2xl);
+}
+
+.metric-card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ text-align: center;
+}
+
+.metric-value {
+ display: block;
+ font-size: 28px;
+ font-weight: 700;
+ color: #6366f1;
+}
+
+.metric-label {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+}
+
+.report-section {
+ margin-bottom: var(--space-2xl);
+}
+
+.report-section h2 {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ margin-bottom: var(--space-md);
+ padding-bottom: var(--space-sm);
+ border-bottom: 1px solid var(--border);
+}
+
+.findings-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.finding {
+ display: flex;
+ gap: var(--space-md);
+ align-items: flex-start;
+}
+
+.finding-icon {
+ font-size: 20px;
+ flex-shrink: 0;
+}
+
+.finding-text {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.finding-text strong {
+ color: var(--text-primary);
+}
+
+.connected-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.connected-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+}
+
+.connected-name {
+ font-size: var(--text-sm);
+ font-weight: 500;
+}
+
+.connected-count {
+ font-size: var(--text-xs);
+ color: var(--text-muted);
+ background: var(--bg-tertiary);
+ padding: 2px 8px;
+ border-radius: 999px;
+}
+
+.type-breakdown {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.type-row {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+}
+
+.type-name {
+ font-size: var(--text-sm);
+ font-weight: 500;
+ min-width: 120px;
+}
+
+.type-bar {
+ flex: 1;
+ height: 8px;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.type-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #6366f1, #06b6d4);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+}
+
+.type-count {
+ font-size: var(--text-xs);
+ color: var(--text-muted);
+ min-width: 80px;
+ text-align: right;
+}
diff --git a/nexus-explorer/src/frontend/src/components/report/ReportView.tsx b/nexus-explorer/src/frontend/src/components/report/ReportView.tsx
new file mode 100644
index 00000000..94fa0be6
--- /dev/null
+++ b/nexus-explorer/src/frontend/src/components/report/ReportView.tsx
@@ -0,0 +1,137 @@
+import { Component, createSignal, createEffect } from "solid-js";
+import { activeDb, nodes, edges } from "../../stores/app";
+import "./ReportView.css";
+
+const ReportView: Component = () => {
+ const [period, setPeriod] = createSignal("all");
+
+ const nodeCount = nodes().length;
+ const edgeCount = edges().length;
+
+ // Count unique types
+ const nodeTypes = new Set(nodes().map(n => n.label.split(":")[0]));
+ const edgeTypes = new Set(edges().map(e => e.label));
+
+ // Most connected nodes
+ const connectionCount = new Map();
+ edges().forEach(e => {
+ connectionCount.set(e.from, (connectionCount.get(e.from) || 0) + 1);
+ connectionCount.set(e.to, (connectionCount.get(e.to) || 0) + 1);
+ });
+ const mostConnected = [...connectionCount.entries()]
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 5);
+
+ const getNodeLabel = (id: number) => {
+ return nodes().find(n => n.id === id)?.label || `ID: ${id}`;
+ };
+
+ return (
+
+
+
+ {/* Metric cards */}
+
+
+ {nodeCount}
+ Total Items
+
+
+ {edgeCount}
+ Connections
+
+
+ {nodeTypes.size}
+ Item Types
+
+
+ {edgeTypes.size}
+ Relationship Types
+
+
+
+ {/* Key findings */}
+
+
Key Findings
+
+ {mostConnected.length > 0 && (
+
+
🔗
+
+ {getNodeLabel(mostConnected[0][0])} is the most connected item with {mostConnected[0][1]} connections
+
+
+ )}
+
+
📊
+
+ Your data contains {nodeTypes.size} different types: {Array.from(nodeTypes).join(", ")}
+
+
+
+
🔀
+
+ {edgeTypes.size} types of relationships connect your items: {Array.from(edgeTypes).join(", ")}
+
+
+
+
+
+ {/* Most connected */}
+ {mostConnected.length > 0 && (
+
+
Most Connected Items
+
+ {mostConnected.map(([id, count]) => (
+
+ {getNodeLabel(id)}
+ {count} connections
+
+ ))}
+
+
+ )}
+
+ {/* Item type breakdown */}
+
+
Item Breakdown by Type
+
+ {Array.from(nodeTypes).map(type => {
+ const count = nodes().filter(n => n.label.startsWith(type)).length;
+ const pct = nodeCount > 0 ? Math.round((count / nodeCount) * 100) : 0;
+ return (
+
+
{type}
+
+
{count} ({pct}%)
+
+ );
+ })}
+
+
+
+ );
+};
+
+function generateReport(): string {
+ return "Report generation - connect to a database to see data";
+}
+
+export default ReportView;
diff --git a/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.css b/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.css
index a61a29ba..3b99dfd8 100644
--- a/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.css
+++ b/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.css
@@ -1,43 +1,44 @@
.sidebar {
- width: 220px;
- background: var(--bg-secondary);
- border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
+ height: 100%;
+ overflow-y: auto;
}
-.sidebar-header {
- padding: 16px;
- border-bottom: 1px solid var(--border);
+.sidebar-section {
+ padding: var(--space-sm);
}
-.sidebar-header h2 {
- font-size: 16px;
+.sidebar-heading {
+ font-size: var(--text-xs);
font-weight: 600;
- margin-bottom: 4px;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ padding: var(--space-xs) var(--space-sm);
+ margin-bottom: var(--space-xs);
}
-.db-badge {
- font-size: 11px;
- background: var(--accent);
- padding: 2px 8px;
- border-radius: 10px;
-}
-
-.nav-list {
- list-style: none;
- padding: 8px;
+.sidebar-divider {
+ height: 1px;
+ background: var(--border);
+ margin: var(--space-xs) var(--space-md);
}
.nav-item {
display: flex;
align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border-radius: 6px;
- cursor: pointer;
+ gap: var(--space-sm);
+ width: 100%;
+ padding: var(--space-sm) var(--space-md);
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
color: var(--text-secondary);
- transition: all 0.15s;
+ cursor: pointer;
+ font-size: var(--text-sm);
+ transition: all var(--transition-fast);
+ text-align: left;
}
.nav-item:hover {
@@ -46,14 +47,53 @@
}
.nav-item.active {
- background: var(--accent);
- color: white;
+ background: rgba(99, 102, 241, 0.15);
+ color: #818cf8;
}
.nav-icon {
- font-size: 16px;
+ font-size: var(--text-base);
+ width: 20px;
+ text-align: center;
}
.nav-label {
- font-size: 14px;
+ flex: 1;
+}
+
+.db-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ width: 100%;
+ padding: var(--space-sm) var(--space-md);
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: var(--text-sm);
+ transition: all var(--transition-fast);
+ text-align: left;
+}
+
+.db-item:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.db-item.active {
+ background: rgba(99, 102, 241, 0.15);
+ color: #818cf8;
+}
+
+.db-icon {
+ font-size: var(--text-base);
+}
+
+.db-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
diff --git a/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.tsx b/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.tsx
index 9b5b48bb..5af6e371 100644
--- a/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.tsx
+++ b/nexus-explorer/src/frontend/src/components/sidebar/Sidebar.tsx
@@ -1,33 +1,100 @@
-import { Component } from "solid-js";
-import { activeView, setActiveView, activeDb } from "../../stores/app";
+import { Component, For } from "solid-js";
+import { activeView, setActiveView, databases, activeDb, setActiveDb, setNodes, setEdges, setSchema } from "../../stores/app";
+import { nodeList, edgeList, schemaGet } from "../../lib/api";
import "./Sidebar.css";
+interface NavItem {
+ id: string;
+ icon: string;
+ label: string;
+ tooltip: string;
+}
+
+const navItems: NavItem[] = [
+ { id: "home", icon: "🏠", label: "Home", tooltip: "Overview and getting started" },
+ { id: "graph", icon: "🔗", label: "Graph", tooltip: "Visualize connections between items" },
+ { id: "chat", icon: "💬", label: "Chat", tooltip: "Ask questions about your data" },
+ { id: "report", icon: "📊", label: "Report", tooltip: "Generate summaries and insights" },
+];
+
+const dataNavItems: NavItem[] = [
+ { id: "nodes", icon: "📦", label: "Items", tooltip: "Browse all items in your database" },
+ { id: "edges", icon: "🔀", label: "Connections", tooltip: "View relationships between items" },
+ { id: "schema", icon: "🏗️", label: "Structure", tooltip: "See how your data is organized" },
+ { id: "nql", icon: "⌨️", label: "NQL Editor", tooltip: "Write advanced queries" },
+];
+
const Sidebar: Component = () => {
- const views = [
- { id: "graph" as const, label: "Graph", icon: "🔗" },
- { id: "nodes" as const, label: "Nodes", icon: "⚫" },
- { id: "edges" as const, label: "Edges", icon: "➡️" },
- { id: "schema" as const, label: "Schema", icon: "📋" },
- { id: "nql" as const, label: "NQL", icon: "💻" },
- ];
+ const loadDbData = async (dbName: string) => {
+ const nodes = await nodeList(dbName);
+ setNodes(nodes);
+ const edges = await edgeList(dbName);
+ setEdges(edges);
+ const schema = await schemaGet(dbName);
+ setSchema(schema);
+ };
+
+ const handleNavClick = (id: string) => {
+ setActiveView(id as any);
+ };
+
+ const handleDbClick = async (db: string) => {
+ setActiveDb(db);
+ await loadDbData(db);
+ };
return (