@medicine-wheel/app 0.3.0 → 0.4.0
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.
- package/app/graph/page.tsx +61 -111
- package/package.json +20 -19
package/app/graph/page.tsx
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 [
|
|
26
|
-
const [
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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(() => {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
loadData();
|
|
59
|
+
}, [loadData]);
|
|
77
60
|
|
|
78
|
-
const
|
|
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">
|
|
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"
|
|
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
|
-
<
|
|
113
|
-
{
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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">{
|
|
195
|
-
<div><p className="text-2xl font-bold">{
|
|
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
|
+
"version": "0.4.0",
|
|
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.
|
|
78
|
-
"@medicine-wheel/community-review": "^0.
|
|
79
|
-
"@medicine-wheel/consent-lifecycle": "^0.
|
|
80
|
-
"@medicine-wheel/data-store": "^0.
|
|
81
|
-
"@medicine-wheel/data-store-postgres": "^0.
|
|
82
|
-
"@medicine-wheel/fire-keeper": "^0.
|
|
83
|
-
"@medicine-wheel/graph-viz": "^0.
|
|
84
|
-
"@medicine-wheel/importance-unit": "^0.
|
|
77
|
+
"@medicine-wheel/ceremony-protocol": "^0.4.0",
|
|
78
|
+
"@medicine-wheel/community-review": "^0.4.0",
|
|
79
|
+
"@medicine-wheel/consent-lifecycle": "^0.4.0",
|
|
80
|
+
"@medicine-wheel/data-store": "^0.4.0",
|
|
81
|
+
"@medicine-wheel/data-store-postgres": "^0.4.0",
|
|
82
|
+
"@medicine-wheel/fire-keeper": "^0.4.0",
|
|
83
|
+
"@medicine-wheel/graph-viz": "^0.4.0",
|
|
84
|
+
"@medicine-wheel/importance-unit": "^0.4.0",
|
|
85
85
|
"@medicine-wheel/mcp": "^4.3.0",
|
|
86
|
-
"@medicine-wheel/narrative-engine": "^0.
|
|
87
|
-
"@medicine-wheel/ontology-core": "^0.
|
|
88
|
-
"@medicine-wheel/prompt-decomposition": "^0.
|
|
89
|
-
"@medicine-wheel/relational-index": "^0.
|
|
90
|
-
"@medicine-wheel/relational-query": "^0.
|
|
91
|
-
"@medicine-wheel/session-reader": "^0.
|
|
92
|
-
"@medicine-wheel/storage-provider": "^0.
|
|
93
|
-
"@medicine-wheel/transformation-tracker": "^0.
|
|
94
|
-
"@medicine-wheel/ui-components": "^0.
|
|
86
|
+
"@medicine-wheel/narrative-engine": "^0.4.0",
|
|
87
|
+
"@medicine-wheel/ontology-core": "^0.4.0",
|
|
88
|
+
"@medicine-wheel/prompt-decomposition": "^0.4.0",
|
|
89
|
+
"@medicine-wheel/relational-index": "^0.4.0",
|
|
90
|
+
"@medicine-wheel/relational-query": "^0.4.0",
|
|
91
|
+
"@medicine-wheel/session-reader": "^0.4.0",
|
|
92
|
+
"@medicine-wheel/storage-provider": "^0.4.0",
|
|
93
|
+
"@medicine-wheel/transformation-tracker": "^0.4.0",
|
|
94
|
+
"@medicine-wheel/ui-components": "^0.4.0",
|
|
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",
|