@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.
- package/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /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
|
-
}
|