stellavault 0.1.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 (294) hide show
  1. package/.env.example +12 -0
  2. package/CLAUDE.md +39 -0
  3. package/CONTRIBUTING.md +65 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/memory/MEMORY.md +25 -0
  7. package/package.json +33 -0
  8. package/packages/cli/bin/ekh.js +2 -0
  9. package/packages/cli/bin/stellavault.js +2 -0
  10. package/packages/cli/dist/commands/brief-cmd.d.ts +2 -0
  11. package/packages/cli/dist/commands/brief-cmd.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/brief-cmd.js +82 -0
  13. package/packages/cli/dist/commands/brief-cmd.js.map +1 -0
  14. package/packages/cli/dist/commands/capture-cmd.d.ts +7 -0
  15. package/packages/cli/dist/commands/capture-cmd.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/capture-cmd.js +31 -0
  17. package/packages/cli/dist/commands/capture-cmd.js.map +1 -0
  18. package/packages/cli/dist/commands/card-cmd.d.ts +4 -0
  19. package/packages/cli/dist/commands/card-cmd.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/card-cmd.js +26 -0
  21. package/packages/cli/dist/commands/card-cmd.js.map +1 -0
  22. package/packages/cli/dist/commands/clip-cmd.d.ts +4 -0
  23. package/packages/cli/dist/commands/clip-cmd.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/clip-cmd.js +151 -0
  25. package/packages/cli/dist/commands/clip-cmd.js.map +1 -0
  26. package/packages/cli/dist/commands/cloud-cmd.d.ts +4 -0
  27. package/packages/cli/dist/commands/cloud-cmd.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/cloud-cmd.js +64 -0
  29. package/packages/cli/dist/commands/cloud-cmd.js.map +1 -0
  30. package/packages/cli/dist/commands/contradictions-cmd.d.ts +2 -0
  31. package/packages/cli/dist/commands/contradictions-cmd.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/contradictions-cmd.js +34 -0
  33. package/packages/cli/dist/commands/contradictions-cmd.js.map +1 -0
  34. package/packages/cli/dist/commands/decay-cmd.d.ts +2 -0
  35. package/packages/cli/dist/commands/decay-cmd.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/decay-cmd.js +48 -0
  37. package/packages/cli/dist/commands/decay-cmd.js.map +1 -0
  38. package/packages/cli/dist/commands/digest-cmd.d.ts +4 -0
  39. package/packages/cli/dist/commands/digest-cmd.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/digest-cmd.js +79 -0
  41. package/packages/cli/dist/commands/digest-cmd.js.map +1 -0
  42. package/packages/cli/dist/commands/duplicates-cmd.d.ts +4 -0
  43. package/packages/cli/dist/commands/duplicates-cmd.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/duplicates-cmd.js +30 -0
  45. package/packages/cli/dist/commands/duplicates-cmd.js.map +1 -0
  46. package/packages/cli/dist/commands/federate-cmd.d.ts +5 -0
  47. package/packages/cli/dist/commands/federate-cmd.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/federate-cmd.js +217 -0
  49. package/packages/cli/dist/commands/federate-cmd.js.map +1 -0
  50. package/packages/cli/dist/commands/gaps-cmd.d.ts +2 -0
  51. package/packages/cli/dist/commands/gaps-cmd.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/gaps-cmd.js +33 -0
  53. package/packages/cli/dist/commands/gaps-cmd.js.map +1 -0
  54. package/packages/cli/dist/commands/graph-cmd.d.ts +2 -0
  55. package/packages/cli/dist/commands/graph-cmd.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/graph-cmd.js +77 -0
  57. package/packages/cli/dist/commands/graph-cmd.js.map +1 -0
  58. package/packages/cli/dist/commands/index-cmd.d.ts +2 -0
  59. package/packages/cli/dist/commands/index-cmd.d.ts.map +1 -0
  60. package/packages/cli/dist/commands/index-cmd.js +57 -0
  61. package/packages/cli/dist/commands/index-cmd.js.map +1 -0
  62. package/packages/cli/dist/commands/init-cmd.d.ts +2 -0
  63. package/packages/cli/dist/commands/init-cmd.d.ts.map +1 -0
  64. package/packages/cli/dist/commands/init-cmd.js +123 -0
  65. package/packages/cli/dist/commands/init-cmd.js.map +1 -0
  66. package/packages/cli/dist/commands/learn-cmd.d.ts +2 -0
  67. package/packages/cli/dist/commands/learn-cmd.d.ts.map +1 -0
  68. package/packages/cli/dist/commands/learn-cmd.js +48 -0
  69. package/packages/cli/dist/commands/learn-cmd.js.map +1 -0
  70. package/packages/cli/dist/commands/pack-cmd.d.ts +15 -0
  71. package/packages/cli/dist/commands/pack-cmd.d.ts.map +1 -0
  72. package/packages/cli/dist/commands/pack-cmd.js +93 -0
  73. package/packages/cli/dist/commands/pack-cmd.js.map +1 -0
  74. package/packages/cli/dist/commands/review-cmd.d.ts +4 -0
  75. package/packages/cli/dist/commands/review-cmd.d.ts.map +1 -0
  76. package/packages/cli/dist/commands/review-cmd.js +107 -0
  77. package/packages/cli/dist/commands/review-cmd.js.map +1 -0
  78. package/packages/cli/dist/commands/search-cmd.d.ts +4 -0
  79. package/packages/cli/dist/commands/search-cmd.d.ts.map +1 -0
  80. package/packages/cli/dist/commands/search-cmd.js +38 -0
  81. package/packages/cli/dist/commands/search-cmd.js.map +1 -0
  82. package/packages/cli/dist/commands/serve-cmd.d.ts +2 -0
  83. package/packages/cli/dist/commands/serve-cmd.d.ts.map +1 -0
  84. package/packages/cli/dist/commands/serve-cmd.js +14 -0
  85. package/packages/cli/dist/commands/serve-cmd.js.map +1 -0
  86. package/packages/cli/dist/commands/status-cmd.d.ts +2 -0
  87. package/packages/cli/dist/commands/status-cmd.d.ts.map +1 -0
  88. package/packages/cli/dist/commands/status-cmd.js +33 -0
  89. package/packages/cli/dist/commands/status-cmd.js.map +1 -0
  90. package/packages/cli/dist/commands/sync-cmd.d.ts +5 -0
  91. package/packages/cli/dist/commands/sync-cmd.d.ts.map +1 -0
  92. package/packages/cli/dist/commands/sync-cmd.js +62 -0
  93. package/packages/cli/dist/commands/sync-cmd.js.map +1 -0
  94. package/packages/cli/dist/commands/vault-cmd.d.ts +10 -0
  95. package/packages/cli/dist/commands/vault-cmd.d.ts.map +1 -0
  96. package/packages/cli/dist/commands/vault-cmd.js +54 -0
  97. package/packages/cli/dist/commands/vault-cmd.js.map +1 -0
  98. package/packages/cli/dist/index.d.ts +2 -0
  99. package/packages/cli/dist/index.d.ts.map +1 -0
  100. package/packages/cli/dist/index.js +156 -0
  101. package/packages/cli/dist/index.js.map +1 -0
  102. package/packages/cli/package.json +24 -0
  103. package/packages/cli/src/commands/brief-cmd.ts +87 -0
  104. package/packages/cli/src/commands/capture-cmd.ts +34 -0
  105. package/packages/cli/src/commands/card-cmd.ts +29 -0
  106. package/packages/cli/src/commands/clip-cmd.ts +172 -0
  107. package/packages/cli/src/commands/cloud-cmd.ts +75 -0
  108. package/packages/cli/src/commands/contradictions-cmd.ts +41 -0
  109. package/packages/cli/src/commands/decay-cmd.ts +57 -0
  110. package/packages/cli/src/commands/digest-cmd.ts +89 -0
  111. package/packages/cli/src/commands/duplicates-cmd.ts +38 -0
  112. package/packages/cli/src/commands/federate-cmd.ts +236 -0
  113. package/packages/cli/src/commands/gaps-cmd.ts +40 -0
  114. package/packages/cli/src/commands/graph-cmd.ts +88 -0
  115. package/packages/cli/src/commands/index-cmd.ts +65 -0
  116. package/packages/cli/src/commands/init-cmd.ts +145 -0
  117. package/packages/cli/src/commands/learn-cmd.ts +56 -0
  118. package/packages/cli/src/commands/pack-cmd.ts +121 -0
  119. package/packages/cli/src/commands/review-cmd.ts +125 -0
  120. package/packages/cli/src/commands/search-cmd.ts +45 -0
  121. package/packages/cli/src/commands/serve-cmd.ts +17 -0
  122. package/packages/cli/src/commands/status-cmd.ts +37 -0
  123. package/packages/cli/src/commands/sync-cmd.ts +68 -0
  124. package/packages/cli/src/commands/vault-cmd.ts +64 -0
  125. package/packages/cli/src/index.ts +187 -0
  126. package/packages/core/package.json +40 -0
  127. package/packages/core/src/api/dashboard.ts +138 -0
  128. package/packages/core/src/api/graph-data.ts +286 -0
  129. package/packages/core/src/api/pwa.ts +82 -0
  130. package/packages/core/src/api/server.ts +660 -0
  131. package/packages/core/src/capture/voice.ts +168 -0
  132. package/packages/core/src/cloud/index.ts +2 -0
  133. package/packages/core/src/cloud/sync.ts +167 -0
  134. package/packages/core/src/config.ts +82 -0
  135. package/packages/core/src/federation/credits.ts +80 -0
  136. package/packages/core/src/federation/hyperswarm.d.ts +19 -0
  137. package/packages/core/src/federation/identity.ts +90 -0
  138. package/packages/core/src/federation/index.ts +8 -0
  139. package/packages/core/src/federation/node.ts +235 -0
  140. package/packages/core/src/federation/privacy.ts +52 -0
  141. package/packages/core/src/federation/reputation.ts +202 -0
  142. package/packages/core/src/federation/search.ts +129 -0
  143. package/packages/core/src/federation/sharing.ts +165 -0
  144. package/packages/core/src/federation/trust.ts +76 -0
  145. package/packages/core/src/federation/types.ts +25 -0
  146. package/packages/core/src/i18n/index.ts +85 -0
  147. package/packages/core/src/index.ts +133 -0
  148. package/packages/core/src/indexer/chunker.ts +180 -0
  149. package/packages/core/src/indexer/embedder.ts +9 -0
  150. package/packages/core/src/indexer/index.ts +113 -0
  151. package/packages/core/src/indexer/local-embedder.ts +35 -0
  152. package/packages/core/src/indexer/scanner.ts +142 -0
  153. package/packages/core/src/indexer/watcher.ts +62 -0
  154. package/packages/core/src/intelligence/contradiction-detector.ts +134 -0
  155. package/packages/core/src/intelligence/decay-engine.ts +229 -0
  156. package/packages/core/src/intelligence/duplicate-detector.ts +71 -0
  157. package/packages/core/src/intelligence/fsrs.ts +79 -0
  158. package/packages/core/src/intelligence/gap-detector.ts +109 -0
  159. package/packages/core/src/intelligence/learning-path.ts +86 -0
  160. package/packages/core/src/intelligence/notifications.ts +106 -0
  161. package/packages/core/src/intelligence/predictive-gaps.ts +94 -0
  162. package/packages/core/src/intelligence/semantic-versioning.ts +97 -0
  163. package/packages/core/src/intelligence/types.ts +28 -0
  164. package/packages/core/src/mcp/custom-tools.ts +97 -0
  165. package/packages/core/src/mcp/index.ts +1 -0
  166. package/packages/core/src/mcp/server.ts +142 -0
  167. package/packages/core/src/mcp/tools/agentic-graph.ts +96 -0
  168. package/packages/core/src/mcp/tools/brief.ts +49 -0
  169. package/packages/core/src/mcp/tools/decay.ts +40 -0
  170. package/packages/core/src/mcp/tools/decision-journal.ts +95 -0
  171. package/packages/core/src/mcp/tools/export.ts +72 -0
  172. package/packages/core/src/mcp/tools/federated-search.ts +43 -0
  173. package/packages/core/src/mcp/tools/generate-claude-md.ts +130 -0
  174. package/packages/core/src/mcp/tools/get-document.ts +26 -0
  175. package/packages/core/src/mcp/tools/get-related.ts +41 -0
  176. package/packages/core/src/mcp/tools/learning-path.ts +52 -0
  177. package/packages/core/src/mcp/tools/list-topics.ts +20 -0
  178. package/packages/core/src/mcp/tools/search.ts +35 -0
  179. package/packages/core/src/mcp/tools/snapshot.ts +98 -0
  180. package/packages/core/src/multi-vault/index.ts +118 -0
  181. package/packages/core/src/pack/creator.ts +127 -0
  182. package/packages/core/src/pack/exporter.ts +21 -0
  183. package/packages/core/src/pack/importer.ts +82 -0
  184. package/packages/core/src/pack/index.ts +5 -0
  185. package/packages/core/src/pack/marketplace.ts +103 -0
  186. package/packages/core/src/pack/pii-masker.ts +38 -0
  187. package/packages/core/src/pack/types.ts +39 -0
  188. package/packages/core/src/plugins/index.ts +100 -0
  189. package/packages/core/src/plugins/webhooks.ts +110 -0
  190. package/packages/core/src/search/bm25.ts +16 -0
  191. package/packages/core/src/search/index.ts +83 -0
  192. package/packages/core/src/search/rrf.ts +31 -0
  193. package/packages/core/src/search/semantic.ts +15 -0
  194. package/packages/core/src/store/index.ts +2 -0
  195. package/packages/core/src/store/sqlite-vec.ts +290 -0
  196. package/packages/core/src/store/types.ts +22 -0
  197. package/packages/core/src/team/index.ts +126 -0
  198. package/packages/core/src/types/chunk.ts +25 -0
  199. package/packages/core/src/types/document.ts +24 -0
  200. package/packages/core/src/types/graph.ts +44 -0
  201. package/packages/core/src/types/index.ts +15 -0
  202. package/packages/core/src/types/search.ts +38 -0
  203. package/packages/core/src/utils/retry.ts +85 -0
  204. package/packages/core/tests/api-card.test.ts +60 -0
  205. package/packages/core/tests/api-routes.test.ts +98 -0
  206. package/packages/core/tests/bm25.test.ts +87 -0
  207. package/packages/core/tests/chunker.test.ts +48 -0
  208. package/packages/core/tests/cluster.test.ts +75 -0
  209. package/packages/core/tests/constellation.test.ts +77 -0
  210. package/packages/core/tests/export-utils.test.ts +97 -0
  211. package/packages/core/tests/fsrs.test.ts +96 -0
  212. package/packages/core/tests/gesture-detector.test.ts +45 -0
  213. package/packages/core/tests/graph-data.test.ts +87 -0
  214. package/packages/core/tests/layout.test.ts +83 -0
  215. package/packages/core/tests/mcp.test.ts +148 -0
  216. package/packages/core/tests/pack.test.ts +127 -0
  217. package/packages/core/tests/pii-masker.test.ts +42 -0
  218. package/packages/core/tests/profile-card.test.ts +62 -0
  219. package/packages/core/tests/rrf.test.ts +29 -0
  220. package/packages/core/tests/search-integration.test.ts +139 -0
  221. package/packages/core/tests/store.test.ts +80 -0
  222. package/packages/graph/click-result.png +0 -0
  223. package/packages/graph/index.html +17 -0
  224. package/packages/graph/package.json +32 -0
  225. package/packages/graph/src/App.tsx +7 -0
  226. package/packages/graph/src/api/client.ts +39 -0
  227. package/packages/graph/src/components/ClusterFilter.tsx +73 -0
  228. package/packages/graph/src/components/ConstellationView.tsx +232 -0
  229. package/packages/graph/src/components/ExportPanel.tsx +177 -0
  230. package/packages/graph/src/components/Graph3D.tsx +230 -0
  231. package/packages/graph/src/components/GraphEdges.tsx +100 -0
  232. package/packages/graph/src/components/GraphNodes.tsx +386 -0
  233. package/packages/graph/src/components/HealthDashboard.tsx +173 -0
  234. package/packages/graph/src/components/Layout.tsx +214 -0
  235. package/packages/graph/src/components/MotionOverlay.tsx +81 -0
  236. package/packages/graph/src/components/MotionToggle.tsx +33 -0
  237. package/packages/graph/src/components/MultiverseView.tsx +286 -0
  238. package/packages/graph/src/components/NodeDetail.tsx +232 -0
  239. package/packages/graph/src/components/PulseParticle.tsx +232 -0
  240. package/packages/graph/src/components/SearchBar.tsx +107 -0
  241. package/packages/graph/src/components/StarField.tsx +197 -0
  242. package/packages/graph/src/components/StatusBar.tsx +53 -0
  243. package/packages/graph/src/components/Timeline.tsx +148 -0
  244. package/packages/graph/src/components/ToolsPanel.tsx +512 -0
  245. package/packages/graph/src/components/Tooltip.tsx +100 -0
  246. package/packages/graph/src/components/TypeFilter.tsx +131 -0
  247. package/packages/graph/src/embed/EmbedGraph.tsx +144 -0
  248. package/packages/graph/src/hooks/useConstellationLOD.ts +76 -0
  249. package/packages/graph/src/hooks/useDecay.ts +37 -0
  250. package/packages/graph/src/hooks/useExport.ts +165 -0
  251. package/packages/graph/src/hooks/useGraph.ts +69 -0
  252. package/packages/graph/src/hooks/useKeyboardNav.ts +122 -0
  253. package/packages/graph/src/hooks/useLayout.ts +45 -0
  254. package/packages/graph/src/hooks/useMotion.ts +120 -0
  255. package/packages/graph/src/hooks/usePulse.ts +58 -0
  256. package/packages/graph/src/hooks/useSearch.ts +71 -0
  257. package/packages/graph/src/lib/constellation.ts +107 -0
  258. package/packages/graph/src/lib/export-utils.ts +48 -0
  259. package/packages/graph/src/lib/gesture-detector.ts +123 -0
  260. package/packages/graph/src/lib/layout.worker.ts +153 -0
  261. package/packages/graph/src/lib/motion-controller.ts +83 -0
  262. package/packages/graph/src/lib/profile-card.ts +122 -0
  263. package/packages/graph/src/main.tsx +4 -0
  264. package/packages/graph/src/stores/graph-store.ts +155 -0
  265. package/packages/graph/success.png +0 -0
  266. package/packages/graph/test-click.mjs +49 -0
  267. package/packages/graph/test-explore.mjs +102 -0
  268. package/packages/graph/test-final.mjs +61 -0
  269. package/packages/graph/test-graph.mjs +139 -0
  270. package/packages/graph/test-hover.mjs +48 -0
  271. package/packages/graph/test-pulse.mjs +68 -0
  272. package/packages/graph/test-screenshot.mjs +56 -0
  273. package/packages/graph/test-v2.mjs +97 -0
  274. package/packages/graph/vite.config.ts +15 -0
  275. package/packages/sync/.env.example +11 -0
  276. package/packages/sync/.sync-state.json +317 -0
  277. package/packages/sync/.upload-state.json +1009 -0
  278. package/packages/sync/create-stella-network-notion.mjs +151 -0
  279. package/packages/sync/create-stellavault-project-notion.mjs +322 -0
  280. package/packages/sync/logs/sync-2026-03-28.log +6 -0
  281. package/packages/sync/logs/sync-2026-03-29.log +12 -0
  282. package/packages/sync/logs/sync-2026-03-30.log +6 -0
  283. package/packages/sync/logs/sync-2026-03-31.log +6 -0
  284. package/packages/sync/logs/sync-2026-04-01.log +6 -0
  285. package/packages/sync/logs/sync-2026-04-02.log +6 -0
  286. package/packages/sync/package-lock.json +373 -0
  287. package/packages/sync/package.json +16 -0
  288. package/packages/sync/run-sync.bat +18 -0
  289. package/packages/sync/run-sync.mjs +46 -0
  290. package/packages/sync/setup-scheduler.mjs +119 -0
  291. package/packages/sync/structured-sync.mjs +187 -0
  292. package/packages/sync/sync-to-obsidian.mjs +264 -0
  293. package/packages/sync/upload-pdca-to-notion.mjs +495 -0
  294. package/tsconfig.base.json +18 -0
