dev3000 0.0.22 → 0.0.24

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.
@@ -66,8 +66,57 @@ function parseLogLine(line: string): LogEntry | null {
66
66
  }
67
67
 
68
68
  function LogEntryComponent({ entry }: { entry: LogEntry }) {
69
+ // Parse log type from message patterns
70
+ const parseLogType = (message: string) => {
71
+ if (message.includes('[INTERACTION]')) return { type: 'INTERACTION', color: 'bg-purple-50 border-purple-200', tag: 'bg-purple-100 text-purple-800' };
72
+ if (message.includes('[CONSOLE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
73
+ if (message.includes('[CONSOLE WARN]')) return { type: 'WARNING', color: 'bg-yellow-50 border-yellow-200', tag: 'bg-yellow-100 text-yellow-800' };
74
+ if (message.includes('[SCREENSHOT]')) return { type: 'SCREENSHOT', color: 'bg-blue-50 border-blue-200', tag: 'bg-blue-100 text-blue-800' };
75
+ if (message.includes('[NAVIGATION]')) return { type: 'NAVIGATION', color: 'bg-indigo-50 border-indigo-200', tag: 'bg-indigo-100 text-indigo-800' };
76
+ if (message.includes('[NETWORK ERROR]')) return { type: 'NETWORK', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
77
+ if (message.includes('[NETWORK REQUEST]')) return { type: 'NETWORK', color: 'bg-gray-50 border-gray-200', tag: 'bg-gray-100 text-gray-700' };
78
+ if (message.includes('[PAGE ERROR]')) return { type: 'ERROR', color: 'bg-red-50 border-red-200', tag: 'bg-red-100 text-red-800' };
79
+ return { type: 'DEFAULT', color: 'border-gray-200', tag: 'bg-gray-100 text-gray-700' };
80
+ };
81
+
82
+ const logTypeInfo = parseLogType(entry.message);
83
+
84
+ // Extract and highlight type tags
85
+ const renderMessage = (message: string) => {
86
+ const typeTagRegex = /\[([A-Z\s]+)\]/g;
87
+ const parts = [];
88
+ let lastIndex = 0;
89
+ let match;
90
+
91
+ while ((match = typeTagRegex.exec(message)) !== null) {
92
+ // Add text before the tag
93
+ if (match.index > lastIndex) {
94
+ parts.push(message.slice(lastIndex, match.index));
95
+ }
96
+
97
+ // Add the tag with styling
98
+ parts.push(
99
+ <span
100
+ key={match.index}
101
+ className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${logTypeInfo.tag} mr-1`}
102
+ >
103
+ {match[1]}
104
+ </span>
105
+ );
106
+
107
+ lastIndex = match.index + match[0].length;
108
+ }
109
+
110
+ // Add remaining text
111
+ if (lastIndex < message.length) {
112
+ parts.push(message.slice(lastIndex));
113
+ }
114
+
115
+ return parts.length > 0 ? parts : message;
116
+ };
117
+
69
118
  return (
70
- <div className="border-l-4 border-gray-200 pl-4 py-2">
119
+ <div className={`border-l-4 ${logTypeInfo.color} pl-4 py-2`}>
71
120
  <div className="flex items-center gap-2 text-xs text-gray-500">
72
121
  <span className="font-mono">
73
122
  {new Date(entry.timestamp).toLocaleTimeString()}
@@ -79,7 +128,7 @@ function LogEntryComponent({ entry }: { entry: LogEntry }) {
79
128
  </span>
80
129
  </div>
81
130
  <div className="mt-1 font-mono text-sm whitespace-pre-wrap break-words overflow-wrap-anywhere">
82
- {entry.message}
131
+ {renderMessage(entry.message)}
83
132
  </div>
84
133
  {entry.screenshot && (
85
134
  <div className="mt-2">
@@ -111,10 +160,21 @@ export default function LogsClient({ version }: LogsClientProps) {
111
160
  const [currentLogFile, setCurrentLogFile] = useState<string>('');
112
161
  const [projectName, setProjectName] = useState<string>('');
113
162
  const [showLogSelector, setShowLogSelector] = useState(false);
163
+ const [isReplaying, setIsReplaying] = useState(false);
164
+ const [showFilters, setShowFilters] = useState(false);
165
+ const [showReplayPreview, setShowReplayPreview] = useState(false);
166
+ const [replayEvents, setReplayEvents] = useState<any[]>([]);
167
+ const [filters, setFilters] = useState({
168
+ browser: true,
169
+ server: true,
170
+ interaction: true,
171
+ screenshot: true
172
+ });
114
173
  const bottomRef = useRef<HTMLDivElement>(null);
115
174
  const containerRef = useRef<HTMLDivElement>(null);
116
175
  const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
117
176
  const dropdownRef = useRef<HTMLDivElement>(null);
177
+ const filterDropdownRef = useRef<HTMLDivElement>(null);
118
178
 
119
179
  const loadAvailableLogs = async () => {
120
180
  try {
@@ -236,19 +296,22 @@ export default function LogsClient({ version }: LogsClientProps) {
236
296
  };
237
297
  }, []);
238
298
 
239
- // Close dropdown when clicking outside
299
+ // Close dropdowns when clicking outside
240
300
  useEffect(() => {
241
301
  const handleClickOutside = (event: MouseEvent) => {
242
302
  if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
243
303
  setShowLogSelector(false);
244
304
  }
305
+ if (filterDropdownRef.current && !filterDropdownRef.current.contains(event.target as Node)) {
306
+ setShowFilters(false);
307
+ }
245
308
  };
246
309
 
247
- if (showLogSelector) {
310
+ if (showLogSelector || showFilters) {
248
311
  document.addEventListener('mousedown', handleClickOutside);
249
312
  return () => document.removeEventListener('mousedown', handleClickOutside);
250
313
  }
251
- }, [showLogSelector]);
314
+ }, [showLogSelector, showFilters]);
252
315
 
253
316
  const handleScroll = () => {
254
317
  if (containerRef.current) {
@@ -258,13 +321,87 @@ export default function LogsClient({ version }: LogsClientProps) {
258
321
  }
259
322
  };
260
323
 
324
+ const handleReplay = async () => {
325
+ if (isReplaying) return;
326
+
327
+ setIsReplaying(true);
328
+
329
+ try {
330
+ // Get replay data from logs
331
+ const response = await fetch('/api/replay?action=parse');
332
+ if (!response.ok) {
333
+ throw new Error('Failed to parse replay data');
334
+ }
335
+
336
+ const replayData = await response.json();
337
+
338
+ if (replayData.interactions.length === 0) {
339
+ alert('No user interactions found in logs to replay');
340
+ return;
341
+ }
342
+
343
+ // Generate CDP commands for replay
344
+ const response2 = await fetch('/api/replay', {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({
348
+ action: 'execute',
349
+ replayData: replayData,
350
+ speed: 2
351
+ })
352
+ });
353
+
354
+ const result = await response2.json();
355
+
356
+ if (result.success) {
357
+ console.log('Replay executed successfully:', result);
358
+ alert(`Replay completed! Executed ${result.totalCommands} commands.`);
359
+ } else {
360
+ console.log('CDP execution failed, showing commands:', result);
361
+ alert(`CDP execution not available. Generated ${result.commands?.length || 0} commands. Check console for details.`);
362
+ }
363
+
364
+ } catch (error) {
365
+ console.error('Replay error:', error);
366
+ alert('Failed to start replay: ' + (error instanceof Error ? error.message : 'Unknown error'));
367
+ } finally {
368
+ setIsReplaying(false);
369
+ }
370
+ };
371
+
372
+ const loadReplayPreview = async () => {
373
+ try {
374
+ const response = await fetch('/api/replay?action=parse');
375
+ if (response.ok) {
376
+ const data = await response.json();
377
+ setReplayEvents(data.interactions || []);
378
+ }
379
+ } catch (error) {
380
+ console.error('Error loading replay preview:', error);
381
+ }
382
+ };
383
+
261
384
  const filteredLogs = useMemo(() => {
262
- return logs;
263
- }, [logs]);
385
+ return logs.filter(entry => {
386
+ // Check specific message types first (these override source filtering)
387
+ const isInteraction = entry.message.includes('[INTERACTION]');
388
+ const isScreenshot = entry.message.includes('[SCREENSHOT]');
389
+
390
+ if (isInteraction) return filters.interaction;
391
+ if (isScreenshot) return filters.screenshot;
392
+
393
+ // For other logs, filter by source
394
+ if (entry.source === 'SERVER') return filters.server;
395
+ if (entry.source === 'BROWSER') return filters.browser;
396
+
397
+ return true;
398
+ });
399
+ }, [logs, filters]);
264
400
 
265
401
  return (
266
- <div className="min-h-screen bg-gray-50">
267
- <div className="bg-white shadow-sm border-b sticky top-0 z-10">
402
+ <div className="h-screen bg-gray-50 flex flex-col">
403
+ {/* Header - Fixed */}
404
+ <div className="bg-white shadow-sm border-b flex-none z-10">
268
405
  <div className="max-w-7xl mx-auto px-4 py-3">
269
406
  <div className="flex items-center justify-between">
270
407
  <div className="flex items-center gap-2 sm:gap-4">
@@ -347,8 +484,129 @@ export default function LogsClient({ version }: LogsClientProps) {
347
484
  )}
348
485
  </div>
349
486
 
350
- {/* Mode Toggle */}
351
- <div className="flex items-center bg-gray-100 rounded-md p-1">
487
+ {/* Mode Toggle with Replay Button */}
488
+ <div className="flex items-center gap-2">
489
+ {/* Replay Button with Hover Preview */}
490
+ <div className="relative">
491
+ <button
492
+ onClick={handleReplay}
493
+ disabled={isReplaying}
494
+ onMouseEnter={() => {
495
+ if (!isReplaying) {
496
+ loadReplayPreview();
497
+ setShowReplayPreview(true);
498
+ }
499
+ }}
500
+ onMouseLeave={() => setShowReplayPreview(false)}
501
+ className={`flex items-center gap-1 px-3 py-1 rounded text-sm font-medium transition-colors whitespace-nowrap ${
502
+ isReplaying
503
+ ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
504
+ : 'bg-purple-100 text-purple-800 hover:bg-purple-200'
505
+ }`}
506
+ >
507
+ {isReplaying ? (
508
+ <>
509
+ <div className="w-3 h-3 border border-purple-300 border-t-purple-600 rounded-full animate-spin"></div>
510
+ Replaying...
511
+ </>
512
+ ) : (
513
+ <>
514
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
515
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h1m4 0h1m6-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M7 7V4a1 1 0 011-1h4a1 1 0 011 1v3m-9 4h16m-5 4v1a1 1 0 01-1 1H8a1 1 0 01-1-1v-1m8 0V9a1 1 0 00-1-1H8a1 1 0 00-1 1v8.001" />
516
+ </svg>
517
+ Replay
518
+ </>
519
+ )}
520
+ </button>
521
+
522
+ {/* Replay Preview Dropdown */}
523
+ {showReplayPreview && !isReplaying && (
524
+ <div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-30 w-80">
525
+ <div className="py-2">
526
+ <div className="px-3 py-2 text-xs font-medium text-gray-500 border-b">
527
+ Replay Events ({replayEvents.length})
528
+ </div>
529
+ <div className="max-h-60 overflow-y-auto">
530
+ {replayEvents.length === 0 ? (
531
+ <div className="px-3 py-4 text-sm text-gray-500 text-center">
532
+ No interactions to replay
533
+ </div>
534
+ ) : (
535
+ replayEvents.map((event, index) => (
536
+ <div key={index} className="px-3 py-2 text-sm hover:bg-gray-50 border-b border-gray-100 last:border-b-0">
537
+ <div className="flex items-center gap-2">
538
+ <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
539
+ event.type === 'CLICK' ? 'bg-blue-100 text-blue-800' :
540
+ event.type === 'SCROLL' ? 'bg-green-100 text-green-800' :
541
+ 'bg-gray-100 text-gray-700'
542
+ }`}>
543
+ {event.type}
544
+ </span>
545
+ <span className="text-xs text-gray-500 font-mono">
546
+ {new Date(event.timestamp).toLocaleTimeString()}
547
+ </span>
548
+ </div>
549
+ <div className="mt-1 text-xs text-gray-600 font-mono truncate">
550
+ {event.type === 'CLICK' && `(${event.x}, ${event.y}) on ${event.target}`}
551
+ {event.type === 'SCROLL' && `${event.direction} ${event.distance}px to (${event.x}, ${event.y})`}
552
+ {event.type === 'KEY' && `${event.key} in ${event.target}`}
553
+ </div>
554
+ </div>
555
+ ))
556
+ )}
557
+ </div>
558
+ </div>
559
+ </div>
560
+ )}
561
+ </div>
562
+
563
+ {/* Filter Button */}
564
+ <div className="relative" ref={filterDropdownRef}>
565
+ <button
566
+ onClick={() => setShowFilters(!showFilters)}
567
+ className="flex items-center gap-1 px-3 py-1 rounded text-sm font-medium transition-colors whitespace-nowrap bg-gray-100 text-gray-700 hover:bg-gray-200"
568
+ >
569
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
570
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
571
+ </svg>
572
+ Filter
573
+ </button>
574
+
575
+ {/* Filter Dropdown */}
576
+ {showFilters && (
577
+ <div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-48">
578
+ <div className="py-2">
579
+ <div className="px-3 py-2 text-xs font-medium text-gray-500 border-b">
580
+ Log Types
581
+ </div>
582
+ {[
583
+ { key: 'server', label: 'Server', count: logs.filter(l => l.source === 'SERVER').length },
584
+ { key: 'browser', label: 'Browser', count: logs.filter(l => l.source === 'BROWSER').length },
585
+ { key: 'interaction', label: 'Interaction', count: logs.filter(l => l.message.includes('[INTERACTION]')).length },
586
+ { key: 'screenshot', label: 'Screenshot', count: logs.filter(l => l.message.includes('[SCREENSHOT]')).length }
587
+ ].map(({ key, label, count }) => (
588
+ <label
589
+ key={key}
590
+ className="flex items-center justify-between px-3 py-2 text-sm hover:bg-gray-50 cursor-pointer"
591
+ >
592
+ <div className="flex items-center gap-2">
593
+ <input
594
+ type="checkbox"
595
+ checked={filters[key as keyof typeof filters]}
596
+ onChange={(e) => setFilters(prev => ({ ...prev, [key]: e.target.checked }))}
597
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
598
+ />
599
+ <span>{label}</span>
600
+ </div>
601
+ <span className="text-xs text-gray-400">{count}</span>
602
+ </label>
603
+ ))}
604
+ </div>
605
+ </div>
606
+ )}
607
+ </div>
608
+
609
+ <div className="flex items-center bg-gray-100 rounded-md p-1">
352
610
  <button
