dev3000 0.0.45 → 0.0.47

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.
@@ -1,125 +1,93 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useState, useEffect, useRef, useMemo } from 'react';
4
- import { useRouter, useSearchParams } from 'next/navigation';
5
- import { LogEntry, LogsApiResponse, ConfigApiResponse, LogFile, LogListResponse } from '@/types';
3
+ import { useState, useEffect, useRef, useMemo } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import {
6
+ LogEntry,
7
+ LogsApiResponse,
8
+ ConfigApiResponse,
9
+ LogFile,
10
+ LogListResponse,
11
+ } from "@/types";
12
+ import { parseLogEntries } from "./utils";
6
13
 
7
14
  // Hook for dark mode with system preference detection
8
15
  function useDarkMode() {
9
16
  const [darkMode, setDarkMode] = useState<boolean>(() => {
10
- if (typeof window !== 'undefined') {
17
+ if (typeof window !== "undefined") {
11
18
  // Check localStorage first
12
- const saved = localStorage.getItem('dev3000-dark-mode');
19
+ const saved = localStorage.getItem("dev3000-dark-mode");
13
20
  if (saved !== null) {
14
21
  return JSON.parse(saved);
15
22
  }
16
23
  // Default to system preference
17
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
24
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
18
25
  }
19
26
  return false;
20
27
  });
21
28
 
22
29
  useEffect(() => {
23
30
  // Save to localStorage
24
- localStorage.setItem('dev3000-dark-mode', JSON.stringify(darkMode));
25
-
31
+ localStorage.setItem("dev3000-dark-mode", JSON.stringify(darkMode));
32
+
26
33
  // Apply dark class to document
27
34
  if (darkMode) {
28
- document.documentElement.classList.add('dark');
35
+ document.documentElement.classList.add("dark");
29
36
  } else {
30
- document.documentElement.classList.remove('dark');
37
+ document.documentElement.classList.remove("dark");
31
38
  }
32
39
  }, [darkMode]);
33
40
 
34
41
  // Listen for system theme changes
35
42
  useEffect(() => {
36
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
43
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
37
44
  const handler = (e: MediaQueryListEvent) => {
38
45
  // Only update if no explicit choice has been made
39
- const saved = localStorage.getItem('dev3000-dark-mode');
46
+ const saved = localStorage.getItem("dev3000-dark-mode");
40
47
  if (saved === null) {
41
48
  setDarkMode(e.matches);
42
49
  }
43
50
  };
44
-
45
- mediaQuery.addEventListener('change', handler);
46
- return () => mediaQuery.removeEventListener('change', handler);
51
+
52
+ mediaQuery.addEventListener("change", handler);
53
+ return () => mediaQuery.removeEventListener("change", handler);
47
54
  }, []);
48
55
 
49
56
  return [darkMode, setDarkMode] as const;
50
57
  }
51
58
 
52
- export function parseLogEntries(logContent: string): LogEntry[] {
53
- // Split by timestamp pattern - each timestamp starts a new log entry
54
- const timestampPattern = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\] \[([^\]]+)\] /;
55
-
56
- const entries: LogEntry[] = [];
57
- const lines = logContent.split('\n');
58
- let currentEntry: LogEntry | null = null;
59
-
60
- for (const line of lines) {
61
- if (!line.trim()) continue;
62
-
63
- const match = line.match(timestampPattern);
64
- if (match) {
65
- // Save previous entry if exists
66
- if (currentEntry) {
67
- entries.push(currentEntry);
68
- }
69
-
70
- // Start new entry
71
- const [fullMatch, timestamp, source] = match;
72
- const message = line.substring(fullMatch.length);
73
- const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
74
-
75
- currentEntry = {
76
- timestamp,
77
- source,
78
- message,
79
- screenshot,
80
- original: line
81
- };
82
- } else if (currentEntry) {
83
- // Append to current entry's message
84
- currentEntry.message += '\n' + line;
85
- currentEntry.original += '\n' + line;
86
- }
87
- }
88
-
89
- // Don't forget the last entry
90
- if (currentEntry) {
91
- entries.push(currentEntry);
92
- }
93
-
94
- return entries;
95
- }
96
-
97
59
  // Keep this for backwards compatibility, but it's not used anymore
98
60
  function parseLogLine(line: string): LogEntry | null {
99
61
  const match = line.match(/^\[([^\]]+)\] \[([^\]]+)\] (.*)$/s);
100
62
  if (!match) return null;
101
-
63
+
102
64
  const [, timestamp, source, message] = match;
103
65
  const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
104
-
66
+
105
67
  return {
106
68
  timestamp,
107
69
  source,
108
70
  message,
109
71
  screenshot,
110
- original: line
72
+ original: line,
111
73
  };
112
74
  }
113
75
 
114
76
  // Component to render truncated URLs with click-to-expand
