@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,306 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { X, Search, Download, ArrowLeft, Loader2, HardDrive, Heart, ChevronRight } from 'lucide-react';
3
- import { useSearchModels, useModelFiles, useDownloadModel, useDownloadJob } from '../hooks/useApi';
4
- import type { HFModelResult } from '../types/api';
5
-
6
- interface SearchModalProps {
7
- isOpen: boolean;
8
- onClose: () => void;
9
- onDownloadComplete: () => void;
10
- }
11
-
12
- type ModalState = 'search' | 'files' | 'downloading';
13
-
14
- export function SearchModal({ isOpen, onClose, onDownloadComplete }: SearchModalProps) {
15
- const [state, setState] = useState<ModalState>('search');
16
- const [searchQuery, setSearchQuery] = useState('');
17
- const [selectedModel, setSelectedModel] = useState<HFModelResult | null>(null);
18
- const [activeJobId, setActiveJobId] = useState<string | null>(null);
19
-
20
- const inputRef = useRef<HTMLInputElement>(null);
21
-
22
- const searchMutation = useSearchModels();
23
- const downloadMutation = useDownloadModel();
24
-
25
- const { data: filesData, isLoading: filesLoading } = useModelFiles(
26
- state === 'files' && selectedModel ? selectedModel.modelId : null
27
- );
28
-
29
- const { data: jobData } = useDownloadJob(activeJobId);
30
-
31
- // Focus input when modal opens
32
- useEffect(() => {
33
- if (isOpen && state === 'search') {
34
- setTimeout(() => inputRef.current?.focus(), 100);
35
- }
36
- }, [isOpen, state]);
37
-
38
- // Reset state when modal closes
39
- useEffect(() => {
40
- if (!isOpen) {
41
- setState('search');
42
- setSearchQuery('');
43
- setSelectedModel(null);
44
- setActiveJobId(null);
45
- searchMutation.reset();
46
- }
47
- }, [isOpen]);
48
-
49
- // Handle download completion
50
- useEffect(() => {
51
- if (jobData?.job?.status === 'completed') {
52
- onDownloadComplete();
53
- onClose();
54
- } else if (jobData?.job?.status === 'failed') {
55
- // Stay in downloading state to show error
56
- }
57
- }, [jobData?.job?.status, onDownloadComplete, onClose]);
58
-
59
- const handleSearch = (e: React.FormEvent) => {
60
- e.preventDefault();
61
- if (searchQuery.trim()) {
62
- searchMutation.mutate({ query: searchQuery.trim() });
63
- }
64
- };
65
-
66
- const handleSelectModel = (model: HFModelResult) => {
67
- setSelectedModel(model);
68
- setState('files');
69
- };
70
-
71
- const handleDownload = async (filename: string) => {
72
- if (!selectedModel) return;
73
-
74
- try {
75
- const result = await downloadMutation.mutateAsync({
76
- repo: selectedModel.modelId,
77
- filename,
78
- });
79
- setActiveJobId(result.jobId);
80
- setState('downloading');
81
- } catch (error) {
82
- // Error handled by mutation
83
- }
84
- };
85
-
86
- const handleBack = () => {
87
- if (state === 'files') {
88
- setState('search');
89
- setSelectedModel(null);
90
- }
91
- };
92
-
93
- const formatNumber = (num: number) => {
94
- if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
95
- if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
96
- return num.toString();
97
- };
98
-
99
- const formatBytes = (bytes: number) => {
100
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
101
- if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
102
- return `${(bytes / 1e3).toFixed(1)} KB`;
103
- };
104
-
105
- if (!isOpen) return null;
106
-
107
- return (
108
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
109
- <div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[80vh] flex flex-col">
110
- {/* Header */}
111
- <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
112
- <div className="flex items-center gap-2">
113
- {state === 'files' && (
114
- <button
115
- onClick={handleBack}
116
- className="p-1 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
117
- >
118
- <ArrowLeft className="w-5 h-5" />
119
- </button>
120
- )}
121
- <h2 className="text-lg font-semibold text-gray-900">
122
- {state === 'search' && 'Pull Model'}
123
- {state === 'files' && selectedModel?.modelName}
124
- {state === 'downloading' && 'Downloading'}
125
- </h2>
126
- </div>
127
- <button
128
- onClick={onClose}
129
- className="p-1 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
130
- >
131
- <X className="w-5 h-5" />
132
- </button>
133
- </div>
134
-
135
- {/* Content */}
136
- <div className="flex-1 overflow-y-auto">
137
- {state === 'search' && (
138
- <div className="p-4">
139
- {/* Search Input */}
140
- <form onSubmit={handleSearch}>
141
- <div className="relative">
142
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
143
- <input
144
- ref={inputRef}
145
- type="text"
146
- value={searchQuery}
147
- onChange={(e) => setSearchQuery(e.target.value)}
148
- placeholder="Search Hugging Face models..."
149
- className="w-full pl-10 pr-4 py-2.5 text-sm border border-gray-200 rounded-lg bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
150
- />
151
- </div>
152
- </form>
153
-
154
- {/* Results */}
155
- <div className="mt-4">
156
- {searchMutation.isPending && (
157
- <div className="flex items-center justify-center py-8">
158
- <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
159
- </div>
160
- )}
161
-
162
- {searchMutation.isError && (
163
- <p className="text-center py-8 text-red-600 text-sm">
164
- Search failed. Please try again.
165
- </p>
166
- )}
167
-
168
- {searchMutation.isSuccess && searchMutation.data.results.length === 0 && (
169
- <p className="text-center py-8 text-gray-500 text-sm">
170
- No GGUF models found for "{searchQuery}"
171
- </p>
172
- )}
173
-
174
- {searchMutation.isSuccess && searchMutation.data.results.length > 0 && (
175
- <div className="space-y-1">
176
- {searchMutation.data.results.map((model) => (
177
- <button
178
- key={model.modelId}
179
- onClick={() => handleSelectModel(model)}
180
- className="w-full text-left px-3 py-3 hover:bg-gray-50 rounded-lg transition-colors group cursor-pointer"
181
- >
182
- <div className="flex items-center justify-between">
183
- <div className="flex-1 min-w-0">
184
- <div className="font-medium text-gray-900 truncate">
185
- {model.modelName}
186
- </div>
187
- <div className="text-sm text-gray-500 truncate">
188
- {model.author}
189
- </div>
190
- </div>
191
- <div className="flex items-center gap-3 ml-4">
192
- <span className="flex items-center gap-1 text-xs text-gray-500">
193
- <Download className="w-3.5 h-3.5" />
194
- {formatNumber(model.downloads)}
195
- </span>
196
- <span className="flex items-center gap-1 text-xs text-gray-500">
197
- <Heart className="w-3.5 h-3.5" />
198
- {formatNumber(model.likes)}
199
- </span>
200
- <ChevronRight className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity" />
201
- </div>
202
- </div>
203
- </button>
204
- ))}
205
- </div>
206
- )}
207
-
208
- {!searchMutation.isPending && !searchMutation.isSuccess && (
209
- <p className="text-center py-8 text-gray-500 text-sm">
210
- Search for GGUF models on Hugging Face
211
- </p>
212
- )}
213
- </div>
214
- </div>
215
- )}
216
-
217
- {state === 'files' && (
218
- <div className="p-4">
219
- {filesLoading && (
220
- <div className="flex items-center justify-center py-8">
221
- <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
222
- </div>
223
- )}
224
-
225
- {filesData && filesData.files.length === 0 && (
226
- <p className="text-center py-8 text-gray-500 text-sm">
227
- No GGUF files found in this repository
228
- </p>
229
- )}
230
-
231
- {filesData && filesData.files.length > 0 && (
232
- <div className="space-y-1">
233
- {filesData.files.map((filename) => (
234
- <button
235
- key={filename}
236
- onClick={() => handleDownload(filename)}
237
- disabled={downloadMutation.isPending}
238
- className="w-full text-left px-3 py-3 hover:bg-gray-50 rounded-lg transition-colors group disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
239
- >
240
- <div className="flex items-center justify-between">
241
- <div className="flex items-center gap-2 flex-1 min-w-0">
242
- <HardDrive className="w-4 h-4 text-gray-400 flex-shrink-0" />
243
- <span className="text-sm text-gray-900 truncate">
244
- {filename}
245
- </span>
246
- </div>
247
- <Download className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 ml-2" />
248
- </div>
249
- </button>
250
- ))}
251
- </div>
252
- )}
253
- </div>
254
- )}
255
-
256
- {state === 'downloading' && (
257
- <div className="p-6">
258
- <div className="text-center">
259
- {jobData?.job?.status === 'failed' ? (
260
- <>
261
- <div className="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center">
262
- <X className="w-6 h-6 text-red-600" />
263
- </div>
264
- <h3 className="font-medium text-gray-900 mb-2">Download Failed</h3>
265
- <p className="text-sm text-gray-500 mb-4">{jobData.job.error}</p>
266
- <button
267
- onClick={handleBack}
268
- className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
269
- >
270
- Try Again
271
- </button>
272
- </>
273
- ) : (
274
- <>
275
- <Loader2 className="w-12 h-12 mx-auto mb-4 animate-spin text-gray-400" />
276
- <h3 className="font-medium text-gray-900 mb-1">
277
- {jobData?.job?.filename || 'Starting download...'}
278
- </h3>
279
- {jobData?.job?.progress && (
280
- <>
281
- <div className="w-full bg-gray-200 rounded-full h-2 mb-2">
282
- <div
283
- className="bg-gray-900 h-2 rounded-full transition-all duration-300"
284
- style={{ width: `${jobData.job.progress.percentage}%` }}
285
- />
286
- </div>
287
- <p className="text-sm text-gray-500">
288
- {formatBytes(jobData.job.progress.downloaded)} / {formatBytes(jobData.job.progress.total)}
289
- {' · '}
290
- {jobData.job.progress.speed}
291
- </p>
292
- </>
293
- )}
294
- {!jobData?.job?.progress && (
295
- <p className="text-sm text-gray-500">Connecting...</p>
296
- )}
297
- </>
298
- )}
299
- </div>
300
- </div>
301
- )}
302
- </div>
303
- </div>
304
- </div>
305
- );
306
- }
@@ -1,291 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { X, Loader2, Save, RotateCcw } from 'lucide-react';
3
- import { useUpdateServer } from '../hooks/useApi';
4
- import type { Server } from '../types/api';
5
-
6
- interface ServerConfigModalProps {
7
- server: Server | null;
8
- isOpen: boolean;
9
- onClose: () => void;
10
- }
11
-
12
- interface FormData {
13
- port: number;
14
- host: string;
15
- threads: number;
16
- ctxSize: number;
17
- gpuLayers: number;
18
- verbose: boolean;
19
- customFlags: string;
20
- }
21
-
22
- export function ServerConfigModal({ server, isOpen, onClose }: ServerConfigModalProps) {
23
- const updateServer = useUpdateServer();
24
-
25
- const [formData, setFormData] = useState<FormData>({
26
- port: 9000,
27
- host: '127.0.0.1',
28
- threads: 4,
29
- ctxSize: 4096,
30
- gpuLayers: 60,
31
- verbose: false,
32
- customFlags: '',
33
- });
34
-
35
- const [restartAfterSave, setRestartAfterSave] = useState(true);
36
- const [error, setError] = useState<string | null>(null);
37
-
38
- // Initialize form when server changes
39
- useEffect(() => {
40
- if (server) {
41
- setFormData({
42
- port: server.port,
43
- host: server.host,
44
- threads: server.threads,
45
- ctxSize: server.ctxSize,
46
- gpuLayers: server.gpuLayers,
47
- verbose: server.verbose,
48
- customFlags: server.customFlags?.join(', ') || '',
49
- });
50
- setError(null);
51
- }
52
- }, [server]);
53
-
54
- const handleSubmit = async (e: React.FormEvent) => {
55
- e.preventDefault();
56
- if (!server) return;
57
-
58
- setError(null);
59
-
60
- try {
61
- const customFlags = formData.customFlags
62
- .split(',')
63
- .map(f => f.trim())
64
- .filter(f => f.length > 0);
65
-
66
- await updateServer.mutateAsync({
67
- id: server.id,
68
- data: {
69
- port: formData.port,
70
- host: formData.host,
71
- threads: formData.threads,
72
- ctxSize: formData.ctxSize,
73
- gpuLayers: formData.gpuLayers,
74
- verbose: formData.verbose,
75
- customFlags: customFlags.length > 0 ? customFlags : undefined,
76
- restart: server.status === 'running' && restartAfterSave,
77
- },
78
- });
79
-
80
- onClose();
81
- } catch (err) {
82
- setError((err as Error).message);
83
- }
84
- };
85
-
86
- const formatContextSize = (size: number) => {
87
- if (size >= 1048576) return `${(size / 1048576).toFixed(1)}M tokens`;
88
- if (size >= 1024) return `${(size / 1024).toFixed(0)}K tokens`;
89
- return `${size} tokens`;
90
- };
91
-
92
- if (!isOpen || !server) return null;
93
-
94
- return (
95
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
96
- <div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4 max-h-[90vh] flex flex-col">
97
- {/* Header */}
98
- <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
99
- <div>
100
- <h2 className="text-lg font-semibold text-gray-900">Configure Server</h2>
101
- <p className="text-sm text-gray-500">{server.modelName.replace('.gguf', '')}</p>
102
- </div>
103
- <button
104
- onClick={onClose}
105
- className="p-1 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
106
- >
107
- <X className="w-5 h-5" />
108
- </button>
109
- </div>
110
-
111
- {/* Form */}
112
- <form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-4 space-y-4">
113
- {/* Port */}
114
- <div>
115
- <label className="block text-sm font-medium text-gray-700 mb-1">
116
- Port
117
- </label>
118
- <input
119
- type="number"
120
- value={formData.port}
121
- onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) || 9000 })}
122
- min={1024}
123
- max={65535}
124
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
125
- />
126
- </div>
127
-
128
- {/* Host */}
129
- <div>
130
- <label className="block text-sm font-medium text-gray-700 mb-1">
131
- Host
132
- </label>
133
- <select
134
- value={formData.host}
135
- onChange={(e) => setFormData({ ...formData, host: e.target.value })}
136
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent bg-white"
137
- >
138
- <option value="127.0.0.1">127.0.0.1 (localhost only)</option>
139
- <option value="0.0.0.0">0.0.0.0 (all interfaces)</option>
140
- </select>
141
- </div>
142
-
143
- {/* Threads */}
144
- <div>
145
- <label className="block text-sm font-medium text-gray-700 mb-1">
146
- Threads
147
- </label>
148
- <input
149
- type="number"
150
- value={formData.threads}
151
- onChange={(e) => setFormData({ ...formData, threads: parseInt(e.target.value) || 1 })}
152
- min={1}
153
- max={256}
154
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
155
- />
156
- <p className="text-xs text-gray-500 mt-1">Number of CPU threads for inference</p>
157
- </div>
158
-
159
- {/* Context Size */}
160
- <div>
161
- <label className="block text-sm font-medium text-gray-700 mb-1">
162
- Context Size
163
- </label>
164
- <input
165
- type="number"
166
- value={formData.ctxSize}
167
- onChange={(e) => setFormData({ ...formData, ctxSize: parseInt(e.target.value) || 2048 })}
168
- min={512}
169
- max={2097152}
170
- step={512}
171
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
172
- />
173
- <p className="text-xs text-gray-500 mt-1">{formatContextSize(formData.ctxSize)}</p>
174
- </div>
175
-
176
- {/* GPU Layers */}
177
- <div>
178
- <label className="block text-sm font-medium text-gray-700 mb-1">
179
- GPU Layers
180
- </label>
181
- <input
182
- type="number"
183
- value={formData.gpuLayers}
184
- onChange={(e) => setFormData({ ...formData, gpuLayers: parseInt(e.target.value) || 0 })}
185
- min={0}
186
- max={999}
187
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
188
- />
189
- <p className="text-xs text-gray-500 mt-1">Layers to offload to GPU (0 = CPU only)</p>
190
- </div>
191
-
192
- {/* Verbose */}
193
- <div className="flex items-center justify-between">
194
- <div>
195
- <label className="text-sm font-medium text-gray-700">Verbose Logging</label>
196
- <p className="text-xs text-gray-500">Log HTTP requests and responses</p>
197
- </div>
198
- <button
199
- type="button"
200
- onClick={() => setFormData({ ...formData, verbose: !formData.verbose })}
201
- className={`relative w-11 h-6 rounded-full transition-colors cursor-pointer ${
202
- formData.verbose ? 'bg-gray-900' : 'bg-gray-200'
203
- }`}
204
- >
205
- <span
206
- className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
207
- formData.verbose ? 'translate-x-5' : ''
208
- }`}
209
- />
210
- </button>
211
- </div>
212
-
213
- {/* Custom Flags */}
214
- <div>
215
- <label className="block text-sm font-medium text-gray-700 mb-1">
216
- Custom Flags
217
- </label>
218
- <input
219
- type="text"
220
- value={formData.customFlags}
221
- onChange={(e) => setFormData({ ...formData, customFlags: e.target.value })}
222
- placeholder="--flash-attn, --cont-batching"
223
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
224
- />
225
- <p className="text-xs text-gray-500 mt-1">Comma-separated additional flags</p>
226
- </div>
227
-
228
- {/* Restart option (only show if server is running) */}
229
- {server.status === 'running' && (
230
- <div className="flex items-center gap-2 pt-2 border-t border-gray-100">
231
- <input
232
- type="checkbox"
233
- id="restartAfterSave"
234
- checked={restartAfterSave}
235
- onChange={(e) => setRestartAfterSave(e.target.checked)}
236
- className="w-4 h-4 text-gray-900 border-gray-300 rounded focus:ring-gray-200"
237
- />
238
- <label htmlFor="restartAfterSave" className="text-sm text-gray-700">
239
- Restart server to apply changes
240
- </label>
241
- </div>
242
- )}
243
-
244
- {/* Error */}
245
- {error && (
246
- <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
247
- <p className="text-sm text-red-700">{error}</p>
248
- </div>
249
- )}
250
- </form>
251
-
252
- {/* Footer */}
253
- <div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-gray-200 bg-gray-50 rounded-b-xl">
254
- <button
255
- type="button"
256
- onClick={onClose}
257
- className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
258
- >
259
- Cancel
260
- </button>
261
- <button
262
- onClick={handleSubmit}
263
- disabled={updateServer.isPending}
264
- className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-wait"
265
- >
266
- {updateServer.isPending ? (
267
- <>
268
- <Loader2 className="w-4 h-4 animate-spin" />
269
- Saving...
270
- </>
271
- ) : (
272
- <>
273
- {server.status === 'running' && restartAfterSave ? (
274
- <>
275
- <RotateCcw className="w-4 h-4" />
276
- Save & Restart
277
- </>
278
- ) : (
279
- <>
280
- <Save className="w-4 h-4" />
281
- Save
282
- </>
283
- )}
284
- </>
285
- )}
286
- </button>
287
- </div>
288
- </div>
289
- </div>
290
- );
291
- }