353
611
  onClick={() => {
354
612
  setMode('head');
@@ -373,40 +631,51 @@ export default function LogsClient({ version }: LogsClientProps) {
373
631
  >
374
632
  Tail
375
633
  </button>
634
+ </div>
376
635
  </div>
377
636
  </div>
378
637
  </div>
379
638
  </div>
380
639
 
381
- <div
382
- ref={containerRef}
383
- className="max-w-7xl mx-auto px-4 py-6 pb-14 max-h-screen overflow-y-auto"
384
- onScroll={handleScroll}
385
- >
386
- {isInitialLoading ? (
387
- <div className="text-center py-12">
388
- <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
389
- <div className="text-gray-500 text-sm mt-4">Loading logs...</div>
390
- </div>
391
- ) : filteredLogs.length === 0 ? (
392
- <div className="text-center py-12">
393
- <div className="text-gray-400 text-lg">📝 No logs yet</div>
394
- <div className="text-gray-500 text-sm mt-2">
395
- Logs will appear here as your development server runs
640
+ {/* Content Area - Fills remaining space */}
641
+ <div className="flex-1 overflow-hidden">
642
+ <div
643
+ ref={containerRef}
644
+ className="max-w-7xl mx-auto px-4 py-6 h-full overflow-y-auto"
645
+ onScroll={handleScroll}
646
+ >
647
+ {isInitialLoading ? (
648
+ <div className="text-center py-12">
649
+ <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
650
+ <div className="text-gray-500 text-sm mt-4">Loading logs...</div>
396
651
  </div>
397
- </div>
398
- ) : (
399
- <div className="space-y-1">
400
- {filteredLogs.map((entry, index) => (
401
- <LogEntryComponent key={index} entry={entry} />
402
- ))}
403
- <div ref={bottomRef} />
404
- </div>
405
- )}
652
+ ) : logs.length === 0 ? (
653
+ <div className="text-center py-12">
654
+ <div className="text-gray-400 text-lg">📝 No logs yet</div>
655
+ <div className="text-gray-500 text-sm mt-2">
656
+ Logs will appear here as your development server runs
657
+ </div>
658
+ </div>
659
+ ) : filteredLogs.length === 0 ? (
660
+ <div className="text-center py-12">
661
+ <div className="text-gray-400 text-lg">🔍 No logs match current filters</div>
662
+ <div className="text-gray-500 text-sm mt-2">
663
+ Try adjusting your filter settings to see more logs
664
+ </div>
665
+ </div>
666
+ ) : (
667
+ <div className="space-y-1 pb-4">
668
+ {filteredLogs.map((entry, index) => (
669
+ <LogEntryComponent key={index} entry={entry} />
670
+ ))}
671
+ <div ref={bottomRef} />
672
+ </div>
673
+ )}
674
+ </div>
406
675
  </div>
407
676
 
408
- {/* Footer with status and scroll indicator - full width like header */}
409
- <div className="border-t border-gray-200 bg-gray-50 fixed bottom-0 left-0 right-0">
677
+ {/* Footer - Fixed */}
678
+ <div className="border-t border-gray-200 bg-gray-50 flex-none">
410
679
  <div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
411
680
  <div className="flex items-center gap-3">
412
681
  {isLoadingNew && (
@@ -447,11 +716,11 @@ export default function LogsClient({ version }: LogsClientProps) {
447
716
  {/* Scroll to bottom button when not at bottom */}
448
717
  <button
449
718
  onClick={() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' })}
450
- className={`absolute top-0 right-0 flex items-center gap-1 px-2 py-0.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700 ${
719
+ className={`absolute top-0 right-0 flex items-center gap-1 px-2 py-0.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700 whitespace-nowrap ${
451
720
  mode === 'tail' && !isAtBottom && !isLoadingNew ? 'visible' : 'invisible'
452
721
  }`}
453
722
  >
454
- ↓ Live updates
723
+ ↓ Live
455
724
  </button>
456
725
  </div>
457
726
  </div>