@appkit/llamacpp-cli 1.12.0 → 1.12.1

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.
Files changed (114) hide show
  1. package/README.md +217 -168
  2. package/package.json +10 -2
  3. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  4. package/web/dist/assets/index-CVmonw3T.js +17 -0
  5. package/web/{index.html → dist/index.html} +2 -1
  6. package/.versionrc.json +0 -16
  7. package/CHANGELOG.md +0 -213
  8. package/docs/images/.gitkeep +0 -1
  9. package/docs/images/web-ui-servers.png +0 -0
  10. package/src/cli.ts +0 -523
  11. package/src/commands/admin/config.ts +0 -121
  12. package/src/commands/admin/logs.ts +0 -91
  13. package/src/commands/admin/restart.ts +0 -26
  14. package/src/commands/admin/start.ts +0 -27
  15. package/src/commands/admin/status.ts +0 -84
  16. package/src/commands/admin/stop.ts +0 -16
  17. package/src/commands/config-global.ts +0 -38
  18. package/src/commands/config.ts +0 -323
  19. package/src/commands/create.ts +0 -183
  20. package/src/commands/delete.ts +0 -74
  21. package/src/commands/list.ts +0 -37
  22. package/src/commands/logs-all.ts +0 -251
  23. package/src/commands/logs.ts +0 -345
  24. package/src/commands/monitor.ts +0 -110
  25. package/src/commands/ps.ts +0 -84
  26. package/src/commands/pull.ts +0 -44
  27. package/src/commands/rm.ts +0 -107
  28. package/src/commands/router/config.ts +0 -116
  29. package/src/commands/router/logs.ts +0 -256
  30. package/src/commands/router/restart.ts +0 -36
  31. package/src/commands/router/start.ts +0 -60
  32. package/src/commands/router/status.ts +0 -119
  33. package/src/commands/router/stop.ts +0 -33
  34. package/src/commands/run.ts +0 -233
  35. package/src/commands/search.ts +0 -107
  36. package/src/commands/server-show.ts +0 -161
  37. package/src/commands/show.ts +0 -207
  38. package/src/commands/start.ts +0 -101
  39. package/src/commands/stop.ts +0 -39
  40. package/src/commands/tui.ts +0 -25
  41. package/src/lib/admin-manager.ts +0 -435
  42. package/src/lib/admin-server.ts +0 -1243
  43. package/src/lib/config-generator.ts +0 -130
  44. package/src/lib/download-job-manager.ts +0 -213
  45. package/src/lib/history-manager.ts +0 -172
  46. package/src/lib/launchctl-manager.ts +0 -225
  47. package/src/lib/metrics-aggregator.ts +0 -257
  48. package/src/lib/model-downloader.ts +0 -328
  49. package/src/lib/model-scanner.ts +0 -157
  50. package/src/lib/model-search.ts +0 -114
  51. package/src/lib/models-dir-setup.ts +0 -46
  52. package/src/lib/port-manager.ts +0 -80
  53. package/src/lib/router-logger.ts +0 -201
  54. package/src/lib/router-manager.ts +0 -414
  55. package/src/lib/router-server.ts +0 -538
  56. package/src/lib/state-manager.ts +0 -206
  57. package/src/lib/status-checker.ts +0 -113
  58. package/src/lib/system-collector.ts +0 -315
  59. package/src/tui/ConfigApp.ts +0 -1085
  60. package/src/tui/HistoricalMonitorApp.ts +0 -587
  61. package/src/tui/ModelsApp.ts +0 -368
  62. package/src/tui/MonitorApp.ts +0 -386
  63. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  64. package/src/tui/RootNavigator.ts +0 -74
  65. package/src/tui/SearchApp.ts +0 -511
  66. package/src/tui/SplashScreen.ts +0 -149
  67. package/src/types/admin-config.ts +0 -25
  68. package/src/types/global-config.ts +0 -26
  69. package/src/types/history-types.ts +0 -39
  70. package/src/types/model-info.ts +0 -8
  71. package/src/types/monitor-types.ts +0 -162
  72. package/src/types/router-config.ts +0 -25
  73. package/src/types/server-config.ts +0 -46
  74. package/src/utils/downsample-utils.ts +0 -128
  75. package/src/utils/file-utils.ts +0 -146
  76. package/src/utils/format-utils.ts +0 -98
  77. package/src/utils/log-parser.ts +0 -284
  78. package/src/utils/log-utils.ts +0 -178
  79. package/src/utils/process-utils.ts +0 -316
  80. package/src/utils/prompt-utils.ts +0 -47
  81. package/test-load.sh +0 -100
  82. package/tsconfig.json +0 -20
  83. package/web/eslint.config.js +0 -23
  84. package/web/llamacpp-web-dist.tar.gz +0 -0
  85. package/web/package-lock.json +0 -4017
  86. package/web/package.json +0 -38
  87. package/web/postcss.config.js +0 -6
  88. package/web/src/App.css +0 -42
  89. package/web/src/App.tsx +0 -86
  90. package/web/src/assets/react.svg +0 -1
  91. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  92. package/web/src/components/CreateServerModal.tsx +0 -372
  93. package/web/src/components/DownloadProgress.tsx +0 -123
  94. package/web/src/components/Nav.tsx +0 -89
  95. package/web/src/components/RouterConfigModal.tsx +0 -240
  96. package/web/src/components/SearchModal.tsx +0 -306
  97. package/web/src/components/ServerConfigModal.tsx +0 -291
  98. package/web/src/hooks/useApi.ts +0 -259
  99. package/web/src/index.css +0 -42
  100. package/web/src/lib/api.ts +0 -226
  101. package/web/src/main.tsx +0 -10
  102. package/web/src/pages/Dashboard.tsx +0 -103
  103. package/web/src/pages/Models.tsx +0 -258
  104. package/web/src/pages/Router.tsx +0 -270
  105. package/web/src/pages/RouterLogs.tsx +0 -201
  106. package/web/src/pages/ServerLogs.tsx +0 -553
  107. package/web/src/pages/Servers.tsx +0 -358
  108. package/web/src/types/api.ts +0 -140
  109. package/web/tailwind.config.js +0 -31
  110. package/web/tsconfig.app.json +0 -28
  111. package/web/tsconfig.json +0 -7
  112. package/web/tsconfig.node.json +0 -26
  113. package/web/vite.config.ts +0 -25
  114. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,553 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { useParams, useNavigate, Link } from 'react-router-dom';
