@backbay/glia-three 0.2.0-alpha.2

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 (172) hide show
  1. package/package.json +43 -0
  2. package/src/environment/AuroraLayer/AuroraLayer.stories.tsx +43 -0
  3. package/src/environment/AuroraLayer/AuroraLayer.tsx +200 -0
  4. package/src/environment/AuroraLayer/index.ts +2 -0
  5. package/src/environment/AuroraLayer/types.ts +30 -0
  6. package/src/environment/EnvironmentLayer/EnvironmentLayer.stories.tsx +262 -0
  7. package/src/environment/EnvironmentLayer/EnvironmentLayer.tsx +105 -0
  8. package/src/environment/EnvironmentLayer/index.ts +4 -0
  9. package/src/environment/EnvironmentLayer/presets.ts +128 -0
  10. package/src/environment/FogLayer/FogLayer.stories.tsx +83 -0
  11. package/src/environment/FogLayer/FogLayer.tsx +113 -0
  12. package/src/environment/FogLayer/index.ts +2 -0
  13. package/src/environment/FogLayer/types.ts +45 -0
  14. package/src/environment/VolumetricLight/VolumetricLight.stories.tsx +55 -0
  15. package/src/environment/VolumetricLight/VolumetricLight.tsx +188 -0
  16. package/src/environment/VolumetricLight/index.ts +2 -0
  17. package/src/environment/VolumetricLight/types.ts +33 -0
  18. package/src/environment/WeatherLayer/WeatherLayer.stories.tsx +348 -0
  19. package/src/environment/WeatherLayer/WeatherLayer.tsx +266 -0
  20. package/src/environment/WeatherLayer/cinematicCanvas.tsx +809 -0
  21. package/src/environment/WeatherLayer/colors.ts +27 -0
  22. package/src/environment/WeatherLayer/index.ts +4 -0
  23. package/src/environment/WeatherLayer/leafPresets.ts +12 -0
  24. package/src/environment/WeatherLayer/particles.ts +227 -0
  25. package/src/environment/WeatherLayer/types.ts +140 -0
  26. package/src/environment/index.ts +17 -0
  27. package/src/environment/shared/index.ts +2 -0
  28. package/src/environment/shared/noise.ts +10 -0
  29. package/src/environment/shared/performance.ts +33 -0
  30. package/src/environment/shared/types.ts +26 -0
  31. package/src/index.ts +2 -0
  32. package/src/lib/utils.ts +6 -0
  33. package/src/lib/vision-types.ts +84 -0
  34. package/src/three/AgentConsole/AgentConsole.stories.tsx +397 -0
  35. package/src/three/AgentConsole/AgentConsole.tsx +195 -0
  36. package/src/three/AgentConsole/ConsoleChat.tsx +237 -0
  37. package/src/three/AgentConsole/FocusConstellation.tsx +297 -0
  38. package/src/three/AgentConsole/GlyphAvatar.stories.tsx +110 -0
  39. package/src/three/AgentConsole/GlyphAvatar.tsx +117 -0
  40. package/src/three/AgentConsole/QuickActions.tsx +111 -0
  41. package/src/three/AgentConsole/index.ts +41 -0
  42. package/src/three/AgentConsole/types.ts +241 -0
  43. package/src/three/AmbientField/AmbientField.stories.tsx +290 -0
  44. package/src/three/AmbientField/AmbientField.tsx +307 -0
  45. package/src/three/AmbientField/BackbayFieldBus.ts +326 -0
  46. package/src/three/AmbientField/FieldProvider.tsx +83 -0
  47. package/src/three/AmbientField/index.ts +37 -0
  48. package/src/three/AmbientField/shaders/constellation.ts +384 -0
  49. package/src/three/AmbientField/types.ts +174 -0
  50. package/src/three/AttackGraph/AttackGraph.stories.tsx +144 -0
  51. package/src/three/AttackGraph/AttackGraph.tsx +325 -0
  52. package/src/three/AttackGraph/index.ts +19 -0
  53. package/src/three/AttackGraph/types.ts +97 -0
  54. package/src/three/AuditTrail/AuditTrail.stories.tsx +567 -0
  55. package/src/three/AuditTrail/AuditTrail.tsx +644 -0
  56. package/src/three/AuditTrail/index.ts +33 -0
  57. package/src/three/AuditTrail/types.ts +192 -0
  58. package/src/three/CrystallineOrganism/Breadcrumb.tsx +61 -0
  59. package/src/three/CrystallineOrganism/CrystallineOrganism.stories.tsx +509 -0
  60. package/src/three/CrystallineOrganism/CrystallineOrganism.tsx +273 -0
  61. package/src/three/CrystallineOrganism/LatticeEdge.tsx +69 -0
  62. package/src/three/CrystallineOrganism/OrganismLattice.tsx +159 -0
  63. package/src/three/CrystallineOrganism/OrganismParticles.tsx +148 -0
  64. package/src/three/CrystallineOrganism/OrganismShell.tsx +277 -0
  65. package/src/three/CrystallineOrganism/constants.ts +161 -0
  66. package/src/three/CrystallineOrganism/index.ts +17 -0
  67. package/src/three/CrystallineOrganism/layouts/hexGrid.ts +85 -0
  68. package/src/three/CrystallineOrganism/layouts/index.ts +1 -0
  69. package/src/three/CrystallineOrganism/types.ts +167 -0
  70. package/src/three/CrystallineOrganism/useOrganismEmotion.ts +84 -0
  71. package/src/three/FirewallBarrier/FirewallBarrier.stories.tsx +167 -0
  72. package/src/three/FirewallBarrier/FirewallBarrier.tsx +259 -0
  73. package/src/three/FirewallBarrier/index.ts +14 -0
  74. package/src/three/FirewallBarrier/types.ts +52 -0
  75. package/src/three/Glyph/GlyphObject.stories.tsx +577 -0
  76. package/src/three/Glyph/GlyphObject.tsx +422 -0
  77. package/src/three/Glyph/index.ts +10 -0
  78. package/src/three/Glyph/types.ts +36 -0
  79. package/src/three/Glyph/useGlyphController.ts +231 -0
  80. package/src/three/Glyph/useGlyphEmotion.ts +70 -0
  81. package/src/three/Graph3D/Graph3D.stories.tsx +269 -0
  82. package/src/three/Graph3D/Graph3D.tsx +248 -0
  83. package/src/three/Graph3D/GraphEdge.tsx +79 -0
  84. package/src/three/Graph3D/GraphNode.tsx +239 -0
  85. package/src/three/Graph3D/types.ts +66 -0
  86. package/src/three/Graph3D/utils.ts +204 -0
  87. package/src/three/IntelFeed/IntelFeed.stories.tsx +168 -0
  88. package/src/three/IntelFeed/IntelFeed.tsx +284 -0
  89. package/src/three/IntelFeed/index.ts +14 -0
  90. package/src/three/IntelFeed/types.ts +56 -0
  91. package/src/three/MetricsGalaxy/MetricsGalaxy.tsx +484 -0
  92. package/src/three/MetricsGalaxy/index.ts +6 -0
  93. package/src/three/MetricsGalaxy/types.ts +26 -0
  94. package/src/three/NetworkTopology/NetworkTopology.stories.tsx +184 -0
  95. package/src/three/NetworkTopology/NetworkTopology.tsx +421 -0
  96. package/src/three/NetworkTopology/index.ts +34 -0
  97. package/src/three/NetworkTopology/types.ts +128 -0
  98. package/src/three/ParticleField/ParticleField.stories.tsx +162 -0
  99. package/src/three/ParticleField/ParticleField.tsx +81 -0
  100. package/src/three/ParticleField/index.ts +1 -0
  101. package/src/three/PermissionMatrix/PermissionMatrix.stories.tsx +475 -0
  102. package/src/three/PermissionMatrix/PermissionMatrix.tsx +380 -0
  103. package/src/three/PermissionMatrix/index.ts +15 -0
  104. package/src/three/PermissionMatrix/types.ts +54 -0
  105. package/src/three/QuantumField/ConstellationField.tsx +238 -0
  106. package/src/three/QuantumField/FieldBus.ts +349 -0
  107. package/src/three/QuantumField/FieldLayer.tsx +430 -0
  108. package/src/three/QuantumField/FieldProvider.tsx +460 -0
  109. package/src/three/QuantumField/PcbField.tsx +406 -0
  110. package/src/three/QuantumField/QuantumField.stories.tsx +1155 -0
  111. package/src/three/QuantumField/TrailRTT.ts +212 -0
  112. package/src/three/QuantumField/WaterField.tsx +226 -0
  113. package/src/three/QuantumField/WaterSimRTT.ts +283 -0
  114. package/src/three/QuantumField/domMapping.ts +185 -0
  115. package/src/three/QuantumField/index.ts +110 -0
  116. package/src/three/QuantumField/styles/index.ts +9 -0
  117. package/src/three/QuantumField/styles/styleA.ts +526 -0
  118. package/src/three/QuantumField/styles/styleB.ts +1210 -0
  119. package/src/three/QuantumField/styles/styleC.ts +266 -0
  120. package/src/three/QuantumField/themes.ts +211 -0
  121. package/src/three/QuantumField/types.ts +380 -0
  122. package/src/three/SOCCommandCenter/SOCCommandCenter.stories.tsx +591 -0
  123. package/src/three/SOCCommandCenter/SOCCommandCenter.tsx +248 -0
  124. package/src/three/SOCCommandCenter/index.ts +26 -0
  125. package/src/three/SOCCommandCenter/types.ts +201 -0
  126. package/src/three/SecurityDashboard/SecurityDashboard.stories.tsx +508 -0
  127. package/src/three/SecurityDashboard/SecurityDashboard.tsx +507 -0
  128. package/src/three/SecurityDashboard/index.ts +37 -0
  129. package/src/three/SecurityDashboard/types.ts +143 -0
  130. package/src/three/SecurityShield/SecurityShield.stories.tsx +257 -0
  131. package/src/three/SecurityShield/SecurityShield.tsx +502 -0
  132. package/src/three/SecurityShield/index.ts +25 -0
  133. package/src/three/SecurityShield/types.ts +64 -0
  134. package/src/three/Sentinel/AvatarMode.tsx +578 -0
  135. package/src/three/Sentinel/AvatarRenderer.tsx +199 -0
  136. package/src/three/Sentinel/CameraPip.tsx +127 -0
  137. package/src/three/Sentinel/CardinalItem.tsx +83 -0
  138. package/src/three/Sentinel/CardinalMenu.tsx +370 -0
  139. package/src/three/Sentinel/DockedMiniOrb.tsx +146 -0
  140. package/src/three/Sentinel/RadialSubmenu.tsx +273 -0
  141. package/src/three/Sentinel/SentinelConversation.tsx +802 -0
  142. package/src/three/Sentinel/SentinelOrb.tsx +316 -0
  143. package/src/three/Sentinel/SentinelOverlay.tsx +146 -0
  144. package/src/three/Sentinel/SentinelProvider.tsx +145 -0
  145. package/src/three/Sentinel/SentinelTether.tsx +182 -0
  146. package/src/three/Sentinel/SigilPlaceholder.tsx +176 -0
  147. package/src/three/Sentinel/VerticalSubmenu.tsx +150 -0
  148. package/src/three/Sentinel/index.ts +145 -0
  149. package/src/three/Sentinel/sentinelStore.ts +196 -0
  150. package/src/three/Sentinel/types.ts +403 -0
  151. package/src/three/Sentinel/useCameraPermission.ts +153 -0
  152. package/src/three/Sentinel/useThrowPhysics.ts +220 -0
  153. package/src/three/SpatialWorkspace/CyntraWorkspace.tsx +84 -0
  154. package/src/three/SpatialWorkspace/JobCluster.tsx +281 -0
  155. package/src/three/SpatialWorkspace/NodeGraph.tsx +236 -0
  156. package/src/three/SpatialWorkspace/ReceiptOrbit.tsx +368 -0
  157. package/src/three/SpatialWorkspace/SpatialWorkspace.stories.tsx +547 -0
  158. package/src/three/SpatialWorkspace/SpatialWorkspace.tsx +428 -0
  159. package/src/three/SpatialWorkspace/TrustRings.tsx +228 -0
  160. package/src/three/SpatialWorkspace/adapters.ts +353 -0
  161. package/src/three/SpatialWorkspace/index.ts +85 -0
  162. package/src/three/SpatialWorkspace/nexusAdapter.ts +182 -0
  163. package/src/three/SpatialWorkspace/types.ts +389 -0
  164. package/src/three/ThreatRadar/ThreatRadar.stories.tsx +451 -0
  165. package/src/three/ThreatRadar/ThreatRadar.tsx +542 -0
  166. package/src/three/ThreatRadar/index.ts +8 -0
  167. package/src/three/ThreatRadar/types.ts +90 -0
  168. package/src/three/ThreeErrorBoundary/ThreeErrorBoundary.tsx +235 -0
  169. package/src/three/ThreeErrorBoundary/index.ts +5 -0
  170. package/src/three/index.ts +56 -0
  171. package/tsconfig.json +20 -0
  172. package/tsup.config.ts +21 -0
