dev3000 0.0.42 → 0.0.44
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/mcp-server/app/api/replay/route.ts +131 -164
- package/mcp-server/app/layout.tsx +11 -2
- package/mcp-server/app/logs/LogsClient.tsx +130 -43
- package/package.json +3 -2
|
@@ -212,24 +212,20 @@ export async function POST(request: NextRequest) {
|
|
|
212
212
|
const { action, replayData, speed = 1 } = body;
|
|
213
213
|
|
|
214
214
|
if (action === 'execute') {
|
|
215
|
-
//
|
|
216
|
-
const cdpCommands = generateCDPCommands(replayData, speed);
|
|
217
|
-
|
|
218
|
-
// Try to execute the commands via CDP
|
|
215
|
+
// Execute replay via MCP server's execute_browser_action tool
|
|
219
216
|
try {
|
|
220
|
-
const result = await
|
|
217
|
+
const result = await executeBrowserActions(replayData, speed);
|
|
221
218
|
return NextResponse.json({
|
|
222
219
|
success: true,
|
|
223
|
-
message: 'Replay executed successfully',
|
|
220
|
+
message: 'Replay executed successfully via MCP server',
|
|
224
221
|
result: result,
|
|
225
|
-
|
|
222
|
+
totalEvents: result.totalEvents,
|
|
223
|
+
executedEvents: result.executed
|
|
226
224
|
});
|
|
227
225
|
} catch (error) {
|
|
228
|
-
// Fallback: return commands for manual execution
|
|
229
226
|
return NextResponse.json({
|
|
230
227
|
success: false,
|
|
231
|
-
message: '
|
|
232
|
-
commands: cdpCommands,
|
|
228
|
+
message: 'MCP server execution failed',
|
|
233
229
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
234
230
|
});
|
|
235
231
|
}
|
|
@@ -245,168 +241,139 @@ export async function POST(request: NextRequest) {
|
|
|
245
241
|
}
|
|
246
242
|
}
|
|
247
243
|
|
|
248
|
-
interface CDPCommand {
|
|
249
|
-
method: string;
|
|
250
|
-
params: any;
|
|
251
|
-
delay: number;
|
|
252
|
-
description: string;
|
|
253
|
-
}
|
|
254
244
|
|
|
255
|
-
function
|
|
256
|
-
const events = [
|
|
257
|
-
...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
|
|
258
|
-
...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
|
|
259
|
-
].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
260
|
-
|
|
261
|
-
const commands: CDPCommand[] = [];
|
|
262
|
-
const startTime = new Date(replayData.startTime).getTime();
|
|
263
|
-
|
|
264
|
-
for (const event of events) {
|
|
265
|
-
const eventTime = new Date(event.timestamp).getTime();
|
|
266
|
-
const delay = Math.max(0, (eventTime - startTime) / speed);
|
|
267
|
-
|
|
268
|
-
if (event.eventType === 'navigation') {
|
|
269
|
-
commands.push({
|
|
270
|
-
method: 'Page.navigate',
|
|
271
|
-
params: { url: event.url },
|
|
272
|
-
delay: delay,
|
|
273
|
-
description: `Navigate to ${event.url}`
|
|
274
|
-
});
|
|
275
|
-
} else if (event.eventType === 'interaction') {
|
|
276
|
-
if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
|
|
277
|
-
// Mouse down
|
|
278
|
-
commands.push({
|
|
279
|
-
method: 'Input.dispatchMouseEvent',
|
|
280
|
-
params: {
|
|
281
|
-
type: 'mousePressed',
|
|
282
|
-
x: event.x,
|
|
283
|
-
y: event.y,
|
|
284
|
-
button: 'left',
|
|
285
|
-
clickCount: 1
|
|
286
|
-
},
|
|
287
|
-
delay: delay,
|
|
288
|
-
description: `Click at (${event.x}, ${event.y}) on ${event.target}`
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// Mouse up (after small delay)
|
|
292
|
-
commands.push({
|
|
293
|
-
method: 'Input.dispatchMouseEvent',
|
|
294
|
-
params: {
|
|
295
|
-
type: 'mouseReleased',
|
|
296
|
-
x: event.x,
|
|
297
|
-
y: event.y,
|
|
298
|
-
button: 'left',
|
|
299
|
-
clickCount: 1
|
|
300
|
-
},
|
|
301
|
-
delay: 50, // 50ms between down and up
|
|
302
|
-
description: `Release click at (${event.x}, ${event.y})`
|
|
303
|
-
});
|
|
304
|
-
} else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
|
|
305
|
-
commands.push({
|
|
306
|
-
method: 'Runtime.evaluate',
|
|
307
|
-
params: {
|
|
308
|
-
expression: `window.scrollTo({left: ${event.x}, top: ${event.y}, behavior: 'smooth'})`
|
|
309
|
-
},
|
|
310
|
-
delay: delay,
|
|
311
|
-
description: `Scroll to (${event.x}, ${event.y})`
|
|
312
|
-
});
|
|
313
|
-
} else if ('type' in event && event.type === 'KEY' && event.key) {
|
|
314
|
-
// Key down
|
|
315
|
-
commands.push({
|
|
316
|
-
method: 'Input.dispatchKeyEvent',
|
|
317
|
-
params: {
|
|
318
|
-
type: 'keyDown',
|
|
319
|
-
key: event.key,
|
|
320
|
-
text: event.key.length === 1 ? event.key : undefined
|
|
321
|
-
},
|
|
322
|
-
delay: delay,
|
|
323
|
-
description: `Key down: ${event.key}`
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// Key up
|
|
327
|
-
commands.push({
|
|
328
|
-
method: 'Input.dispatchKeyEvent',
|
|
329
|
-
params: {
|
|
330
|
-
type: 'keyUp',
|
|
331
|
-
key: event.key
|
|
332
|
-
},
|
|
333
|
-
delay: 50,
|
|
334
|
-
description: `Key up: ${event.key}`
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return commands;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async function executeCDPCommands(commands: CDPCommand[]): Promise<any> {
|
|
344
|
-
const WebSocket = require('ws');
|
|
345
|
-
|
|
245
|
+
async function executeBrowserActions(replayData: ReplayData, speed: number): Promise<any> {
|
|
346
246
|
try {
|
|
347
|
-
//
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const
|
|
247
|
+
// Get MCP server URL from environment (defaults to local MCP server)
|
|
248
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL || 'http://localhost:3684';
|
|
249
|
+
|
|
250
|
+
const events = [
|
|
251
|
+
...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
|
|
252
|
+
...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
|
|
253
|
+
].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
351
254
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
255
|
+
const results: any[] = [];
|
|
256
|
+
const startTime = new Date(replayData.startTime).getTime();
|
|
257
|
+
|
|
258
|
+
// Execute events sequentially with proper timing
|
|
259
|
+
for (let i = 0; i < events.length; i++) {
|
|
260
|
+
const event = events[i];
|
|
261
|
+
const eventTime = new Date(event.timestamp).getTime();
|
|
262
|
+
const delay = Math.max(0, (eventTime - startTime) / speed);
|
|
357
263
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
ws.close();
|
|
363
|
-
resolve({ executed: commandIndex, results });
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const command = commands[commandIndex];
|
|
368
|
-
const message = {
|
|
369
|
-
id: nextId++,
|
|
370
|
-
method: command.method,
|
|
371
|
-
params: command.params
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
ws.send(JSON.stringify(message));
|
|
375
|
-
commandIndex++;
|
|
376
|
-
|
|
377
|
-
// Schedule next command with delay
|
|
378
|
-
setTimeout(executeNext, command.delay || 100);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// Start executing commands
|
|
382
|
-
executeNext();
|
|
383
|
-
});
|
|
264
|
+
// Wait for the calculated delay
|
|
265
|
+
if (delay > 0) {
|
|
266
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
267
|
+
}
|
|
384
268
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
269
|
+
try {
|
|
270
|
+
let response;
|
|
271
|
+
|
|
272
|
+
if (event.eventType === 'navigation') {
|
|
273
|
+
// Navigate to URL
|
|
274
|
+
response = await fetch(`${mcpServerUrl}/mcp`, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
jsonrpc: '2.0',
|
|
279
|
+
id: i + 1,
|
|
280
|
+
method: 'tools/call',
|
|
281
|
+
params: {
|
|
282
|
+
name: 'execute_browser_action',
|
|
283
|
+
arguments: {
|
|
284
|
+
action: 'navigate',
|
|
285
|
+
url: event.url
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
} else if (event.eventType === 'interaction') {
|
|
291
|
+
if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
|
|
292
|
+
// Click action
|
|
293
|
+
response = await fetch(`${mcpServerUrl}/mcp`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
jsonrpc: '2.0',
|
|
298
|
+
id: i + 1,
|
|
299
|
+
method: 'tools/call',
|
|
300
|
+
params: {
|
|
301
|
+
name: 'execute_browser_action',
|
|
302
|
+
arguments: {
|
|
303
|
+
action: 'click',
|
|
304
|
+
x: event.x,
|
|
305
|
+
y: event.y
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
} else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
|
|
311
|
+
// Scroll action
|
|
312
|
+
response = await fetch(`${mcpServerUrl}/mcp`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
jsonrpc: '2.0',
|
|
317
|
+
id: i + 1,
|
|
318
|
+
method: 'tools/call',
|
|
319
|
+
params: {
|
|
320
|
+
name: 'execute_browser_action',
|
|
321
|
+
arguments: {
|
|
322
|
+
action: 'scroll',
|
|
323
|
+
x: 0,
|
|
324
|
+
y: 0,
|
|
325
|
+
deltaX: event.x,
|
|
326
|
+
deltaY: event.y
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
});
|
|
331
|
+
} else if ('type' in event && event.type === 'KEY' && event.key) {
|
|
332
|
+
// Type action
|
|
333
|
+
response = await fetch(`${mcpServerUrl}/mcp`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
jsonrpc: '2.0',
|
|
338
|
+
id: i + 1,
|
|
339
|
+
method: 'tools/call',
|
|
340
|
+
params: {
|
|
341
|
+
name: 'execute_browser_action',
|
|
342
|
+
arguments: {
|
|
343
|
+
action: 'type',
|
|
344
|
+
text: event.key
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
});
|
|
390
349
|
}
|
|
391
|
-
} catch (error) {
|
|
392
|
-
// Ignore parsing errors for events
|
|
393
350
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
351
|
+
|
|
352
|
+
if (response) {
|
|
353
|
+
const result = await response.json();
|
|
354
|
+
results.push({
|
|
355
|
+
event,
|
|
356
|
+
result,
|
|
357
|
+
description: `${event.eventType}: ${event.eventType === 'navigation' ? event.url : ('type' in event ? event.type : 'unknown')}`
|
|
358
|
+
});
|
|
400
359
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
360
|
+
|
|
361
|
+
} catch (error) {
|
|
362
|
+
results.push({
|
|
363
|
+
event,
|
|
364
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
365
|
+
description: `Failed: ${event.eventType}`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
executed: results.length,
|
|
372
|
+
results,
|
|
373
|
+
totalEvents: events.length
|
|
374
|
+
};
|
|
375
|
+
|
|
409
376
|
} catch (error) {
|
|
410
|
-
throw new Error(`Failed to
|
|
377
|
+
throw new Error(`Failed to execute replay via MCP server: ${error}`);
|
|
411
378
|
}
|
|
412
379
|
}
|
|
@@ -4,12 +4,21 @@ export default function RootLayout({
|
|
|
4
4
|
children,
|
|
5
5
|
}: any) {
|
|
6
6
|
return (
|
|
7
|
-
<html lang="en">
|
|
7
|
+
<html lang="en" className="h-full">
|
|
8
8
|
<head>
|
|
9
9
|
<title>🎯 dev3000</title>
|
|
10
10
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
<script
|
|
12
|
+
dangerouslySetInnerHTML={{
|
|
13
|
+
__html: `
|
|
14
|
+
tailwind.config = {
|
|
15
|
+
darkMode: 'class',
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
11
20
|
</head>
|
|
12
|
-
<body>{children}</body>
|
|
21
|
+
<body className="h-full">{children}</body>
|
|
13
22
|
</html>
|
|
14
23
|
);
|
|
15
24
|
}
|
|
@@ -3,6 +3,51 @@
|
|
|
3
3
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
4
4
|
import { LogEntry, LogsApiResponse, ConfigApiResponse, LogFile, LogListResponse } from '@/types';
|
|
5
5
|
|
|
6
|
+
// Hook for dark mode with system preference detection
|
|
7
|
+
function useDarkMode() {
|
|
8
|
+
const [darkMode, setDarkMode] = useState<boolean>(() => {
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
// Check localStorage first
|
|
11
|
+
const saved = localStorage.getItem('dev3000-dark-mode');
|
|
12
|
+
if (saved !== null) {
|
|
13
|
+
return JSON.parse(saved);
|
|
14
|
+
}
|
|
15
|
+
// Default to system preference
|
|
16
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Save to localStorage
|
|
23
|
+
localStorage.setItem('dev3000-dark-mode', JSON.stringify(darkMode));
|
|
24
|
+
|
|
25
|
+
// Apply dark class to document
|
|
26
|
+
if (darkMode) {
|
|
27
|
+
document.documentElement.classList.add('dark');
|
|
28
|
+
} else {
|
|
29
|
+
document.documentElement.classList.remove('dark');
|
|
30
|
+
}
|
|
31
|
+
}, [darkMode]);
|
|
32
|
+
|
|
33
|
+
// Listen for system theme changes
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
36
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
37
|
+
// Only update if no explicit choice has been made
|
|
38
|
+
const saved = localStorage.getItem('dev3000-dark-mode');
|
|
39
|
+
if (saved === null) {
|
|
40
|
+
setDarkMode(e.matches);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
mediaQuery.addEventListener('change', handler);
|
|
45
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return [darkMode, setDarkMode] as const;
|
|
49
|
+
}
|
|
50
|
+
|
|
6
51
|
export function parseLogEntries(logContent: string): LogEntry[] {
|
|
7
52
|
// Split by timestamp pattern - each timestamp starts a new log entry
|
|
8
53
|
const timestampPattern = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\] \[([^\]]+)\] /;
|
|
@@ -75,7 +120,7 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
75
120
|
href={url}
|
|
76
121
|
target="_blank"
|
|
77
122
|
rel="noopener noreferrer"
|
|
78
|
-
className="text-blue-600 hover:text-blue-800 underline"
|
|
123
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
79
124
|
>
|
|
80
125
|
{url}
|
|
81
126
|
</a>
|
|
@@ -92,13 +137,13 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
92
137
|
href={url}
|
|
93
138
|
target="_blank"
|
|
94
139
|
rel="noopener noreferrer"
|
|
95
|
-
className="text-blue-600 hover:text-blue-800 underline"
|
|
140
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
96
141
|
>
|
|
97
142
|
{url}
|
|
98
143
|
</a>
|
|
99
144
|
<button
|
|
100
145
|
onClick={() => setIsExpanded(false)}
|
|
101
|
-
className="ml-2 text-xs text-gray-500 hover:text-gray-700 px-1 py-0.5 rounded hover:bg-gray-100"
|
|
146
|
+
className="ml-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
102
147
|
>
|
|
103
148
|
[collapse]
|
|
104
149
|
</button>
|
|
@@ -109,13 +154,13 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
109
154
|
href={url}
|
|
110
155
|
target="_blank"
|
|
111
156
|
rel="noopener noreferrer"
|
|
112
|
-
className="text-blue-600 hover:text-blue-800 underline"
|
|
157
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
113
158
|
>
|
|
114
159
|
{truncated}
|
|
115
160
|
</a>
|
|
116
161
|
<button
|
|
117
162
|
onClick={() => setIsExpanded(true)}
|
|
118
|
-
className="ml-1 text-xs text-gray-500 hover:text-gray-700 px-1 py-0.5 rounded hover:bg-gray-100"
|
|
163
|
+
className="ml-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-1 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
119
164
|
>
|
|
120
165
|
[expand]
|
|
121
166
|
</button>
|
|
@@ -247,17 +292,17 @@ function ObjectRenderer({ content }: { content: string }) {
|
|
|
247
292
|
}
|
|
248
293
|
|
|
249
294
|
function LogEntryComponent({ entry }: { entry: LogEntry }) {
|
|
250
|
-
// Parse log type from message patterns
|
|
295
|
+
// Parse log type from message patterns with dark mode support
|
|
251
296
|
const parseLogType = (message: string) => {
|
|
252
|
-
if (message.includes('[INTERACTION]')) return { type: 'INTERACTION', color: 'bg-purple-50 border-purple-200', tag: 'bg-purple-100 text-purple-800' };
|
|
253
|
-
if (message.includes('[CONSOLE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
|
|
254
|
-
if (message.includes('[CONSOLE WARN]')) return { type: 'WARNING', color: 'bg-yellow-50 border-yellow-200', tag: 'bg-yellow-100 text-yellow-800' };
|
|
255
|
-
if (message.includes('[SCREENSHOT]')) return { type: 'SCREENSHOT', color: 'bg-blue-50 border-blue-200', tag: 'bg-blue-100 text-blue-800' };
|
|
256
|
-
if (message.includes('[NAVIGATION]')) return { type: 'NAVIGATION', color: 'bg-indigo-50 border-indigo-200', tag: 'bg-indigo-100 text-indigo-800' };
|
|
257
|
-
if (message.includes('[NETWORK ERROR]')) return { type: 'NETWORK', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
|
|
258
|
-
if (message.includes('[NETWORK REQUEST]')) return { type: 'NETWORK', color: 'bg-gray-50 border-gray-200', tag: 'bg-gray-100 text-gray-700' };
|
|
259
|
-
if (message.includes('[PAGE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
|
|
260
|
-
return { type: 'DEFAULT', color: 'border-gray-200', tag: 'bg-gray-100 text-gray-700' };
|
|
297
|
+
if (message.includes('[INTERACTION]')) return { type: 'INTERACTION', color: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800', tag: 'bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200' };
|
|
298
|
+
if (message.includes('[CONSOLE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
|
|
299
|
+
if (message.includes('[CONSOLE WARN]')) return { type: 'WARNING', color: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800', tag: 'bg-yellow-100 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200' };
|
|
300
|
+
if (message.includes('[SCREENSHOT]')) return { type: 'SCREENSHOT', color: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800', tag: 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' };
|
|
301
|
+
if (message.includes('[NAVIGATION]')) return { type: 'NAVIGATION', color: 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800', tag: 'bg-indigo-100 dark:bg-indigo-800 text-indigo-800 dark:text-indigo-200' };
|
|
302
|
+
if (message.includes('[NETWORK ERROR]')) return { type: 'NETWORK', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
|
|
303
|
+
if (message.includes('[NETWORK REQUEST]')) return { type: 'NETWORK', color: 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700', tag: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' };
|
|
304
|
+
if (message.includes('[PAGE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', tag: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' };
|
|
305
|
+
return { type: 'DEFAULT', color: 'border-gray-200 dark:border-gray-700', tag: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' };
|
|
261
306
|
};
|
|
262
307
|
|
|
263
308
|
const logTypeInfo = parseLogType(entry.message);
|
|
@@ -350,19 +395,19 @@ function LogEntryComponent({ entry }: { entry: LogEntry }) {
|
|
|
350
395
|
{/* Table-like layout using CSS Grid */}
|
|
351
396
|
<div className="grid grid-cols-[auto_auto_1fr] gap-3 items-start">
|
|
352
397
|
{/* Column 1: Timestamp */}
|
|
353
|
-
<div className="text-xs text-gray-500 font-mono whitespace-nowrap">
|
|
398
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono whitespace-nowrap pt-1">
|
|
354
399
|
{new Date(entry.timestamp).toLocaleTimeString()}
|
|
355
400
|
</div>
|
|
356
401
|
|
|
357
402
|
{/* Column 2: Source */}
|
|
358
403
|
<div className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
|
|
359
|
-
entry.source === 'SERVER' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
|
|
404
|
+
entry.source === 'SERVER' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200'
|
|
360
405
|
}`}>
|
|
361
406
|
{entry.source}
|
|
362
407
|
</div>
|
|
363
408
|
|
|
364
409
|
{/* Column 3: Message content */}
|
|
365
|
-
<div className="font-mono text-sm min-w-0">
|
|
410
|
+
<div className="font-mono text-sm min-w-0 text-gray-900 dark:text-gray-100">
|
|
366
411
|
{renderMessage(entry.message)}
|
|
367
412
|
</div>
|
|
368
413
|
</div>
|
|
@@ -386,6 +431,7 @@ interface LogsClientProps {
|
|
|
386
431
|
}
|
|
387
432
|
|
|
388
433
|
export default function LogsClient({ version }: LogsClientProps) {
|
|
434
|
+
const [darkMode, setDarkMode] = useDarkMode();
|
|
389
435
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
390
436
|
const [mode, setMode] = useState<'head' | 'tail'>('tail');
|
|
391
437
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
@@ -607,16 +653,38 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
607
653
|
}
|
|
608
654
|
};
|
|
609
655
|
|
|
610
|
-
const loadReplayPreview =
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
656
|
+
const loadReplayPreview = () => {
|
|
657
|
+
// Extract interactions from current logs instead of making API call
|
|
658
|
+
const interactions = logs
|
|
659
|
+
.filter(log => log.message.includes('[INTERACTION]'))
|
|
660
|
+
.map(log => {
|
|
661
|
+
const match = log.message.match(/\[INTERACTION\] (.+)/);
|
|
662
|
+
if (match) {
|
|
663
|
+
try {
|
|
664
|
+
// Try parsing as JSON (new format)
|
|
665
|
+
const data = JSON.parse(match[1]);
|
|
666
|
+
return {
|
|
667
|
+
timestamp: log.timestamp,
|
|
668
|
+
type: data.type,
|
|
669
|
+
details: data
|
|
670
|
+
};
|
|
671
|
+
} catch {
|
|
672
|
+
// Fallback to old format parsing
|
|
673
|
+
const oldMatch = match[1].match(/(CLICK|TAP|SCROLL|KEY) (.+)/);
|
|
674
|
+
if (oldMatch) {
|
|
675
|
+
return {
|
|
676
|
+
timestamp: log.timestamp,
|
|
677
|
+
type: oldMatch[1],
|
|
678
|
+
details: oldMatch[2]
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
})
|
|
685
|
+
.filter(Boolean);
|
|
686
|
+
|
|
687
|
+
setReplayEvents(interactions);
|
|
620
688
|
};
|
|
621
689
|
|
|
622
690
|
const handleRotateLog = async () => {
|
|
@@ -674,15 +742,15 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
674
742
|
}, [logs, filters]);
|
|
675
743
|
|
|
676
744
|
return (
|
|
677
|
-
<div className="h-screen bg-gray-50 flex flex-col">
|
|
745
|
+
<div className="h-screen bg-gray-50 dark:bg-gray-900 flex flex-col transition-colors">
|
|
678
746
|
{/* Header - Fixed */}
|
|
679
|
-
<div className="bg-white shadow-sm border-b flex-none z-10">
|
|
747
|
+
<div className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-none z-10">
|
|
680
748
|
<div className="max-w-7xl mx-auto px-4 py-3">
|
|
681
749
|
<div className="flex items-center justify-between">
|
|
682
750
|
<div className="flex items-center gap-2 sm:gap-4">
|
|
683
751
|
<div className="flex items-center gap-1">
|
|
684
|
-
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 whitespace-nowrap">dev3000</h1>
|
|
685
|
-
<span className="text-xs text-gray-400 whitespace-nowrap">(v{version})</span>
|
|
752
|
+
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">dev3000</h1>
|
|
753
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">(v{version})</span>
|
|
686
754
|
</div>
|
|
687
755
|
|
|
688
756
|
{/* Log File Selector */}
|
|
@@ -690,11 +758,11 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
690
758
|
<div className="relative" ref={dropdownRef}>
|
|
691
759
|
<button
|
|
692
760
|
onClick={() => setShowLogSelector(!showLogSelector)}
|
|
693
|
-
className="flex items-center gap-2 px-3 py-1 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md transition-colors"
|
|
761
|
+
className="flex items-center gap-2 px-3 py-1 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors"
|
|
694
762
|
>
|
|
695
763
|
<span className="font-mono text-xs whitespace-nowrap">
|
|
696
764
|
{isInitialLoading && !currentLogFile ? (
|
|
697
|
-
<div className="h-4 bg-gray-200 rounded animate-pulse" style={{width: '220px'}} />
|
|
765
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" style={{width: '220px'}} />
|
|
698
766
|
) : (
|
|
699
767
|
currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
|
|
700
768
|
)}
|
|
@@ -711,9 +779,9 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
711
779
|
|
|
712
780
|
{/* Dropdown */}
|
|
713
781
|
{showLogSelector && availableLogs.length > 1 && (
|
|
714
|
-
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-80">
|
|
782
|
+
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg z-20 min-w-80">
|
|
715
783
|
<div className="py-1 max-h-60 overflow-y-auto">
|
|
716
|
-
<div className="px-3 py-2 text-xs font-medium text-gray-500 border-b">
|
|
784
|
+
<div className="px-3 py-2 text-xs font-medium text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
|
|
717
785
|
{projectName} logs ({availableLogs.length})
|
|
718
786
|
</div>
|
|
719
787
|
{availableLogs.map((logFile) => (
|
|
@@ -919,6 +987,25 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
919
987
|
Tail
|
|
920
988
|
</button>
|
|
921
989
|
</div>
|
|
990
|
+
|
|
991
|
+
{/* Dark Mode Toggle */}
|
|
992
|
+
<button
|
|
993
|
+
onClick={() => setDarkMode(!darkMode)}
|
|
994
|
+
className="p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
995
|
+
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
996
|
+
>
|
|
997
|
+
{darkMode ? (
|
|
998
|
+
// Sun icon for light mode
|
|
999
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1000
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
1001
|
+
</svg>
|
|
1002
|
+
) : (
|
|
1003
|
+
// Moon icon for dark mode
|
|
1004
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1005
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
1006
|
+
</svg>
|
|
1007
|
+
)}
|
|
1008
|
+
</button>
|
|
922
1009
|
</div>
|
|
923
1010
|
</div>
|
|
924
1011
|
</div>
|
|
@@ -938,15 +1025,15 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
938
1025
|
</div>
|
|
939
1026
|
) : logs.length === 0 ? (
|
|
940
1027
|
<div className="text-center py-12">
|
|
941
|
-
<div className="text-gray-400 text-lg">📝 No logs yet</div>
|
|
942
|
-
<div className="text-gray-500 text-sm mt-2">
|
|
1028
|
+
<div className="text-gray-400 dark:text-gray-500 text-lg">📝 No logs yet</div>
|
|
1029
|
+
<div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
|
|
943
1030
|
Logs will appear here as your development server runs
|
|
944
1031
|
</div>
|
|
945
1032
|
</div>
|
|
946
1033
|
) : filteredLogs.length === 0 ? (
|
|
947
1034
|
<div className="text-center py-12">
|
|
948
|
-
<div className="text-gray-400 text-lg">🔍 No logs match current filters</div>
|
|
949
|
-
<div className="text-gray-500 text-sm mt-2">
|
|
1035
|
+
<div className="text-gray-400 dark:text-gray-500 text-lg">🔍 No logs match current filters</div>
|
|
1036
|
+
<div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
|
|
950
1037
|
Try adjusting your filter settings to see more logs
|
|
951
1038
|
</div>
|
|
952
1039
|
</div>
|
|
@@ -962,17 +1049,17 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
962
1049
|
</div>
|
|
963
1050
|
|
|
964
1051
|
{/* Footer - Fixed */}
|
|
965
|
-
<div className="border-t border-gray-200 bg-gray-50 flex-none">
|
|
1052
|
+
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-none">
|
|
966
1053
|
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
967
1054
|
<div className="flex items-center gap-3">
|
|
968
1055
|
{isLoadingNew && (
|
|
969
1056
|
<div className="flex items-center gap-1">
|
|
970
1057
|
<div className="w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
|
971
|
-
<span className="text-xs text-gray-500">Loading...</span>
|
|
1058
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">Loading...</span>
|
|
972
1059
|
</div>
|
|
973
1060
|
)}
|
|
974
1061
|
{!isLoadingNew && lastFetched && (
|
|
975
|
-
<span className="text-xs text-gray-400 font-mono">
|
|
1062
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
976
1063
|
Last updated {lastFetched.toLocaleTimeString()}
|
|
977
1064
|
</span>
|
|
978
1065
|
)}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev3000",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "AI-powered development tools with browser monitoring and MCP server integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"dev3000": "dist/cli.js"
|
|
8
|
+
"dev3000": "dist/cli.js",
|
|
9
|
+
"d3k": "dist/cli.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist/",
|