agents-dojo 0.1.6 → 0.1.8
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/dist/agent-executor.js +0 -28
- package/monitor/.env.development +1 -0
- package/monitor/.env.production +1 -0
- package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
- package/monitor/index.html +12 -0
- package/monitor/package-lock.json +2160 -0
- package/monitor/package.json +25 -0
- package/monitor/public/bg.png +0 -0
- package/monitor/public/bg_clean.png +0 -0
- package/monitor/public/positions.json +215 -0
- package/monitor/public/sprites/agent_default.png +0 -0
- package/monitor/public/sprites/cushion_0.png +0 -0
- package/monitor/public/sprites/cushion_1.png +0 -0
- package/monitor/public/sprites/cushion_10.png +0 -0
- package/monitor/public/sprites/cushion_2.png +0 -0
- package/monitor/public/sprites/cushion_3.png +0 -0
- package/monitor/public/sprites/cushion_4.png +0 -0
- package/monitor/public/sprites/cushion_5.png +0 -0
- package/monitor/public/sprites/cushion_6.png +0 -0
- package/monitor/public/sprites/cushion_7.png +0 -0
- package/monitor/public/sprites/cushion_8.png +0 -0
- package/monitor/public/sprites/cushion_9.png +0 -0
- package/monitor/public/sprites/master.png +0 -0
- package/monitor/public/sprites/stake_0.png +0 -0
- package/monitor/public/sprites/stake_1.png +0 -0
- package/monitor/public/sprites/stake_10.png +0 -0
- package/monitor/public/sprites/stake_2.png +0 -0
- package/monitor/public/sprites/stake_3.png +0 -0
- package/monitor/public/sprites/stake_4.png +0 -0
- package/monitor/public/sprites/stake_5.png +0 -0
- package/monitor/public/sprites/stake_6.png +0 -0
- package/monitor/public/sprites/stake_7.png +0 -0
- package/monitor/public/sprites/stake_8.png +0 -0
- package/monitor/public/sprites/stake_9.png +0 -0
- package/monitor/scripts/record-gif.py +53 -0
- package/monitor/src/App.tsx +22 -0
- package/monitor/src/components/AgentMenu.tsx +67 -0
- package/monitor/src/components/ChatPanel.tsx +214 -0
- package/monitor/src/components/LogPage.tsx +173 -0
- package/monitor/src/components/Stage.tsx +39 -0
- package/monitor/src/components/StatusBar.tsx +50 -0
- package/monitor/src/lib/dojo-app.ts +799 -0
- package/monitor/src/lib/interactables.ts +162 -0
- package/monitor/src/lib/store.ts +352 -0
- package/monitor/src/lib/types.ts +72 -0
- package/monitor/src/lib/ws-client.ts +66 -0
- package/monitor/src/main.tsx +9 -0
- package/monitor/src/vite-env.d.ts +1 -0
- package/monitor/tsconfig.json +14 -0
- package/monitor/vite.config.ts +16 -0
- package/package.json +2 -1
- package/dist/agent-logger.d.ts +0 -26
- package/dist/agent-logger.js +0 -63
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|