@akiojin/gwt 4.1.1 → 4.3.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.
Files changed (135) hide show
  1. package/README.md +28 -3
  2. package/dist/claude.d.ts +4 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +13 -1
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +68 -68
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/common/Select.d.ts +3 -1
  10. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  11. package/dist/cli/ui/components/common/Select.js +13 -2
  12. package/dist/cli/ui/components/common/Select.js.map +1 -1
  13. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  14. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
  15. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  16. package/dist/client/assets/index-ChHC-Puh.css +1 -0
  17. package/dist/client/assets/index-PqK9jkug.js +78 -0
  18. package/dist/client/index.html +2 -2
  19. package/dist/config/builtin-tools.d.ts.map +1 -1
  20. package/dist/config/builtin-tools.js +3 -0
  21. package/dist/config/builtin-tools.js.map +1 -1
  22. package/dist/config/tools.d.ts.map +1 -1
  23. package/dist/config/tools.js +10 -1
  24. package/dist/config/tools.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +37 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/launcher.d.ts.map +1 -1
  29. package/dist/launcher.js +15 -0
  30. package/dist/launcher.js.map +1 -1
  31. package/dist/services/aiToolResolver.d.ts.map +1 -1
  32. package/dist/services/aiToolResolver.js +55 -8
  33. package/dist/services/aiToolResolver.js.map +1 -1
  34. package/dist/services/customToolResolver.d.ts.map +1 -1
  35. package/dist/services/customToolResolver.js +22 -17
  36. package/dist/services/customToolResolver.js.map +1 -1
  37. package/dist/utils/prompt.d.ts +12 -0
  38. package/dist/utils/prompt.d.ts.map +1 -1
  39. package/dist/utils/prompt.js +60 -10
  40. package/dist/utils/prompt.js.map +1 -1
  41. package/dist/utils/webui.js +1 -1
  42. package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
  43. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  44. package/dist/web/client/src/components/BranchGraph.js +35 -108
  45. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  46. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
  47. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
  48. package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
  49. package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
  50. package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
  51. package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
  52. package/dist/web/client/src/components/graph/BranchNode.js +103 -0
  53. package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
  54. package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
  55. package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
  56. package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
  57. package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
  58. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
  59. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
  60. package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
  61. package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
  62. package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
  63. package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
  64. package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
  65. package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
  66. package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
  67. package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
  68. package/dist/web/client/src/components/graph/graphUtils.js +175 -0
  69. package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
  70. package/dist/web/client/src/components/graph/index.d.ts +10 -0
  71. package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
  72. package/dist/web/client/src/components/graph/index.js +10 -0
  73. package/dist/web/client/src/components/graph/index.js.map +1 -0
  74. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  75. package/dist/web/client/src/lib/websocket.js +2 -1
  76. package/dist/web/client/src/lib/websocket.js.map +1 -1
  77. package/dist/web/client/vite.config.js +1 -1
  78. package/dist/web/server/env/importer.d.ts.map +1 -1
  79. package/dist/web/server/env/importer.js +4 -0
  80. package/dist/web/server/env/importer.js.map +1 -1
  81. package/dist/web/server/index.d.ts.map +1 -1
  82. package/dist/web/server/index.js +9 -0
  83. package/dist/web/server/index.js.map +1 -1
  84. package/dist/web/server/pty/manager.d.ts.map +1 -1
  85. package/dist/web/server/pty/manager.js +24 -1
  86. package/dist/web/server/pty/manager.js.map +1 -1
  87. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  88. package/dist/web/server/routes/sessions.js +7 -0
  89. package/dist/web/server/routes/sessions.js.map +1 -1
  90. package/dist/web/server/tray.d.ts +1 -1
  91. package/dist/web/server/tray.d.ts.map +1 -1
  92. package/dist/web/server/tray.js +52 -34
  93. package/dist/web/server/tray.js.map +1 -1
  94. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  95. package/dist/web/server/websocket/handler.js +4 -0
  96. package/dist/web/server/websocket/handler.js.map +1 -1
  97. package/package.json +6 -3
  98. package/src/claude.ts +15 -1
  99. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +2 -1
  100. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +142 -8
  101. package/src/cli/ui/__tests__/components/App.test.tsx +4 -3
  102. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +1 -0
  103. package/src/cli/ui/__tests__/components/common/Select.test.tsx +45 -0
  104. package/src/cli/ui/components/App.tsx +91 -81
  105. package/src/cli/ui/components/common/Select.tsx +14 -1
  106. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  107. package/src/cli/ui/types.ts +1 -1
  108. package/src/config/builtin-tools.ts +3 -0
  109. package/src/config/tools.ts +24 -1
  110. package/src/index.ts +50 -3
  111. package/src/launcher.ts +26 -0
  112. package/src/services/aiToolResolver.ts +75 -9
  113. package/src/services/customToolResolver.ts +32 -17
  114. package/src/utils/__tests__/prompt.test.ts +72 -35
  115. package/src/utils/prompt.ts +79 -10
  116. package/src/utils/webui.ts +1 -1
  117. package/src/web/client/src/components/BranchGraph.tsx +51 -208
  118. package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
  119. package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
  120. package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
  121. package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
  122. package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
  123. package/src/web/client/src/components/graph/graphUtils.ts +265 -0
  124. package/src/web/client/src/components/graph/index.ts +10 -0
  125. package/src/web/client/src/index.css +314 -29
  126. package/src/web/client/src/lib/websocket.ts +2 -1
  127. package/src/web/client/vite.config.ts +1 -1
  128. package/src/web/server/env/importer.ts +5 -0
  129. package/src/web/server/index.ts +10 -0
  130. package/src/web/server/pty/manager.ts +43 -1
  131. package/src/web/server/routes/sessions.ts +15 -0
  132. package/src/web/server/tray.ts +62 -46
  133. package/src/web/server/websocket/handler.ts +13 -0
  134. package/dist/client/assets/index-DsDNCy5f.css +0 -1
  135. package/dist/client/assets/index-v8smkNOL.js +0 -72