@@ -0,0 +1,122 @@
1
+ // Keyboard-Only Graph Navigation (F-A18)
2
+ // Tab: cycle nodes, Arrow keys: traverse connections, Enter: select, Esc: deselect
3
+
4
+ import { useEffect, useCallback } from 'react';
5
+ import { useGraphStore } from '../stores/graph-store.js';
6
+
7
+ export function useKeyboardNav() {
8
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
9
+ // Don't interfere with input fields
10
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
11
+
12
+ const state = useGraphStore.getState();
13
+ const { nodes, edges, selectedNodeId, selectNode, hoverNode } = state;
14
+ if (nodes.length === 0) return;
15
+
16
+ switch (e.key) {
17
+ case 'Tab': {
18
+ e.preventDefault();
19
+ // Cycle through nodes
20
+ const currentIdx = selectedNodeId
21
+ ? nodes.findIndex(n => n.id === selectedNodeId)
22
+ : -1;
23
+ const nextIdx = e.shiftKey
24
+ ? (currentIdx <= 0 ? nodes.length - 1 : currentIdx - 1)
25
+ : (currentIdx + 1) % nodes.length;
26
+ selectNode(nodes[nextIdx].id);
27
+ focusNode(nodes[nextIdx].id);
28
+ break;
29
+ }
30
+
31
+ case 'ArrowRight':
32
+ case 'ArrowDown': {
33
+ e.preventDefault();
34
+ if (!selectedNodeId) { selectNode(nodes[0].id); focusNode(nodes[0].id); break; }
35
+ // Navigate to next connected node
36
+ const neighbors = getNeighbors(selectedNodeId, edges);
37
+ if (neighbors.length > 0) {
38
+ const currentNeighborIdx = neighbors.indexOf(selectedNodeId);
39
+ const nextNeighbor = neighbors[(currentNeighborIdx + 1) % neighbors.length];
40
+ selectNode(nextNeighbor);
41
+ focusNode(nextNeighbor);
42
+ }
43
+ break;
44
+ }
45
+
46
+ case 'ArrowLeft':
47
+ case 'ArrowUp': {
48
+ e.preventDefault();
49
+ if (!selectedNodeId) break;
50
+ const neighbors = getNeighbors(selectedNodeId, edges);
51
+ if (neighbors.length > 0) {
52
+ const currentNeighborIdx = neighbors.indexOf(selectedNodeId);
53
+ const prevNeighbor = neighbors[(currentNeighborIdx - 1 + neighbors.length) % neighbors.length];
54
+ selectNode(prevNeighbor);
55
+ focusNode(prevNeighbor);
56
+ }
57
+ break;
58
+ }
59
+
60
+ case 'Enter': {
61
+ if (selectedNodeId) {
62
+ // Trigger node detail view (already handled by selectNode)
63
+ hoverNode(null);
64
+ }
65
+ break;
66
+ }
67
+
68
+ case 'Escape': {
69
+ selectNode(null);
70
+ hoverNode(null);
71
+ (window as any).__sv_resetCamera?.();
72
+ break;
73
+ }
74
+
75
+ case '/': {
76
+ e.preventDefault();
77
+ // Focus search bar
78
+ const searchInput = document.querySelector('input[placeholder*="Search"]') as HTMLInputElement;
79
+ searchInput?.focus();
80
+ break;
81
+ }
82
+
83
+ case '?': {
84
+ if (!e.shiftKey) break;
85
+ // Show keyboard shortcuts help
86
+ console.log('Keyboard shortcuts: Tab=cycle, Arrow=traverse, Enter=select, Esc=reset, /=search');
87
+ break;
88
+ }
89
+ }
90
+ }, []);
91
+
92
+ useEffect(() => {
93
+ window.addEventListener('keydown', handleKeyDown);
94
+ return () => window.removeEventListener('keydown', handleKeyDown);
95
+ }, [handleKeyDown]);
96
+ }
97
+
98
+ function getNeighbors(nodeId: string, edges: Array<{ source: string; target: string }>): string[] {
99
+ const neighbors = new Set<string>();
100
+ for (const e of edges) {
101
+ if (e.source === nodeId) neighbors.add(e.target);
102
+ if (e.target === nodeId) neighbors.add(e.source);
103
+ }
104
+ return [...neighbors];
105
+ }
106
+
107
+ function focusNode(nodeId: string) {
108
+ // Trigger camera focus via the same mechanism as search
109
+ const state = useGraphStore.getState();
110
+ const node = state.nodes.find(n => n.id === nodeId);
111
+ if (!node?.position) return;
112
+
113
+ const controls = (window as any).__sv_controls?.current;
114
+ if (!controls) return;
115
+
116
+ const THREE = (window as any).__THREE__;
117
+ if (!THREE) return;
118
+
119
+ const target = new THREE.Vector3(...node.position);
120
+ controls.target.copy(target);
121
+ controls.update();
122
+ }
@@ -0,0 +1,45 @@
1
+ // Force layout — 첫 로드 시 1회만 실행, 모드 전환 시 재실행 안 함
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useGraphStore } from '../stores/graph-store.js';
5
+
6
+ export function useLayout() {
7
+ const ranRef = useRef(false);
8
+ const nodes = useGraphStore((s) => s.nodes);
9
+ const edges = useGraphStore((s) => s.edges);
10
+ const clusters = useGraphStore((s) => s.clusters);
11
+ const setGraphData = useGraphStore((s) => s.setGraphData);
12
+
13
+ useEffect(() => {
14
+ // 첫 로드에만 실행
15
+ if (nodes.length === 0 || ranRef.current) return;
16
+ ranRef.current = true;
17
+
18
+ const worker = new Worker(
19
+ new URL('../lib/layout.worker.ts', import.meta.url),
20
+ { type: 'module' },
21
+ );
22
+
23
+ worker.onmessage = (e) => {
24
+ const { type, positions } = e.data;
25
+ if (type === 'progress' || type === 'done') {
26
+ // 위치만 업데이트, 현재 클러스터 유지
27
+ const currentState = useGraphStore.getState();
28
+ const updated = currentState.nodes.map((n, i) => ({
29
+ ...n,
30
+ position: positions[i] as [number, number, number],
31
+ }));
32
+ setGraphData(updated, currentState.edges, currentState.clusters);
33
+ }
34
+ if (type === 'done') worker.terminate();
35
+ };
36
+
37
+ worker.postMessage({
38
+ type: 'init',
39
+ nodes: nodes.map(n => ({ id: n.id, clusterId: n.clusterId, size: n.size })),
40
+ edges,
41
+ });
42
+
43
+ return () => worker.terminate();
44
+ }, [nodes.length > 0]);
45
+ }
@@ -0,0 +1,120 @@
1
+ // Design Ref: §2.3 — 제스처 → OrbitControls 매핑
2
+
3
+ import { useRef, useCallback, useEffect } from 'react';
4
+ import { createMotionController, type MotionController } from '../lib/motion-controller.js';
5
+ import type { GestureResult } from '../lib/gesture-detector.js';
6
+ import { useGraphStore } from '../stores/graph-store.js';
7
+
8
+ export interface UseMotionReturn {
9
+ start: () => Promise<void>;
10
+ stop: () => void;
11
+ isActive: boolean;
12
+ videoRef: React.RefObject<HTMLVideoElement | null>;
13
+ currentGesture: React.RefObject<string>;
14
+ }
15
+
16
+ export function useMotion(controlsRef: React.RefObject<any>): UseMotionReturn {
17
+ const controllerRef = useRef<MotionController | null>(null);
18
+ const activeRef = useRef(false);
19
+ const videoRef = useRef<HTMLVideoElement | null>(null);
20
+ const currentGesture = useRef<string>('none');
21
+ const initialZoomRef = useRef<number | null>(null);
22
+ const pinchBaseRef = useRef<number>(0);
23
+
24
+ const handleGesture = useCallback((gesture: GestureResult) => {
25
+ const controls = controlsRef.current;
26
+ if (!controls) return;
27
+
28
+ currentGesture.current = gesture.type;
29
+ const sensitivity = 3;
30
+
31
+ switch (gesture.type) {
32
+ case 'rotate':
33
+ // 손 이동 → 회전
34
+ controls.setAzimuthalAngle(
35
+ controls.getAzimuthalAngle() - gesture.delta.x * sensitivity
36
+ );
37
+ controls.setPolarAngle(
38
+ Math.max(0.1, Math.min(Math.PI - 0.1,
39
+ controls.getPolarAngle() + gesture.delta.y * sensitivity
40
+ ))
41
+ );
42
+ controls.update();
43
+ break;
44
+
45
+ case 'pan': {
46
+ // 주먹 이동 → 패닝
47
+ const target = controls.target;
48
+ target.x -= gesture.delta.x * 500;
49
+ target.y += gesture.delta.y * 500;
50
+ controls.update();
51
+ break;
52
+ }
53
+
54
+ case 'zoom': {
55
+ // 핀치 거리 → 줌
56
+ if (pinchBaseRef.current === 0) pinchBaseRef.current = gesture.pinchDistance;
57
+ const zoomDelta = (gesture.pinchDistance - pinchBaseRef.current) * 2000;
58
+ const cam = controls.object;
59
+ if (cam) {
60
+ const dir = cam.position.clone().sub(controls.target).normalize();
61
+ cam.position.addScaledVector(dir, -zoomDelta);
62
+ pinchBaseRef.current = gesture.pinchDistance;
63
+ }
64
+ controls.update();
65
+ break;
66
+ }
67
+
68
+ case 'select': {
69
+ // 검지 포인팅 → 노드 호버 (화면 좌표로 변환)
70
+ // 간단 구현: 검지 위치를 hoveredNodeId로 매핑 (향후 raycast 개선)
71
+ break;
72
+ }
73
+
74
+ case 'reset': {
75
+ // 손 흔들기 → 초기 위치
76
+ const cam = controls.object;
77
+ if (cam) {
78
+ cam.position.set(0, 100, 600);
79
+ controls.target.set(0, 0, 0);
80
+ controls.update();
81
+ }
82
+ const state = useGraphStore.getState();
83
+ state.selectNode(null);
84
+ state.setHighlightedNodes([]);
85
+ break;
86
+ }
87
+ }
88
+
89
+ // 핀치 외 제스처에서 pinchBase 리셋
90
+ if (gesture.type !== 'zoom') pinchBaseRef.current = 0;
91
+ }, [controlsRef]);
92
+
93
+ const start = useCallback(async () => {
94
+ if (!controllerRef.current) {
95
+ controllerRef.current = createMotionController();
96
+ }
97
+ controllerRef.current.onGesture(handleGesture);
98
+ await controllerRef.current.start();
99
+ videoRef.current = controllerRef.current.getVideoElement();
100
+ activeRef.current = true;
101
+ }, [handleGesture]);
102
+
103
+ const stop = useCallback(() => {
104
+ controllerRef.current?.stop();
105
+ activeRef.current = false;
106
+ videoRef.current = null;
107
+ currentGesture.current = 'none';
108
+ }, []);
109
+
110
+ // 컴포넌트 언마운트 시 정리
111
+ useEffect(() => () => { controllerRef.current?.stop(); }, []);
112
+
113
+ return {
114
+ start,
115
+ stop,
116
+ get isActive() { return activeRef.current; },
117
+ videoRef,
118
+ currentGesture,
119
+ };
120
+ }
@@ -0,0 +1,58 @@
1
+ // 빛 입자 탐색 — 방문 순서만 계산, 실제 애니메이션은 PulseAnimator에서
2
+
3
+ import { useRef, useCallback } from 'react';
4
+ import { useGraphStore } from '../stores/graph-store.js';
5
+
6
+ export interface PulseData {
7
+ visitOrder: string[];
8
+ positions: Map<string, [number, number, number]>;
9
+ running: boolean;
10
+ }
11
+
12
+ // Scene 내부 PulseAnimator가 읽을 수 있도록 전역 ref
13
+ export let pulseData: PulseData = { visitOrder: [], positions: new Map(), running: false };
14
+
15
+ export function usePulse() {
16
+ const startPulse = useCallback((startId: string) => {
17
+ const { edges, nodes } = useGraphStore.getState();
18
+
19
+ // BFS
20
+ const adj = new Map<string, string[]>();
21
+ for (const e of edges) {
22
+ if (!adj.has(e.source)) adj.set(e.source, []);
23
+ if (!adj.has(e.target)) adj.set(e.target, []);
24
+ adj.get(e.source)!.push(e.target);
25
+ adj.get(e.target)!.push(e.source);
26
+ }
27
+
28
+ const visited = new Set<string>();
29
+ const visitOrder: string[] = [];
30
+ const queue = [startId];
31
+ visited.add(startId);
32
+ while (queue.length > 0 && visitOrder.length < 50) {
33
+ const id = queue.shift()!;
34
+ visitOrder.push(id);
35
+ for (const nid of (adj.get(id) ?? [])) {
36
+ if (!visited.has(nid)) { visited.add(nid); queue.push(nid); }
37
+ }
38
+ }
39
+
40
+ const positions = new Map(nodes.map(n => [n.id, (n.position ?? [0, 0, 0]) as [number, number, number]]));
41
+
42
+ pulseData = { visitOrder, positions, running: true };
43
+
44
+ // 시작 노드 점등 (기존 하이라이트 유지하지 않고 pulse용으로 리셋)
45
+ useGraphStore.getState().setHighlightedNodes([startId]);
46
+ useGraphStore.getState().setPulseParticlePos(positions.get(startId) ?? [0, 0, 0]);
47
+ // 선택 유지 (노드 라벨이 사라지지 않도록)
48
+ useGraphStore.getState().selectNode(startId);
49
+ }, []);
50
+
51
+ const stopPulse = useCallback(() => {
52
+ pulseData = { visitOrder: [], positions: new Map(), running: false };
53
+ useGraphStore.getState().setHighlightedNodes([]);
54
+ useGraphStore.getState().setPulseParticlePos(null);
55
+ }, []);
56
+
57
+ return { startPulse, stopPulse };
58
+ }
@@ -0,0 +1,71 @@
1
+ // 검색 → API → 노드 하이라이트 + 첫 결과로 카메라 이동
2
+
3
+ import { useCallback, useRef } from 'react';
4
+ import * as THREE from 'three';
5
+ import { fetchSearch } from '../api/client.js';
6
+ import { useGraphStore } from '../stores/graph-store.js';
7
+
8
+ function focusOnNode(nodeId: string) {
9
+ const state = useGraphStore.getState();
10
+ const node = state.nodes.find(n => n.id === nodeId);
11
+ if (!node?.position) return;
12
+
13
+ const controls = (window as any).__sv_controls?.current;
14
+ if (!controls) return;
15
+
16
+ const target = new THREE.Vector3(...node.position);
17
+ const startTarget = controls.target.clone();
18
+ const startPos = controls.object.position.clone();
19
+ const dir = startPos.clone().sub(target).normalize();
20
+ const endPos = target.clone().add(dir.multiplyScalar(250));
21
+
22
+ let t = 0;
23
+ function animate() {
24
+ t += 0.03;
25
+ if (t > 1) t = 1;
26
+ const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
27
+ controls.target.lerpVectors(startTarget, target, ease);
28
+ controls.object.position.lerpVectors(startPos, endPos, ease);
29
+ controls.update();
30
+ if (t < 1) requestAnimationFrame(animate);
31
+ }
32
+ requestAnimationFrame(animate);
33
+ }
34
+
35
+ export function useSearch() {
36
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37
+
38
+ const search = useCallback((query: string) => {
39
+ const { setSearchQuery, setHighlightedNodes } = useGraphStore.getState();
40
+ setSearchQuery(query);
41
+
42
+ if (!query.trim()) {
43
+ setHighlightedNodes([]);
44
+ return;
45
+ }
46
+
47
+ if (timerRef.current) clearTimeout(timerRef.current);
48
+ timerRef.current = setTimeout(async () => {
49
+ try {
50
+ const res = await fetchSearch(query, 20);
51
+ const ids = (res.results ?? []).map((r: any) => r.documentId);
52
+ useGraphStore.getState().setHighlightedNodes(ids);
53
+ if (ids.length > 0) useGraphStore.getState().addSearchHistory(query);
54
+
55
+ // 첫 번째 결과로 카메라 이동
56
+ if (ids.length > 0) focusOnNode(ids[0]);
57
+ } catch {
58
+ useGraphStore.getState().setHighlightedNodes([]);
59
+ }
60
+ }, 300);
61
+ }, []);
62
+
63
+ const clearSearch = useCallback(() => {
64
+ if (timerRef.current) clearTimeout(timerRef.current);
65
+ useGraphStore.getState().setSearchQuery('');
66
+ useGraphStore.getState().setHighlightedNodes([]);
67
+ (window as any).__sv_resetCamera?.();
68
+ }, []);
69
+
70
+ return { search, clearSearch };
71
+ }
@@ -0,0 +1,107 @@
1
+ // Design Ref: §3.1 — MST 별자리 생성
2
+ // Prim's MST: 클러스터 내 노드를 최소 거리로 연결 → 자연스러운 별자리 형태
3
+
4
+ interface Node {
5
+ id: string;
6
+ clusterId: number;
7
+ position?: [number, number, number];
8
+ }
9
+
10
+ interface Cluster {
11
+ id: number;
12
+ label: string;
13
+ color: string;
14
+ }
15
+
16
+ export interface ConstellationLine {
17
+ from: [number, number, number];
18
+ to: [number, number, number];
19
+ clusterId: number;
20
+ }
21
+
22
+ export interface ConstellationLabel {
23
+ position: [number, number, number];
24
+ text: string;
25
+ color: string;
26
+ clusterId: number;
27
+ }
28
+
29
+ export interface ConstellationData {
30
+ lines: ConstellationLine[];
31
+ labels: ConstellationLabel[];
32
+ }
33
+
34
+ export function buildConstellations(nodes: Node[], clusters: Cluster[]): ConstellationData {
35
+ const lines: ConstellationLine[] = [];
36
+ const labels: ConstellationLabel[] = [];
37
+
38
+ for (const cluster of clusters) {
39
+ const clusterNodes = nodes.filter(n => n.clusterId === cluster.id && n.position);
40
+ if (clusterNodes.length < 2) {
41
+ // 단일 노드 클러스터도 라벨은 표시
42
+ if (clusterNodes.length === 1 && clusterNodes[0].position) {
43
+ labels.push({
44
+ position: clusterNodes[0].position!,
45
+ text: cluster.label,
46
+ color: cluster.color,
47
+ clusterId: cluster.id,
48
+ });
49
+ }
50
+ continue;
51
+ }
52
+
53
+ // Prim's MST
54
+ const positions = clusterNodes.map(n => n.position!);
55
+ const n = positions.length;
56
+ const inMST = new Array(n).fill(false);
57
+ const minEdge = new Array(n).fill(Infinity);
58
+ const parent = new Array(n).fill(-1);
59
+ minEdge[0] = 0;
60
+
61
+ for (let iter = 0; iter < n; iter++) {
62
+ // 최소 비용 노드 선택
63
+ let u = -1;
64
+ for (let i = 0; i < n; i++) {
65
+ if (!inMST[i] && (u === -1 || minEdge[i] < minEdge[u])) u = i;
66
+ }
67
+ if (u === -1) break;
68
+ inMST[u] = true;
69
+
70
+ // 엣지 추가
71
+ if (parent[u] !== -1) {
72
+ lines.push({
73
+ from: positions[parent[u]],
74
+ to: positions[u],
75
+ clusterId: cluster.id,
76
+ });
77
+ }
78
+
79
+ // 인접 노드 업데이트
80
+ for (let v = 0; v < n; v++) {
81
+ if (inMST[v]) continue;
82
+ const d = dist3D(positions[u], positions[v]);
83
+ if (d < minEdge[v]) {
84
+ minEdge[v] = d;
85
+ parent[v] = u;
86
+ }
87
+ }
88
+ }
89
+
90
+ // 클러스터 중심에 라벨
91
+ const cx = positions.reduce((s, p) => s + p[0], 0) / n;
92
+ const cy = positions.reduce((s, p) => s + p[1], 0) / n + 20; // 약간 위
93
+ const cz = positions.reduce((s, p) => s + p[2], 0) / n;
94
+ labels.push({
95
+ position: [cx, cy, cz],
96
+ text: cluster.label,
97
+ color: cluster.color,
98
+ clusterId: cluster.id,
99
+ });
100
+ }
101
+
102
+ return { lines, labels };
103
+ }
104
+
105
+ function dist3D(a: [number, number, number], b: [number, number, number]): number {
106
+ return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2);
107
+ }
@@ -0,0 +1,48 @@
1
+ // Design Ref: §6 — PNG/WebM 변환 유틸리티 (순수 함수)
2
+
3
+ export function generateFilename(type: 'screenshot' | 'recording', extension: string): string {
4
+ const now = new Date();
5
+ const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
6
+ return `stellavault-${type}-${ts}.${extension}`;
7
+ }
8
+
9
+ export function downloadBlob(blob: Blob, filename: string): void {
10
+ const url = URL.createObjectURL(blob);
11
+ const link = document.createElement('a');
12
+ link.href = url;
13
+ link.download = filename;
14
+ link.click();
15
+ URL.revokeObjectURL(url);
16
+ }
17
+
18
+ export function addWatermark(
19
+ canvas: HTMLCanvasElement,
20
+ text: string,
21
+ ): HTMLCanvasElement {
22
+ const ctx = canvas.getContext('2d');
23
+ if (!ctx) return canvas;
24
+
25
+ ctx.save();
26
+ ctx.font = '14px monospace';
27
+ ctx.fillStyle = 'rgba(200, 200, 255, 0.4)';
28
+ ctx.textAlign = 'right';
29
+ ctx.textBaseline = 'bottom';
30
+ ctx.fillText(text, canvas.width - 16, canvas.height - 12);
31
+ ctx.restore();
32
+
33
+ return canvas;
34
+ }
35
+
36
+ export function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
37
+ return new Promise((resolve, reject) => {
38
+ canvas.toBlob((blob) => {
39
+ if (blob) resolve(blob);
40
+ else reject(new Error('Failed to create blob from canvas'));
41
+ }, 'image/png');
42
+ });
43
+ }
44
+
45
+ export function isMediaRecorderSupported(): boolean {
46
+ return typeof MediaRecorder !== 'undefined' &&
47
+ MediaRecorder.isTypeSupported('video/webm');
48
+ }
@@ -0,0 +1,123 @@
1
+ // Design Ref: §2.1 — 21 landmarks → 6 제스처 분류
2
+ // 3프레임 안정화 + confidence threshold
3
+
4
+ export type GestureType = 'rotate' | 'pan' | 'zoom' | 'select' | 'reset' | 'none';
5
+
6
+ export interface GestureResult {
7
+ type: GestureType;
8
+ confidence: number;
9
+ position: { x: number; y: number };
10
+ delta: { x: number; y: number };
11
+ pinchDistance: number;
12
+ }
13
+
14
+ interface Landmark { x: number; y: number; z: number; }
15
+
16
+ // 손가락 끝 인덱스
17
+ const TIPS = [4, 8, 12, 16, 20]; // 엄지, 검지, 중지, 약지, 소지
18
+ const PIPS = [3, 6, 10, 14, 18]; // 각 손가락 두 번째 관절
19
+
20
+ // 이전 상태 (안정화용)
21
+ let prevPosition = { x: 0.5, y: 0.5 };
22
+ let gestureHistory: GestureType[] = [];
23
+ let positionHistory: Array<{ x: number; y: number }> = [];
24
+
25
+ export function detectGesture(landmarks: Landmark[]): GestureResult {
26
+ if (!landmarks || landmarks.length < 21) {
27
+ return { type: 'none', confidence: 0, position: prevPosition, delta: { x: 0, y: 0 }, pinchDistance: 1 };
28
+ }
29
+
30
+ // 손바닥 중심
31
+ const palm = landmarks[9]; // 중지 MCP
32
+ const position = { x: palm.x, y: palm.y };
33
+ const delta = { x: position.x - prevPosition.x, y: position.y - prevPosition.y };
34
+
35
+ // 핀치 거리 (엄지-검지)
36
+ const pinchDistance = dist2D(landmarks[4], landmarks[8]);
37
+
38
+ // 펼쳐진 손가락 수
39
+ const extended = countExtended(landmarks);
40
+
41
+ // 제스처 판별
42
+ let type: GestureType = 'none';
43
+ let confidence = 0.8;
44
+
45
+ if (pinchDistance < 0.06) {
46
+ type = 'zoom';
47
+ confidence = 0.9;
48
+ } else if (extended >= 4) {
49
+ type = 'rotate';
50
+ confidence = 0.85;
51
+ } else if (extended === 0) {
52
+ type = 'pan';
53
+ confidence = 0.85;
54
+ } else if (extended === 1 && isFingerExtended(landmarks, 1)) {
55
+ type = 'select';
56
+ confidence = 0.9;
57
+ }
58
+
59
+ // 흔들기 감지 (최근 10프레임 x 방향 전환 3회 이상)
60
+ positionHistory.push({ ...position });
61
+ if (positionHistory.length > 10) positionHistory.shift();
62
+ if (isWaving(positionHistory)) {
63
+ type = 'reset';
64
+ confidence = 0.85;
65
+ }
66
+
67
+ // confidence < 0.7 무시 (Design §2.4)
68
+ if (confidence < 0.7) {
69
+ type = 'none';
70
+ confidence = 0;
71
+ }
72
+
73
+ // 3프레임 안정화
74
+ gestureHistory.push(type);
75
+ if (gestureHistory.length > 3) gestureHistory.shift();
76
+ const stable = gestureHistory.length === 3 && gestureHistory.every(g => g === type);
77
+
78
+ prevPosition = position;
79
+
80
+ return {
81
+ type: stable ? type : 'none',
82
+ confidence: stable ? confidence : 0,
83
+ position,
84
+ delta,
85
+ pinchDistance,
86
+ };
87
+ }
88
+
89
+ export function resetGestureState() {
90
+ prevPosition = { x: 0.5, y: 0.5 };
91
+ gestureHistory = [];
92
+ positionHistory = [];
93
+ }
94
+
95
+ function countExtended(lm: Landmark[]): number {
96
+ let count = 0;
97
+ // 엄지: 팁이 IP보다 x 방향으로 멀면 펼침
98
+ if (Math.abs(lm[4].x - lm[2].x) > 0.04) count++;
99
+ // 나머지 4개: 팁이 PIP보다 y가 작으면 (위쪽) 펼침
100
+ for (let i = 1; i < 5; i++) {
101
+ if (lm[TIPS[i]].y < lm[PIPS[i]].y - 0.02) count++;
102
+ }
103
+ return count;
104
+ }
105
+
106
+ function isFingerExtended(lm: Landmark[], fingerIdx: number): boolean {
107
+ return lm[TIPS[fingerIdx]].y < lm[PIPS[fingerIdx]].y - 0.02;
108
+ }
109
+
110
+ function dist2D(a: Landmark, b: Landmark): number {
111
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
112
+ }
113
+
114
+ function isWaving(history: Array<{ x: number; y: number }>): boolean {
115
+ if (history.length < 8) return false;
116
+ let dirChanges = 0;
117
+ for (let i = 2; i < history.length; i++) {
118
+ const prev = history[i - 1].x - history[i - 2].x;
119
+ const curr = history[i].x - history[i - 1].x;
120
+ if (prev * curr < 0 && Math.abs(curr) > 0.005) dirChanges++;
121
+ }
122
+ return dirChanges >= 3;
123
+ }