@@ -0,0 +1,428 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SpatialWorkspace - 3D container for Backbay entity visualization
5
+ *
6
+ * Integrates JobCluster, NodeGraph, ReceiptOrbit, TrustRings, and
7
+ * camera controls into a unified spatial workspace experience.
8
+ */
9
+
10
+ import * as React from "react";
11
+ import { Canvas, useThree } from "@react-three/fiber";
12
+ import { OrbitControls } from "@react-three/drei";
13
+ import type { Job, Node, Receipt, Dispute, TrustTier } from "@backbay/contract";
14
+ import { JobCluster } from "./JobCluster";
15
+ import { NodeGraph } from "./NodeGraph";
16
+ import { ReceiptOrbit } from "./ReceiptOrbit";
17
+ import { TrustRings } from "./TrustRings";
18
+ import type {
19
+ SpatialWorkspaceProps,
20
+ WorkspaceSelection,
21
+ TopologySlice,
22
+ } from "./types";
23
+ import type { VisionTopology, VisionTopologyEdge, VisionTopologyNode } from "../../lib/vision-types";
24
+
25
+ // -----------------------------------------------------------------------------
26
+ // Workspace Scene (internal component inside Canvas)
27
+ // -----------------------------------------------------------------------------
28
+
29
+ interface WorkspaceSceneProps {
30
+ jobs: Job[];
31
+ nodes: Node[];
32
+ receipts: Receipt[];
33
+ disputes: Dispute[];
34
+ currentTrustTier: TrustTier;
35
+ selection: WorkspaceSelection[];
36
+ hovered: WorkspaceSelection | null;
37
+ onSelectionChange: (selection: WorkspaceSelection[]) => void;
38
+ onHoverChange: (hovered: WorkspaceSelection | null) => void;
39
+ filters: SpatialWorkspaceProps["filters"];
40
+ autoRotate: boolean;
41
+ enableZoom: boolean;
42
+ enablePan: boolean;
43
+ topologyId?: string;
44
+ topologyUpdateMs?: number;
45
+ onTopologyChange?: (topology: VisionTopology) => void;
46
+ onCanvasReady?: (canvas: HTMLCanvasElement) => void;
47
+ }
48
+
49
+ function WorkspaceScene({
50
+ jobs,
51
+ nodes,
52
+ receipts,
53
+ disputes: _disputes, // Reserved for DisputeBeacon component (Phase 4 extension)
54
+ currentTrustTier,
55
+ selection,
56
+ hovered,
57
+ onSelectionChange,
58
+ onHoverChange,
59
+ filters,
60
+ autoRotate,
61
+ enableZoom,
62
+ enablePan,
63
+ topologyId,
64
+ topologyUpdateMs,
65
+ onTopologyChange,
66
+ onCanvasReady,
67
+ }: WorkspaceSceneProps) {
68
+ const { gl } = useThree();
69
+
70
+ React.useEffect(() => {
71
+ if (!onCanvasReady) return;
72
+ onCanvasReady(gl.domElement);
73
+ }, [gl, onCanvasReady]);
74
+ const controlsRef = React.useRef<any>(null);
75
+
76
+ // Extract selected IDs by type
77
+ const selectedJobIds = selection.filter((s) => s.type === "job").map((s) => s.id);
78
+ const selectedNodeIds = selection.filter((s) => s.type === "node").map((s) => s.id);
79
+ const selectedReceiptIds = selection.filter((s) => s.type === "receipt").map((s) => s.id);
80
+
81
+ // Hovered IDs
82
+ const hoveredJobId = hovered?.type === "job" ? hovered.id : null;
83
+ const hoveredNodeId = hovered?.type === "node" ? hovered.id : null;
84
+ const hoveredReceiptId = hovered?.type === "receipt" ? hovered.id : null;
85
+
86
+ // Filter jobs and nodes based on filters
87
+ const filteredJobs = React.useMemo(() => {
88
+ if (!filters?.showJobs) return [];
89
+ let filtered = jobs;
90
+ if (filters?.jobStatuses?.length) {
91
+ filtered = filtered.filter((j) => filters.jobStatuses!.includes(j.status));
92
+ }
93
+ return filtered;
94
+ }, [jobs, filters]);
95
+
96
+ const filteredNodes = React.useMemo(() => {
97
+ if (!filters?.showNodes) return [];
98
+ let filtered = nodes;
99
+ if (filters?.nodeTypes?.length) {
100
+ filtered = filtered.filter((n) => filters.nodeTypes!.includes(n.type));
101
+ }
102
+ if (filters?.nodeStatuses?.length) {
103
+ filtered = filtered.filter((n) => filters.nodeStatuses!.includes(n.status));
104
+ }
105
+ if (filters?.trustTiers?.length) {
106
+ filtered = filtered.filter((n) => filters.trustTiers!.includes(n.trust_tier));
107
+ }
108
+ return filtered;
109
+ }, [nodes, filters]);
110
+
111
+ const filteredReceipts = React.useMemo(() => {
112
+ if (!filters?.showReceipts) return [];
113
+ let filtered = receipts;
114
+ if (filters?.receiptStatuses?.length) {
115
+ filtered = filtered.filter((r) => filters.receiptStatuses!.includes(r.status));
116
+ }
117
+ return filtered;
118
+ }, [receipts, filters]);
119
+
120
+ const topologySlicesRef = React.useRef<{
121
+ jobs?: TopologySlice;
122
+ nodes?: TopologySlice;
123
+ receipts?: TopologySlice;
124
+ }>({});
125
+
126
+ const lastTopologyEmitRef = React.useRef(0);
127
+
128
+ const emitTopology = React.useCallback(() => {
129
+ if (!onTopologyChange) return;
130
+ const now = Date.now();
131
+ if (topologyUpdateMs && now - lastTopologyEmitRef.current < topologyUpdateMs) {
132
+ return;
133
+ }
134
+ lastTopologyEmitRef.current = now;
135
+
136
+ const slices = Object.values(topologySlicesRef.current).filter(Boolean) as TopologySlice[];
137
+ const nodesMap = new Map<string, VisionTopologyNode>();
138
+ const edges: VisionTopologyEdge[] = [];
139
+
140
+ slices.forEach((slice) => {
141
+ slice.nodes.forEach((node) => nodesMap.set(node.id, node));
142
+ slice.edges?.forEach((edge) => edges.push(edge));
143
+ });
144
+
145
+ const topology: VisionTopology = {
146
+ id: topologyId,
147
+ source: "SpatialWorkspace",
148
+ updatedAt: now,
149
+ nodes: Array.from(nodesMap.values()),
150
+ edges,
151
+ meta: {
152
+ currentTrustTier,
153
+ filters,
154
+ },
155
+ };
156
+
157
+ onTopologyChange(topology);
158
+ }, [onTopologyChange, topologyId, topologyUpdateMs, currentTrustTier, filters]);
159
+
160
+ const handleTopologyChange = React.useCallback(
161
+ (key: "jobs" | "nodes" | "receipts", slice: TopologySlice) => {
162
+ topologySlicesRef.current[key] = slice;
163
+ emitTopology();
164
+ },
165
+ [emitTopology]
166
+ );
167
+
168
+ // Selection handlers
169
+ const handleJobClick = (job: Job) => {
170
+ const existing = selection.find((s) => s.type === "job" && s.id === job.id);
171
+ if (existing) {
172
+ onSelectionChange(selection.filter((s) => s !== existing));
173
+ } else {
174
+ onSelectionChange([...selection, { type: "job", id: job.id }]);
175
+ }
176
+ };
177
+
178
+ const handleNodeClick = (node: Node) => {
179
+ const existing = selection.find((s) => s.type === "node" && s.id === node.id);
180
+ if (existing) {
181
+ onSelectionChange(selection.filter((s) => s !== existing));
182
+ } else {
183
+ onSelectionChange([...selection, { type: "node", id: node.id }]);
184
+ }
185
+ };
186
+
187
+ const handleReceiptClick = (receipt: Receipt) => {
188
+ const existing = selection.find((s) => s.type === "receipt" && s.id === receipt.id);
189
+ if (existing) {
190
+ onSelectionChange(selection.filter((s) => s !== existing));
191
+ } else {
192
+ onSelectionChange([...selection, { type: "receipt", id: receipt.id }]);
193
+ }
194
+ };
195
+
196
+ // Hover handlers
197
+ const handleJobHover = (job: Job | null) => {
198
+ onHoverChange(job ? { type: "job", id: job.id } : null);
199
+ };
200
+
201
+ const handleNodeHover = (node: Node | null) => {
202
+ onHoverChange(node ? { type: "node", id: node.id } : null);
203
+ };
204
+
205
+ const handleReceiptHover = (receipt: Receipt | null) => {
206
+ onHoverChange(receipt ? { type: "receipt", id: receipt.id } : null);
207
+ };
208
+
209
+ // Click on background to clear selection
210
+ const handleBackgroundClick = () => {
211
+ onSelectionChange([]);
212
+ };
213
+
214
+ return (
215
+ <>
216
+ {/* Camera controls */}
217
+ <OrbitControls
218
+ ref={controlsRef}
219
+ autoRotate={autoRotate}
220
+ autoRotateSpeed={0.5}
221
+ enableDamping
222
+ dampingFactor={0.05}
223
+ minDistance={5}
224
+ maxDistance={30}
225
+ maxPolarAngle={Math.PI / 2 + 0.3}
226
+ enableZoom={enableZoom}
227
+ enablePan={enablePan}
228
+ />
229
+
230
+ {/* Ambient and directional lighting */}
231
+ <ambientLight intensity={0.4} />
232
+ <directionalLight position={[10, 10, 5]} intensity={0.8} />
233
+ <directionalLight position={[-10, -10, -5]} intensity={0.3} />
234
+
235
+ {/* Background click plane */}
236
+ <mesh
237
+ position={[0, -5, 0]}
238
+ rotation={[-Math.PI / 2, 0, 0]}
239
+ onClick={handleBackgroundClick}
240
+ >
241
+ <planeGeometry args={[100, 100]} />
242
+ <meshBasicMaterial visible={false} />
243
+ </mesh>
244
+
245
+ {/* Trust Rings (background layer) */}
246
+ {filters?.showTrustRings && (
247
+ <TrustRings
248
+ currentTier={currentTrustTier}
249
+ position={[0, -2, 0]}
250
+ showLabels={true}
251
+ interactive={false}
252
+ />
253
+ )}
254
+
255
+ {/* Job Clusters */}
256
+ {filteredJobs.length > 0 && (
257
+ <JobCluster
258
+ jobs={filteredJobs}
259
+ selectedIds={selectedJobIds}
260
+ hoveredId={hoveredJobId}
261
+ onJobClick={handleJobClick}
262
+ onJobHover={handleJobHover}
263
+ position={[-4, 0, 0]}
264
+ onTopologyChange={(slice) => handleTopologyChange("jobs", slice)}
265
+ />
266
+ )}
267
+
268
+ {/* Node Graph */}
269
+ {filteredNodes.length > 0 && (
270
+ <NodeGraph
271
+ nodes={filteredNodes}
272
+ selectedIds={selectedNodeIds}
273
+ hoveredId={hoveredNodeId}
274
+ onNodeClick={handleNodeClick}
275
+ onNodeHover={handleNodeHover}
276
+ showConnections={true}
277
+ position={[4, 0, 0]}
278
+ onTopologyChange={(slice) => handleTopologyChange("nodes", slice)}
279
+ />
280
+ )}
281
+
282
+ {/* Receipt Orbits - group by job */}
283
+ {filteredReceipts.length > 0 && filteredJobs.length > 0 && (
284
+ <group position={[-4, 0, 0]}>
285
+ {filteredJobs
286
+ .filter((job) => job.status === "running" || job.status === "completed")
287
+ .slice(0, 3) // Limit for performance
288
+ .map((job, index) => {
289
+ const jobReceipts = filteredReceipts.filter((r) => r.job_id === job.id);
290
+ if (jobReceipts.length === 0) return null;
291
+
292
+ // Position receipts near the center (running jobs area)
293
+ const angle = (index / 3) * Math.PI * 2 - Math.PI / 2;
294
+ const distance = 0.8;
295
+
296
+ return (
297
+ <ReceiptOrbit
298
+ key={job.id}
299
+ receipts={jobReceipts}
300
+ parentPosition={[
301
+ Math.cos(angle) * distance,
302
+ 0,
303
+ Math.sin(angle) * distance,
304
+ ]}
305
+ orbitRadius={0.5}
306
+ selectedIds={selectedReceiptIds}
307
+ hoveredId={hoveredReceiptId}
308
+ onReceiptClick={handleReceiptClick}
309
+ onReceiptHover={handleReceiptHover}
310
+ origin={[-4, 0, 0]}
311
+ onTopologyChange={(slice) => handleTopologyChange("receipts", slice)}
312
+ />
313
+ );
314
+ })}
315
+ </group>
316
+ )}
317
+
318
+ {/* Grid helper for spatial reference */}
319
+ <gridHelper args={[20, 20, "#1a1a2e", "#1a1a2e"]} position={[0, -3, 0]} />
320
+ </>
321
+ );
322
+ }
323
+
324
+ // -----------------------------------------------------------------------------
325
+ // Main SpatialWorkspace Component
326
+ // -----------------------------------------------------------------------------
327
+
328
+ export function SpatialWorkspace({
329
+ jobs = [],
330
+ nodes = [],
331
+ receipts = [],
332
+ disputes = [],
333
+ currentTrustTier = "bronze",
334
+ selection = [],
335
+ onSelectionChange,
336
+ hovered = null,
337
+ onHoverChange,
338
+ topologyId,
339
+ topologyUpdateMs,
340
+ onTopologyChange,
341
+ onCanvasReady,
342
+ filters = {
343
+ showJobs: true,
344
+ showNodes: true,
345
+ showReceipts: true,
346
+ showDisputes: true,
347
+ showTrustRings: true,
348
+ },
349
+ autoRotate = true,
350
+ enableZoom = true,
351
+ enablePan = true,
352
+ className,
353
+ style,
354
+ }: SpatialWorkspaceProps) {
355
+ // Internal state if no external handlers
356
+ const [internalSelection, setInternalSelection] = React.useState<WorkspaceSelection[]>([]);
357
+ const [internalHovered, setInternalHovered] = React.useState<WorkspaceSelection | null>(null);
358
+
359
+ const effectiveSelection = onSelectionChange ? selection : internalSelection;
360
+ const effectiveHovered = onHoverChange ? hovered : internalHovered;
361
+ const handleSelectionChange = onSelectionChange || setInternalSelection;
362
+ const handleHoverChange = onHoverChange || setInternalHovered;
363
+
364
+ return (
365
+ <div
366
+ className={className}
367
+ style={{
368
+ position: "relative",
369
+ width: "100%",
370
+ height: "100%",
371
+ background: "linear-gradient(to bottom, #050812, #0a0f1a)",
372
+ ...style,
373
+ }}
374
+ >
375
+ <Canvas
376
+ camera={{
377
+ position: [0, 8, 12],
378
+ fov: 50,
379
+ near: 0.1,
380
+ far: 100,
381
+ }}
382
+ dpr={[1, 2]}
383
+ >
384
+ <WorkspaceScene
385
+ jobs={jobs}
386
+ nodes={nodes}
387
+ receipts={receipts}
388
+ disputes={disputes}
389
+ currentTrustTier={currentTrustTier}
390
+ selection={effectiveSelection}
391
+ hovered={effectiveHovered}
392
+ onSelectionChange={handleSelectionChange}
393
+ onHoverChange={handleHoverChange}
394
+ filters={filters}
395
+ autoRotate={autoRotate}
396
+ enableZoom={enableZoom}
397
+ enablePan={enablePan}
398
+ topologyId={topologyId}
399
+ topologyUpdateMs={topologyUpdateMs}
400
+ onTopologyChange={onTopologyChange}
401
+ onCanvasReady={onCanvasReady}
402
+ />
403
+ </Canvas>
404
+
405
+ {/* Selection info panel */}
406
+ {effectiveSelection.length > 0 && (
407
+ <div className="absolute bottom-4 left-4 bg-black/80 text-white text-xs font-mono p-3 rounded">
408
+ <div className="text-white/60 mb-1">SELECTED ({effectiveSelection.length})</div>
409
+ {effectiveSelection.slice(0, 5).map((s) => (
410
+ <div key={`${s.type}-${s.id}`} className="text-cyan-400">
411
+ {s.type.toUpperCase()}: {s.id.slice(0, 8)}
412
+ </div>
413
+ ))}
414
+ {effectiveSelection.length > 5 && (
415
+ <div className="text-white/40">+{effectiveSelection.length - 5} more</div>
416
+ )}
417
+ </div>
418
+ )}
419
+
420
+ {/* Stats overlay */}
421
+ <div className="absolute top-4 right-4 bg-black/60 text-white/60 text-xs font-mono p-2 rounded">
422
+ <div>Jobs: {jobs.length}</div>
423
+ <div>Nodes: {nodes.length}</div>
424
+ <div>Receipts: {receipts.length}</div>
425
+ </div>
426
+ </div>
427
+ );
428
+ }
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TrustRings - Concentric rings representing trust tiers
5
+ *
6
+ * Three tiers (bronze, silver, gold) displayed as metallic rings
7
+ * with the user's current tier highlighted.
8
+ */
9
+
10
+ import * as React from "react";
11
+ import { useFrame } from "@react-three/fiber";
12
+ import { Html } from "@react-three/drei";
13
+ import * as THREE from "three";
14
+ import type { TrustTier } from "@backbay/contract";
15
+ import { TRUST_RING_CONFIG } from "./types";
16
+ import type { TrustRingsProps } from "./types";
17
+
18
+ // -----------------------------------------------------------------------------
19
+ // Single Trust Ring
20
+ // -----------------------------------------------------------------------------
21
+
22
+ interface TrustRingProps {
23
+ tier: TrustTier;
24
+ isCurrent: boolean;
25
+ isHovered: boolean;
26
+ interactive: boolean;
27
+ showLabel: boolean;
28
+ onClick: () => void;
29
+ onHover: (hovered: boolean) => void;
30
+ }
31
+
32
+ function TrustRing({
33
+ tier,
34
+ isCurrent,
35
+ isHovered,
36
+ interactive,
37
+ showLabel,
38
+ onClick,
39
+ onHover,
40
+ }: TrustRingProps) {
41
+ const meshRef = React.useRef<THREE.Mesh>(null);
42
+ const glowRef = React.useRef<THREE.Mesh>(null);
43
+ const config = TRUST_RING_CONFIG[tier];
44
+
45
+ const tubeRadius = 0.05;
46
+
47
+ // Slow rotation and current tier effects
48
+ useFrame((state) => {
49
+ if (!meshRef.current) return;
50
+
51
+ // Slow rotation
52
+ meshRef.current.rotation.z = state.clock.elapsedTime * 0.05;
53
+
54
+ // Pulse for current tier
55
+ if (isCurrent && glowRef.current) {
56
+ const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.2 + 0.8;
57
+ glowRef.current.scale.setScalar(pulse);
58
+ }
59
+ });
60
+
61
+ const color = new THREE.Color(config.color);
62
+
63
+ return (
64
+ <group>
65
+ {/* Main ring */}
66
+ <mesh
67
+ ref={meshRef}
68
+ rotation={[Math.PI / 2, 0, 0]}
69
+ onClick={interactive ? (e) => {
70
+ e.stopPropagation();
71
+ onClick();
72
+ } : undefined}
73
+ onPointerOver={interactive ? (e) => {
74
+ e.stopPropagation();
75
+ onHover(true);
76
+ document.body.style.cursor = "pointer";
77
+ } : undefined}
78
+ onPointerOut={interactive ? () => {
79
+ onHover(false);
80
+ document.body.style.cursor = "auto";
81
+ } : undefined}
82
+ >
83
+ <torusGeometry args={[config.radius, tubeRadius, 16, 64]} />
84
+ <meshStandardMaterial
85
+ color={color}
86
+ metalness={config.metalness}
87
+ roughness={config.roughness}
88
+ transparent
89
+ opacity={isCurrent ? 1.0 : isHovered ? 0.8 : 0.4}
90
+ emissive={color}
91
+ emissiveIntensity={isCurrent ? 0.3 : isHovered ? 0.2 : 0.05}
92
+ />
93
+ </mesh>
94
+
95
+ {/* Glow ring for current tier */}
96
+ {isCurrent && (
97
+ <mesh ref={glowRef} rotation={[Math.PI / 2, 0, 0]}>
98
+ <torusGeometry args={[config.radius, tubeRadius * 2, 16, 64]} />
99
+ <meshBasicMaterial
100
+ color={color}
101
+ transparent
102
+ opacity={0.15}
103
+ />
104
+ </mesh>
105
+ )}
106
+
107
+ {/* Label */}
108
+ {(showLabel || isHovered) && (
109
+ <Html
110
+ position={[config.radius + 0.3, 0, 0]}
111
+ center
112
+ style={{ pointerEvents: "none" }}
113
+ >
114
+ <div className={`text-xs font-mono px-2 py-1 rounded ${
115
+ isCurrent ? "bg-black/80" : "bg-black/60"
116
+ }`}>
117
+ <span style={{ color: config.color }} className="font-bold">
118
+ {tier.toUpperCase()}
119
+ </span>
120
+ {isCurrent && (
121
+ <span className="text-white/60 ml-2">← YOU</span>
122
+ )}
123
+ </div>
124
+ </Html>
125
+ )}
126
+
127
+ {/* Tier markers around the ring */}
128
+ {isCurrent && (
129
+ <group rotation={[Math.PI / 2, 0, 0]}>
130
+ {[0, 1, 2, 3].map((i) => {
131
+ const angle = (i / 4) * Math.PI * 2;
132
+ const x = Math.cos(angle) * config.radius;
133
+ const y = Math.sin(angle) * config.radius;
134
+ return (
135
+ <mesh key={i} position={[x, y, 0]}>
136
+ <sphereGeometry args={[0.06, 16, 16]} />
137
+ <meshStandardMaterial
138
+ color={color}
139
+ metalness={config.metalness}
140
+ roughness={config.roughness}
141
+ emissive={color}
142
+ emissiveIntensity={0.5}
143
+ />
144
+ </mesh>
145
+ );
146
+ })}
147
+ </group>
148
+ )}
149
+ </group>
150
+ );
151
+ }
152
+
153
+ // -----------------------------------------------------------------------------
154
+ // Trust Rings Container
155
+ // -----------------------------------------------------------------------------
156
+
157
+ export function TrustRings({
158
+ currentTier,
159
+ position = [0, 0, 0],
160
+ showLabels = true,
161
+ interactive = true,
162
+ onTierClick,
163
+ }: TrustRingsProps) {
164
+ const [hoveredTier, setHoveredTier] = React.useState<TrustTier | null>(null);
165
+ const tiers: TrustTier[] = ["bronze", "silver", "gold"];
166
+
167
+ return (
168
+ <group position={position}>
169
+ {/* Base plane for visual grounding */}
170
+ <mesh rotation={[Math.PI / 2, 0, 0]} position={[0, -0.1, 0]}>
171
+ <ringGeometry args={[0.5, TRUST_RING_CONFIG.gold.radius + 0.5, 64]} />
172
+ <meshBasicMaterial
173
+ color="#000000"
174
+ transparent
175
+ opacity={0.3}
176
+ side={THREE.DoubleSide}
177
+ />
178
+ </mesh>
179
+
180
+ {/* Trust rings */}
181
+ {tiers.map((tier) => (
182
+ <TrustRing
183
+ key={tier}
184
+ tier={tier}
185
+ isCurrent={tier === currentTier}
186
+ isHovered={hoveredTier === tier}
187
+ interactive={interactive}
188
+ showLabel={showLabels}
189
+ onClick={() => onTierClick?.(tier)}
190
+ onHover={(hovered) => setHoveredTier(hovered ? tier : null)}
191
+ />
192
+ ))}
193
+
194
+ {/* Center indicator */}
195
+ <mesh>
196
+ <sphereGeometry args={[0.15, 32, 32]} />
197
+ <meshStandardMaterial
198
+ color={TRUST_RING_CONFIG[currentTier].color}
199
+ metalness={0.8}
200
+ roughness={0.2}
201
+ emissive={new THREE.Color(TRUST_RING_CONFIG[currentTier].color)}
202
+ emissiveIntensity={0.3}
203
+ />
204
+ </mesh>
205
+
206
+ {/* Connection lines from center to current ring */}
207
+ {[0, Math.PI / 2, Math.PI, -Math.PI / 2].map((angle, i) => {
208
+ const endRadius = TRUST_RING_CONFIG[currentTier].radius;
209
+ const endX = Math.cos(angle) * endRadius;
210
+ const endZ = Math.sin(angle) * endRadius;
211
+
212
+ const points = [new THREE.Vector3(0, 0, 0), new THREE.Vector3(endX, 0, endZ)];
213
+ const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
214
+
215
+ return (
216
+ <line key={`line-${i}`}>
217
+ <primitive object={lineGeometry} attach="geometry" />
218
+ <lineBasicMaterial
219
+ color={TRUST_RING_CONFIG[currentTier].color}
220
+ transparent
221
+ opacity={0.2}
222
+ />
223
+ </line>
224
+ );
225
+ })}
226
+ </group>
227
+ );
228
+ }