115
- function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number }) {
77
+ function URLRenderer({
78
+ url,
79
+ maxLength = 60,
80
+ }: {
81
+ url: string;
82
+ maxLength?: number;
83
+ }) {
116
84
  const [isExpanded, setIsExpanded] = useState(false);
117
-
85
+
118
86
  if (url.length <= maxLength) {
119
87
  return (
120
- <a
121
- href={url}
122
- target="_blank"
88
+ <a
89
+ href={url}
90
+ target="_blank"
123
91
  rel="noopener noreferrer"
124
92
  className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
125
93
  >
@@ -127,16 +95,16 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
127
95
  </a>
128
96
  );
129
97
  }
130
-
131
- const truncated = url.substring(0, maxLength) + '...';
132
-
98
+
99
+ const truncated = url.substring(0, maxLength) + "...";
100
+
133
101
  return (
134
102
  <span className="inline-block">
135
103
  {isExpanded ? (
136
104
  <span>
137
- <a
138
- href={url}
139
- target="_blank"
105
+ <a
106
+ href={url}
107
+ target="_blank"
140
108
  rel="noopener noreferrer"
141
109
  className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
142
110
  >
@@ -151,9 +119,9 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
151
119
  </span>
152
120
  ) : (
153
121
  <span>
154
- <a
155
- href={url}
156
- target="_blank"
122
+ <a
123
+ href={url}
124
+ target="_blank"
157
125
  rel="noopener noreferrer"
158
126
  className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
159
127
  >
@@ -174,79 +142,110 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
174
142
  // Component to render Chrome DevTools-style collapsible objects
175
143
  function ObjectRenderer({ content }: { content: string }) {
176
144
  const [isExpanded, setIsExpanded] = useState(false);
177
-
145
+
178
146
  try {
179
147
  const obj = JSON.parse(content);
180
-
148
+
181
149
  // Check if it's a Chrome DevTools object representation
182
- if (obj && typeof obj === 'object' && obj.type === 'object' && obj.properties) {
150
+ if (
151
+ obj &&
152
+ typeof obj === "object" &&
153
+ obj.type === "object" &&
154
+ obj.properties
155
+ ) {
183
156
  const properties = obj.properties;
184
- const description = obj.description || 'Object';
157
+ const description = obj.description || "Object";
185
158
  const overflow = obj.overflow;
186
-
159
+
187
160
  return (
188
161
  <div className="inline-block">
189
162
  <button
190
163
  onClick={() => setIsExpanded(!isExpanded)}
191
164
  className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-mono text-sm"
192
165
  >
193
- <svg
194
- className={`w-3 h-3 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
195
- fill="currentColor"
166
+ <svg
167
+ className={`w-3 h-3 transition-transform ${
168
+ isExpanded ? "rotate-90" : ""
169
+ }`}
170
+ fill="currentColor"
196
171
  viewBox="0 0 20 20"
197
172
  >
198
- <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
173
+ <path
174
+ fillRule="evenodd"
175
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
176
+ clipRule="evenodd"
177
+ />
199
178
  </svg>
200
179
  <span className="text-purple-600">{description}</span>
201
180
  {!isExpanded && (
202
181
  <span className="text-gray-500">
203
- {overflow ? '...' : ''} {'{'}
182
+ {overflow ? "..." : ""} {"{"}
204
183
  {properties.slice(0, 3).map((prop: any, idx: number) => (
205
184
  <span key={idx}>
206
- {idx > 0 && ', '}
207
- <span className="text-red-600">{prop.name}</span>:
185
+ {idx > 0 && ", "}
186
+ <span className="text-red-600">{prop.name}</span>:
208
187
  <span className="text-blue-600">
209
- {prop.type === 'string' ? `"${prop.value}"` :
210
- prop.type === 'number' ? prop.value :
211
- prop.type === 'object' ? (prop.subtype === 'array' ? prop.value : '{...}') :
212
- prop.value}
188
+ {prop.type === "string"
189
+ ? `"${prop.value}"`
190
+ : prop.type === "number"
191
+ ? prop.value
192
+ : prop.type === "object"
193
+ ? prop.subtype === "array"
194
+ ? prop.value
195
+ : "{...}"
196
+ : prop.value}
213
197
  </span>
214
198
  </span>
215
199
  ))}
216
- {properties.length > 3 && ', ...'}
217
- {'}'}
200
+ {properties.length > 3 && ", ..."}
201
+ {"}"}
218
202
  </span>
219
203
  )}
220
204
  </button>
221
-
205
+
222
206
  {isExpanded && (
223
207
  <div className="mt-1 ml-4 border-l-2 border-gray-200 pl-3">
224
208
  <div className="font-mono text-sm">
225
- <div className="text-gray-600">{description} {'{'}
226
- <div className="ml-4">
227
- {properties.map((prop: any, idx: number) => (
228
- <div key={idx} className="py-0.5">
229
- <span className="text-red-600">{prop.name}</span>
230
- <span className="text-gray-500">: </span>
231
- <span className={
232
- prop.type === 'string' ? 'text-green-600' :
233
- prop.type === 'number' ? 'text-blue-600' :
234
- prop.type === 'object' ? 'text-purple-600' :
235
- 'text-orange-600'
236
- }>
237
- {prop.type === 'string' ? `"${prop.value}"` :
238
- prop.type === 'number' ? prop.value :
239
- prop.type === 'object' ? (prop.subtype === 'array' ? prop.value : '{...}') :
240
- prop.value}
241
- </span>
242
- {idx < properties.length - 1 && <span className="text-gray-500">,</span>}
243
- </div>
244
- ))}
245
- {overflow && (
246
- <div className="text-gray-500 italic">... and more properties</div>
247
- )}
248
- </div>
249
- <div className="text-gray-600">{'}'}</div>
209
+ <div className="text-gray-600">
210
+ {description} {"{"}
211
+ <div className="ml-4">
212
+ {properties.map((prop: any, idx: number) => (
213
+ <div key={idx} className="py-0.5">
214
+ <span className="text-red-600">{prop.name}</span>
215
+ <span className="text-gray-500">: </span>
216
+ <span
217
+ className={
218
+ prop.type === "string"
219
+ ? "text-green-600"
220
+ : prop.type === "number"
221
+ ? "text-blue-600"
222
+ : prop.type === "object"
223
+ ? "text-purple-600"
224
+ : "text-orange-600"
225
+ }
226
+ >
227
+ {prop.type === "string"
228
+ ? `"${prop.value}"`
229
+ : prop.type === "number"
230
+ ? prop.value
231
+ : prop.type === "object"
232
+ ? prop.subtype === "array"
233
+ ? prop.value
234
+ : "{...}"
235
+ : prop.value}
236
+ </span>
237
+ {idx < properties.length - 1 && (
238
+ <span className="text-gray-500">,</span>
239
+ )}
240
+ </div>
241
+ ))}
242
+ {overflow && (
243
+ <div className="text-gray-500 italic">
244
+ ... and more properties
245
+ </div>
246
+ )}
247
+ </div>
248
+ <div className="text-gray-600">{"}"}</div>
250
249
  </div>
251
250
  </div>
252
251
  </div>
@@ -254,7 +253,7 @@ function ObjectRenderer({ content }: { content: string }) {
254
253
  </div>
255
254
  );
256
255
  }
257
-
256
+
258
257
  // For regular JSON objects, render them nicely too
259
258
  return (
260
259
  <div className="inline-block">
@@ -262,21 +261,27 @@ function ObjectRenderer({ content }: { content: string }) {
262
261
  onClick={() => setIsExpanded(!isExpanded)}
263
262
  className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-mono text-sm"
264
263
  >
265
- <svg
266
- className={`w-3 h-3 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
267
- fill="currentColor"
264
+ <svg
265
+ className={`w-3 h-3 transition-transform ${
266
+ isExpanded ? "rotate-90" : ""
267
+ }`}
268
+ fill="currentColor"
268
269
  viewBox="0 0 20 20"
269
270
  >
270
- <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
271
+ <path
272
+ fillRule="evenodd"
273
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
274
+ clipRule="evenodd"
275
+ />
271
276
  </svg>
272
277
  <span className="text-purple-600">Object</span>
273
278
  {!isExpanded && (
274
279
  <span className="text-gray-500">
275
- {'{'}...{'}'}
280
+ {"{"}...{"}"}
276
281
  </span>
277
282
  )}
278
283
  </button>
279
-
284
+
280
285
  {isExpanded && (
281
286
  <div className="mt-1 ml-4 border-l-2 border-gray-200 pl-3">
282
287
  <pre className="font-mono text-sm text-gray-700 whitespace-pre-wrap">
@@ -295,99 +300,157 @@ function ObjectRenderer({ content }: { content: string }) {
295
300
  function LogEntryComponent({ entry }: { entry: LogEntry }) {
296
301
  // Parse log type from message patterns with dark mode support
297
302
  const parseLogType = (message: string) => {
298
- 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' };
299
- 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' };
300
- 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' };
301
- 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' };
302
- 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' };
303
- 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' };
304
- 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' };
305
- 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' };
306
- 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' };
303
+ if (message.includes("[INTERACTION]"))
304
+ return {
305
+ type: "INTERACTION",
306
+ color:
307
+ "bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800",
308
+ tag: "bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200",
309
+ };
310
+ if (message.includes("[CONSOLE ERROR]"))
311
+ return {
312
+ type: "ERROR",
313
+ color:
314
+ "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
315
+ tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
316
+ };
317
+ if (message.includes("[CONSOLE WARN]"))
318
+ return {
319
+ type: "WARNING",
320
+ color:
321
+ "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",
322
+ tag: "bg-yellow-100 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200",
323
+ };
324
+ if (message.includes("[SCREENSHOT]"))
325
+ return {
326
+ type: "SCREENSHOT",
327
+ color:
328
+ "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",
329
+ tag: "bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200",
330
+ };
331
+ if (message.includes("[NAVIGATION]"))
332
+ return {
333
+ type: "NAVIGATION",
334
+ color:
335
+ "bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800",
336
+ tag: "bg-indigo-100 dark:bg-indigo-800 text-indigo-800 dark:text-indigo-200",
337
+ };
338
+ if (message.includes("[NETWORK ERROR]"))
339
+ return {
340
+ type: "NETWORK",
341
+ color:
342
+ "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
343
+ tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
344
+ };
345
+ if (message.includes("[NETWORK REQUEST]"))
346
+ return {
347
+ type: "NETWORK",
348
+ color:
349
+ "bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700",
350
+ tag: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300",
351
+ };
352
+ if (message.includes("[PAGE ERROR]"))
353
+ return {
354
+ type: "ERROR",
355
+ color:
356
+ "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
357
+ tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
358
+ };
359
+ return {
360
+ type: "DEFAULT",
361
+ color: "border-gray-200 dark:border-gray-700",
362
+ tag: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300",
363
+ };
307
364
  };
308
365
 
309
366
  const logTypeInfo = parseLogType(entry.message);
310
-
367
+
311
368
  // Extract and highlight type tags, detect JSON objects and URLs
312
369
  const renderMessage = (message: string) => {
313
370
  const typeTagRegex = /\[([A-Z\s]+)\]/g;
314
371
  const jsonRegex = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
315
372
  const urlRegex = /(https?:\/\/[^\s]+)/g;
316
-
373
+
317
374
  const parts = [];
318
375
  let lastIndex = 0;
319
376
  let match;
320
-
377
+
321
378
  // First, handle type tags
322
379
  while ((match = typeTagRegex.exec(message)) !== null) {
323
380
  // Add text before the tag
324
381
  if (match.index > lastIndex) {
325
382
  parts.push(message.slice(lastIndex, match.index));
326
383
  }
327
-
384
+
328
385
  // Add the tag with styling
329
386
  parts.push(
330
- <span
387
+ <span
331
388
  key={match.index}
332
389
  className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${logTypeInfo.tag} mr-1`}
333
390
  >
334
391
  {match[1]}
335
392
  </span>
336
393
  );
337
-
394
+
338
395
  lastIndex = match.index + match[0].length;
339
396
  }
340
-
397
+
341
398
  // Add remaining text
342
399
  let remainingText = message.slice(lastIndex);
343
-
400
+
344
401
  // Process remaining text for JSON objects and URLs
345
402
  const processTextForObjects = (text: string, keyPrefix: string) => {
346
403
  const jsonMatches = [...text.matchAll(jsonRegex)];
347
404
  const urlMatches = [...text.matchAll(urlRegex)];
348
- const allMatches = [...jsonMatches.map(m => ({ ...m, type: 'json' })), ...urlMatches.map(m => ({ ...m, type: 'url' }))];
349
-
405
+ const allMatches = [
406
+ ...jsonMatches.map((m) => ({ ...m, type: "json" })),
407
+ ...urlMatches.map((m) => ({ ...m, type: "url" })),
408
+ ];
409
+
350
410
  // Sort matches by index
351
411
  allMatches.sort((a, b) => a.index! - b.index!);
352
-
412
+
353
413
  if (allMatches.length === 0) {
354
414
  return [text];
355
415
  }
356
-
416
+
357
417
  const finalParts = [];
358
418
  let textLastIndex = 0;
359
-
419
+
360
420
  allMatches.forEach((objMatch, idx) => {
361
421
  // Add text before match
362
422
  if (objMatch.index! > textLastIndex) {
363
423
  finalParts.push(text.slice(textLastIndex, objMatch.index));
364
424
  }
365
-
425
+
366
426
  // Add appropriate renderer
367
- if (objMatch.type === 'json') {
427
+ if (objMatch.type === "json") {
368
428
  finalParts.push(
369
- <ObjectRenderer key={`${keyPrefix}-json-${idx}`} content={objMatch[0]} />
429
+ <ObjectRenderer
430
+ key={`${keyPrefix}-json-${idx}`}
431
+ content={objMatch[0]}
432
+ />
370
433
  );
371
- } else if (objMatch.type === 'url') {
434
+ } else if (objMatch.type === "url") {
372
435
  finalParts.push(
373
436
  <URLRenderer key={`${keyPrefix}-url-${idx}`} url={objMatch[0]} />
374
437
  );
375
438
  }
376
-
439
+
377
440
  textLastIndex = objMatch.index! + objMatch[0].length;
378
441
  });
379
-
442
+
380
443
  // Add any text after the last match
381
444
  if (textLastIndex < text.length) {
382
445
  finalParts.push(text.slice(textLastIndex));
383
446
  }
384
-
447
+
385
448
  return finalParts;
386
449
  };
387
-
388
- const processedRemaining = processTextForObjects(remainingText, 'main');
450
+
451
+ const processedRemaining = processTextForObjects(remainingText, "main");
389
452
  parts.push(...processedRemaining);
390
-
453
+
391
454
  return parts.length > 0 ? parts : message;
392
455
  };
393
456
 
@@ -399,27 +462,31 @@ function LogEntryComponent({ entry }: { entry: LogEntry }) {
399
462
  <div className="text-xs text-gray-500 dark:text-gray-400 font-mono whitespace-nowrap pt-1">
400
463
  {new Date(entry.timestamp).toLocaleTimeString()}
401
464
  </div>
402
-
465
+
403
466
  {/* Column 2: Source */}
404
- <div className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
405
- 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'
406
- }`}>
467
+ <div
468
+ className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
469
+ entry.source === "SERVER"
470
+ ? "bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200"
471
+ : "bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200"
472
+ }`}
473
+ >
407
474
  {entry.source}
408
475
  </div>
409
-
476
+
410
477
  {/* Column 3: Message content */}
411
478
  <div className="font-mono text-sm min-w-0 text-gray-900 dark:text-gray-100">
412
479
  {renderMessage(entry.message)}
413
480
  </div>
414
481
  </div>
415
-
482
+
416
483
  {entry.screenshot && (
417
484
  <div className="mt-2">
418
- <img
419
- src={`/screenshots/${entry.screenshot}`}
420
- alt="Screenshot"
485
+ <img
486
+ src={`/screenshots/${entry.screenshot}`}
487
+ alt="Screenshot"
421
488
  className="max-w-full h-auto border rounded shadow-sm"
422
- style={{ maxHeight: '400px' }}
489
+ style={{ maxHeight: "400px" }}
423
490
  />
424
491
  </div>
425
492
  )}
@@ -433,7 +500,7 @@ interface LogsClientProps {
433
500
  logs: LogEntry[];
434
501
  logFiles: any[];
435
502
  currentLogFile: string;
436
- mode: 'head' | 'tail';
503
+ mode: "head" | "tail";
437
504
  };
438
505
  }
439
506
 
@@ -442,15 +509,23 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
442
509
  const searchParams = useSearchParams();
443
510
  const [darkMode, setDarkMode] = useDarkMode();
444
511
  const [logs, setLogs] = useState<LogEntry[]>(initialData?.logs || []);
445
- const [mode, setMode] = useState<'head' | 'tail'>(initialData?.mode || 'tail');
512
+ const [mode, setMode] = useState<"head" | "tail">(
513
+ initialData?.mode || "tail"
514
+ );
446
515
  const [isAtBottom, setIsAtBottom] = useState(true);
447
516
  const [isLoadingNew, setIsLoadingNew] = useState(false);
448
517
  const [isInitialLoading, setIsInitialLoading] = useState(!initialData);
449
- const [lastLogCount, setLastLogCount] = useState(initialData?.logs.length || 0);
518
+ const [lastLogCount, setLastLogCount] = useState(
519
+ initialData?.logs.length || 0
520
+ );
450
521
  const [lastFetched, setLastFetched] = useState<Date | null>(null);
451
- const [availableLogs, setAvailableLogs] = useState<LogFile[]>(initialData?.logFiles || []);
452
- const [currentLogFile, setCurrentLogFile] = useState<string>(initialData?.currentLogFile || '');
453
- const [projectName, setProjectName] = useState<string>('');
522
+ const [availableLogs, setAvailableLogs] = useState<LogFile[]>(
523
+ initialData?.logFiles || []
524
+ );
525
+ const [currentLogFile, setCurrentLogFile] = useState<string>(
526
+ initialData?.currentLogFile || ""
527
+ );
528
+ const [projectName, setProjectName] = useState<string>("");
454
529
  const [showLogSelector, setShowLogSelector] = useState(false);
455
530
  const [isReplaying, setIsReplaying] = useState(false);
456
531
  const [showFilters, setShowFilters] = useState(false);
@@ -461,7 +536,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
461
536
  browser: true,
462
537
  server: true,
463
538
  interaction: true,
464
- screenshot: true
539
+ screenshot: true,
465
540
  });
466
541
  const bottomRef = useRef<HTMLDivElement>(null);
467
542
  const containerRef = useRef<HTMLDivElement>(null);
@@ -471,7 +546,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
471
546
 
472
547
  const loadAvailableLogs = async () => {
473
548
  try {
474
- const response = await fetch('/api/logs/list');
549
+ const response = await fetch("/api/logs/list");
475
550
  if (response.ok) {
476
551
  const data: LogListResponse = await response.json();
477
552
  setAvailableLogs(data.files);
@@ -479,28 +554,28 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
479
554
  setProjectName(data.projectName);
480
555
  }
481
556
  } catch (error) {
482
- console.error('Error loading available logs:', error);
557
+ console.error("Error loading available logs:", error);
483
558
  }
484
559
  };
485
560
 
486
561
  const pollForNewLogs = async () => {
487
- if (mode !== 'tail' || !isAtBottom) return;
488
-
562
+ if (mode !== "tail" || !isAtBottom) return;
563
+
489
564
  try {
490
- const response = await fetch('/api/logs/tail?lines=1000');
565
+ const response = await fetch("/api/logs/tail?lines=1000");
491
566
  if (!response.ok) {
492
567
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
493
568
  }
494
-
569
+
495
570
  const data: LogsApiResponse = await response.json();
496
-
571
+
497
572
  if (!data.logs) {
498
- console.warn('No logs data in response');
573
+ console.warn("No logs data in response");
499
574
  return;
500
575
  }
501
-
576
+
502
577
  const entries = parseLogEntries(data.logs);
503
-
578
+
504
579
  if (entries.length > lastLogCount) {
505
580
  setIsLoadingNew(true);
506
581
  setLastFetched(new Date());
@@ -510,19 +585,19 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
510
585
  setIsLoadingNew(false);
511
586
  // Auto-scroll to bottom for new content
512
587
  setTimeout(() => {
513
- bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
588
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
514
589
  }, 50);
515
590
  }, 250);
516
591
  }
517
592
  } catch (error) {
518
- console.error('Error polling logs:', error);
593
+ console.error("Error polling logs:", error);
519
594
  // Don't spam console on network errors during polling
520
595
  }
521
596
  };
522
597
 
523
598
  // Start/stop polling based on mode and scroll position
524
599
  useEffect(() => {
525
- if (mode === 'tail' && isAtBottom) {
600
+ if (mode === "tail" && isAtBottom) {
526
601
  pollIntervalRef.current = setInterval(pollForNewLogs, 2000); // Poll every 2 seconds
527
602
  return () => {
528
603
  if (pollIntervalRef.current) {
@@ -538,41 +613,41 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
538
613
 
539
614
  const loadInitialLogs = async () => {
540
615
  setIsInitialLoading(true);
541
-
616
+
542
617
  // Load available logs list first
543
618
  await loadAvailableLogs();
544
-
619
+
545
620
  try {
546
621
  const response = await fetch(`/api/logs/${mode}?lines=1000`);
547
622
  if (!response.ok) {
548
623
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
549
624
  }
550
-
625
+
551
626
  const data: LogsApiResponse = await response.json();
552
-
627
+
553
628
  if (!data.logs) {
554
- console.warn('No logs data in response');
629
+ console.warn("No logs data in response");
555
630
  setLogs([]);
556
631
  setIsInitialLoading(false);
557
632
  return;
558
633
  }
559
-
634
+
560
635
  const entries = parseLogEntries(data.logs);
561
-
636
+
562
637
  setLogs(entries);
563
638
  setLastLogCount(entries.length);
564
639
  setLastFetched(new Date());
565
640
  setIsInitialLoading(false);
566
-
641
+
567
642
  // Auto-scroll to bottom for tail mode
568
- if (mode === 'tail') {
643
+ if (mode === "tail") {
569
644
  setTimeout(() => {
570
- bottomRef.current?.scrollIntoView({ behavior: 'auto' });
645
+ bottomRef.current?.scrollIntoView({ behavior: "auto" });
571
646
  setIsAtBottom(true);
572
647
  }, 100);
573
648
  }
574
649
  } catch (error) {
575
- console.error('Error loading logs:', error);
650
+ console.error("Error loading logs:", error);
576
651
  setLogs([]);
577
652
  }
578
653
  };
@@ -584,7 +659,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
584
659
  } else {
585
660
  // We have initial data, just start polling for updates
586
661
  setIsInitialLoading(false);
587
- if (mode === 'tail' && isAtBottom) {
662
+ if (mode === "tail" && isAtBottom) {
588
663
  // Set up polling timer for new logs
589
664
  pollIntervalRef.current = setInterval(() => {
590
665
  pollForNewLogs();
@@ -604,17 +679,24 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
604
679
  // Close dropdowns when clicking outside
605
680
  useEffect(() => {
606
681
  const handleClickOutside = (event: MouseEvent) => {
607
- if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
682
+ if (
683
+ dropdownRef.current &&
684
+ !dropdownRef.current.contains(event.target as Node)
685
+ ) {
608
686
  setShowLogSelector(false);
609
687
  }
610
- if (filterDropdownRef.current && !filterDropdownRef.current.contains(event.target as Node)) {
688
+ if (
689
+ filterDropdownRef.current &&
690
+ !filterDropdownRef.current.contains(event.target as Node)
691
+ ) {
611
692
  setShowFilters(false);
612
693
  }
613
694
  };
614
695
 
615
696
  if (showLogSelector || showFilters) {
616
- document.addEventListener('mousedown', handleClickOutside);
617
- return () => document.removeEventListener('mousedown', handleClickOutside);
697
+ document.addEventListener("mousedown", handleClickOutside);
698
+ return () =>
699
+ document.removeEventListener("mousedown", handleClickOutside);
618
700
  }
619
701
  }, [showLogSelector, showFilters]);
620
702
 
@@ -628,47 +710,53 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
628
710
 
629
711
  const handleReplay = async () => {
630
712
  if (isReplaying) return;
631
-
713
+
632
714
  setIsReplaying(true);
633
-
715
+
634
716
  try {
635
717
  // Get replay data from logs
636
- const response = await fetch('/api/replay?action=parse');
718
+ const response = await fetch("/api/replay?action=parse");
637
719
  if (!response.ok) {
638
- throw new Error('Failed to parse replay data');
720
+ throw new Error("Failed to parse replay data");
639
721
  }
640
-
722
+
641
723
  const replayData = await response.json();
642
-
724
+
643
725
  if (replayData.interactions.length === 0) {
644
- alert('No user interactions found in logs to replay');
726
+ alert("No user interactions found in logs to replay");
645
727
  return;
646
728
  }
647
-
729
+
648
730
  // Generate CDP commands for replay
649
- const response2 = await fetch('/api/replay', {
650
- method: 'POST',
651
- headers: { 'Content-Type': 'application/json' },
731
+ const response2 = await fetch("/api/replay", {
732
+ method: "POST",
733
+ headers: { "Content-Type": "application/json" },
652
734
  body: JSON.stringify({
653
- action: 'execute',
735
+ action: "execute",
654
736
  replayData: replayData,
655
- speed: 2
656
- })
737
+ speed: 2,
738
+ }),
657
739
  });
658
-
740
+
659
741
  const result = await response2.json();
660
-
742
+
661
743
  if (result.success) {
662
- console.log('Replay executed successfully:', result);
744
+ console.log("Replay executed successfully:", result);
663
745
  alert(`Replay completed! Executed ${result.totalCommands} commands.`);
664
746
  } else {
665
- console.log('CDP execution failed, showing commands:', result);
666
- alert(`CDP execution not available. Generated ${result.commands?.length || 0} commands. Check console for details.`);
747
+ console.log("CDP execution failed, showing commands:", result);
748
+ alert(
749
+ `CDP execution not available. Generated ${
750
+ result.commands?.length || 0
751
+ } commands. Check console for details.`
752
+ );
667
753
  }
668
-
669
754
  } catch (error) {
670
- console.error('Replay error:', error);
671
- alert('Failed to start replay: ' + (error instanceof Error ? error.message : 'Unknown error'));
755
+ console.error("Replay error:", error);
756
+ alert(
757
+ "Failed to start replay: " +
758
+ (error instanceof Error ? error.message : "Unknown error")
759
+ );
672
760
  } finally {
673
761
  setIsReplaying(false);
674
762
  }
@@ -677,8 +765,8 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
677
765
  const loadReplayPreview = () => {
678
766
  // Extract interactions from current logs instead of making API call
679
767
  const interactions = logs
680
- .filter(log => log.message.includes('[INTERACTION]'))
681
- .map(log => {
768
+ .filter((log) => log.message.includes("[INTERACTION]"))
769
+ .map((log) => {
682
770
  const match = log.message.match(/\[INTERACTION\] (.+)/);
683
771
  if (match) {
684
772
  try {
@@ -687,7 +775,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
687
775
  return {
688
776
  timestamp: log.timestamp,
689
777
  type: data.type,
690
- details: data
778
+ details: data,
691
779
  };
692
780
  } catch {
693
781
  // Fallback to old format parsing
@@ -696,7 +784,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
696
784
  return {
697
785
  timestamp: log.timestamp,
698
786
  type: oldMatch[1],
699
- details: oldMatch[2]
787
+ details: oldMatch[2],
700
788
  };
701
789
  }
702
790
  }
@@ -704,19 +792,19 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
704
792
  return null;
705
793
  })
706
794
  .filter(Boolean);
707
-
795
+
708
796
  setReplayEvents(interactions);
709
797
  };
710
798
 
711
799
  const handleRotateLog = async () => {
712
800
  if (!currentLogFile || isRotatingLog) return;
713
-
801
+
714
802
  setIsRotatingLog(true);
715
803
  try {
716
- const response = await fetch('/api/logs/rotate', {
717
- method: 'POST',
804
+ const response = await fetch("/api/logs/rotate", {
805
+ method: "POST",
718
806
  headers: {
719
- 'Content-Type': 'application/json',
807
+ "Content-Type": "application/json",
720
808
  },
721
809
  body: JSON.stringify({ currentLogPath: currentLogFile }),
722
810
  });
@@ -726,38 +814,38 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
726
814
  setLogs([]);
727
815
  setLastLogCount(0);
728
816
  setLastFetched(null);
729
-
817
+
730
818
  // Reload available logs to show the new archived file
731
819
  await loadAvailableLogs();
732
-
820
+
733
821
  // Start fresh polling
734
822
  await loadInitialLogs();
735
823
  } else {
736
824
  const error = await response.json();
737
- console.error('Failed to rotate log:', error);
738
- alert('Failed to rotate log: ' + error.error);
825
+ console.error("Failed to rotate log:", error);
826
+ alert("Failed to rotate log: " + error.error);
739
827
  }
740
828
  } catch (error) {
741
- console.error('Error rotating log:', error);
742
- alert('Error rotating log');
829
+ console.error("Error rotating log:", error);
830
+ alert("Error rotating log");
743
831
  } finally {
744
832
  setIsRotatingLog(false);
745
833
  }
746
834
  };
747
835
 
748
836
  const filteredLogs = useMemo(() => {
749
- return logs.filter(entry => {
837
+ return logs.filter((entry) => {
750
838
  // Check specific message types first (these override source filtering)
751
- const isInteraction = entry.message.includes('[INTERACTION]');
752
- const isScreenshot = entry.message.includes('[SCREENSHOT]');
753
-
839
+ const isInteraction = entry.message.includes("[INTERACTION]");
840
+ const isScreenshot = entry.message.includes("[SCREENSHOT]");
841
+
754
842
  if (isInteraction) return filters.interaction;
755
843
  if (isScreenshot) return filters.screenshot;
756
-
844
+
757
845
  // For other logs, filter by source
758
- if (entry.source === 'SERVER') return filters.server;
759
- if (entry.source === 'BROWSER') return filters.browser;
760
-
846
+ if (entry.source === "SERVER") return filters.server;
847
+ if (entry.source === "BROWSER") return filters.browser;
848
+
761
849
  return true;
762
850
  });
763
851
  }, [logs, filters]);
@@ -770,10 +858,13 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
770
858
  <div className="flex items-center justify-between">
771
859
  <div className="flex items-center gap-2 sm:gap-4">
772
860
  <div className="flex items-center gap-1">
773
- <h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">dev3000</h1>
774
- <span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">(v{version})</span>
861
+ <h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">
862
+ dev3000
863
+ </h1>
864
+ <span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
865
+ (v{version})
866
+ </span>
775
867
  </div>
776
-
777
868
  {/* Log File Selector */}
778
869
  {availableLogs.length > 1 ? (
779
870
  <div className="relative" ref={dropdownRef}>
@@ -783,63 +874,88 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
783
874
  >
784
875
  <span className="font-mono text-xs whitespace-nowrap">
785
876
  {isInitialLoading && !currentLogFile ? (
786
- <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" style={{width: '220px'}} />
877
+ <div
878
+ className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"
879
+ style={{ width: "220px" }}
880
+ />
881
+ ) : currentLogFile ? (
882
+ currentLogFile.split("/").pop()
787
883
  ) : (
788
- currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
884
+ "dev3000.log"
789
885
  )}
790
886
  </span>
791
- <svg
792
- className={`w-4 h-4 transition-transform ${showLogSelector ? 'rotate-180' : ''}`}
793
- fill="none"
794
- stroke="currentColor"
887
+ <svg
888
+ className={`w-4 h-4 transition-transform ${
889
+ showLogSelector ? "rotate-180" : ""
890
+ }`}
891
+ fill="none"
892
+ stroke="currentColor"
795
893
  viewBox="0 0 24 24"
796
894
  >
797
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
895
+ <path
896
+ strokeLinecap="round"
897
+ strokeLinejoin="round"
898
+ strokeWidth={2}
899
+ d="M19 9l-7 7-7-7"
900
+ />
798
901
  </svg>
799
902
  </button>
800
-
801
- {/* Dropdown */}
802
- {showLogSelector && availableLogs.length > 1 && (
803
- <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">
804
- <div className="py-1 max-h-60 overflow-y-auto">
805
- <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">
806
- {projectName} logs ({availableLogs.length})
903
+ {/* Dropdown */}
904
+ {showLogSelector && availableLogs.length > 1 && (
905
+ <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">
906
+ <div className="py-1 max-h-60 overflow-y-auto">
907
+ <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">
908
+ {projectName} logs ({availableLogs.length})
909
+ </div>
910
+ {availableLogs.map((logFile) => (
911
+ <button
912
+ key={logFile.path}
913
+ onClick={() => {
914
+ setShowLogSelector(false);
915
+ router.push(
916
+ `/logs?file=${encodeURIComponent(
917
+ logFile.name
918
+ )}&mode=${mode}`
919
+ );
920
+ }}
921
+ className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between ${
922
+ logFile.isCurrent
923
+ ? "bg-blue-50 text-blue-900"
924
+ : "text-gray-700"
925
+ }`}
926
+ >
927
+ <div className="flex flex-col">
928
+ <span className="font-mono text-xs">
929
+ {logFile.name}
930
+ </span>
931
+ <span className="text-xs text-gray-500">
932
+ {new Date(logFile.mtime).toLocaleString()} •{" "}
933
+ {Math.round(logFile.size / 1024)}KB
934
+ </span>
935
+ </div>
936
+ {logFile.isCurrent && (
937
+ <span className="text-xs text-blue-600 font-medium">
938
+ current
939
+ </span>
940
+ )}
941
+ </button>
942
+ ))}
807
943
  </div>
808
- {availableLogs.map((logFile) => (
809
- <button
810
- key={logFile.path}
811
- onClick={() => {
812
- setShowLogSelector(false);
813
- router.push(`/logs?file=${encodeURIComponent(logFile.name)}&mode=${mode}`);
814
- }}
815
- className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between ${
816
- logFile.isCurrent ? 'bg-blue-50 text-blue-900' : 'text-gray-700'
817
- }`}
818
- >
819
- <div className="flex flex-col">
820
- <span className="font-mono text-xs">
821
- {logFile.name}
822
- </span>
823
- <span className="text-xs text-gray-500">
824
- {new Date(logFile.mtime).toLocaleString()} • {Math.round(logFile.size / 1024)}KB
825
- </span>
826
- </div>
827
- {logFile.isCurrent && (
828
- <span className="text-xs text-blue-600 font-medium">current</span>
829
- )}
830
- </button>
831
- ))}
832
944
  </div>
833
- </div>
834
- )}
945
+ )}
835
946
  </div>
836
947
  ) : (
837
948
  <div className="flex items-center gap-2">
838
949
  <span className="font-mono text-xs text-gray-600 px-3 py-1 whitespace-nowrap">
839
950
  {isInitialLoading && !currentLogFile ? (
840
- <div className="h-4 bg-gray-200 rounded animate-pulse" style={{width: '220px'}} />
951
+ <div
952
+ className="h-4 bg-gray-200 rounded animate-pulse"
953
+ style={{ width: "220px" }}
954
+ />
955
+ ) : currentLogFile ? (
956
+ currentLogFile.split("/").pop()
841
957
  ) : (
842
- currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
958
+ "dev3000.log"
843
959
  )}
844
960
  </span>
845
961
  {currentLogFile && !isInitialLoading && (
@@ -849,17 +965,18 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
849
965
  className="px-2 py-1 text-xs bg-orange-100 text-orange-700 hover:bg-orange-200 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
850
966
  title="Clear logs (rotate current log to archive and start fresh)"
851
967
  >
852
- {isRotatingLog ? '...' : 'Clear'}
968
+ {isRotatingLog ? "..." : "Clear"}
853
969
  </button>
854
970
  )}
855
971
  </div>
856
972
  )}
857
-
858
973
  {logs.length > 0 && (
859
- <span className="text-sm text-gray-500 hidden sm:inline">{logs.length} entries</span>
974
+ <span className="text-sm text-gray-500 hidden sm:inline">
975
+ {logs.length} entries
976
+ </span>
860
977
  )}
861
978
  </div>
862
-
979
+
863
980
  {/* Mode Toggle with Replay Button */}
864
981
  <div className="flex items-center gap-2">
865
982
  {/* Replay Button with Hover Preview */}
@@ -876,8 +993,8 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
876
993
  onMouseLeave={() => setShowReplayPreview(false)}
877
994
  className={`flex items-center gap-1 px-3 py-1 rounded text-sm font-medium transition-colors whitespace-nowrap ${
878
995
  isReplaying
879
- ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
880
- : 'bg-purple-100 text-purple-800 hover:bg-purple-200'
996
+ ? "bg-gray-100 text-gray-400 cursor-not-allowed"
997
+ : "bg-purple-100 text-purple-800 hover:bg-purple-200"
881
998
  }`}
882
999
  >
883
1000
  {isReplaying ? (
@@ -887,14 +1004,23 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
887
1004
  </>
888
1005
  ) : (
889
1006
  <>
890
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
891
- <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" />
1007
+ <svg
1008
+ className="w-4 h-4"
1009
+ fill="none"
1010
+ stroke="currentColor"
1011
+ viewBox="0 0 24 24"
1012
+ >
1013
+ <path
1014
+ strokeLinecap="round"
1015
+ strokeLinejoin="round"
1016
+ strokeWidth={2}
1017
+ 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"
1018
+ />
892
1019
  </svg>
893
1020
  Replay
894
1021
  </>
895
1022
  )}
896
1023
  </button>
897
-
898
1024
  {/* Replay Preview Dropdown */}
899
1025
  {showReplayPreview && !isReplaying && (
900
1026
  <div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-30 w-80">
@@ -909,23 +1035,35 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
909
1035
  </div>
910
1036
  ) : (
911
1037
  replayEvents.map((event, index) => (
912
- <div key={index} className="px-3 py-2 text-sm hover:bg-gray-50 border-b border-gray-100 last:border-b-0">
1038
+ <div
1039
+ key={index}
1040
+ className="px-3 py-2 text-sm hover:bg-gray-50 border-b border-gray-100 last:border-b-0"
1041
+ >
913
1042
  <div className="flex items-center gap-2">
914
- <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
915
- event.type === 'CLICK' ? 'bg-blue-100 text-blue-800' :
916
- event.type === 'SCROLL' ? 'bg-green-100 text-green-800' :
917
- 'bg-gray-100 text-gray-700'
918
- }`}>
1043
+ <span
1044
+ className={`px-1.5 py-0.5 rounded text-xs font-medium ${
1045
+ event.type === "CLICK"
1046
+ ? "bg-blue-100 text-blue-800"
1047
+ : event.type === "SCROLL"
1048
+ ? "bg-green-100 text-green-800"
1049
+ : "bg-gray-100 text-gray-700"
1050
+ }`}
1051
+ >
919
1052
  {event.type}
920
1053
  </span>
921
1054
  <span className="text-xs text-gray-500 font-mono">
922
- {new Date(event.timestamp).toLocaleTimeString()}
1055
+ {new Date(
1056
+ event.timestamp
1057
+ ).toLocaleTimeString()}
923
1058
  </span>
924
1059
  </div>
925
1060
  <div className="mt-1 text-xs text-gray-600 font-mono truncate">
926
- {event.type === 'CLICK' && `(${event.x}, ${event.y}) on ${event.target}`}
927
- {event.type === 'SCROLL' && `${event.direction} ${event.distance}px to (${event.x}, ${event.y})`}
928
- {event.type === 'KEY' && `${event.key} in ${event.target}`}
1061
+ {event.type === "CLICK" &&
1062
+ `(${event.x}, ${event.y}) on ${event.target}`}
1063
+ {event.type === "SCROLL" &&
1064
+ `${event.direction} ${event.distance}px to (${event.x}, ${event.y})`}
1065
+ {event.type === "KEY" &&
1066
+ `${event.key} in ${event.target}`}
929
1067
  </div>
930
1068
  </div>
931
1069
  ))
@@ -935,19 +1073,27 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
935
1073
  </div>
936
1074
  )}
937
1075
  </div>
938
-
939
1076
  {/* Filter Button */}
940
1077
  <div className="relative" ref={filterDropdownRef}>
941
1078
  <button
942
1079
  onClick={() => setShowFilters(!showFilters)}
943
1080
  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"
944
1081
  >
945
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
946
- <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" />
1082
+ <svg
1083
+ className="w-4 h-4"
1084
+ fill="none"
1085
+ stroke="currentColor"
1086
+ viewBox="0 0 24 24"
1087
+ >
1088
+ <path
1089
+ strokeLinecap="round"
1090
+ strokeLinejoin="round"
1091
+ strokeWidth={2}
1092
+ 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"
1093
+ />
947
1094
  </svg>
948
1095
  Filter
949
1096
  </button>
950
-
951
1097
  {/* Filter Dropdown */}
952
1098
  {showFilters && (
953
1099
  <div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-48">
@@ -956,10 +1102,32 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
956
1102
  Log Types
957
1103
  </div>
958
1104
  {[
959
- { key: 'server', label: 'Server', count: logs.filter(l => l.source === 'SERVER').length },
960
- { key: 'browser', label: 'Browser', count: logs.filter(l => l.source === 'BROWSER').length },
961
- { key: 'interaction', label: 'Interaction', count: logs.filter(l => l.message.includes('[INTERACTION]')).length },
962
- { key: 'screenshot', label: 'Screenshot', count: logs.filter(l => l.message.includes('[SCREENSHOT]')).length }
1105
+ {
1106
+ key: "server",
1107
+ label: "Server",
1108
+ count: logs.filter((l) => l.source === "SERVER")
1109
+ .length,
1110
+ },
1111
+ {
1112
+ key: "browser",
1113
+ label: "Browser",
1114
+ count: logs.filter((l) => l.source === "BROWSER")
1115
+ .length,
1116
+ },
1117
+ {
1118
+ key: "interaction",
1119
+ label: "Interaction",
1120
+ count: logs.filter((l) =>
1121
+ l.message.includes("[INTERACTION]")
1122
+ ).length,
1123
+ },
1124
+ {
1125
+ key: "screenshot",
1126
+ label: "Screenshot",
1127
+ count: logs.filter((l) =>
1128
+ l.message.includes("[SCREENSHOT]")
1129
+ ).length,
1130
+ },
963
1131
  ].map(({ key, label, count }) => (
964
1132
  <label
965
1133
  key={key}
@@ -969,7 +1137,12 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
969
1137
  <input
970
1138
  type="checkbox"
971
1139
  checked={filters[key as keyof typeof filters]}
972
- onChange={(e) => setFilters(prev => ({ ...prev, [key]: e.target.checked }))}
1140
+ onChange={(e) =>
1141
+ setFilters((prev) => ({
1142
+ ...prev,
1143
+ [key]: e.target.checked,
1144
+ }))
1145
+ }
973
1146
  className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
974
1147
  />
975
1148
  <span>{label}</span>
@@ -981,55 +1154,83 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
981
1154
  </div>
982
1155
  )}
983
1156
  </div>
984
-
985
1157
  <div className="flex items-center bg-gray-100 rounded-md p-1">
986
- <button
987
- onClick={() => {
988
- const currentFile = searchParams.get('file');
989
- if (currentFile) {
990
- router.push(`/logs?file=${encodeURIComponent(currentFile)}&mode=head`);
991
- }
992
- }}
993
- className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
994
- mode === 'head'
995
- ? 'bg-white text-gray-900 shadow-sm'
996
- : 'text-gray-600 hover:text-gray-900'
997
- }`}
998
- >
999
- Head
1000
- </button>
1001
- <button
1002
- onClick={() => {
1003
- const currentFile = searchParams.get('file');
1004
- if (currentFile) {
1005
- router.push(`/logs?file=${encodeURIComponent(currentFile)}&mode=tail`);
1006
- }
1007
- }}
1008
- className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
1009
- mode === 'tail'
1010
- ? 'bg-white text-gray-900 shadow-sm'
1011
- : 'text-gray-600 hover:text-gray-900'
1012
- }`}
1013
- >
1014
- Tail
1015
- </button>
1158
+ <button
1159
+ onClick={() => {
1160
+ const currentFile = searchParams.get("file");
1161
+ if (currentFile) {
1162
+ router.push(
1163
+ `/logs?file=${encodeURIComponent(
1164
+ currentFile
1165
+ )}&mode=head`
1166
+ );
1167
+ }
1168
+ }}
1169
+ className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
1170
+ mode === "head"
1171
+ ? "bg-white text-gray-900 shadow-sm"
1172
+ : "text-gray-600 hover:text-gray-900"
1173
+ }`}
1174
+ >
1175
+ Head
1176
+ </button>
1177
+ <button
1178
+ onClick={() => {
1179
+ const currentFile = searchParams.get("file");
1180
+ if (currentFile) {
1181
+ router.push(
1182
+ `/logs?file=${encodeURIComponent(
1183
+ currentFile
1184
+ )}&mode=tail`
1185
+ );
1186
+ }
1187
+ }}
1188
+ className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
1189
+ mode === "tail"
1190
+ ? "bg-white text-gray-900 shadow-sm"
1191
+ : "text-gray-600 hover:text-gray-900"
1192
+ }`}
1193
+ >
1194
+ Tail
1195
+ </button>
1016
1196
  </div>
1017
-
1018
- {/* Dark Mode Toggle */}
1197
+ {/* Dark Mode Toggle - moved to last item */}
1019
1198
  <button
1020
1199
  onClick={() => setDarkMode(!darkMode)}
1021
- 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"
1022
- title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
1200
+ 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 ml-2"
1201
+ title={
1202
+ darkMode ? "Switch to light mode" : "Switch to dark mode"
1203
+ }
1023
1204
  >
1024
1205
  {darkMode ? (
1025
1206
  // Sun icon for light mode
1026
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1027
- <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" />
1207
+ <svg
1208
+ className="w-4 h-4"
1209
+ fill="none"
1210
+ stroke="currentColor"
1211
+ viewBox="0 0 24 24"
1212
+ >
1213
+ <path
1214
+ strokeLinecap="round"
1215
+ strokeLinejoin="round"
1216
+ strokeWidth={2}
1217
+ 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"
1218
+ />
1028
1219
  </svg>
1029
1220
  ) : (
1030
1221
  // Moon icon for dark mode
1031
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1032
- <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" />
1222
+ <svg
1223
+ className="w-4 h-4"
1224
+ fill="none"
1225
+ stroke="currentColor"
1226
+ viewBox="0 0 24 24"
1227
+ >
1228
+ <path
1229
+ strokeLinecap="round"
1230
+ strokeLinejoin="round"
1231
+ strokeWidth={2}
1232
+ 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"
1233
+ />
1033
1234
  </svg>
1034
1235
  )}
1035
1236
  </button>
@@ -1040,7 +1241,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1040
1241
 
1041
1242
  {/* Content Area - Fills remaining space */}
1042
1243
  <div className="flex-1 overflow-hidden">
1043
- <div
1244
+ <div
1044
1245
  ref={containerRef}
1045
1246
  className="max-w-7xl mx-auto px-4 py-6 h-full overflow-y-auto"
1046
1247
  onScroll={handleScroll}
@@ -1052,14 +1253,18 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1052
1253
  </div>
1053
1254
  ) : logs.length === 0 ? (
1054
1255
  <div className="text-center py-12">
1055
- <div className="text-gray-400 dark:text-gray-500 text-lg">📝 No logs yet</div>
1256
+ <div className="text-gray-400 dark:text-gray-500 text-lg">
1257
+ 📝 No logs yet
1258
+ </div>
1056
1259
  <div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
1057
1260
  Logs will appear here as your development server runs
1058
1261
  </div>
1059
1262
  </div>
1060
1263
  ) : filteredLogs.length === 0 ? (
1061
1264
  <div className="text-center py-12">
1062
- <div className="text-gray-400 dark:text-gray-500 text-lg">🔍 No logs match current filters</div>
1265
+ <div className="text-gray-400 dark:text-gray-500 text-lg">
1266
+ 🔍 No logs match current filters
1267
+ </div>
1063
1268
  <div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
1064
1269
  Try adjusting your filter settings to see more logs
1065
1270
  </div>
@@ -1074,7 +1279,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1074
1279
  )}
1075
1280
  </div>
1076
1281
  </div>
1077
-
1282
+
1078
1283
  {/* Footer - Fixed */}
1079
1284
  <div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-none">
1080
1285
  <div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
@@ -1082,7 +1287,9 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1082
1287
  {isLoadingNew && (
1083
1288
  <div className="flex items-center gap-1">
1084
1289
  <div className="w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
1085
- <span className="text-xs text-gray-500 dark:text-gray-400">Loading...</span>
1290
+ <span className="text-xs text-gray-500 dark:text-gray-400">
1291
+ Loading...
1292
+ </span>
1086
1293
  </div>
1087
1294
  )}
1088
1295
  {!isLoadingNew && lastFetched && (
@@ -1091,9 +1298,9 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1091
1298
  </span>
1092
1299
  )}
1093
1300
  {currentLogFile && (
1094
- <a
1095
- href={`file://${currentLogFile}`}
1096
- target="_blank"
1301
+ <a
1302
+ href={`file://${currentLogFile}`}
1303
+ target="_blank"
1097
1304
  rel="noopener noreferrer"
1098
1305
  className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
1099
1306
  >
@@ -1101,24 +1308,30 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1101
1308
  </a>
1102
1309
  )}
1103
1310
  </div>
1104
-
1311
+
1105
1312
  {/* Live indicator or scroll to bottom button - positioned on the right */}
1106
1313
  <div className="relative">
1107
1314
  {/* Live indicator when at bottom */}
1108
- <div
1315
+ <div
1109
1316
  className={`flex items-center gap-1 text-green-600 ${
1110
- mode === 'tail' && isAtBottom && !isLoadingNew ? 'visible' : 'invisible'
1317
+ mode === "tail" && isAtBottom && !isLoadingNew
1318
+ ? "visible"
1319
+ : "invisible"
1111
1320
  }`}
1112
1321
  >
1113
1322
  <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
1114
1323
  <span className="text-xs">Live</span>
1115
1324
  </div>
1116
-
1325
+
1117
1326
  {/* Scroll to bottom button when not at bottom */}
1118
1327
  <button
1119
- onClick={() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' })}
1328
+ onClick={() =>
1329
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" })
1330
+ }
1120
1331
  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 ${
1121
- mode === 'tail' && !isAtBottom && !isLoadingNew ? 'visible' : 'invisible'
1332
+ mode === "tail" && !isAtBottom && !isLoadingNew
1333
+ ? "visible"
1334
+ : "invisible"
1122
1335
  }`}
1123
1336
  >
1124
1337
  ↓ Live
@@ -1128,4 +1341,4 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
1128
1341
  </div>
1129
1342
  </div>
1130
1343
  );
1131
- }
1344
+ }