3
- import { ArrowLeft, Loader2, ChevronDown, Trash2 } from 'lucide-react';
4
- import { useServerLogs, useServer } from '../hooks/useApi';
5
-
6
- type LogFilter = 'all' | 'requests' | 'errors' | 'warnings' | 'system';
7
- type LogSort = 'newest' | 'oldest';
8
- type ViewMode = 'formatted' | 'raw';
9
-
10
- interface ParsedLogLine {
11
- raw: string;
12
- formatted?: string;
13
- timestamp?: string;
14
- type: 'request' | 'error' | 'warning' | 'system';
15
- isHealthCheck?: boolean;
16
- }
17
-
18
- // Health check endpoints to filter by default
19
- const HEALTH_CHECK_ENDPOINTS = ['/health', '/slots', '/props'];
20
-
21
- // Cache for timestamps - maps raw log content to its assigned timestamp
22
- const timestampCache = new Map<string, string>();
23
-
24
- // Generate a current timestamp
25
- function getCurrentTimestamp(): string {
26
- const now = new Date();
27
- return now.toISOString().substring(0, 19).replace('T', ' ');
28
- }
29
-
30
- // Log parser utilities - returns cached timestamp or generates new one
31
- function extractTimestamp(line: string, rawKey: string): string {
32
- // Try bracket format: [2025-12-09 10:13:45]
33
- const bracketMatch = line.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
34
- if (bracketMatch) return bracketMatch[1];
35
-
36
- // Try plain format at start of line: 2025-12-09 10:13:45
37
- const plainMatch = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
38
- if (plainMatch) return plainMatch[1];
39
-
40
- // Check cache for previously generated timestamp
41
- const cached = timestampCache.get(rawKey);
42
- if (cached) return cached;
43
-
44
- // Generate and cache new timestamp
45
- const newTimestamp = getCurrentTimestamp();
46
- timestampCache.set(rawKey, newTimestamp);
47
- return newTimestamp;
48
- }
49
-
50
- function extractJson(line: string): any {
51
- const jsonStart = line.indexOf('{');
52
- if (jsonStart === -1) return null;
53
- try {
54
- return JSON.parse(line.substring(jsonStart));
55
- } catch {
56
- return null;
57
- }
58
- }
59
-
60
- function extractUserMessage(requestJson: any): string {
61
- const messages = requestJson.messages || [];
62
- const userMsg = messages.find((m: any) => m.role === 'user');
63
- if (!userMsg || !userMsg.content) return '';
64
-
65
- let content: string;
66
- if (Array.isArray(userMsg.content)) {
67
- const textPart = userMsg.content.find((p: any) => p.type === 'text');
68
- content = textPart?.text || '';
69
- } else {
70
- content = userMsg.content;
71
- }
72
-
73
- content = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
74
- return content.length > 50 ? content.substring(0, 47) + '...' : content;
75
- }
76
-
77
- function extractResponseTime(responseJson: any): number {
78
- const verboseTimings = responseJson.__verbose?.timings;
79
- if (verboseTimings) {
80
- return Math.round((verboseTimings.prompt_ms || 0) + (verboseTimings.predicted_ms || 0));
81
- }
82
- const timings = responseJson.timings;
83
- if (timings) {
84
- return Math.round((timings.prompt_ms || 0) + (timings.predicted_ms || 0));
85
- }
86
- return 0;
87
- }
88
-
89
- function parseLogsToFormatted(lines: string[]): ParsedLogLine[] {
90
- const results: ParsedLogLine[] = [];
91
- let buffer: string[] = [];
92
- let isBuffering = false;
93
-
94
- const isRequestStatusLine = (line: string): boolean => {
95
- return (
96
- (line.includes('log_server_r: request:') || line.includes('log_server_r: done request:')) &&
97
- !line.includes('{') &&
98
- /(?:done )?request: (POST|GET|PUT|DELETE)/.test(line)
99
- );
100
- };
101
-
102
- const consolidateRequest = (bufferedLines: string[]): ParsedLogLine | null => {
103
- try {
104
- const firstLine = bufferedLines[0];
105
- const rawKey = bufferedLines.join('\n'); // Use full request as cache key
106
- const timestamp = extractTimestamp(firstLine, rawKey);
107
- const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
108
- if (!requestMatch) return null;
109
-
110
- const [, method, endpoint, ip, status] = requestMatch;
111
-
112
- // Parse request JSON
113
- const requestLine = bufferedLines.find(l => l.includes('log_server_r: request:') && l.includes('{'));
114
- let userMessage = '';
115
- if (requestLine) {
116
- const requestJson = extractJson(requestLine);
117
- if (requestJson) {
118
- userMessage = extractUserMessage(requestJson);
119
- }
120
- }
121
-
122
- // Parse response JSON
123
- const responseLine = bufferedLines.find(l => l.includes('log_server_r: response:'));
124
- let tokensIn = 0;
125
- let tokensOut = 0;
126
- let responseTimeMs = 0;
127
-
128
- if (responseLine) {
129
- const responseJson = extractJson(responseLine);
130
- if (responseJson) {
131
- tokensIn = responseJson.usage?.prompt_tokens || 0;
132
- tokensOut = responseJson.usage?.completion_tokens || 0;
133
- responseTimeMs = extractResponseTime(responseJson);
134
- }
135
- }
136
-
137
- const formatted = `${timestamp} ${method} ${endpoint} ${ip} ${status} "${userMessage}" ${tokensIn} ${tokensOut} ${responseTimeMs}`;
138
-
139
- // Check if this is a health check request
140
- const isHealthCheck = HEALTH_CHECK_ENDPOINTS.some(ep => endpoint === ep);
141
-
142
- return {
143
- raw: bufferedLines.join('\n'),
144
- formatted,
145
- timestamp,
146
- type: 'request',
147
- isHealthCheck,
148
- };
149
- } catch {
150
- return null;
151
- }
152
- };
153
-
154
- const parseSimpleFormat = (line: string): ParsedLogLine | null => {
155
- try {
156
- const timestamp = extractTimestamp(line, line); // Use line as cache key
157
- const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
158
- if (!requestMatch) return null;
159
-
160
- const [, method, endpoint, ip, status] = requestMatch;
161
- const formatted = `${timestamp} ${method} ${endpoint} ${ip} ${status} "" 0 0 0`;
162
-
163
- // Check if this is a health check request
164
- const isHealthCheck = HEALTH_CHECK_ENDPOINTS.some(ep => endpoint === ep);
165
-
166
- return {
167
- raw: line,
168
- formatted,
169
- timestamp,
170
- type: 'request',
171
- isHealthCheck,
172
- };
173
- } catch {
174
- return null;
175
- }
176
- };
177
-
178
- const parseNonRequestLine = (line: string): ParsedLogLine => {
179
- const trimmed = line.trim();
180
-
181
- // Error patterns
182
- if (trimmed.toLowerCase().includes('error') ||
183
- trimmed.toLowerCase().includes('failed') ||
184
- trimmed.toLowerCase().includes('exception') ||
185
- trimmed.includes('ERR') ||
186
- trimmed.includes('FATAL')) {
187
- return { raw: line, type: 'error' };
188
- }
189
-
190
- // Warning patterns
191
- if (trimmed.toLowerCase().includes('warn') ||
192
- trimmed.toLowerCase().includes('warning') ||
193
- trimmed.includes('WARN')) {
194
- return { raw: line, type: 'warning' };
195
- }
196
-
197
- return { raw: line, type: 'system' };
198
- };
199
-
200
- for (const line of lines) {
201
- if (isRequestStatusLine(line)) {
202
- if (isBuffering) {
203
- // Process previous buffer
204
- const result = consolidateRequest(buffer);
205
- if (result) results.push(result);
206
- buffer = [];
207
- isBuffering = false;
208
- }
209
- isBuffering = true;
210
- buffer = [line];
211
- } else if (isBuffering) {
212
- buffer.push(line);
213
- if (line.includes('log_server_r: response:')) {
214
- const result = consolidateRequest(buffer);
215
- if (result) results.push(result);
216
- buffer = [];
217
- isBuffering = false;
218
- }
219
- } else {
220
- // Non-buffered line (not part of a request)
221
- const trimmed = line.trim();
222
- if (trimmed && !trimmed.includes('log_server_r')) {
223
- results.push(parseNonRequestLine(line));
224
- }
225
- }
226
- }
227
-
228
- // Flush remaining buffer (simple format without response)
229
- if (isBuffering && buffer.length === 1) {
230
- const result = parseSimpleFormat(buffer[0]);
231
- if (result) results.push(result);
232
- }
233
-
234
- return results;
235
- }
236
-
237
- export function ServerLogs() {
238
- const { id } = useParams<{ id: string }>();
239
- const navigate = useNavigate();
240
-
241
- const [activeFilter, setActiveFilter] = useState<LogFilter>('all');
242
- const [sortOrder, setSortOrder] = useState<LogSort>('newest');
243
- const [viewMode, setViewMode] = useState<ViewMode>(() => {
244
- const saved = localStorage.getItem('serverLogs.viewMode');
245
- return (saved === 'raw' || saved === 'formatted') ? saved : 'formatted';
246
- });
247
- const [showSortDropdown, setShowSortDropdown] = useState(false);
248
- const [autoScroll, setAutoScroll] = useState(true);
249
- const [showHealthChecks, setShowHealthChecks] = useState(false);
250
-
251
- // Persist viewMode to localStorage
252
- useEffect(() => {
253
- localStorage.setItem('serverLogs.viewMode', viewMode);
254
- }, [viewMode]);
255
-
256
- const logContainerRef = useRef<HTMLDivElement>(null);
257
- const dropdownRef = useRef<HTMLDivElement>(null);
258
-
259
- const { data: serverData, isLoading: serverLoading } = useServer(id || '');
260
- // Fetch many lines since verbose logs have lots of non-request output
261
- // CLI uses lines * 100 multiplier, we use 50000 to get more parsed request lines
262
- const { data: logsData, isLoading: logsLoading } = useServerLogs(id || null, 50000);
263
-
264
- const server = serverData?.server;
265
-
266
- // Auto-scroll to bottom when new logs arrive
267
- useEffect(() => {
268
- if (autoScroll && logContainerRef.current) {
269
- logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
270
- }
271
- }, [logsData, autoScroll, sortOrder]);
272
-
273
- // Close dropdown on outside click
274
- useEffect(() => {
275
- const handleClickOutside = (e: MouseEvent) => {
276
- if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
277
- setShowSortDropdown(false);
278
- }
279
- };
280
- document.addEventListener('mousedown', handleClickOutside);
281
- return () => document.removeEventListener('mousedown', handleClickOutside);
282
- }, []);
283
-
284
- const getFilteredLogs = (): { logs: ParsedLogLine[]; hasRawLogs: boolean; formattedCount: number } => {
285
- if (!logsData) return { logs: [], hasRawLogs: false, formattedCount: 0 };
286
-
287
- // Combine stdout and stderr
288
- const allLines = [
289
- ...(logsData.stderr || '').split('\n'),
290
- ...(logsData.stdout || '').split('\n'),
291
- ].filter(line => line.trim());
292
-
293
- // Parse logs
294
- const parsed = parseLogsToFormatted(allLines);
295
-
296
- // Track counts for empty state messaging
297
- const hasRawLogs = allLines.length > 0;
298
- const formattedCount = parsed.filter(log => log.formatted && !log.isHealthCheck).length;
299
-
300
- // In formatted view, only show request logs (lines with formatted output)
301
- let filtered = parsed;
302
- if (viewMode === 'formatted') {
303
- filtered = parsed.filter(log => log.formatted);
304
- }
305
-
306
- // Filter out health check requests unless toggle is enabled
307
- if (!showHealthChecks) {
308
- filtered = filtered.filter(log => !log.isHealthCheck);
309
- }
310
-
311
- // Apply filter
312
- if (activeFilter !== 'all') {
313
- filtered = filtered.filter(log => {
314
- switch (activeFilter) {
315
- case 'requests':
316
- return log.type === 'request';
317
- case 'errors':
318
- return log.type === 'error';
319
- case 'warnings':
320
- return log.type === 'warning';
321
- case 'system':
322
- return log.type === 'system';
323
- default:
324
- return true;
325
- }
326
- });
327
- }
328
-
329
- // Apply sort
330
- if (sortOrder === 'oldest') {
331
- return { logs: filtered, hasRawLogs, formattedCount };
332
- }
333
-
334
- return { logs: [...filtered].reverse(), hasRawLogs, formattedCount };
335
- };
336
-
337
- const { logs: filteredLogs, hasRawLogs, formattedCount } = getFilteredLogs();
338
-
339
- const filters: { id: LogFilter; label: string }[] = [
340
- { id: 'all', label: 'All' },
341
- { id: 'requests', label: 'Requests' },
342
- { id: 'errors', label: 'Errors' },
343
- { id: 'warnings', label: 'Warnings' },
344
- { id: 'system', label: 'System' },
345
- ];
346
-
347
- const getLogTypeColor = (type: string): string => {
348
- switch (type) {
349
- case 'request':
350
- return 'text-blue-400';
351
- case 'error':
352
- return 'text-red-400';
353
- case 'warning':
354
- return 'text-yellow-400';
355
- default:
356
- return 'text-gray-400';
357
- }
358
- };
359
-
360
- if (serverLoading) {
361
- return (
362
- <div className="max-w-6xl mx-auto px-4 py-12">
363
- <p className="text-gray-500 text-center">Loading...</p>
364
- </div>
365
- );
366
- }
367
-
368
- if (!server) {
369
- return (
370
- <div className="max-w-6xl mx-auto px-4 py-12">
371
- <p className="text-gray-500 text-center">Server not found</p>
372
- <div className="text-center mt-4">
373
- <Link
374
- to="/servers"
375
- className="text-sm text-blue-600 hover:text-blue-800"
376
- >
377
- Back to Servers
378
- </Link>
379
- </div>
380
- </div>
381
- );
382
- }
383
-
384
- return (
385
- <div className="h-[calc(100vh-56px)] flex flex-col">
386
- {/* Header */}
387
- <div className="flex items-center px-4 py-3 border-b border-gray-200 bg-white">
388
- <div className="flex items-center gap-3">
389
- <button
390
- onClick={() => navigate('/servers')}
391
- className="p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
392
- >
393
- <ArrowLeft className="w-5 h-5" />
394
- </button>
395
- <div>
396
- <h1 className="text-lg font-semibold text-gray-900">Server Logs</h1>
397
- <p className="text-sm text-gray-500">{server.modelName.replace('.gguf', '')} · Port {server.port}</p>
398
- </div>
399
- </div>
400
- </div>
401
-
402
- {/* Filter Bar */}
403
- <div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 bg-gray-50">
404
- {/* Filter Pills */}
405
- <div className="flex items-center gap-2">
406
- {filters.map((filter) => (
407
- <button
408
- key={filter.id}
409
- onClick={() => setActiveFilter(filter.id)}
410
- className={`px-3 py-1.5 text-sm font-medium rounded-full border transition-colors cursor-pointer ${
411
- activeFilter === filter.id
412
- ? 'bg-gray-900 text-white border-gray-900'
413
- : 'bg-white text-gray-600 border-gray-200 hover:border-gray-300 hover:bg-gray-50'
414
- }`}
415
- >
416
- {filter.label}
417
- </button>
418
- ))}
419
- </div>
420
-
421
- {/* Health Checks Toggle + View Mode + Sort */}
422
- <div className="flex items-center gap-2">
423
- {/* Health Checks Toggle */}
424
- <button
425
- onClick={() => setShowHealthChecks(!showHealthChecks)}
426
- className={`px-3 py-1.5 text-sm font-medium rounded-lg border transition-colors cursor-pointer ${
427
- showHealthChecks
428
- ? 'bg-blue-50 text-blue-700 border-blue-200'
429
- : 'bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:bg-gray-50'
430
- }`}
431
- title="Show /health, /slots, /props requests (filtered by default)"
432
- >
433
- Health Checks
434
- </button>
435
-
436
- {/* View Mode Toggle */}
437
- <div className="flex items-center bg-white border border-gray-200 rounded-lg overflow-hidden">
438
- <button
439
- onClick={() => setViewMode('formatted')}
440
- className={`px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
441
- viewMode === 'formatted'
442
- ? 'bg-gray-100 text-gray-900'
443
- : 'text-gray-600 hover:bg-gray-50'
444
- }`}
445
- >
446
- Formatted
447
- </button>
448
- <button
449
- onClick={() => setViewMode('raw')}
450
- className={`px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
451
- viewMode === 'raw'
452
- ? 'bg-gray-100 text-gray-900'
453
- : 'text-gray-600 hover:bg-gray-50'
454
- }`}
455
- >
456
- Raw
457
- </button>
458
- </div>
459
-
460
- {/* Sort Dropdown */}
461
- <div className="relative" ref={dropdownRef}>
462
- <button
463
- onClick={() => setShowSortDropdown(!showSortDropdown)}
464
- className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:border-gray-300 transition-colors cursor-pointer"
465
- >
466
- {sortOrder === 'newest' ? 'Newest' : 'Oldest'}
467
- <ChevronDown className="w-4 h-4" />
468
- </button>
469
-
470
- {showSortDropdown && (
471
- <div className="absolute right-0 top-full mt-1 w-32 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
472
- <button
473
- onClick={() => { setSortOrder('newest'); setShowSortDropdown(false); }}
474
- className={`w-full text-left px-3 py-2 text-sm transition-colors cursor-pointer ${
475
- sortOrder === 'newest' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50'
476
- }`}
477
- >
478
- Newest
479
- </button>
480
- <button
481
- onClick={() => { setSortOrder('oldest'); setShowSortDropdown(false); }}
482
- className={`w-full text-left px-3 py-2 text-sm transition-colors cursor-pointer ${
483
- sortOrder === 'oldest' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50'
484
- }`}
485
- >
486
- Oldest
487
- </button>
488
- </div>
489
- )}
490
- </div>
491
- </div>
492
- </div>
493
-
494
- {/* Log Content */}
495
- <div
496
- ref={logContainerRef}
497
- className="flex-1 overflow-y-auto bg-gray-900 p-4 font-mono text-sm"
498
- onScroll={(e) => {
499
- const target = e.target as HTMLDivElement;
500
- const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 50;
501
- setAutoScroll(isAtBottom);
502
- }}
503
- >
504
- {logsLoading ? (
505
- <div className="flex items-center justify-center h-full">
506
- <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
507
- </div>
508
- ) : filteredLogs.length === 0 ? (
509
- <div className="flex flex-col items-center justify-center h-full text-gray-500">
510
- <Trash2 className="w-8 h-8 mb-2 opacity-50" />
511
- <p>No {viewMode === 'formatted' ? 'formatted ' : ''}logs found</p>
512
- {viewMode === 'formatted' && hasRawLogs && formattedCount === 0 ? (
513
- <p className="text-sm mt-1">
514
- No HTTP requests yet.{' '}
515
- <button
516
- onClick={() => setViewMode('raw')}
517
- className="text-blue-400 hover:text-blue-300 underline cursor-pointer"
518
- >
519
- View raw logs
520
- </button>
521
- </p>
522
- ) : activeFilter !== 'all' ? (
523
- <p className="text-sm mt-1">Try selecting a different filter</p>
524
- ) : null}
525
- </div>
526
- ) : (
527
- <div className="space-y-0.5">
528
- {filteredLogs.map((log, index) => (
529
- <div
530
- key={index}
531
- className={`${getLogTypeColor(log.type)} break-all whitespace-pre-wrap leading-relaxed`}
532
- >
533
- {viewMode === 'formatted' && log.formatted ? log.formatted : log.raw}
534
- </div>
535
- ))}
536
- </div>
537
- )}
538
- </div>
539
-
540
- {/* Footer with stats */}
541
- <div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 text-sm text-gray-500">
542
- <span>
543
- {filteredLogs.length} {filteredLogs.length === 1 ? 'line' : 'lines'}
544
- {activeFilter !== 'all' && ` (filtered)`}
545
- </span>
546
- <span className="flex items-center gap-2">
547
- <span className={`w-2 h-2 rounded-full ${autoScroll ? 'bg-green-500' : 'bg-gray-300'}`} />
548
- {autoScroll ? 'Auto-scroll on' : 'Auto-scroll off'}
549
- </span>
550
- </div>
551
- </div>
552
- );
553
- }