@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,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
- }