@@ -0,0 +1,211 @@
1
+ /**
2
+ * クラスタノードコンポーネント
3
+ *
4
+ * ニューロン群(神経細胞集合体)をイメージした有機的ノード
5
+ * 変形する膜と軌道を描く核で複数ブランチのグループを表現
6
+ */
7
+
8
+ import React, { memo, useMemo } from "react";
9
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
10
+ import { cn } from "@/lib/utils";
11
+ import type { GraphNode } from "./graphUtils";
12
+
13
+ type ClusterNodeProps = NodeProps<GraphNode>;
14
+
15
+ export const ClusterNode = memo(function ClusterNode({
16
+ data,
17
+ selected,
18
+ }: ClusterNodeProps) {
19
+ const { clusterSize = 0, expanded } = data;
20
+
21
+ // クラスタサイズに応じてノードサイズを調整
22
+ const baseSize = 70;
23
+ const size = Math.min(baseSize + clusterSize * 5, 120);
24
+
25
+ // 内部の核の数(最大8個)
26
+ const nucleiCount = Math.min(clusterSize, 8);
27
+
28
+ // 核の配置角度を計算
29
+ const nuclei = useMemo(() => {
30
+ return Array.from({ length: nucleiCount }).map((_, i) => {
31
+ const angle = (360 / nucleiCount) * i;
32
+ const delay = i * 0.5;
33
+ const orbitRadius = size * 0.28;
34
+ return { angle, delay, orbitRadius };
35
+ });
36
+ }, [nucleiCount, size]);
37
+
38
+ return (
39
+ <>
40
+ {/* 入力ハンドル */}
41
+ <Handle
42
+ type="target"
43
+ position={Position.Top}
44
+ className="!h-3 !w-3 !rounded-full !border-2 !bg-transparent transition-all duration-300"
45
+ style={{
46
+ borderColor: "hsl(185 100% 65%)",
47
+ boxShadow: "0 0 8px hsl(185 100% 65% / 0.4)",
48
+ }}
49
+ />
50
+
51
+ {/* クラスタ本体 */}
52
+ <div
53
+ className={cn(
54
+ "group relative flex cursor-pointer items-center justify-center",
55
+ "transition-all duration-500 ease-out",
56
+ "animate-membrane-morph",
57
+ selected &&
58
+ "ring-2 ring-primary ring-offset-4 ring-offset-background",
59
+ )}
60
+ style={{
61
+ width: size,
62
+ height: size,
63
+ background: expanded
64
+ ? "radial-gradient(circle at 40% 40%, hsl(185 100% 65% / 0.15), hsl(310 85% 60% / 0.08) 50%, transparent 70%)"
65
+ : "radial-gradient(circle at 40% 40%, hsl(220 30% 15% / 0.8), hsl(220 25% 10% / 0.6) 60%, transparent 80%)",
66
+ border: expanded
67
+ ? "2px solid hsl(185 100% 65% / 0.6)"
68
+ : "2px dashed hsl(200 30% 30%)",
69
+ boxShadow: expanded
70
+ ? "0 0 30px hsl(185 100% 65% / 0.3), inset 0 0 30px hsl(185 100% 65% / 0.1)"
71
+ : "0 0 20px hsl(220 30% 0% / 0.5), inset 0 0 20px hsl(220 30% 5% / 0.5)",
72
+ }}
73
+ >
74
+ {/* 外殻グロー */}
75
+ <div
76
+ className={cn(
77
+ "absolute -inset-3 rounded-full blur-lg transition-opacity duration-500",
78
+ expanded ? "opacity-40" : "opacity-20",
79
+ )}
80
+ style={{
81
+ backgroundColor: expanded
82
+ ? "hsl(185 100% 65%)"
83
+ : "hsl(200 30% 40%)",
84
+ borderRadius: "inherit",
85
+ }}
86
+ />
87
+
88
+ {/* 内部膜 */}
89
+ <div
90
+ className="absolute inset-3 rounded-full border border-dashed opacity-30"
91
+ style={{
92
+ borderColor: expanded ? "hsl(185 100% 65%)" : "hsl(200 30% 40%)",
93
+ borderRadius: "inherit",
94
+ }}
95
+ />
96
+
97
+ {/* 軌道を描く核 */}
98
+ <div className="absolute inset-0 flex items-center justify-center">
99
+ {nuclei.map((nucleus, i) => (
100
+ <div
101
+ key={i}
102
+ className="absolute animate-nucleus-orbit"
103
+ style={{
104
+ animationDelay: `${-nucleus.delay}s`,
105
+ animationDuration: `${8 + i * 0.5}s`,
106
+ }}
107
+ >
108
+ <div
109
+ className="h-2.5 w-2.5 rounded-full animate-vesicle-release"
110
+ style={{
111
+ backgroundColor: expanded
112
+ ? `hsl(${185 + i * 15} 80% 60%)`
113
+ : `hsl(${200 + i * 10} 50% 50%)`,
114
+ boxShadow: expanded
115
+ ? `0 0 8px hsl(${185 + i * 15} 80% 60% / 0.8)`
116
+ : `0 0 4px hsl(${200 + i * 10} 50% 50% / 0.5)`,
117
+ animationDelay: `${nucleus.delay * 0.3}s`,
118
+ transform: `translateX(${nucleus.orbitRadius}px)`,
119
+ }}
120
+ />
121
+ </div>
122
+ ))}
123
+ </div>
124
+
125
+ {/* 中心核 - クラスタサイズ表示 */}
126
+ <div
127
+ className={cn(
128
+ "relative z-10 flex items-center justify-center rounded-full",
129
+ "border-2 backdrop-blur-sm transition-all duration-300",
130
+ )}
131
+ style={{
132
+ width: size * 0.4,
133
+ height: size * 0.4,
134
+ backgroundColor: expanded
135
+ ? "hsl(220 30% 8% / 0.9)"
136
+ : "hsl(220 30% 10% / 0.95)",
137
+ borderColor: expanded
138
+ ? "hsl(185 100% 65% / 0.8)"
139
+ : "hsl(200 30% 35%)",
140
+ boxShadow: expanded
141
+ ? "0 0 20px hsl(185 100% 65% / 0.4), inset 0 0 15px hsl(185 100% 65% / 0.1)"
142
+ : "0 0 10px hsl(220 30% 0% / 0.5)",
143
+ }}
144
+ >
145
+ <span
146
+ className={cn(
147
+ "text-sm font-bold transition-colors duration-300",
148
+ expanded ? "text-primary" : "text-muted-foreground",
149
+ )}
150
+ >
151
+ {clusterSize}
152
+ </span>
153
+ </div>
154
+
155
+ {/* 展開/折りたたみインジケータ */}
156
+ <div
157
+ className={cn(
158
+ "absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full",
159
+ "border-2 text-xs font-bold transition-all duration-300",
160
+ )}
161
+ style={{
162
+ backgroundColor: expanded
163
+ ? "hsl(185 100% 65%)"
164
+ : "hsl(220 30% 12%)",
165
+ borderColor: expanded ? "hsl(185 100% 70%)" : "hsl(200 30% 25%)",
166
+ color: expanded ? "hsl(220 30% 5%)" : "hsl(185 100% 65%)",
167
+ boxShadow: expanded
168
+ ? "0 0 15px hsl(185 100% 65% / 0.6)"
169
+ : "0 0 8px hsl(220 30% 0% / 0.5)",
170
+ }}
171
+ >
172
+ {expanded ? "−" : "+"}
173
+ </div>
174
+
175
+ {/* ホバーツールチップ */}
176
+ <div
177
+ className={cn(
178
+ "absolute -bottom-14 left-1/2 z-50 -translate-x-1/2",
179
+ "rounded-lg border border-border/50 bg-card/95 px-3 py-2 backdrop-blur-md",
180
+ "opacity-0 shadow-xl transition-all duration-300",
181
+ "group-hover:opacity-100 group-hover:-translate-y-1",
182
+ )}
183
+ style={{
184
+ boxShadow:
185
+ "0 4px 20px hsl(220 30% 0% / 0.5), 0 0 20px hsl(185 100% 65% / 0.2)",
186
+ }}
187
+ >
188
+ <p className="whitespace-nowrap text-xs font-medium text-foreground">
189
+ {clusterSize} branches
190
+ </p>
191
+ <p className="text-[10px] text-muted-foreground">
192
+ Click to {expanded ? "collapse" : "expand"}
193
+ </p>
194
+ </div>
195
+ </div>
196
+
197
+ {/* 出力ハンドル */}
198
+ <Handle
199
+ type="source"
200
+ position={Position.Bottom}
201
+ className="!h-3 !w-3 !rounded-full !border-2 !bg-transparent transition-all duration-300"
202
+ style={{
203
+ borderColor: expanded ? "hsl(185 100% 65%)" : "hsl(200 30% 40%)",
204
+ boxShadow: expanded
205
+ ? "0 0 8px hsl(185 100% 65% / 0.4)"
206
+ : "0 0 6px hsl(200 30% 40% / 0.3)",
207
+ }}
208
+ />
209
+ </>
210
+ );
211
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * シナプティックキャンバス
3
+ *
4
+ * React Flowベースのブランチグラフ表示コンテナ
5
+ * パン/ズーム/ミニマップを提供
6
+ */
7
+
8
+ import React, { useCallback, useEffect, useState } from "react";
9
+ import {
10
+ ReactFlow,
11
+ Background,
12
+ Controls,
13
+ MiniMap,
14
+ useNodesState,
15
+ useEdgesState,
16
+ type NodeMouseHandler,
17
+ BackgroundVariant,
18
+ } from "@xyflow/react";
19
+ import "@xyflow/react/dist/style.css";
20
+
21
+ import type { Branch } from "../../../../../types/api.js";
22
+ import { BranchNode } from "./BranchNode";
23
+ import { ClusterNode } from "./ClusterNode";
24
+ import { SynapticEdge } from "./SynapticEdge";
25
+ import { branchesToGraph, type GraphNode, type GraphEdge } from "./graphUtils";
26
+
27
+ /** カスタムノードタイプ */
28
+ const nodeTypes = {
29
+ branch: BranchNode,
30
+ cluster: ClusterNode,
31
+ };
32
+
33
+ /** カスタムエッジタイプ */
34
+ const edgeTypes = {
35
+ synaptic: SynapticEdge,
36
+ };
37
+
38
+ interface SynapticCanvasProps {
39
+ branches: Branch[];
40
+ onNodeClick?: (branch: Branch | null) => void;
41
+ className?: string;
42
+ }
43
+
44
+ export function SynapticCanvas({
45
+ branches,
46
+ onNodeClick,
47
+ className,
48
+ }: SynapticCanvasProps) {
49
+ const [nodes, setNodes, onNodesChange] = useNodesState<GraphNode>([]);
50
+ const [edges, setEdges, onEdgesChange] = useEdgesState<GraphEdge>([]);
51
+ const [expandedClusters, setExpandedClusters] = useState<Set<string>>(
52
+ new Set(),
53
+ );
54
+ const [isLayouting, setIsLayouting] = useState(false);
55
+
56
+ // レイアウト計算
57
+ useEffect(() => {
58
+ if (branches.length === 0) return;
59
+
60
+ setIsLayouting(true);
61
+
62
+ // キャンバスサイズ(仮)
63
+ const width = 800;
64
+ const height = 600;
65
+
66
+ branchesToGraph(branches, expandedClusters, width, height)
67
+ .then(({ nodes: newNodes, edges: newEdges }) => {
68
+ setNodes(newNodes);
69
+ setEdges(newEdges);
70
+ })
71
+ .catch((error) => {
72
+ console.error("Failed to calculate graph layout:", error);
73
+ })
74
+ .finally(() => {
75
+ setIsLayouting(false);
76
+ });
77
+ }, [branches, expandedClusters, setNodes, setEdges]);
78
+
79
+ // ノードクリックハンドラ
80
+ const handleNodeClick: NodeMouseHandler<GraphNode> = useCallback(
81
+ (event, node) => {
82
+ if (node.data.isCluster) {
83
+ // クラスタノード: 展開/折りたたみ
84
+ setExpandedClusters((prev) => {
85
+ const next = new Set(prev);
86
+ if (next.has(node.id)) {
87
+ next.delete(node.id);
88
+ } else {
89
+ next.add(node.id);
90
+ }
91
+ return next;
92
+ });
93
+ } else {
94
+ // ブランチノード: 詳細パネル表示
95
+ onNodeClick?.(node.data.branch ?? null);
96
+ }
97
+ },
98
+ [onNodeClick],
99
+ );
100
+
101
+ // ミニマップのノードカラー
102
+ const minimapNodeColor = useCallback((node: GraphNode) => {
103
+ if (node.data.isCluster) {
104
+ return "hsl(var(--muted-foreground))";
105
+ }
106
+ if (node.data.branch?.worktreePath) {
107
+ return "hsl(var(--success))";
108
+ }
109
+ if (node.data.branch?.type === "local") {
110
+ return "hsl(var(--local))";
111
+ }
112
+ return "hsl(var(--remote))";
113
+ }, []);
114
+
115
+ return (
116
+ <div className={className} style={{ width: "100%", height: "100%" }}>
117
+ <ReactFlow
118
+ nodes={nodes}
119
+ edges={edges}
120
+ onNodesChange={onNodesChange}
121
+ onEdgesChange={onEdgesChange}
122
+ onNodeClick={handleNodeClick}
123
+ nodeTypes={nodeTypes}
124
+ edgeTypes={edgeTypes}
125
+ fitView
126
+ fitViewOptions={{
127
+ padding: 0.2,
128
+ maxZoom: 1.5,
129
+ }}
130
+ minZoom={0.1}
131
+ maxZoom={2}
132
+ attributionPosition="bottom-left"
133
+ proOptions={{ hideAttribution: true }}
134
+ className="bg-gradient-to-br from-background via-background to-muted/20"
135
+ >
136
+ {/* 背景グリッド */}
137
+ <Background
138
+ variant={BackgroundVariant.Dots}
139
+ gap={20}
140
+ size={1}
141
+ color="hsl(var(--muted-foreground) / 0.15)"
142
+ />
143
+
144
+ {/* コントロール */}
145
+ <Controls
146
+ showInteractive={false}
147
+ className="!bg-card !border !border-border !shadow-md"
148
+ />
149
+
150
+ {/* ミニマップ */}
151
+ <MiniMap
152
+ nodeColor={minimapNodeColor}
153
+ maskColor="hsl(var(--background) / 0.8)"
154
+ className="!bg-card !border !border-border !shadow-md"
155
+ />
156
+ </ReactFlow>
157
+
158
+ {/* ローディングオーバーレイ */}
159
+ {isLayouting && (
160
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm">
161
+ <div className="flex items-center gap-2 rounded-lg bg-card px-4 py-2 shadow-lg">
162
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
163
+ <span className="text-sm text-muted-foreground">
164
+ Calculating layout...
165
+ </span>
166
+ </div>
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * シナプスエッジコンポーネント
3
+ *
4
+ * 神経細胞の軸索(アクソン)をイメージした脈動するエッジ
5
+ * 神経伝達物質の放出と伝播を表現するアニメーション
6
+ */
7
+
8
+ import React, { memo, useMemo } from "react";
9
+ import {
10
+ BaseEdge,
11
+ EdgeLabelRenderer,
12
+ getBezierPath,
13
+ type EdgeProps,
14
+ } from "@xyflow/react";
15
+ import type { GraphEdge } from "./graphUtils";
16
+
17
+ type SynapticEdgeProps = EdgeProps<GraphEdge>;
18
+
19
+ export const SynapticEdge = memo(function SynapticEdge({
20
+ id,
21
+ sourceX,
22
+ sourceY,
23
+ targetX,
24
+ targetY,
25
+ sourcePosition,
26
+ targetPosition,
27
+ style = {},
28
+ markerEnd,
29
+ }: SynapticEdgeProps) {
30
+ const [edgePath, labelX, labelY] = getBezierPath({
31
+ sourceX,
32
+ sourceY,
33
+ sourcePosition,
34
+ targetX,
35
+ targetY,
36
+ targetPosition,
37
+ curvature: 0.25,
38
+ });
39
+
40
+ // エッジの長さに基づいてパーティクル数を調整
41
+ const edgeLength = useMemo(() => {
42
+ const dx = targetX - sourceX;
43
+ const dy = targetY - sourceY;
44
+ return Math.sqrt(dx * dx + dy * dy);
45
+ }, [sourceX, sourceY, targetX, targetY]);
46
+
47
+ const particleCount = Math.max(2, Math.min(4, Math.floor(edgeLength / 100)));
48
+
49
+ return (
50
+ <>
51
+ {/* 外側グロー - 深い影 */}
52
+ <BaseEdge
53
+ id={`${id}-outer-glow`}
54
+ path={edgePath}
55
+ style={{
56
+ ...style,
57
+ strokeWidth: 12,
58
+ stroke: "hsl(185 100% 65% / 0.08)",
59
+ filter: "blur(8px)",
60
+ }}
61
+ />
62
+
63
+ {/* 中間グロー */}
64
+ <BaseEdge
65
+ id={`${id}-mid-glow`}
66
+ path={edgePath}
67
+ style={{
68
+ ...style,
69
+ strokeWidth: 6,
70
+ stroke: "hsl(185 100% 65% / 0.15)",
71
+ filter: "blur(4px)",
72
+ }}
73
+ />
74
+
75
+ {/* メインエッジ - 軸索 */}
76
+ <BaseEdge
77
+ id={id}
78
+ path={edgePath}
79
+ {...(markerEnd ? { markerEnd } : {})}
80
+ style={{
81
+ ...style,
82
+ strokeWidth: 2,
83
+ stroke: "hsl(185 100% 65% / 0.5)",
84
+ strokeLinecap: "round",
85
+ }}
86
+ />
87
+
88
+ {/* 内側の明るいコア */}
89
+ <BaseEdge
90
+ id={`${id}-core`}
91
+ path={edgePath}
92
+ style={{
93
+ ...style,
94
+ strokeWidth: 1,
95
+ stroke: "hsl(185 100% 75% / 0.6)",
96
+ strokeLinecap: "round",
97
+ }}
98
+ />
99
+
100
+ {/* SVGアニメーション定義 */}
101
+ <EdgeLabelRenderer>
102
+ <svg
103
+ style={{
104
+ position: "absolute",
105
+ left: 0,
106
+ top: 0,
107
+ width: "100%",
108
+ height: "100%",
109
+ pointerEvents: "none",
110
+ overflow: "visible",
111
+ }}
112
+ >
113
+ <defs>
114
+ {/* 神経伝達物質パルスグラデーション */}
115
+ <linearGradient
116
+ id={`vesicle-pulse-${id}`}
117
+ x1="0%"
118
+ y1="0%"
119
+ x2="100%"
120
+ y2="0%"
121
+ >
122
+ <stop offset="0%" stopColor="transparent">
123
+ <animate
124
+ attributeName="offset"
125
+ values="-0.3;1"
126
+ dur="2.5s"
127
+ repeatCount="indefinite"
128
+ />
129
+ </stop>
130
+ <stop offset="5%" stopColor="hsl(185 100% 70% / 0.3)">
131
+ <animate
132
+ attributeName="offset"
133
+ values="-0.25;1.05"
134
+ dur="2.5s"
135
+ repeatCount="indefinite"
136
+ />
137
+ </stop>
138
+ <stop offset="10%" stopColor="hsl(185 100% 80%)">
139
+ <animate
140
+ attributeName="offset"
141
+ values="-0.2;1.1"
142
+ dur="2.5s"
143
+ repeatCount="indefinite"
144
+ />
145
+ </stop>
146
+ <stop offset="15%" stopColor="hsl(185 100% 70% / 0.3)">
147
+ <animate
148
+ attributeName="offset"
149
+ values="-0.15;1.15"
150
+ dur="2.5s"
151
+ repeatCount="indefinite"
152
+ />
153
+ </stop>
154
+ <stop offset="20%" stopColor="transparent">
155
+ <animate
156
+ attributeName="offset"
157
+ values="-0.1;1.2"
158
+ dur="2.5s"
159
+ repeatCount="indefinite"
160
+ />
161
+ </stop>
162
+ </linearGradient>
163
+
164
+ {/* 二次パルス(シナプス間隙) */}
165
+ <linearGradient
166
+ id={`synapse-pulse-${id}`}
167
+ x1="0%"
168
+ y1="0%"
169
+ x2="100%"
170
+ y2="0%"
171
+ >
172
+ <stop offset="0%" stopColor="transparent">
173
+ <animate
174
+ attributeName="offset"
175
+ values="-0.4;1"
176
+ dur="3s"
177
+ begin="0.8s"
178
+ repeatCount="indefinite"
179
+ />
180
+ </stop>
181
+ <stop offset="8%" stopColor="hsl(310 85% 65% / 0.6)">
182
+ <animate
183
+ attributeName="offset"
184
+ values="-0.32;1.08"
185
+ dur="3s"
186
+ begin="0.8s"
187
+ repeatCount="indefinite"
188
+ />
189
+ </stop>
190
+ <stop offset="16%" stopColor="transparent">
191
+ <animate
192
+ attributeName="offset"
193
+ values="-0.24;1.16"
194
+ dur="3s"
195
+ begin="0.8s"
196
+ repeatCount="indefinite"
197
+ />
198
+ </stop>
199
+ </linearGradient>
200
+
201
+ {/* グロー効果フィルター */}
202
+ <filter
203
+ id={`glow-${id}`}
204
+ x="-50%"
205
+ y="-50%"
206
+ width="200%"
207
+ height="200%"
208
+ >
209
+ <feGaussianBlur stdDeviation="2" result="coloredBlur" />
210
+ <feMerge>
211
+ <feMergeNode in="coloredBlur" />
212
+ <feMergeNode in="SourceGraphic" />
213
+ </feMerge>
214
+ </filter>
215
+ </defs>
216
+
217
+ {/* メインパルス */}
218
+ <path
219
+ d={edgePath}
220
+ fill="none"
221
+ stroke={`url(#vesicle-pulse-${id})`}
222
+ strokeWidth={4}
223
+ strokeLinecap="round"
224
+ filter={`url(#glow-${id})`}
225
+ style={{ mixBlendMode: "screen" }}
226
+ />
227
+
228
+ {/* セカンダリパルス(マゼンタ) */}
229
+ <path
230
+ d={edgePath}
231
+ fill="none"
232
+ stroke={`url(#synapse-pulse-${id})`}
233
+ strokeWidth={3}
234
+ strokeLinecap="round"
235
+ style={{ mixBlendMode: "screen" }}
236
+ />
237
+ </svg>
238
+ </EdgeLabelRenderer>
239
+
240
+ {/* 流れるパーティクル(神経伝達物質) - SVG animateMotion使用 */}
241
+ <EdgeLabelRenderer>
242
+ <svg
243
+ style={{
244
+ position: "absolute",
245
+ left: 0,
246
+ top: 0,
247
+ width: "100%",
248
+ height: "100%",
249
+ pointerEvents: "none",
250
+ overflow: "visible",
251
+ }}
252
+ >
253
+ {Array.from({ length: particleCount }).map((_, i) => (
254
+ <circle
255
+ key={i}
256
+ r={3 - i * 0.3}
257
+ fill="hsl(185 100% 80%)"
258
+ opacity={0}
259
+ >
260
+ <animateMotion
261
+ dur={`${2 + i * 0.3}s`}
262
+ repeatCount="indefinite"
263
+ path={edgePath}
264
+ begin={`${i * 0.6}s`}
265
+ />
266
+ <animate
267
+ attributeName="opacity"
268
+ values="0;1;1;0"
269
+ keyTimes="0;0.1;0.9;1"
270
+ dur={`${2 + i * 0.3}s`}
271
+ repeatCount="indefinite"
272
+ begin={`${i * 0.6}s`}
273
+ />
274
+ </circle>
275
+ ))}
276
+ </svg>
277
+ </EdgeLabelRenderer>
278
+
279
+ {/* シナプス接合点グロー */}
280
+ <EdgeLabelRenderer>
281
+ <div
282
+ className="absolute"
283
+ style={{
284
+ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
285
+ }}
286
+ >
287
+ {/* 外側の拡散グロー */}
288
+ <div
289
+ className="absolute -inset-3 rounded-full opacity-30"
290
+ style={{
291
+ background:
292
+ "radial-gradient(circle, hsl(185 100% 65%) 0%, transparent 70%)",
293
+ animation: "synapse-spark 3s ease-in-out infinite",
294
+ }}
295
+ />
296
+ {/* 中心核 */}
297
+ <div
298
+ className="h-2 w-2 rounded-full"
299
+ style={{
300
+ background:
301
+ "radial-gradient(circle at 30% 30%, hsl(185 100% 80%), hsl(185 100% 60%))",
302
+ boxShadow:
303
+ "0 0 10px hsl(185 100% 65% / 0.8), 0 0 20px hsl(185 100% 65% / 0.4)",
304
+ animation: "vesicle-release 2s ease-in-out infinite",
305
+ }}
306
+ />
307
+ </div>
308
+ </EdgeLabelRenderer>
309
+ </>
310
+ );
311
+ });