@appkit/llamacpp-cli 1.12.0 → 1.13.0

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 (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /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
- }