ai-agent-session-center 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +484 -429
- package/docs/3D/ADAPTATION_GUIDE.md +592 -0
- package/docs/3D/index.html +754 -0
- package/docs/AGENT_TEAM_TASKS.md +716 -0
- package/docs/CYBERDROME_V2_SPEC.md +531 -0
- package/docs/ERROR_185_ANALYSIS.md +263 -0
- package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
- package/docs/SESSION_DETAIL_FEATURES.md +98 -0
- package/docs/_3d_multimedia_features.md +1080 -0
- package/docs/_frontend_features.md +1057 -0
- package/docs/_server_features.md +1077 -0
- package/docs/session-duplication-fixes.md +271 -0
- package/docs/session-terminal-linkage.md +412 -0
- package/package.json +63 -5
- package/public/apple-touch-icon.svg +21 -0
- package/public/css/dashboard.css +0 -161
- package/public/css/detail-panel.css +25 -0
- package/public/css/layout.css +18 -1
- package/public/css/modals.css +0 -26
- package/public/css/settings.css +0 -150
- package/public/css/terminal.css +34 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +6 -26
- package/public/js/alarmManager.js +0 -21
- package/public/js/app.js +21 -7
- package/public/js/detailPanel.js +63 -64
- package/public/js/historyPanel.js +61 -55
- package/public/js/quickActions.js +132 -48
- package/public/js/sessionCard.js +5 -20
- package/public/js/sessionControls.js +8 -0
- package/public/js/settingsManager.js +0 -142
- package/server/apiRouter.js +60 -15
- package/server/apiRouter.ts +774 -0
- package/server/approvalDetector.ts +94 -0
- package/server/authManager.ts +144 -0
- package/server/autoIdleManager.ts +110 -0
- package/server/config.ts +121 -0
- package/server/constants.ts +150 -0
- package/server/db.ts +475 -0
- package/server/hookInstaller.d.ts +3 -0
- package/server/hookProcessor.ts +108 -0
- package/server/hookRouter.ts +18 -0
- package/server/hookStats.ts +116 -0
- package/server/index.js +15 -1
- package/server/index.ts +230 -0
- package/server/logger.ts +75 -0
- package/server/mqReader.ts +349 -0
- package/server/portManager.ts +55 -0
- package/server/processMonitor.ts +239 -0
- package/server/serverConfig.ts +29 -0
- package/server/sessionMatcher.js +17 -6
- package/server/sessionMatcher.ts +403 -0
- package/server/sessionStore.js +109 -3
- package/server/sessionStore.ts +1145 -0
- package/server/sshManager.js +167 -24
- package/server/sshManager.ts +671 -0
- package/server/teamManager.ts +289 -0
- package/server/wsManager.ts +200 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# Cyberdrome 3D Robot → Agent Manager Adaptation Guide
|
|
2
|
+
|
|
3
|
+
Replace the 20 CSS-only 2D character models on session cards with interactive Three.js 3D robotic characters driven by real session state.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Architecture Overview
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
CURRENT (agent-manager) TARGET
|
|
11
|
+
─────────────────────────────────────────────────────────────────
|
|
12
|
+
SessionCard SessionCard
|
|
13
|
+
└─ .robotViewport (empty div) └─ <Robot3DViewport>
|
|
14
|
+
↓ styled by CSS ↓ Three.js canvas
|
|
15
|
+
CharacterModel (20 CSS divs) Robot3D (boxy mech)
|
|
16
|
+
data-status → CSS keyframes sessionStatus → 3D state machine
|
|
17
|
+
--robot-color → CSS var neonColor → emissive material
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### What Gets Replaced
|
|
21
|
+
|
|
22
|
+
| Current | Replacement |
|
|
23
|
+
|---------|-------------|
|
|
24
|
+
| `src/components/character/CharacterModel.tsx` | `src/components/character/Robot3DModel.tsx` |
|
|
25
|
+
| `src/components/character/RobotViewport.tsx` | `src/components/character/Robot3DViewport.tsx` |
|
|
26
|
+
| `src/components/character/CharacterSelector.tsx` | Updated to show 3D previews |
|
|
27
|
+
| `src/styles/characters/*.css` (22 files, ~80KB) | Removed (Three.js handles visuals) |
|
|
28
|
+
| `src/styles/animations.css` (48KB) | Reduced (keep card glow/border effects only) |
|
|
29
|
+
|
|
30
|
+
### What Stays Unchanged
|
|
31
|
+
|
|
32
|
+
- `SessionCard.tsx` — only the `.robotViewport` child changes
|
|
33
|
+
- `SessionCard.module.css` — card border/glow/status-badge CSS stays
|
|
34
|
+
- `sessionStore.ts`, `settingsStore.ts` — no schema changes
|
|
35
|
+
- `session.ts` types — `AnimationState` finally gets used
|
|
36
|
+
- Server/WebSocket/API — zero backend changes
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Technical Stack Delta
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
NEW DEPENDENCIES
|
|
44
|
+
────────────────
|
|
45
|
+
three ^0.160.0 # 3D engine
|
|
46
|
+
@react-three/fiber ^8.15.0 # React reconciler for Three.js
|
|
47
|
+
@react-three/drei ^9.92.0 # Helpers (OrbitControls, useGLTF, etc.)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd /Users/kasonzhan/Documents/claude/agent-manager
|
|
52
|
+
npm install three @react-three/fiber @react-three/drei
|
|
53
|
+
npm install -D @types/three
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Session Status → 3D State Machine
|
|
59
|
+
|
|
60
|
+
The current CSS system drives animations via `data-status`. The 3D system maps the same `SessionStatus` values to robot behavior states:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// src/lib/robotStateMap.ts
|
|
64
|
+
|
|
65
|
+
import type { SessionStatus } from '@/types/session'
|
|
66
|
+
|
|
67
|
+
export type Robot3DState =
|
|
68
|
+
| 'idle' // gentle hover, slow blink, antenna pulse
|
|
69
|
+
| 'thinking' // head tilt, eye glow cycle, antenna fast pulse
|
|
70
|
+
| 'working' // seated at desk, typing animation, sweat particles
|
|
71
|
+
| 'waiting' // standing, looking around, slow bounce
|
|
72
|
+
| 'alert' // urgent bounce, visor flash yellow, floating "!"
|
|
73
|
+
| 'input' // gentle sway, visor flash purple, floating "?"
|
|
74
|
+
| 'offline' // powered down, dim materials, no glow
|
|
75
|
+
| 'connecting' // boot-up sequence, parts assembling
|
|
76
|
+
|
|
77
|
+
export function mapStatusTo3D(status: SessionStatus): Robot3DState {
|
|
78
|
+
const map: Record<SessionStatus, Robot3DState> = {
|
|
79
|
+
idle: 'idle',
|
|
80
|
+
prompting: 'thinking',
|
|
81
|
+
working: 'working',
|
|
82
|
+
waiting: 'waiting',
|
|
83
|
+
approval: 'alert',
|
|
84
|
+
input: 'input',
|
|
85
|
+
ended: 'offline',
|
|
86
|
+
connecting: 'connecting',
|
|
87
|
+
}
|
|
88
|
+
return map[status]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Animation Details Per State
|
|
93
|
+
|
|
94
|
+
| Robot3DState | Body | Arms | Legs | Visor | Core | Special |
|
|
95
|
+
|---|---|---|---|---|---|---|
|
|
96
|
+
| `idle` | gentle Y bob (sin, 0.03 amp) | rest at sides | standing | neon color, slow pulse | slow pulse | antenna tip pulse |
|
|
97
|
+
| `thinking` | slight lean forward | one arm to chin | standing | brighter, faster pulse | fast pulse | head tilt L/R |
|
|
98
|
+
| `working` | seated (y=-0.12) | typing oscillation (±0.05) | bent forward (1.2 rad) | steady bright | rapid pulse | sweat particle emitter* |
|
|
99
|
+
| `waiting` | bounce (sin, 0.08 amp) | slight swing | standing | blue tint | blue pulse | look-around head rotation |
|
|
100
|
+
| `alert` | urgent bounce (fast) | raised | standing | **yellow**, flash | **yellow**, urgent | floating "!" mesh, shake |
|
|
101
|
+
| `input` | gentle sway | one raised (question) | standing | **purple**, gentle | **purple** | floating "?" mesh |
|
|
102
|
+
| `offline` | static, y=-0.05 | limp at sides | standing | **grey**, no glow | **off** | dim all emissive to 0 |
|
|
103
|
+
| `connecting` | scale 0→1 over 1s | animate into position | animate into position | flicker on | boot pulse | parts "assemble" |
|
|
104
|
+
|
|
105
|
+
*Sweat particle = small sphere emitter near head, matches the CSS sweat-drop effect
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## File-by-File Implementation Plan
|
|
110
|
+
|
|
111
|
+
### 1. `src/lib/robot3DGeometry.ts` — Shared Geometry + Materials
|
|
112
|
+
|
|
113
|
+
Extract from `index.html` lines 430-460. All geometries created once, shared across all robot instances.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// src/lib/robot3DGeometry.ts
|
|
117
|
+
import * as THREE from 'three'
|
|
118
|
+
|
|
119
|
+
// Shared geometries (create once, reuse for every robot)
|
|
120
|
+
export const robotGeo = {
|
|
121
|
+
head: new THREE.BoxGeometry(0.28, 0.24, 0.26),
|
|
122
|
+
visor: new THREE.BoxGeometry(0.24, 0.065, 0.02),
|
|
123
|
+
antenna: new THREE.CylinderGeometry(0.007, 0.007, 0.14, 4),
|
|
124
|
+
aTip: new THREE.SphereGeometry(0.02, 6, 6),
|
|
125
|
+
torso: new THREE.BoxGeometry(0.32, 0.38, 0.2),
|
|
126
|
+
core: new THREE.SphereGeometry(0.032, 8, 8),
|
|
127
|
+
joint: new THREE.SphereGeometry(0.035, 8, 8),
|
|
128
|
+
arm: new THREE.BoxGeometry(0.08, 0.26, 0.08),
|
|
129
|
+
leg: new THREE.BoxGeometry(0.09, 0.28, 0.09),
|
|
130
|
+
foot: new THREE.BoxGeometry(0.1, 0.045, 0.12),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const robotEdgeGeo = {
|
|
134
|
+
head: new THREE.EdgesGeometry(robotGeo.head),
|
|
135
|
+
torso: new THREE.EdgesGeometry(robotGeo.torso),
|
|
136
|
+
arm: new THREE.EdgesGeometry(robotGeo.arm),
|
|
137
|
+
leg: new THREE.EdgesGeometry(robotGeo.leg),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Shared metallic body materials (same for all robots)
|
|
141
|
+
export const metalMat = new THREE.MeshStandardMaterial({
|
|
142
|
+
color: '#2a2a3e', roughness: 0.3, metalness: 0.85,
|
|
143
|
+
})
|
|
144
|
+
export const darkMat = new THREE.MeshStandardMaterial({
|
|
145
|
+
color: '#1c1c2c', roughness: 0.4, metalness: 0.7,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Per-color neon materials (pooled by the 8-color palette)
|
|
149
|
+
export function createNeonMat(hex: string) {
|
|
150
|
+
const c = new THREE.Color(hex)
|
|
151
|
+
return new THREE.MeshStandardMaterial({
|
|
152
|
+
color: c, emissive: c, emissiveIntensity: 2,
|
|
153
|
+
roughness: 0.2, metalness: 0.3,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createEdgeMat(hex: string) {
|
|
158
|
+
return new THREE.LineBasicMaterial({
|
|
159
|
+
color: hex, transparent: true, opacity: 0.3,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 2. `src/components/character/Robot3DModel.tsx` — The 3D Robot Component
|
|
165
|
+
|
|
166
|
+
This is a `@react-three/fiber` component that builds the robot mesh hierarchy and runs the state-driven animation loop.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// src/components/character/Robot3DModel.tsx
|
|
170
|
+
import { useRef, useMemo } from 'react'
|
|
171
|
+
import { useFrame } from '@react-three/fiber'
|
|
172
|
+
import * as THREE from 'three'
|
|
173
|
+
import { robotGeo, robotEdgeGeo, metalMat, darkMat, createNeonMat, createEdgeMat } from '@/lib/robot3DGeometry'
|
|
174
|
+
import type { Robot3DState } from '@/lib/robotStateMap'
|
|
175
|
+
|
|
176
|
+
interface Robot3DModelProps {
|
|
177
|
+
neonColor: string // e.g. '#00f0ff'
|
|
178
|
+
state: Robot3DState // mapped from SessionStatus
|
|
179
|
+
scale?: number // default 1
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function Robot3DModel({ neonColor, state, scale = 1 }: Robot3DModelProps) {
|
|
183
|
+
const groupRef = useRef<THREE.Group>(null)
|
|
184
|
+
const armPivots = useRef<THREE.Group[]>([])
|
|
185
|
+
const legPivots = useRef<THREE.Group[]>([])
|
|
186
|
+
const bodyRef = useRef<THREE.Mesh>(null)
|
|
187
|
+
const bodyEdgeRef = useRef<THREE.LineSegments>(null)
|
|
188
|
+
const coreRef = useRef<THREE.Mesh>(null)
|
|
189
|
+
const aTipRef = useRef<THREE.Mesh>(null)
|
|
190
|
+
const glowRef = useRef<THREE.Mesh>(null)
|
|
191
|
+
const phase = useRef(Math.random() * Math.PI * 2)
|
|
192
|
+
|
|
193
|
+
const { neonMat, edgeMat } = useMemo(() => ({
|
|
194
|
+
neonMat: createNeonMat(neonColor),
|
|
195
|
+
edgeMat: createEdgeMat(neonColor),
|
|
196
|
+
}), [neonColor])
|
|
197
|
+
|
|
198
|
+
useFrame((_, delta) => {
|
|
199
|
+
if (!groupRef.current) return
|
|
200
|
+
const t = performance.now() / 1000
|
|
201
|
+
const p = phase.current
|
|
202
|
+
|
|
203
|
+
// Antenna + core pulse (always active)
|
|
204
|
+
if (aTipRef.current) {
|
|
205
|
+
aTipRef.current.scale.setScalar(
|
|
206
|
+
0.8 + ((Math.sin(t * 6 + p) + 1) * 0.5) * 0.4
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
if (coreRef.current) {
|
|
210
|
+
coreRef.current.scale.setScalar(
|
|
211
|
+
0.9 + Math.sin(t * 3 + p) * 0.12
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const [lp0, lp1] = legPivots.current
|
|
216
|
+
const [ap0, ap1] = armPivots.current
|
|
217
|
+
|
|
218
|
+
switch (state) {
|
|
219
|
+
case 'idle': {
|
|
220
|
+
groupRef.current.position.y = Math.sin(t * 2 + p) * 0.03
|
|
221
|
+
if (ap0) ap0.rotation.x *= 0.9
|
|
222
|
+
if (ap1) ap1.rotation.x *= 0.9
|
|
223
|
+
if (lp0) lp0.rotation.x *= 0.9
|
|
224
|
+
if (lp1) lp1.rotation.x *= 0.9
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
case 'thinking': {
|
|
228
|
+
groupRef.current.position.y = Math.sin(t * 2 + p) * 0.02
|
|
229
|
+
if (bodyRef.current) {
|
|
230
|
+
bodyRef.current.rotation.z = Math.sin(t * 0.8 + p) * 0.04
|
|
231
|
+
}
|
|
232
|
+
// One arm up to "chin"
|
|
233
|
+
if (ap0) ap0.rotation.x = -0.7 + Math.sin(t * 1.5 + p) * 0.05
|
|
234
|
+
if (ap1) ap1.rotation.x *= 0.9
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
case 'working': {
|
|
238
|
+
// Seated pose
|
|
239
|
+
groupRef.current.position.y = -0.12
|
|
240
|
+
if (lp0) lp0.rotation.x = 1.2
|
|
241
|
+
if (lp1) lp1.rotation.x = 1.2
|
|
242
|
+
const typing = Math.sin(t * 10 + p) * 0.05
|
|
243
|
+
if (ap0) ap0.rotation.x = -0.5 + typing
|
|
244
|
+
if (ap1) ap1.rotation.x = -0.5 - typing
|
|
245
|
+
if (bodyRef.current) {
|
|
246
|
+
bodyRef.current.rotation.z = Math.sin(t * 0.7 + p) * 0.012
|
|
247
|
+
}
|
|
248
|
+
if (glowRef.current) glowRef.current.position.y = 0.13
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
case 'waiting': {
|
|
252
|
+
groupRef.current.position.y = Math.abs(Math.sin(t * 3 + p)) * 0.08
|
|
253
|
+
if (bodyRef.current) {
|
|
254
|
+
bodyRef.current.rotation.y = Math.sin(t * 0.5 + p) * 0.3
|
|
255
|
+
}
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
case 'alert': {
|
|
259
|
+
groupRef.current.position.y = Math.abs(Math.sin(t * 6 + p)) * 0.1
|
|
260
|
+
groupRef.current.position.x = Math.sin(t * 15 + p) * 0.02
|
|
261
|
+
break
|
|
262
|
+
}
|
|
263
|
+
case 'input': {
|
|
264
|
+
groupRef.current.position.y = Math.sin(t * 1.5 + p) * 0.04
|
|
265
|
+
groupRef.current.rotation.z = Math.sin(t * 1 + p) * 0.03
|
|
266
|
+
if (ap1) ap1.rotation.x = -0.8 + Math.sin(t * 1.5) * 0.05
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
case 'offline': {
|
|
270
|
+
groupRef.current.position.y = -0.05
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
case 'connecting': {
|
|
274
|
+
const boot = Math.min(1, t % 2)
|
|
275
|
+
groupRef.current.scale.setScalar(scale * boot)
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Sync body edge rotation
|
|
281
|
+
if (bodyEdgeRef.current && bodyRef.current) {
|
|
282
|
+
bodyEdgeRef.current.rotation.copy(bodyRef.current.rotation)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Reset glow for non-sitting states
|
|
286
|
+
if (state !== 'working' && glowRef.current) {
|
|
287
|
+
glowRef.current.position.y = 0.01
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<group ref={groupRef} scale={scale}>
|
|
293
|
+
{/* HEAD */}
|
|
294
|
+
<mesh geometry={robotGeo.head} material={metalMat} position={[0, 1.32, 0]} castShadow />
|
|
295
|
+
<lineSegments geometry={robotEdgeGeo.head} material={edgeMat} position={[0, 1.32, 0]} />
|
|
296
|
+
<mesh geometry={robotGeo.visor} material={neonMat} position={[0, 1.32, 0.13]} />
|
|
297
|
+
<mesh geometry={robotGeo.antenna} material={darkMat} position={[0.05, 1.52, 0]} />
|
|
298
|
+
<mesh ref={aTipRef} geometry={robotGeo.aTip} material={neonMat} position={[0.05, 1.6, 0]} />
|
|
299
|
+
|
|
300
|
+
{/* TORSO */}
|
|
301
|
+
<mesh ref={bodyRef} geometry={robotGeo.torso} material={metalMat} position={[0, 0.87, 0]} castShadow />
|
|
302
|
+
<lineSegments ref={bodyEdgeRef} geometry={robotEdgeGeo.torso} material={edgeMat} position={[0, 0.87, 0]} />
|
|
303
|
+
<mesh ref={coreRef} geometry={robotGeo.core} material={neonMat} position={[0, 0.91, 0.105]} />
|
|
304
|
+
|
|
305
|
+
{/* SHOULDERS */}
|
|
306
|
+
{[-1, 1].map(s => (
|
|
307
|
+
<mesh key={`sh${s}`} geometry={robotGeo.joint} material={neonMat} position={[s * 0.21, 1.07, 0]} />
|
|
308
|
+
))}
|
|
309
|
+
|
|
310
|
+
{/* ARMS */}
|
|
311
|
+
{[-1, 1].map((s, i) => (
|
|
312
|
+
<group key={`arm${s}`} ref={el => { if (el) armPivots.current[i] = el }} position={[s * 0.21, 1.07, 0]}>
|
|
313
|
+
<mesh geometry={robotGeo.arm} material={darkMat} position={[0, -0.18, 0]} castShadow />
|
|
314
|
+
<lineSegments geometry={robotEdgeGeo.arm} material={edgeMat} position={[0, -0.18, 0]} />
|
|
315
|
+
</group>
|
|
316
|
+
))}
|
|
317
|
+
|
|
318
|
+
{/* HIPS */}
|
|
319
|
+
{[-1, 1].map(s => (
|
|
320
|
+
<mesh key={`hp${s}`} geometry={robotGeo.joint} material={neonMat} position={[s * 0.09, 0.54, 0]} scale={0.9} />
|
|
321
|
+
))}
|
|
322
|
+
|
|
323
|
+
{/* LEGS */}
|
|
324
|
+
{[-1, 1].map((s, i) => (
|
|
325
|
+
<group key={`leg${s}`} ref={el => { if (el) legPivots.current[i] = el }} position={[s * 0.09, 0.54, 0]}>
|
|
326
|
+
<mesh geometry={robotGeo.leg} material={darkMat} position={[0, -0.19, 0]} castShadow />
|
|
327
|
+
<lineSegments geometry={robotEdgeGeo.leg} material={edgeMat} position={[0, -0.19, 0]} />
|
|
328
|
+
<mesh geometry={robotGeo.foot} material={metalMat} position={[0, -0.36, 0.012]} castShadow />
|
|
329
|
+
</group>
|
|
330
|
+
))}
|
|
331
|
+
|
|
332
|
+
{/* GROUND GLOW (placeholder — needs CanvasTexture) */}
|
|
333
|
+
{/* <mesh ref={glowRef} rotation={[-Math.PI/2,0,0]} position={[0,0.01,0]}> ... </mesh> */}
|
|
334
|
+
</group>
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### 3. `src/components/character/Robot3DViewport.tsx` — Canvas Container
|
|
340
|
+
|
|
341
|
+
Replaces `RobotViewport.tsx`. Wraps the 3D robot in a `<Canvas>` with lighting.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// src/components/character/Robot3DViewport.tsx
|
|
345
|
+
import { useMemo } from 'react'
|
|
346
|
+
import { Canvas } from '@react-three/fiber'
|
|
347
|
+
import { Robot3DModel } from './Robot3DModel'
|
|
348
|
+
import { mapStatusTo3D } from '@/lib/robotStateMap'
|
|
349
|
+
import { useSettingsStore } from '@/stores/settingsStore'
|
|
350
|
+
import type { SessionStatus } from '@/types/session'
|
|
351
|
+
|
|
352
|
+
const COLOR_PALETTE = [
|
|
353
|
+
'#00f0ff', '#ff00aa', '#a855f7', '#00ff88',
|
|
354
|
+
'#ff4444', '#ffaa00', '#00aaff', '#ff66ff',
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
interface Robot3DViewportProps {
|
|
358
|
+
sessionId: string
|
|
359
|
+
status: SessionStatus
|
|
360
|
+
accentColor?: string
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let globalColorIdx = 0
|
|
364
|
+
|
|
365
|
+
export function Robot3DViewport({ sessionId, status, accentColor }: Robot3DViewportProps) {
|
|
366
|
+
const color = useMemo(
|
|
367
|
+
() => accentColor || COLOR_PALETTE[globalColorIdx++ % COLOR_PALETTE.length],
|
|
368
|
+
[accentColor]
|
|
369
|
+
)
|
|
370
|
+
const state3D = mapStatusTo3D(status)
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<Canvas
|
|
374
|
+
camera={{ position: [0, 1.2, 2.8], fov: 40 }}
|
|
375
|
+
gl={{ antialias: true, alpha: true }}
|
|
376
|
+
style={{ background: 'transparent' }}
|
|
377
|
+
dpr={[1, 1.5]}
|
|
378
|
+
>
|
|
379
|
+
<ambientLight intensity={0.8} color="#2a2040" />
|
|
380
|
+
<directionalLight position={[3, 5, 3]} intensity={1.5} color="#c8b8e0" />
|
|
381
|
+
<pointLight position={[-2, 2, -1]} intensity={0.6} color={color} />
|
|
382
|
+
<Robot3DModel neonColor={color} state={state3D} scale={0.85} />
|
|
383
|
+
</Canvas>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 4. Integration into SessionCard
|
|
389
|
+
|
|
390
|
+
The existing `SessionCard.tsx` has a placeholder:
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
{/* Robot viewport placeholder */}
|
|
394
|
+
<div className={styles.robotViewport} />
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Replace with:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
<div className={styles.robotViewport}>
|
|
401
|
+
<Robot3DViewport
|
|
402
|
+
sessionId={session.sessionId}
|
|
403
|
+
status={session.status}
|
|
404
|
+
accentColor={session.accentColor}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Ensure the `.robotViewport` CSS module class has:
|
|
410
|
+
|
|
411
|
+
```css
|
|
412
|
+
.robotViewport {
|
|
413
|
+
width: 100%;
|
|
414
|
+
height: 120px; /* adjust to card layout */
|
|
415
|
+
overflow: hidden;
|
|
416
|
+
border-radius: 8px;
|
|
417
|
+
}
|
|
418
|
+
.robotViewport canvas {
|
|
419
|
+
width: 100% !important;
|
|
420
|
+
height: 100% !important;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Performance Considerations
|
|
427
|
+
|
|
428
|
+
### Problem: Many Canvases
|
|
429
|
+
|
|
430
|
+
Each `SessionCard` would mount its own `<Canvas>`, each creating a separate WebGL context. Browsers limit contexts to ~8-16; beyond that, earlier ones are lost.
|
|
431
|
+
|
|
432
|
+
### Solution: Shared Canvas with Offscreen Viewports
|
|
433
|
+
|
|
434
|
+
Use a **single shared Canvas** that renders all robots, with each card showing a CSS-clipped region. `@react-three/drei` provides `<View>` for this:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
// src/components/character/Robot3DStage.tsx
|
|
438
|
+
import { Canvas } from '@react-three/fiber'
|
|
439
|
+
import { View } from '@react-three/drei'
|
|
440
|
+
|
|
441
|
+
// Mount ONCE at the app root (e.g., in App.tsx or LiveView.tsx)
|
|
442
|
+
export function Robot3DStage({ children }: { children: React.ReactNode }) {
|
|
443
|
+
return (
|
|
444
|
+
<>
|
|
445
|
+
{children}
|
|
446
|
+
<Canvas
|
|
447
|
+
style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }}
|
|
448
|
+
gl={{ antialias: true, alpha: true }}
|
|
449
|
+
dpr={[1, 1.5]}
|
|
450
|
+
eventSource={document.body}
|
|
451
|
+
>
|
|
452
|
+
<View.Port />
|
|
453
|
+
</Canvas>
|
|
454
|
+
</>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Each card uses <View> to claim a region of the shared canvas:
|
|
459
|
+
// src/components/character/Robot3DViewport.tsx (updated)
|
|
460
|
+
import { View } from '@react-three/drei'
|
|
461
|
+
|
|
462
|
+
export function Robot3DViewport({ status, accentColor }: Props) {
|
|
463
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
464
|
+
const state3D = mapStatusTo3D(status)
|
|
465
|
+
const color = accentColor || '#00f0ff'
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<div ref={ref} style={{ width: '100%', height: '120px' }}>
|
|
469
|
+
<View track={ref}>
|
|
470
|
+
<ambientLight intensity={0.8} color="#2a2040" />
|
|
471
|
+
<directionalLight position={[3, 5, 3]} intensity={1.5} />
|
|
472
|
+
<pointLight position={[-2, 2, -1]} intensity={0.6} color={color} />
|
|
473
|
+
<Robot3DModel neonColor={color} state={state3D} scale={0.85} />
|
|
474
|
+
</View>
|
|
475
|
+
</div>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This approach uses **1 WebGL context** for all robots. Critical for dashboards showing 10-50+ sessions.
|
|
481
|
+
|
|
482
|
+
### Other Performance Tips
|
|
483
|
+
|
|
484
|
+
| Concern | Solution |
|
|
485
|
+
|---|---|
|
|
486
|
+
| 50+ robots on screen | Use `<Instances>` from drei for shared geometry instancing |
|
|
487
|
+
| Material per color | Pool materials by palette index (8 materials, not N) |
|
|
488
|
+
| Offscreen robots | Pause `useFrame` when card is not in viewport (`IntersectionObserver`) |
|
|
489
|
+
| Mobile/low-end | Fall back to CSS characters via `settingsStore.use3D` toggle |
|
|
490
|
+
| Shadow maps | Disable `castShadow` in card viewport (only needed for full scene) |
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Migration Checklist
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
Phase 1: Foundation
|
|
498
|
+
─────────────────────────────────────────
|
|
499
|
+
[ ] npm install three @react-three/fiber @react-three/drei @types/three
|
|
500
|
+
[ ] Create src/lib/robot3DGeometry.ts (shared geo + materials)
|
|
501
|
+
[ ] Create src/lib/robotStateMap.ts (SessionStatus → Robot3DState)
|
|
502
|
+
[ ] Create src/components/character/Robot3DModel.tsx
|
|
503
|
+
[ ] Create src/components/character/Robot3DViewport.tsx
|
|
504
|
+
[ ] Create src/components/character/Robot3DStage.tsx (shared canvas)
|
|
505
|
+
|
|
506
|
+
Phase 2: Integration
|
|
507
|
+
─────────────────────────────────────────
|
|
508
|
+
[ ] Wrap App.tsx (or LiveView.tsx) with <Robot3DStage>
|
|
509
|
+
[ ] Replace .robotViewport placeholder in SessionCard with <Robot3DViewport>
|
|
510
|
+
[ ] Wire session.status and session.accentColor props through
|
|
511
|
+
[ ] Test with 1 session, then 10, then 30+
|
|
512
|
+
|
|
513
|
+
Phase 3: Polish
|
|
514
|
+
─────────────────────────────────────────
|
|
515
|
+
[ ] Add state transitions (smooth lerp between poses, not instant snap)
|
|
516
|
+
[ ] Add floating "!" / "?" meshes for approval/input states
|
|
517
|
+
[ ] Add sweat particle emitter for working state
|
|
518
|
+
[ ] Add boot-up animation for connecting state
|
|
519
|
+
[ ] Handle emote system (Wave/ThumbsUp/Jump/Yes → one-shot 3D anim)
|
|
520
|
+
|
|
521
|
+
Phase 4: Cleanup
|
|
522
|
+
─────────────────────────────────────────
|
|
523
|
+
[ ] Add settingsStore.use3D toggle (bool) for CSS fallback
|
|
524
|
+
[ ] Update CharacterSelector to show 3D preview (single shared canvas)
|
|
525
|
+
[ ] Remove src/styles/characters/*.css (22 files) if CSS chars fully deprecated
|
|
526
|
+
[ ] Slim down src/styles/animations.css (keep card effects, remove character anims)
|
|
527
|
+
[ ] Update Vitest + Playwright tests
|
|
528
|
+
|
|
529
|
+
Phase 5: Optional Enhancements
|
|
530
|
+
─────────────────────────────────────────
|
|
531
|
+
[ ] Per-session robot customization (color picker already exists via accentColor)
|
|
532
|
+
[ ] Use AnimationState field to drive walk/run/dance animations
|
|
533
|
+
[ ] Add desk/monitor mesh when robot is in "working" state
|
|
534
|
+
[ ] Add OrbitControls on detail panel (enlarged robot view when card is selected)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Key Mapping Reference
|
|
540
|
+
|
|
541
|
+
### Color Palette (reuse existing)
|
|
542
|
+
|
|
543
|
+
```
|
|
544
|
+
Current RobotViewport → Robot3DViewport
|
|
545
|
+
COLOR_PALETTE[0] '#00e5ff' → Neon visor + joints + edges
|
|
546
|
+
COLOR_PALETTE[1] '#ff9100' → Neon visor + joints + edges
|
|
547
|
+
... → ...
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The existing auto-assignment logic in `RobotViewport` (cycling `globalColorIndex`, persisting via `PUT /api/sessions/{id}/accent-color`) works identically — just pass the color as `neonColor` to the 3D model.
|
|
551
|
+
|
|
552
|
+
### AnimationState → Enhancement Layer
|
|
553
|
+
|
|
554
|
+
The `AnimationState` enum (`Idle/Walking/Running/Waiting/Death/Dance`) stored on sessions is currently unused. With 3D robots, it can drive additional behavior:
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
// Optional: enhance the 3D state with AnimationState
|
|
558
|
+
if (session.animationState === 'Dance') {
|
|
559
|
+
// Override normal state with celebration animation
|
|
560
|
+
}
|
|
561
|
+
if (session.animationState === 'Death') {
|
|
562
|
+
// Play shutdown/collapse sequence instead of normal 'offline'
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Settings Integration
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
settingsStore.animationIntensity → Scale all useFrame amplitudes
|
|
570
|
+
settingsStore.animationSpeed → Multiply time factor in useFrame
|
|
571
|
+
settingsStore.characterModel → Ignored (single 3D robot model)
|
|
572
|
+
OR use as variant selector if you add multiple 3D models
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## File Structure After Adaptation
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
src/components/character/
|
|
581
|
+
├── Robot3DModel.tsx # NEW — Three.js robot mesh + animation
|
|
582
|
+
├── Robot3DViewport.tsx # NEW — Per-card viewport (uses drei View)
|
|
583
|
+
├── Robot3DStage.tsx # NEW — Shared Canvas at app root
|
|
584
|
+
├── CharacterModel.tsx # KEEP — CSS fallback (if use3D is false)
|
|
585
|
+
├── RobotViewport.tsx # KEEP — CSS fallback wrapper
|
|
586
|
+
├── CharacterSelector.tsx # UPDATE — add 3D preview option
|
|
587
|
+
|
|
588
|
+
src/lib/
|
|
589
|
+
├── robot3DGeometry.ts # NEW — shared Three.js geometries + materials
|
|
590
|
+
├── robotStateMap.ts # NEW — SessionStatus → Robot3DState mapper
|
|
591
|
+
├── ...existing files...
|
|
592
|
+
```
|