@medicine-wheel/app 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/app/graph/page.tsx +61 -111
  2. package/package.json +21 -20
@@ -1,30 +1,37 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState, useCallback } from "react";
3
+ import "@xyflow/react/dist/style.css";
4
+
5
+ import { useEffect, useState, useCallback, useMemo } from "react";
6
+ import dynamic from "next/dynamic";
4
7
  import { type RelationalNode, type RelationalEdge, DIRECTION_COLORS } from "@/lib/types";
8
+ import {
9
+ buildGraphData,
10
+ type MWGraphData,
11
+ type MWGraphNode,
12
+ } from "@medicine-wheel/graph-viz";
5
13
  import { toast } from "sonner";
6
14
 
7
- interface GraphNode {
8
- id: string;
9
- label: string;
10
- type: string;
11
- direction?: string;
12
- x: number;
13
- y: number;
14
- }
15
-
16
- interface GraphLink {
17
- source: string;
18
- target: string;
19
- label: string;
20
- ceremonyHonored: boolean;
21
- strength: number;
22
- }
15
+ // React Flow touches `window`/`document`, so the interactive renderer is
16
+ // loaded client-side only via the `./interactive` subpath export.
17
+ const MedicineWheelFlowGraph = dynamic(
18
+ () =>
19
+ import("@medicine-wheel/graph-viz/interactive").then(
20
+ (m) => m.MedicineWheelFlowGraph,
21
+ ),
22
+ {
23
+ ssr: false,
24
+ loading: () => (
25
+ <div className="flex items-center justify-center h-[600px] text-gray-500">
26
+ Loading interactive graph…
27
+ </div>
28
+ ),
29
+ },
30
+ );
23
31
 
24
32
  export default function GraphPage() {
25
- const [nodes, setNodes] = useState<GraphNode[]>([]);
26
- const [links, setLinks] = useState<GraphLink[]>([]);
27
- const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
33
+ const [graph, setGraph] = useState<MWGraphData>({ nodes: [], links: [] });
34
+ const [selectedNode, setSelectedNode] = useState<MWGraphNode | null>(null);
28
35
  const [loading, setLoading] = useState(true);
29
36
  const [showLabels, setShowLabels] = useState(true);
30
37
 
@@ -33,39 +40,13 @@ export default function GraphPage() {
33
40
  const [nodesRes, edgesRes] = await Promise.all([fetch("/api/nodes"), fetch("/api/edges")]);
34
41
  const nodesResponse = await nodesRes.json();
35
42
  const edgesData: RelationalEdge[] = await edgesRes.json();
36
-
37
- // API returns { nodes: [...], provider: '...', count: N }
38
- const nodesData: RelationalNode[] = Array.isArray(nodesResponse) ? nodesResponse : (nodesResponse.nodes || []);
39
-
40
- // Position nodes by direction on a circular layout
41
- const CX = 350, CY = 300, R = 220;
42
- const dirAngles: Record<string, number> = { east: 0, south: 90, west: 180, north: 270 };
43
43
 
44
- const graphNodes = nodesData.map((n, i) => {
45
- const baseAngle = n.direction ? dirAngles[n.direction] ?? 0 : (360 * i) / nodesData.length;
46
- const jitter = (Math.random() - 0.5) * 60;
47
- const angle = ((baseAngle + jitter) * Math.PI) / 180;
48
- const r = R * (0.5 + Math.random() * 0.4);
49
- return {
50
- id: n.id,
51
- label: n.name,
52
- type: n.type,
53
- direction: n.direction,
54
- x: CX + r * Math.cos(angle),
55
- y: CY + r * Math.sin(angle),
56
- };
57
- });
58
-
59
- const graphLinks = edgesData.map((e) => ({
60
- source: e.from_id,
61
- target: e.to_id,
62
- label: e.relationship_type,
63
- ceremonyHonored: e.ceremony_honored,
64
- strength: e.strength,
65
- }));
44
+ // API returns { nodes: [...], provider: '...', count: N }
45
+ const nodesData: RelationalNode[] = Array.isArray(nodesResponse)
46
+ ? nodesResponse
47
+ : nodesResponse.nodes || [];
66
48
 
67
- setNodes(graphNodes);
68
- setLinks(graphLinks);
49
+ setGraph(buildGraphData(nodesData, edgesData));
69
50
  } catch {
70
51
  toast.error("Failed to load graph data");
71
52
  } finally {
@@ -73,9 +54,18 @@ export default function GraphPage() {
73
54
  }
74
55
  }, []);
75
56
 
76
- useEffect(() => { loadData(); }, [loadData]);
57
+ useEffect(() => {
58
+ loadData();
59
+ }, [loadData]);
77
60
 
78
- const nodeMap = new Map(nodes.map((n) => [n.id, n]));
61
+ const ceremoniedCount = useMemo(
62
+ () => graph.links.filter((l) => l.ceremonyHonored).length,
63
+ [graph.links],
64
+ );
65
+ const directionCount = useMemo(
66
+ () => new Set(graph.nodes.map((n) => n.direction).filter(Boolean)).size,
67
+ [graph.nodes],
68
+ );
79
69
 
