agents-dojo 0.1.6 → 0.1.7

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 (50) hide show
  1. package/monitor/.env.development +1 -0
  2. package/monitor/.env.production +1 -0
  3. package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
  4. package/monitor/index.html +12 -0
  5. package/monitor/package-lock.json +2160 -0
  6. package/monitor/package.json +25 -0
  7. package/monitor/public/bg.png +0 -0
  8. package/monitor/public/bg_clean.png +0 -0
  9. package/monitor/public/positions.json +215 -0
  10. package/monitor/public/sprites/agent_default.png +0 -0
  11. package/monitor/public/sprites/cushion_0.png +0 -0
  12. package/monitor/public/sprites/cushion_1.png +0 -0
  13. package/monitor/public/sprites/cushion_10.png +0 -0
  14. package/monitor/public/sprites/cushion_2.png +0 -0
  15. package/monitor/public/sprites/cushion_3.png +0 -0
  16. package/monitor/public/sprites/cushion_4.png +0 -0
  17. package/monitor/public/sprites/cushion_5.png +0 -0
  18. package/monitor/public/sprites/cushion_6.png +0 -0
  19. package/monitor/public/sprites/cushion_7.png +0 -0
  20. package/monitor/public/sprites/cushion_8.png +0 -0
  21. package/monitor/public/sprites/cushion_9.png +0 -0
  22. package/monitor/public/sprites/master.png +0 -0
  23. package/monitor/public/sprites/stake_0.png +0 -0
  24. package/monitor/public/sprites/stake_1.png +0 -0
  25. package/monitor/public/sprites/stake_10.png +0 -0
  26. package/monitor/public/sprites/stake_2.png +0 -0
  27. package/monitor/public/sprites/stake_3.png +0 -0
  28. package/monitor/public/sprites/stake_4.png +0 -0
  29. package/monitor/public/sprites/stake_5.png +0 -0
  30. package/monitor/public/sprites/stake_6.png +0 -0
  31. package/monitor/public/sprites/stake_7.png +0 -0
  32. package/monitor/public/sprites/stake_8.png +0 -0
  33. package/monitor/public/sprites/stake_9.png +0 -0
  34. package/monitor/scripts/record-gif.py +53 -0
  35. package/monitor/src/App.tsx +22 -0
  36. package/monitor/src/components/AgentMenu.tsx +67 -0
  37. package/monitor/src/components/ChatPanel.tsx +214 -0
  38. package/monitor/src/components/LogPage.tsx +173 -0
  39. package/monitor/src/components/Stage.tsx +39 -0
  40. package/monitor/src/components/StatusBar.tsx +50 -0
  41. package/monitor/src/lib/dojo-app.ts +799 -0
  42. package/monitor/src/lib/interactables.ts +162 -0
  43. package/monitor/src/lib/store.ts +352 -0
  44. package/monitor/src/lib/types.ts +72 -0
  45. package/monitor/src/lib/ws-client.ts +66 -0
  46. package/monitor/src/main.tsx +9 -0
  47. package/monitor/src/vite-env.d.ts +1 -0
  48. package/monitor/tsconfig.json +14 -0
  49. package/monitor/vite.config.ts +13 -0
  50. package/package.json +2 -1
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "agents-dojo-monitor",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "pixi.js": "^8.6.0",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0",
15
+ "zustand": "^5.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "@vitejs/plugin-react": "^4.3.0",
21
+ "puppeteer": "^25.1.0",
22
+ "typescript": "^5.6.0",
23
+ "vite": "^5.4.0"
24
+ }
25
+ }
Binary file
Binary file
@@ -0,0 +1,215 @@
1
+ {
2
+ "stakes": [
3
+ {
4
+ "x": 84,
5
+ "y_top": 230,
6
+ "y_bot": 289,
7
+ "file": "stake_0.png",
8
+ "orig_x": 158,
9
+ "orig_y": 574,
10
+ "w": 104,
11
+ "h": 148
12
+ },
13
+ {
14
+ "x": 165,
15
+ "y_top": 230,
16
+ "y_bot": 289,
17
+ "file": "stake_1.png",
18
+ "orig_x": 360,
19
+ "orig_y": 574,
20
+ "w": 104,
21
+ "h": 148
22
+ },
23
+ {
24
+ "x": 244,
25
+ "y_top": 230,
26
+ "y_bot": 289,
27
+ "file": "stake_2.png",
28
+ "orig_x": 558,
29
+ "orig_y": 574,
30
+ "w": 104,
31
+ "h": 148
32
+ },
33
+ {
34
+ "x": 324,
35
+ "y_top": 249,
36
+ "y_bot": 308,
37
+ "file": "stake_3.png",
38
+ "orig_x": 758,
39
+ "orig_y": 622,
40
+ "w": 104,
41
+ "h": 148
42
+ },
43
+ {
44
+ "x": 405,
45
+ "y_top": 230,
46
+ "y_bot": 289,
47
+ "file": "stake_4.png",
48
+ "orig_x": 960,
49
+ "orig_y": 574,
50
+ "w": 104,
51
+ "h": 148
52
+ },
53
+ {
54
+ "x": 487,
55
+ "y_top": 230,
56
+ "y_bot": 289,
57
+ "file": "stake_5.png",
58
+ "orig_x": 1166,
59
+ "orig_y": 574,
60
+ "w": 104,
61
+ "h": 148
62
+ },
63
+ {
64
+ "x": 557,
65
+ "y_top": 249,
66
+ "y_bot": 308,
67
+ "file": "stake_6.png",
68
+ "orig_x": 1340,
69
+ "orig_y": 622,
70
+ "w": 104,
71
+ "h": 148
72
+ },
73
+ {
74
+ "x": 636,
75
+ "y_top": 230,
76
+ "y_bot": 289,
77
+ "file": "stake_7.png",
78
+ "orig_x": 1538,
79
+ "orig_y": 574,
80
+ "w": 104,
81
+ "h": 148
82
+ },
83
+ {
84
+ "x": 725,
85
+ "y_top": 230,
86
+ "y_bot": 289,
87
+ "file": "stake_8.png",
88
+ "orig_x": 1760,
89
+ "orig_y": 574,
90
+ "w": 104,
91
+ "h": 148
92
+ },
93
+ {
94
+ "x": 804,
95
+ "y_top": 249,
96
+ "y_bot": 308,
97
+ "file": "stake_9.png",
98
+ "orig_x": 1958,
99
+ "orig_y": 622,
100
+ "w": 104,
101
+ "h": 148
102
+ },
103
+ {
104
+ "x": 889,
105
+ "y_top": 230,
106
+ "y_bot": 289,
107
+ "file": "stake_10.png",
108
+ "orig_x": 2170,
109
+ "orig_y": 574,
110
+ "w": 104,
111
+ "h": 148
112
+ }
113
+ ],
114
+ "cushions": [
115
+ {
116
+ "x": 224,
117
+ "y": 365,
118
+ "file": "cushion_0.png",
119
+ "orig_x": 504,
120
+ "orig_y": 888,
121
+ "w": 116,
122
+ "h": 52
123
+ },
124
+ {
125
+ "x": 688,
126
+ "y": 367,
127
+ "file": "cushion_1.png",
128
+ "orig_x": 1664,
129
+ "orig_y": 892,
130
+ "w": 116,
131
+ "h": 52
132
+ },
133
+ {
134
+ "x": 88,
135
+ "y": 368,
136
+ "file": "cushion_2.png",
137
+ "orig_x": 164,
138
+ "orig_y": 896,
139
+ "w": 116,
140
+ "h": 52
141
+ },
142
+ {
143
+ "x": 352,
144
+ "y": 372,
145
+ "file": "cushion_3.png",
146
+ "orig_x": 824,
147
+ "orig_y": 904,
148
+ "w": 116,
149
+ "h": 52
150
+ },
151
+ {
152
+ "x": 816,
153
+ "y": 372,
154
+ "file": "cushion_4.png",
155
+ "orig_x": 1984,
156
+ "orig_y": 904,
157
+ "w": 116,
158
+ "h": 52
159
+ },
160
+ {
161
+ "x": 616,
162
+ "y": 375,
163
+ "file": "cushion_5.png",
164
+ "orig_x": 1484,
165
+ "orig_y": 912,
166
+ "w": 116,
167
+ "h": 52
168
+ },
169
+ {
170
+ "x": 152,
171
+ "y": 376,
172
+ "file": "cushion_6.png",
173
+ "orig_x": 324,
174
+ "orig_y": 916,
175
+ "w": 116,
176
+ "h": 52
177
+ },
178
+ {
179
+ "x": 877,
180
+ "y": 378,
181
+ "file": "cushion_7.png",
182
+ "orig_x": 2136,
183
+ "orig_y": 920,
184
+ "w": 116,
185
+ "h": 52
186
+ },
187
+ {
188
+ "x": 288,
189
+ "y": 381,
190
+ "file": "cushion_8.png",
191
+ "orig_x": 664,
192
+ "orig_y": 928,
193
+ "w": 116,
194
+ "h": 52
195
+ },
196
+ {
197
+ "x": 752,
198
+ "y": 381,
199
+ "file": "cushion_9.png",
200
+ "orig_x": 1824,
201
+ "orig_y": 928,
202
+ "w": 116,
203
+ "h": 52
204
+ },
205
+ {
206
+ "x": 480,
207
+ "y": 448,
208
+ "file": "cushion_10.png",
209
+ "orig_x": 1144,
210
+ "orig_y": 1096,
211
+ "w": 116,
212
+ "h": 52
213
+ }
214
+ ]
215
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Record Monitor animation as GIF.
4
+
5
+ Usage:
6
+ python3 scripts/record-gif.py [duration_sec] [output_path]
7
+
8
+ Example:
9
+ python3 scripts/record-gif.py 6 /tmp/dojo.gif
10
+ """
11
+ import sys, time, os, glob
12
+ from playwright.sync_api import sync_playwright
13
+
14
+ duration = int(sys.argv[1]) if len(sys.argv) > 1 else 6
15
+ output = sys.argv[2] if len(sys.argv) > 2 else '/tmp/dojo_monitor.gif'
16
+ fps = 5
17
+ url = os.environ.get('MONITOR_URL', 'http://localhost:5173')
18
+ frame_dir = '/tmp/dojo_gif_frames'
19
+
20
+ os.makedirs(frame_dir, exist_ok=True)
21
+ for f in glob.glob(f'{frame_dir}/*.png'):
22
+ os.remove(f)
23
+
24
+ print(f'Recording {duration}s at {fps}fps from {url} ...')
25
+
26
+ with sync_playwright() as p:
27
+ browser = p.chromium.launch(headless=True, args=[
28
+ '--enable-unsafe-swiftshader', '--use-gl=swiftshader',
29
+ ])
30
+ page = browser.new_page(viewport={'width': 960, 'height': 600})
31
+ page.goto(url)
32
+ page.wait_for_load_state('networkidle')
33
+ time.sleep(3) # let Pixi fully render
34
+
35
+ total_frames = duration * fps
36
+ interval = 1.0 / fps
37
+ for i in range(total_frames):
38
+ page.screenshot(path=f'{frame_dir}/f_{i:04d}.png')
39
+ time.sleep(interval)
40
+
41
+ browser.close()
42
+
43
+ print(f'Captured {total_frames} frames. Assembling GIF...')
44
+
45
+ import imageio.v3 as iio
46
+
47
+ frames = []
48
+ for i in range(total_frames):
49
+ img = iio.imread(f'{frame_dir}/f_{i:04d}.png')
50
+ frames.append(img)
51
+
52
+ iio.imwrite(output, frames, duration=int(1000 / fps), loop=0)
53
+ print(f'GIF saved: {output} ({os.path.getsize(output) // 1024} KB)')
@@ -0,0 +1,22 @@
1
+ import React, { useState } from 'react';
2
+ import { Stage } from './components/Stage.js';
3
+ import { LogPage } from './components/LogPage.js';
4
+
5
+ export function App() {
6
+ const [page, setPage] = useState<'monitor' | 'logs'>('monitor');
7
+
8
+ return (
9
+ <>
10
+ {/* Keep Stage always mounted so Pixi app is never destroyed */}
11
+ <div style={{
12
+ width: '100vw',
13
+ height: '100vh',
14
+ background: '#2a1a0a',
15
+ display: page === 'monitor' ? 'block' : 'none',
16
+ }}>
17
+ <Stage onShowLogs={() => setPage('logs')} />
18
+ </div>
19
+ {page === 'logs' && <LogPage onBack={() => setPage('monitor')} />}
20
+ </>
21
+ );
22
+ }
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useMonitorStore } from '../lib/store.js';
3
+
4
+ export function AgentMenu() {
5
+ const menuAgentId = useMonitorStore((s) => s.menuAgentId);
6
+ const menuPosition = useMonitorStore((s) => s.menuPosition);
7
+ const closeMenu = useMonitorStore((s) => s.closeMenu);
8
+ const openChat = useMonitorStore((s) => s.openChat);
9
+ const ref = useRef<HTMLDivElement>(null);
10
+
11
+ // Close on outside click
12
+ useEffect(() => {
13
+ if (!menuAgentId) return;
14
+ const handler = (e: MouseEvent) => {
15
+ if (ref.current && !ref.current.contains(e.target as Node)) {
16
+ closeMenu();
17
+ }
18
+ };
19
+ // Delay to avoid the same click closing the menu
20
+ const timer = setTimeout(() => document.addEventListener('mousedown', handler), 50);
21
+ return () => { clearTimeout(timer); document.removeEventListener('mousedown', handler); };
22
+ }, [menuAgentId, closeMenu]);
23
+
24
+ if (!menuAgentId || !menuPosition) return null;
25
+
26
+ const agent = useMonitorStore.getState().agents.get(menuAgentId);
27
+ const displayName = agent ? `${agent.id}` : menuAgentId;
28
+
29
+ return (
30
+ <div
31
+ ref={ref}
32
+ style={{
33
+ position: 'absolute',
34
+ left: menuPosition.x,
35
+ top: menuPosition.y,
36
+ background: 'rgba(40, 28, 15, 0.95)',
37
+ border: '1px solid rgba(200, 160, 100, 0.4)',
38
+ borderRadius: 6,
39
+ padding: '4px 0',
40
+ minWidth: 120,
41
+ fontFamily: 'monospace',
42
+ fontSize: 12,
43
+ color: '#f5e6d0',
44
+ zIndex: 1000,
45
+ boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
46
+ }}
47
+ >
48
+ {/* Header */}
49
+ <div style={{ padding: '4px 12px', fontSize: 10, color: 'rgba(200,160,100,0.6)', borderBottom: '1px solid rgba(200,160,100,0.2)' }}>
50
+ {displayName}
51
+ </div>
52
+ {/* Chat option */}
53
+ <div
54
+ onClick={() => openChat(menuAgentId)}
55
+ style={{
56
+ padding: '6px 12px',
57
+ cursor: 'pointer',
58
+ transition: 'background 0.15s',
59
+ }}
60
+ onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(200,160,100,0.15)')}
61
+ onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
62
+ >
63
+ Chat
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,214 @@
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react';
2
+ import { useMonitorStore } from '../lib/store.js';
3
+ import { sendChatMessage } from '../lib/ws-client.js';
4
+
5
+ export function ChatPanel() {
6
+ const userChat = useMonitorStore((s) => s.userChat);
7
+ const closeChat = useMonitorStore((s) => s.closeChat);
8
+ const [input, setInput] = useState('');
9
+ const messagesEndRef = useRef<HTMLDivElement>(null);
10
+ const inputRef = useRef<HTMLInputElement>(null);
11
+ const panelRef = useRef<HTMLDivElement>(null);
12
+
13
+ // Draggable position (null = default position)
14
+ const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
15
+ const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
16
+
17
+ const onDragStart = useCallback((e: React.MouseEvent) => {
18
+ // Only drag from the header
19
+ const panel = panelRef.current;
20
+ if (!panel) return;
21
+ const rect = panel.getBoundingClientRect();
22
+ dragRef.current = {
23
+ startX: e.clientX,
24
+ startY: e.clientY,
25
+ origX: rect.left,
26
+ origY: rect.top,
27
+ };
28
+ e.preventDefault();
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ const onMove = (e: MouseEvent) => {
33
+ if (!dragRef.current) return;
34
+ const dx = e.clientX - dragRef.current.startX;
35
+ const dy = e.clientY - dragRef.current.startY;
36
+ setPos({
37
+ x: Math.max(0, Math.min(window.innerWidth - 360, dragRef.current.origX + dx)),
38
+ y: Math.max(0, Math.min(window.innerHeight - 100, dragRef.current.origY + dy)),
39
+ });
40
+ };
41
+ const onUp = () => { dragRef.current = null; };
42
+ document.addEventListener('mousemove', onMove);
43
+ document.addEventListener('mouseup', onUp);
44
+ return () => {
45
+ document.removeEventListener('mousemove', onMove);
46
+ document.removeEventListener('mouseup', onUp);
47
+ };
48
+ }, []);
49
+
50
+ // Reset position when opening for a new agent
51
+ useEffect(() => { setPos(null); }, [userChat?.agentId]);
52
+
53
+ // Auto-scroll to bottom
54
+ useEffect(() => {
55
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
56
+ }, [userChat?.messages.length]);
57
+
58
+ // Focus input on open
59
+ useEffect(() => {
60
+ if (userChat) inputRef.current?.focus();
61
+ }, [userChat?.agentId]);
62
+
63
+ if (!userChat) return null;
64
+
65
+ const handleSend = () => {
66
+ const text = input.trim();
67
+ if (!text || userChat.loading) return;
68
+ sendChatMessage(userChat.agentId, text);
69
+ setInput('');
70
+ };
71
+
72
+ const handleKeyDown = (e: React.KeyboardEvent) => {
73
+ if (e.key === 'Enter' && !e.shiftKey) {
74
+ e.preventDefault();
75
+ handleSend();
76
+ }
77
+ if (e.key === 'Escape') {
78
+ closeChat();
79
+ }
80
+ };
81
+
82
+ const posStyle = pos
83
+ ? { left: pos.x, top: pos.y, right: 'auto' as const, bottom: 'auto' as const }
84
+ : { right: 16, bottom: 16 };
85
+
86
+ return (
87
+ <div ref={panelRef} style={{
88
+ position: 'absolute',
89
+ ...posStyle,
90
+ width: 360,
91
+ maxHeight: '60vh',
92
+ background: 'rgba(30, 20, 10, 0.96)',
93
+ border: '1px solid rgba(200, 160, 100, 0.3)',
94
+ borderRadius: 10,
95
+ display: 'flex',
96
+ flexDirection: 'column',
97
+ fontFamily: 'monospace',
98
+ fontSize: 13,
99
+ color: '#f5e6d0',
100
+ zIndex: 900,
101
+ boxShadow: '0 8px 24px rgba(0,0,0,0.6)',
102
+ overflow: 'hidden',
103
+ }}>
104
+ {/* Header — drag handle */}
105
+ <div
106
+ onMouseDown={onDragStart}
107
+ style={{
108
+ display: 'flex',
109
+ justifyContent: 'space-between',
110
+ alignItems: 'center',
111
+ padding: '8px 12px',
112
+ background: 'rgba(90, 60, 30, 0.6)',
113
+ borderBottom: '1px solid rgba(200,160,100,0.2)',
114
+ cursor: 'grab',
115
+ userSelect: 'none',
116
+ }}
117
+ >
118
+ <span style={{ fontWeight: 'bold' }}>Chat: {userChat.agentId}</span>
119
+ <span
120
+ onClick={closeChat}
121
+ style={{ cursor: 'pointer', color: 'rgba(200,160,100,0.6)', fontSize: 16 }}
122
+ >
123
+ x
124
+ </span>
125
+ </div>
126
+
127
+ {/* Messages */}
128
+ <div style={{
129
+ flex: 1,
130
+ overflowY: 'auto',
131
+ padding: '8px 12px',
132
+ display: 'flex',
133
+ flexDirection: 'column',
134
+ gap: 8,
135
+ minHeight: 120,
136
+ maxHeight: 'calc(60vh - 100px)',
137
+ }}>
138
+ {userChat.messages.length === 0 && !userChat.loading && (
139
+ <div style={{ color: 'rgba(200,160,100,0.4)', fontSize: 11, textAlign: 'center', marginTop: 20 }}>
140
+ Send a message to {userChat.agentId}
141
+ </div>
142
+ )}
143
+ {userChat.messages.map((msg, i) => (
144
+ <div key={i} style={{
145
+ alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
146
+ maxWidth: '85%',
147
+ }}>
148
+ <div style={{
149
+ background: msg.role === 'user' ? 'rgba(100, 140, 80, 0.3)' : 'rgba(200, 160, 100, 0.15)',
150
+ padding: '6px 10px',
151
+ borderRadius: 8,
152
+ fontSize: 12,
153
+ lineHeight: 1.4,
154
+ whiteSpace: 'pre-wrap',
155
+ wordBreak: 'break-word',
156
+ }}>
157
+ {msg.text}
158
+ </div>
159
+ </div>
160
+ ))}
161
+ {userChat.loading && (
162
+ <div style={{ color: 'rgba(200,160,100,0.5)', fontSize: 11, fontStyle: 'italic' }}>
163
+ thinking...
164
+ </div>
165
+ )}
166
+ <div ref={messagesEndRef} />
167
+ </div>
168
+
169
+ {/* Input */}
170
+ <div style={{
171
+ display: 'flex',
172
+ padding: '8px 12px',
173
+ borderTop: '1px solid rgba(200,160,100,0.2)',
174
+ gap: 8,
175
+ }}>
176
+ <input
177
+ ref={inputRef}
178
+ value={input}
179
+ onChange={(e) => setInput(e.target.value)}
180
+ onKeyDown={handleKeyDown}
181
+ placeholder="Type a message..."
182
+ disabled={userChat.loading}
183
+ style={{
184
+ flex: 1,
185
+ background: 'rgba(200,160,100,0.1)',
186
+ border: '1px solid rgba(200,160,100,0.25)',
187
+ borderRadius: 6,
188
+ padding: '6px 10px',
189
+ color: '#f5e6d0',
190
+ fontFamily: 'monospace',
191
+ fontSize: 12,
192
+ outline: 'none',
193
+ }}
194
+ />
195
+ <button
196
+ onClick={handleSend}
197
+ disabled={userChat.loading || !input.trim()}
198
+ style={{
199
+ background: userChat.loading || !input.trim() ? 'rgba(200,160,100,0.15)' : 'rgba(100, 140, 80, 0.4)',
200
+ border: '1px solid rgba(200,160,100,0.25)',
201
+ borderRadius: 6,
202
+ padding: '6px 12px',
203
+ color: '#f5e6d0',
204
+ fontFamily: 'monospace',
205
+ fontSize: 12,
206
+ cursor: userChat.loading || !input.trim() ? 'default' : 'pointer',
207
+ }}
208
+ >
209
+ Send
210
+ </button>
211
+ </div>
212
+ </div>
213
+ );
214
+ }