@medicine-wheel/app 0.2.8 → 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/api/charts/route.ts +74 -0
- package/app/api/mcp/route.ts +138 -0
- package/app/api/mmots/route.ts +64 -0
- package/app/graph/page.tsx +61 -111
- package/package.json +29 -24
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
function getChartsFile(): string {
|
|
6
|
+
const dataDir = process.env.MW_DATA_DIR ?? path.join(process.cwd(), ".mw", "store");
|
|
7
|
+
return path.join(dataDir, "charts.jsonl");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readJsonl<T>(filePath: string): T[] {
|
|
11
|
+
if (!fs.existsSync(filePath)) return [];
|
|
12
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
13
|
+
return content
|
|
14
|
+
.split("\n")
|
|
15
|
+
.filter((line) => line.trim())
|
|
16
|
+
.map((line) => JSON.parse(line) as T);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function appendJsonl<T>(filePath: string, item: T): void {
|
|
20
|
+
const dir = path.dirname(filePath);
|
|
21
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function GET(request: Request) {
|
|
26
|
+
try {
|
|
27
|
+
const { searchParams } = new URL(request.url);
|
|
28
|
+
const direction = searchParams.get("direction");
|
|
29
|
+
|
|
30
|
+
let charts = readJsonl<any>(getChartsFile());
|
|
31
|
+
|
|
32
|
+
if (direction) {
|
|
33
|
+
charts = charts.filter((c) => c.direction === direction);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
charts.sort(
|
|
37
|
+
(a: any, b: any) =>
|
|
38
|
+
Date.parse(b.updated_at ?? b.created_at) -
|
|
39
|
+
Date.parse(a.updated_at ?? a.created_at)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(charts);
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function POST(request: Request) {
|
|
50
|
+
try {
|
|
51
|
+
const body = await request.json();
|
|
52
|
+
|
|
53
|
+
const chart = {
|
|
54
|
+
id: body.id || crypto.randomUUID(),
|
|
55
|
+
desired_outcome: body.desired_outcome,
|
|
56
|
+
current_reality: body.current_reality,
|
|
57
|
+
direction: body.direction || "east",
|
|
58
|
+
action_steps: body.action_steps ?? [],
|
|
59
|
+
due_date: body.due_date,
|
|
60
|
+
created_at: body.created_at || new Date().toISOString(),
|
|
61
|
+
updated_at: new Date().toISOString(),
|
|
62
|
+
phase: body.phase || "current_reality",
|
|
63
|
+
ceremonies_linked: body.ceremonies_linked ?? [],
|
|
64
|
+
wilson_alignment: body.wilson_alignment,
|
|
65
|
+
cycle_id: body.cycle_id,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
appendJsonl(getChartsFile(), chart);
|
|
69
|
+
return NextResponse.json({ success: true, chart }, { status: 201 });
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamableHTTP MCP endpoint
|
|
3
|
+
*
|
|
4
|
+
* Enables MCP clients to connect to the Medicine Wheel server
|
|
5
|
+
* via HTTP POST instead of stdio. Accepts JSON-RPC MCP messages
|
|
6
|
+
* and dispatches them to the same tool/resource/prompt registry
|
|
7
|
+
* that the stdio transport uses.
|
|
8
|
+
*
|
|
9
|
+
* Supported methods:
|
|
10
|
+
* - tools/list → lists all registered MCP tools
|
|
11
|
+
* - tools/call → invokes a tool by name with arguments
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/jgwill/medicine-wheel/issues/69
|
|
14
|
+
*/
|
|
15
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
16
|
+
import { allTools } from "@medicine-wheel/mcp/all-tools";
|
|
17
|
+
|
|
18
|
+
export async function POST(request: NextRequest) {
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json();
|
|
21
|
+
|
|
22
|
+
if (!body.jsonrpc || body.jsonrpc !== "2.0") {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{
|
|
25
|
+
jsonrpc: "2.0",
|
|
26
|
+
error: {
|
|
27
|
+
code: -32600,
|
|
28
|
+
message: "Invalid Request: expected JSON-RPC 2.0",
|
|
29
|
+
},
|
|
30
|
+
id: body.id ?? null,
|
|
31
|
+
},
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { method, params, id } = body;
|
|
37
|
+
|
|
38
|
+
// ── tools/list ──
|
|
39
|
+
if (method === "tools/list") {
|
|
40
|
+
return NextResponse.json({
|
|
41
|
+
jsonrpc: "2.0",
|
|
42
|
+
result: {
|
|
43
|
+
tools: allTools.map((t) => ({
|
|
44
|
+
name: t.name,
|
|
45
|
+
description: t.description,
|
|
46
|
+
inputSchema: t.inputSchema,
|
|
47
|
+
})),
|
|
48
|
+
},
|
|
49
|
+
id: id ?? null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── tools/call ──
|
|
54
|
+
if (method === "tools/call") {
|
|
55
|
+
const toolName = params?.name;
|
|
56
|
+
const tool = allTools.find((t) => t.name === toolName);
|
|
57
|
+
if (!tool) {
|
|
58
|
+
return NextResponse.json({
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
error: {
|
|
61
|
+
code: -32602,
|
|
62
|
+
message: `Unknown tool: ${toolName}`,
|
|
63
|
+
},
|
|
64
|
+
id: id ?? null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await tool.handler(params?.arguments || {});
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
result: {
|
|
73
|
+
content: [
|
|
74
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
id: id ?? null,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const errorMessage =
|
|
81
|
+
error instanceof Error ? error.message : String(error);
|
|
82
|
+
return NextResponse.json({
|
|
83
|
+
jsonrpc: "2.0",
|
|
84
|
+
result: {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
error: errorMessage,
|
|
91
|
+
direction:
|
|
92
|
+
"Please ensure all required parameters are provided",
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
isError: true,
|
|
100
|
+
},
|
|
101
|
+
id: id ?? null,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Unsupported method ──
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{
|
|
109
|
+
jsonrpc: "2.0",
|
|
110
|
+
error: {
|
|
111
|
+
code: -32601,
|
|
112
|
+
message: `Method '${method}' is not supported. Available: tools/list, tools/call`,
|
|
113
|
+
},
|
|
114
|
+
id: id ?? null,
|
|
115
|
+
},
|
|
116
|
+
{ status: 200 }
|
|
117
|
+
);
|
|
118
|
+
} catch {
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{
|
|
121
|
+
jsonrpc: "2.0",
|
|
122
|
+
error: { code: -32700, message: "Parse error" },
|
|
123
|
+
id: null,
|
|
124
|
+
},
|
|
125
|
+
{ status: 400 }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function GET() {
|
|
131
|
+
return NextResponse.json({
|
|
132
|
+
status: "available",
|
|
133
|
+
transport: "StreamableHTTP",
|
|
134
|
+
description:
|
|
135
|
+
"Medicine Wheel MCP StreamableHTTP endpoint. Send JSON-RPC 2.0 POST requests to interact.",
|
|
136
|
+
capabilities: ["tools", "resources", "prompts"],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
function getMmotsFile(): string {
|
|
6
|
+
const dataDir = process.env.MW_DATA_DIR ?? path.join(process.cwd(), ".mw", "store");
|
|
7
|
+
return path.join(dataDir, "mmots.jsonl");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readJsonl<T>(filePath: string): T[] {
|
|
11
|
+
if (!fs.existsSync(filePath)) return [];
|
|
12
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
13
|
+
return content
|
|
14
|
+
.split("\n")
|
|
15
|
+
.filter((line) => line.trim())
|
|
16
|
+
.map((line) => JSON.parse(line) as T);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function appendJsonl<T>(filePath: string, item: T): void {
|
|
20
|
+
const dir = path.dirname(filePath);
|
|
21
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function GET(request: Request) {
|
|
26
|
+
try {
|
|
27
|
+
const { searchParams } = new URL(request.url);
|
|
28
|
+
const chartId = searchParams.get("chart_id");
|
|
29
|
+
|
|
30
|
+
let mmots = readJsonl<any>(getMmotsFile());
|
|
31
|
+
|
|
32
|
+
if (chartId) {
|
|
33
|
+
mmots = mmots.filter((m) => m.chart_id === chartId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return NextResponse.json(mmots);
|
|
37
|
+
} catch (error: unknown) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function POST(request: Request) {
|
|
44
|
+
try {
|
|
45
|
+
const body = await request.json();
|
|
46
|
+
|
|
47
|
+
const mmot = {
|
|
48
|
+
id: body.id || crypto.randomUUID(),
|
|
49
|
+
chart_id: body.chart_id,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
step1_expected: body.step1_expected,
|
|
52
|
+
step1_actual: body.step1_actual,
|
|
53
|
+
step2_analysis: body.step2_analysis,
|
|
54
|
+
step3_adjustments: body.step3_adjustments ?? [],
|
|
55
|
+
step4_feedback: body.step4_feedback,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
appendJsonl(getMmotsFile(), mmot);
|
|
59
|
+
return NextResponse.json({ success: true, mmot }, { status: 201 });
|
|
60
|
+
} catch (error: unknown) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
63
|
+
}
|
|
64
|
+
}
|
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",
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"build": "next build",
|
|
53
53
|
"start": "next start -p 3940",
|
|
54
54
|
"lint": "next lint",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
55
57
|
"version:patch": "node scripts/bump-versions.mjs patch && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
|
|
56
58
|
"version:minor": "node scripts/bump-versions.mjs minor && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
|
|
57
59
|
"version:major": "node scripts/bump-versions.mjs major && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
|
|
@@ -72,33 +74,35 @@
|
|
|
72
74
|
"release:major": "npm run version:major && npm run publish:all && npm run release:commit"
|
|
73
75
|
},
|
|
74
76
|
"dependencies": {
|
|
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
|
+
"@medicine-wheel/mcp": "^4.3.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
|
+
"@neondatabase/serverless": "^0.10.0",
|
|
96
|
+
"clsx": "^2.1.1",
|
|
97
|
+
"lucide-react": "^0.475.0",
|
|
75
98
|
"next": "^15.3.0",
|
|
99
|
+
"next-themes": "^0.4.6",
|
|
76
100
|
"react": "^19.0.0",
|
|
77
101
|
"react-dom": "^19.0.0",
|
|
78
|
-
"
|
|
102
|
+
"recharts": "^2.15.4",
|
|
79
103
|
"sonner": "^1.7.0",
|
|
80
|
-
"lucide-react": "^0.475.0",
|
|
81
|
-
"clsx": "^2.1.1",
|
|
82
104
|
"tailwind-merge": "^3.0.2",
|
|
83
|
-
"
|
|
84
|
-
"@medicine-wheel/ontology-core": "^0.2.8",
|
|
85
|
-
"@medicine-wheel/ceremony-protocol": "^0.2.8",
|
|
86
|
-
"@medicine-wheel/narrative-engine": "^0.2.8",
|
|
87
|
-
"@medicine-wheel/graph-viz": "^0.2.8",
|
|
88
|
-
"@medicine-wheel/relational-query": "^0.2.8",
|
|
89
|
-
"@medicine-wheel/prompt-decomposition": "^0.2.8",
|
|
90
|
-
"@medicine-wheel/ui-components": "^0.2.8",
|
|
91
|
-
"@medicine-wheel/data-store": "^0.2.8",
|
|
92
|
-
"@medicine-wheel/session-reader": "^0.2.8",
|
|
93
|
-
"@medicine-wheel/fire-keeper": "^0.2.8",
|
|
94
|
-
"@medicine-wheel/importance-unit": "^0.2.8",
|
|
95
|
-
"@medicine-wheel/relational-index": "^0.2.8",
|
|
96
|
-
"@medicine-wheel/transformation-tracker": "^0.2.8",
|
|
97
|
-
"@medicine-wheel/storage-provider": "^0.2.8",
|
|
98
|
-
"@medicine-wheel/community-review": "^0.2.8",
|
|
99
|
-
"@medicine-wheel/consent-lifecycle": "^0.2.8",
|
|
100
|
-
"@medicine-wheel/data-store-postgres": "^0.2.8",
|
|
101
|
-
"@neondatabase/serverless": "^0.10.0"
|
|
105
|
+
"@xyflow/react": "^12.3.0"
|
|
102
106
|
},
|
|
103
107
|
"devDependencies": {
|
|
104
108
|
"@tailwindcss/postcss": "^4.1.0",
|
|
@@ -107,6 +111,7 @@
|
|
|
107
111
|
"@types/react-dom": "^19.0.0",
|
|
108
112
|
"shx": "^0.4.0",
|
|
109
113
|
"tailwindcss": "^4.1.0",
|
|
110
|
-
"typescript": "^5.7.0"
|
|
114
|
+
"typescript": "^5.7.0",
|
|
115
|
+
"vitest": "^4.1.8"
|
|
111
116
|
}
|
|
112
117
|
}
|