80
70
  return (
81
71
  <div className="min-h-screen bg-[#0a0a1a] text-white p-6">
@@ -85,7 +75,9 @@ export default function GraphPage() {
85
75
  <h1 className="text-2xl font-bold flex items-center gap-2">
86
76
  <span className="text-3xl">🔮</span> Medicine Wheel Graph
87
77
  </h1>
88
- <p className="text-gray-400 text-sm mt-1">Relational visualization across four directions</p>
78
+ <p className="text-gray-400 text-sm mt-1">
79
+ Interactive relational web — drag, pan, zoom across the four directions
80
+ </p>
89
81
  </div>
90
82
  <div className="flex gap-2">
91
83
  <button
@@ -94,7 +86,9 @@ export default function GraphPage() {
94
86
  >
95
87
  Labels {showLabels ? "ON" : "OFF"}
96
88
  </button>
97
- <button onClick={loadData} className="px-3 py-1.5 rounded text-sm bg-white/5 hover:bg-white/10">↻ Refresh</button>
89
+ <button onClick={loadData} className="px-3 py-1.5 rounded text-sm bg-white/5 hover:bg-white/10">
90
+ ↻ Refresh
91
+ </button>
98
92
  </div>
99
93
  </div>
100
94
 
@@ -102,64 +96,20 @@ export default function GraphPage() {
102
96
  <div className="flex-1 rounded-xl border border-white/10 overflow-hidden">
103
97
  {loading ? (
104
98
  <div className="flex items-center justify-center h-[600px] text-gray-500">Loading graph data...</div>
105
- ) : nodes.length === 0 ? (
99
+ ) : graph.nodes.length === 0 ? (
106
100
  <div className="flex flex-col items-center justify-center h-[600px] text-gray-500">
107
101
  <span className="text-4xl mb-3">🌀</span>
108
102
  <p>No relational nodes yet.</p>
109
103
  <p className="text-sm mt-1">Create nodes via the Nodes page.</p>
110
104
  </div>
111
105
  ) : (
112
- <svg viewBox="0 0 700 600" className="w-full h-[600px]">
113
- {/* Quadrant backgrounds */}
114
- {[
115
- { dir: "east", cx: 525, cy: 300 },
116
- { dir: "south", cx: 350, cy: 475 },
117
- { dir: "west", cx: 175, cy: 300 },
118
- { dir: "north", cx: 350, cy: 125 },
119
- ].map(({ dir, cx, cy }) => (
120
- <g key={dir}>
121
- <circle cx={cx} cy={cy} r={40} fill={(DIRECTION_COLORS as any)[dir]} opacity={0.1} />
122
- <text x={cx} y={cy + 4} textAnchor="middle" fill={(DIRECTION_COLORS as any)[dir]} className="text-xs font-bold capitalize" opacity={0.6}>
123
- {dir}
124
- </text>
125
- </g>
126
- ))}
127
-
128
- {/* Links */}
129
- {links.map((link, i) => {
130
- const source = nodeMap.get(link.source);
131
- const target = nodeMap.get(link.target);
132
- if (!source || !target) return null;
133
- return (
134
- <line
135
- key={i}
136
- x1={source.x} y1={source.y} x2={target.x} y2={target.y}
137
- stroke={link.ceremonyHonored ? "#FFD700" : "#555"}
138
- strokeWidth={1 + link.strength * 2}
139
- strokeDasharray={link.ceremonyHonored ? "none" : "6,4"}
140
- opacity={0.5}
141
- />
142
- );
143
- })}
144
-
145
- {/* Nodes */}
146
- {nodes.map((node) => (
147
- <g key={node.id} className="cursor-pointer" onClick={() => setSelectedNode(node)}>
148
- <circle
149
- cx={node.x} cy={node.y} r={selectedNode?.id === node.id ? 18 : 14}
150
- fill={node.direction ? (DIRECTION_COLORS as any)[node.direction] || "#888" : "#888"}
151
- stroke={selectedNode?.id === node.id ? "#FFD700" : "#333"}
152
- strokeWidth={selectedNode?.id === node.id ? 3 : 1}
153
- opacity={0.9}
154
- />
155
- {showLabels && (
156
- <text x={node.x} y={node.y + 26} textAnchor="middle" fill="#ccc" className="text-[10px]">
157
- {node.label.length > 15 ? node.label.slice(0, 14) + "…" : node.label}
158
- </text>
159
- )}
160
- </g>
161
- ))}
162
- </svg>
106
+ <MedicineWheelFlowGraph
107
+ data={graph}
108
+ height={600}
109
+ darkMode
110
+ showNodeLabels={showLabels}
111
+ onNodeClick={(node) => setSelectedNode(node)}
112
+ />
163
113
  )}
164
114
  </div>
165
115
 
@@ -189,10 +139,10 @@ export default function GraphPage() {
189
139
  <div className="rounded-xl border border-white/10 p-4">
190
140
  <h3 className="text-sm font-semibold text-gray-400 mb-3">Graph Stats</h3>
191
141
  <div className="grid grid-cols-2 gap-3 text-center">
192
- <div><p className="text-2xl font-bold">{nodes.length}</p><p className="text-xs text-gray-500">Nodes</p></div>
193
- <div><p className="text-2xl font-bold">{links.length}</p><p className="text-xs text-gray-500">Relations</p></div>
194
- <div><p className="text-2xl font-bold">{links.filter((l) => l.ceremonyHonored).length}</p><p className="text-xs text-gray-500">Ceremonied</p></div>
195
- <div><p className="text-2xl font-bold">{new Set(nodes.map((n) => n.direction).filter(Boolean)).size}</p><p className="text-xs text-gray-500">Directions</p></div>
142
+ <div><p className="text-2xl font-bold">{graph.nodes.length}</p><p className="text-xs text-gray-500">Nodes</p></div>
143
+ <div><p className="text-2xl font-bold">{graph.links.length}</p><p className="text-xs text-gray-500">Relations</p></div>
144
+ <div><p className="text-2xl font-bold">{ceremoniedCount}</p><p className="text-xs text-gray-500">Ceremonied</p></div>
145
+ <div><p className="text-2xl font-bold">{directionCount}</p><p className="text-xs text-gray-500">Directions</p></div>
196
146
  </div>
197
147
  </div>
198
148
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/app",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Medicine Wheel — Interactive visual layer for Indigenous relational research with Four Directions, ceremonies, and narrative arcs",
5
5
  "bin": {
6
6
  "mw": "dist/cli/mw.js",
@@ -74,24 +74,24 @@
74
74
  "release:major": "npm run version:major && npm run publish:all && npm run release:commit"
75
75
  },
76
76
  "dependencies": {
77
- "@medicine-wheel/ceremony-protocol": "^0.3.0",
78
- "@medicine-wheel/community-review": "^0.3.0",
79
- "@medicine-wheel/consent-lifecycle": "^0.3.0",
80
- "@medicine-wheel/data-store": "^0.3.0",
81
- "@medicine-wheel/data-store-postgres": "^0.3.0",
82
- "@medicine-wheel/fire-keeper": "^0.3.0",
83
- "@medicine-wheel/graph-viz": "^0.3.0",
84
- "@medicine-wheel/importance-unit": "^0.3.0",
85
- "@medicine-wheel/mcp": "^4.3.0",
86
- "@medicine-wheel/narrative-engine": "^0.3.0",
87
- "@medicine-wheel/ontology-core": "^0.3.0",
88
- "@medicine-wheel/prompt-decomposition": "^0.3.0",
89
- "@medicine-wheel/relational-index": "^0.3.0",
90
- "@medicine-wheel/relational-query": "^0.3.0",
91
- "@medicine-wheel/session-reader": "^0.3.0",
92
- "@medicine-wheel/storage-provider": "^0.3.0",
93
- "@medicine-wheel/transformation-tracker": "^0.3.0",
94
- "@medicine-wheel/ui-components": "^0.3.0",
77
+ "@medicine-wheel/ceremony-protocol": "^0.4.1",
78
+ "@medicine-wheel/community-review": "^0.4.1",
79
+ "@medicine-wheel/consent-lifecycle": "^0.4.1",
80
+ "@medicine-wheel/data-store": "^0.4.1",
81
+ "@medicine-wheel/data-store-postgres": "^0.4.1",
82
+ "@medicine-wheel/fire-keeper": "^0.4.1",
83
+ "@medicine-wheel/graph-viz": "^0.4.1",
84
+ "@medicine-wheel/importance-unit": "^0.4.1",
85
+ "@medicine-wheel/mcp": "^4.4.1",
86
+ "@medicine-wheel/narrative-engine": "^0.4.1",
87
+ "@medicine-wheel/ontology-core": "^0.4.1",
88
+ "@medicine-wheel/prompt-decomposition": "^0.4.1",
89
+ "@medicine-wheel/relational-index": "^0.4.1",
90
+ "@medicine-wheel/relational-query": "^0.4.1",
91
+ "@medicine-wheel/session-reader": "^0.4.1",
92
+ "@medicine-wheel/storage-provider": "^0.4.1",
93
+ "@medicine-wheel/transformation-tracker": "^0.4.1",
94
+ "@medicine-wheel/ui-components": "^0.4.1",
95
95
  "@neondatabase/serverless": "^0.10.0",
96
96
  "clsx": "^2.1.1",
97
97
  "lucide-react": "^0.475.0",
@@ -101,7 +101,8 @@
101
101
  "react-dom": "^19.0.0",
102
102
  "recharts": "^2.15.4",
103
103
  "sonner": "^1.7.0",
104
- "tailwind-merge": "^3.0.2"
104
+ "tailwind-merge": "^3.0.2",
105
+ "@xyflow/react": "^12.3.0"
105
106
  },
106
107
  "devDependencies": {
107
108
  "@tailwindcss/postcss": "^4.1.0",