dev3000 0.0.44 → 0.0.46

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