@appkit/llamacpp-cli 1.11.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.
- package/README.md +572 -170
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin/config.d.ts +10 -0
- package/dist/commands/admin/config.d.ts.map +1 -0
- package/dist/commands/admin/config.js +100 -0
- package/dist/commands/admin/config.js.map +1 -0
- package/dist/commands/admin/logs.d.ts +10 -0
- package/dist/commands/admin/logs.d.ts.map +1 -0
- package/dist/commands/admin/logs.js +114 -0
- package/dist/commands/admin/logs.js.map +1 -0
- package/dist/commands/admin/restart.d.ts +2 -0
- package/dist/commands/admin/restart.d.ts.map +1 -0
- package/dist/commands/admin/restart.js +29 -0
- package/dist/commands/admin/restart.js.map +1 -0
- package/dist/commands/admin/start.d.ts +2 -0
- package/dist/commands/admin/start.d.ts.map +1 -0
- package/dist/commands/admin/start.js +30 -0
- package/dist/commands/admin/start.js.map +1 -0
- package/dist/commands/admin/status.d.ts +2 -0
- package/dist/commands/admin/status.d.ts.map +1 -0
- package/dist/commands/admin/status.js +82 -0
- package/dist/commands/admin/status.js.map +1 -0
- package/dist/commands/admin/stop.d.ts +2 -0
- package/dist/commands/admin/stop.d.ts.map +1 -0
- package/dist/commands/admin/stop.js +21 -0
- package/dist/commands/admin/stop.js.map +1 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +22 -0
- package/dist/commands/logs.js.map +1 -1
- package/dist/lib/admin-manager.d.ts +111 -0
- package/dist/lib/admin-manager.d.ts.map +1 -0
- package/dist/lib/admin-manager.js +413 -0
- package/dist/lib/admin-manager.js.map +1 -0
- package/dist/lib/admin-server.d.ts +148 -0
- package/dist/lib/admin-server.d.ts.map +1 -0
- package/dist/lib/admin-server.js +1161 -0
- package/dist/lib/admin-server.js.map +1 -0
- package/dist/lib/download-job-manager.d.ts +64 -0
- package/dist/lib/download-job-manager.d.ts.map +1 -0
- package/dist/lib/download-job-manager.js +164 -0
- package/dist/lib/download-job-manager.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.js +1 -1
- package/dist/types/admin-config.d.ts +19 -0
- package/dist/types/admin-config.d.ts.map +1 -0
- package/dist/types/admin-config.js +3 -0
- package/dist/types/admin-config.js.map +1 -0
- package/dist/utils/log-parser.d.ts +9 -0
- package/dist/utils/log-parser.d.ts.map +1 -1
- package/dist/utils/log-parser.js +11 -0
- package/dist/utils/log-parser.js.map +1 -1
- package/package.json +10 -2
- package/web/README.md +429 -0
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/dist/index.html +14 -0
- package/web/dist/vite.svg +1 -0
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -203
- package/MONITORING-ACCURACY-FIX.md +0 -199
- package/PER-PROCESS-METRICS.md +0 -190
- package/docs/images/.gitkeep +0 -1
- package/src/cli.ts +0 -423
- 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 -321
- 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/config-generator.ts +0 -130
- 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/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 -271
- 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/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# llamacpp-cli Web UI
|
|
2
|
+
|
|
3
|
+
A React-based admin web interface for managing llama.cpp servers through the Admin REST API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Web UI provides a clean, modern interface for managing your llama.cpp servers remotely. It's inspired by the Llama website design and built with:
|
|
8
|
+
|
|
9
|
+
- **React 19** - Modern React with concurrent features
|
|
10
|
+
- **Vite 7** - Fast build tooling and dev server
|
|
11
|
+
- **TypeScript 5.9** - Type-safe development
|
|
12
|
+
- **Tailwind CSS 4** - Utility-first styling with dark mode support
|
|
13
|
+
- **React Query** - Server state management with auto-refetch
|
|
14
|
+
- **React Router** - Client-side routing for SPA
|
|
15
|
+
- **Lucide React** - Beautiful icon library
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
### File Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
web/
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── components/
|
|
25
|
+
│ │ └── Nav.tsx # Navigation component with gradient logo
|
|
26
|
+
│ ├── pages/
|
|
27
|
+
│ │ ├── Dashboard.tsx # System overview and stats
|
|
28
|
+
│ │ ├── Servers.tsx # Server management (CRUD operations)
|
|
29
|
+
│ │ └── Models.tsx # Model management
|
|
30
|
+
│ ├── hooks/
|
|
31
|
+
│ │ └── useApi.ts # React Query hooks for all API operations
|
|
32
|
+
│ ├── lib/
|
|
33
|
+
│ │ └── api.ts # API client class with methods for all endpoints
|
|
34
|
+
│ ├── types/
|
|
35
|
+
│ │ └── api.ts # TypeScript types mirroring backend API
|
|
36
|
+
│ ├── App.tsx # Root component with routing
|
|
37
|
+
│ ├── main.tsx # Entry point with React Query provider
|
|
38
|
+
│ └── index.css # Global styles with dark theme
|
|
39
|
+
├── vite.config.ts # Vite configuration with API proxy
|
|
40
|
+
├── tailwind.config.js # Tailwind CSS configuration
|
|
41
|
+
├── postcss.config.js # PostCSS with Tailwind and Autoprefixer
|
|
42
|
+
├── package.json # Dependencies and scripts
|
|
43
|
+
└── README.md # This file
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### API Client
|
|
47
|
+
|
|
48
|
+
The API client (`src/lib/api.ts`) provides a full-featured client for the Admin API:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
class ApiClient {
|
|
52
|
+
// Server operations
|
|
53
|
+
async listServers(): Promise<{ servers: Server[] }>
|
|
54
|
+
async getServer(id: string): Promise<Server>
|
|
55
|
+
async createServer(data: CreateServerRequest): Promise<Server>
|
|
56
|
+
async updateServer(id: string, data: UpdateServerRequest): Promise<Server>
|
|
57
|
+
async deleteServer(id: string): Promise<void>
|
|
58
|
+
async startServer(id: string): Promise<Server>
|
|
59
|
+
async stopServer(id: string): Promise<Server>
|
|
60
|
+
async restartServer(id: string): Promise<Server>
|
|
61
|
+
|
|
62
|
+
// Model operations
|
|
63
|
+
async listModels(): Promise<{ models: Model[] }>
|
|
64
|
+
async getModel(name: string): Promise<Model>
|
|
65
|
+
async deleteModel(name: string, cascade?: boolean): Promise<void>
|
|
66
|
+
|
|
67
|
+
// System operations
|
|
68
|
+
async getHealth(): Promise<{ status: string }>
|
|
69
|
+
async getSystemStatus(): Promise<SystemStatus>
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Authentication:**
|
|
74
|
+
- API key stored in localStorage as `llama_admin_api_key`
|
|
75
|
+
- Sent as `Bearer` token in Authorization header
|
|
76
|
+
- Set via UI prompt on first load
|
|
77
|
+
|
|
78
|
+
### React Query Integration
|
|
79
|
+
|
|
80
|
+
All API operations are wrapped in React Query hooks (`src/hooks/useApi.ts`) for:
|
|
81
|
+
|
|
82
|
+
- **Auto-refetch:** Servers/status every 5s, models every 10s
|
|
83
|
+
- **Cache invalidation:** Mutations automatically invalidate relevant queries
|
|
84
|
+
- **Loading states:** Built-in loading/error states for all operations
|
|
85
|
+
- **Optimistic updates:** UI updates immediately on mutations
|
|
86
|
+
|
|
87
|
+
Example usage:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
function Servers() {
|
|
91
|
+
const { data, isLoading } = useServers();
|
|
92
|
+
const startServer = useStartServer();
|
|
93
|
+
|
|
94
|
+
const handleStart = async (id: string) => {
|
|
95
|
+
await startServer.mutateAsync(id);
|
|
96
|
+
// Query automatically refetches servers list
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Pages
|
|
102
|
+
|
|
103
|
+
#### Dashboard (`/dashboard`)
|
|
104
|
+
|
|
105
|
+
**Features:**
|
|
106
|
+
- 4 stat cards: Total Servers, Running, Stopped, Models
|
|
107
|
+
- Running servers list with details (port, threads, context)
|
|
108
|
+
- Auto-refresh every 5 seconds
|
|
109
|
+
- Clean gradient design
|
|
110
|
+
|
|
111
|
+
#### Servers (`/servers`)
|
|
112
|
+
|
|
113
|
+
**Features:**
|
|
114
|
+
- Table of all servers with status badges
|
|
115
|
+
- Per-server actions: Start/Stop/Restart/Delete
|
|
116
|
+
- Configuration display: threads, context size, GPU layers
|
|
117
|
+
- PID and uptime for running servers
|
|
118
|
+
- Confirmation dialogs for destructive actions
|
|
119
|
+
- Loading states for async operations
|
|
120
|
+
|
|
121
|
+
#### Models (`/models`)
|
|
122
|
+
|
|
123
|
+
**Features:**
|
|
124
|
+
- Table of all models with size and modified date
|
|
125
|
+
- Shows server usage count per model
|
|
126
|
+
- Delete with cascade option (also deletes associated servers)
|
|
127
|
+
- Formatted file sizes and dates
|
|
128
|
+
- Protection against deleting models in use
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
### Prerequisites
|
|
133
|
+
|
|
134
|
+
- Node.js 18+ (24.3.0 recommended)
|
|
135
|
+
- npm 9+ (11.4.2 recommended)
|
|
136
|
+
- Running Admin API server on `localhost:9200`
|
|
137
|
+
|
|
138
|
+
### Installation
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd web
|
|
142
|
+
npm install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Development Server
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run dev
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This starts Vite dev server on `http://localhost:5173` with:
|
|
152
|
+
- Hot Module Replacement (HMR)
|
|
153
|
+
- API proxy to `localhost:9200` for `/api` and `/health`
|
|
154
|
+
- Fast refresh for instant updates
|
|
155
|
+
|
|
156
|
+
### Vite Proxy Configuration
|
|
157
|
+
|
|
158
|
+
The dev server proxies API requests to avoid CORS issues:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// vite.config.ts
|
|
162
|
+
server: {
|
|
163
|
+
proxy: {
|
|
164
|
+
'/api': {
|
|
165
|
+
target: 'http://localhost:9200',
|
|
166
|
+
changeOrigin: true,
|
|
167
|
+
},
|
|
168
|
+
'/health': {
|
|
169
|
+
target: 'http://localhost:9200',
|
|
170
|
+
changeOrigin: true,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Production Build
|
|
177
|
+
|
|
178
|
+
### Build Static Assets
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run build
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Output: `web/dist/` directory with:
|
|
185
|
+
- `index.html` - Entry point
|
|
186
|
+
- `assets/*.js` - Bundled JavaScript (code-split)
|
|
187
|
+
- `assets/*.css` - Bundled CSS
|
|
188
|
+
|
|
189
|
+
### Serving Static Files
|
|
190
|
+
|
|
191
|
+
The Admin API server automatically serves static files from `web/dist/`:
|
|
192
|
+
|
|
193
|
+
1. **Build the web UI:**
|
|
194
|
+
```bash
|
|
195
|
+
cd web
|
|
196
|
+
npm install
|
|
197
|
+
npm run build
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
2. **Start Admin API:**
|
|
201
|
+
```bash
|
|
202
|
+
llamacpp admin start
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
3. **Access UI:**
|
|
206
|
+
Open `http://localhost:9200` in your browser
|
|
207
|
+
|
|
208
|
+
**SPA Routing:**
|
|
209
|
+
- Non-API routes (`/dashboard`, `/servers`, `/models`) serve `index.html`
|
|
210
|
+
- API routes (`/api/*`) handled by REST API
|
|
211
|
+
- Static assets (`/assets/*`) served with long-term caching
|
|
212
|
+
|
|
213
|
+
**Error Handling:**
|
|
214
|
+
- If `web/dist` doesn't exist, returns helpful error:
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"error": "Not Found",
|
|
218
|
+
"details": "Static files not built. Run: cd web && npm install && npm run build"
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Environment Configuration
|
|
223
|
+
|
|
224
|
+
### API Endpoint
|
|
225
|
+
|
|
226
|
+
By default, the UI connects to the Admin API on the same host. To configure:
|
|
227
|
+
|
|
228
|
+
**Development:**
|
|
229
|
+
- Edit `vite.config.ts` proxy target
|
|
230
|
+
|
|
231
|
+
**Production:**
|
|
232
|
+
- API calls are relative (`/api/*`)
|
|
233
|
+
- Served from same origin as UI
|
|
234
|
+
|
|
235
|
+
### API Key
|
|
236
|
+
|
|
237
|
+
- Prompted on first load
|
|
238
|
+
- Stored in localStorage
|
|
239
|
+
- Can be cleared to re-prompt
|
|
240
|
+
|
|
241
|
+
## Styling
|
|
242
|
+
|
|
243
|
+
### Dark Theme
|
|
244
|
+
|
|
245
|
+
The UI uses a dark theme by default:
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
:root {
|
|
249
|
+
color-scheme: dark;
|
|
250
|
+
background-color: #0a0a0a;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Tailwind CSS
|
|
255
|
+
|
|
256
|
+
Utility-first styling with responsive design:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
|
260
|
+
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">
|
|
261
|
+
Title
|
|
262
|
+
</h1>
|
|
263
|
+
</div>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Component Patterns
|
|
267
|
+
|
|
268
|
+
- Consistent spacing: `px-6 py-8` for containers
|
|
269
|
+
- Borders: `border border-gray-200 dark:border-gray-800`
|
|
270
|
+
- Rounded corners: `rounded-lg` for cards
|
|
271
|
+
- Hover states: `hover:bg-gray-50 dark:hover:bg-gray-800/50`
|
|
272
|
+
- Status badges: Color-coded pills for server/model status
|
|
273
|
+
|
|
274
|
+
## API Integration
|
|
275
|
+
|
|
276
|
+
### Authentication
|
|
277
|
+
|
|
278
|
+
The UI handles API key authentication:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// On mount, check localStorage for API key
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
let key = localStorage.getItem('llama_admin_api_key');
|
|
284
|
+
if (!key) {
|
|
285
|
+
key = prompt('Enter Admin API Key:');
|
|
286
|
+
if (key) {
|
|
287
|
+
localStorage.setItem('llama_admin_api_key', key);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
api.setApiKey(key);
|
|
291
|
+
}, []);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Error Handling
|
|
295
|
+
|
|
296
|
+
All API calls handle errors gracefully:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
try {
|
|
300
|
+
await startServer.mutateAsync(id);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Failed to start server:', error);
|
|
303
|
+
// React Query shows error state in UI
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Loading States
|
|
308
|
+
|
|
309
|
+
React Query provides loading states:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const { data, isLoading, error } = useServers();
|
|
313
|
+
|
|
314
|
+
if (isLoading) {
|
|
315
|
+
return <div>Loading...</div>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (error) {
|
|
319
|
+
return <div>Error: {error.message}</div>;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Testing
|
|
324
|
+
|
|
325
|
+
### Manual Testing Checklist
|
|
326
|
+
|
|
327
|
+
**Dashboard:**
|
|
328
|
+
- [ ] Stats display correctly
|
|
329
|
+
- [ ] Running servers list populates
|
|
330
|
+
- [ ] Auto-refresh updates data every 5s
|
|
331
|
+
|
|
332
|
+
**Servers:**
|
|
333
|
+
- [ ] Table shows all servers
|
|
334
|
+
- [ ] Start button works on stopped servers
|
|
335
|
+
- [ ] Stop button works on running servers
|
|
336
|
+
- [ ] Restart button works on running servers
|
|
337
|
+
- [ ] Delete shows confirmation dialog
|
|
338
|
+
- [ ] Status badges show correct colors
|
|
339
|
+
- [ ] PID and uptime display for running servers
|
|
340
|
+
|
|
341
|
+
**Models:**
|
|
342
|
+
- [ ] Table shows all models
|
|
343
|
+
- [ ] Sizes formatted correctly (GB/MB)
|
|
344
|
+
- [ ] Server usage count shows
|
|
345
|
+
- [ ] Delete asks for cascade confirmation
|
|
346
|
+
- [ ] Delete protected if servers use model
|
|
347
|
+
|
|
348
|
+
**Navigation:**
|
|
349
|
+
- [ ] Nav links work
|
|
350
|
+
- [ ] Active page highlighted
|
|
351
|
+
- [ ] Logo gradient displays
|
|
352
|
+
|
|
353
|
+
**Authentication:**
|
|
354
|
+
- [ ] API key prompt on first load
|
|
355
|
+
- [ ] API key persists in localStorage
|
|
356
|
+
- [ ] 401 errors handled
|
|
357
|
+
|
|
358
|
+
## Troubleshooting
|
|
359
|
+
|
|
360
|
+
### UI not loading
|
|
361
|
+
|
|
362
|
+
1. Check Admin API is running:
|
|
363
|
+
```bash
|
|
364
|
+
llamacpp admin status
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
2. Check static files built:
|
|
368
|
+
```bash
|
|
369
|
+
ls -la web/dist/
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
3. Rebuild if needed:
|
|
373
|
+
```bash
|
|
374
|
+
cd web && npm run build
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### API calls failing
|
|
378
|
+
|
|
379
|
+
1. Check API key is correct:
|
|
380
|
+
```bash
|
|
381
|
+
llamacpp admin status # Shows current API key
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
2. Clear localStorage and re-enter key:
|
|
385
|
+
```javascript
|
|
386
|
+
localStorage.removeItem('llama_admin_api_key');
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
3. Check browser console for errors
|
|
390
|
+
|
|
391
|
+
### Development server not starting
|
|
392
|
+
|
|
393
|
+
1. Check port 5173 is available
|
|
394
|
+
2. Check node_modules installed:
|
|
395
|
+
```bash
|
|
396
|
+
ls -la node_modules/
|
|
397
|
+
```
|
|
398
|
+
3. Try reinstalling:
|
|
399
|
+
```bash
|
|
400
|
+
rm -rf node_modules package-lock.json
|
|
401
|
+
npm install
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Future Enhancements
|
|
405
|
+
|
|
406
|
+
Potential features for future versions:
|
|
407
|
+
|
|
408
|
+
- [ ] Server configuration editing from UI
|
|
409
|
+
- [ ] Model search and download from HuggingFace
|
|
410
|
+
- [ ] Real-time logs viewer (WebSocket)
|
|
411
|
+
- [ ] Performance graphs (CPU, memory, GPU over time)
|
|
412
|
+
- [ ] Dark/light theme toggle
|
|
413
|
+
- [ ] Server templates for quick creation
|
|
414
|
+
- [ ] Bulk operations (start/stop multiple servers)
|
|
415
|
+
- [ ] User preferences (polling interval, theme, etc.)
|
|
416
|
+
|
|
417
|
+
## Contributing
|
|
418
|
+
|
|
419
|
+
When adding new features:
|
|
420
|
+
|
|
421
|
+
1. Update types in `src/types/api.ts`
|
|
422
|
+
2. Add API methods to `src/lib/api.ts`
|
|
423
|
+
3. Add React Query hooks to `src/hooks/useApi.ts`
|
|
424
|
+
4. Create/update page components in `src/pages/`
|
|
425
|
+
5. Update this README
|
|
426
|
+
|
|
427
|
+
## License
|
|
428
|
+
|
|
429
|
+
Same license as llamacpp-cli project.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-600:oklch(64.6% .222 41.116);--color-yellow-400:oklch(85.2% .199 91.936);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.top-0\.5{top:calc(var(--spacing)*.5)}.top-1\/2{top:50%}.top-full{top:100%}.right-0{right:calc(var(--spacing)*0)}.right-4{right:calc(var(--spacing)*4)}.bottom-4{bottom:calc(var(--spacing)*4)}.left-0\.5{left:calc(var(--spacing)*.5)}.left-3{left:calc(var(--spacing)*3)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-16{height:calc(var(--spacing)*16)}.h-\[calc\(100vh-56px\)\]{height:calc(100vh - 56px)}.h-full{height:100%}.max-h-48{max-height:calc(var(--spacing)*48)}.max-h-60{max-height:calc(var(--spacing)*60)}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2{width:calc(var(--spacing)*2)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-11{width:calc(var(--spacing)*11)}.w-12{width:calc(var(--spacing)*12)}.w-32{width:calc(var(--spacing)*32)}.w-72{width:calc(var(--spacing)*72)}.w-80{width:calc(var(--spacing)*80)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.translate-x-5{--tw-translate-x:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*10)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-200{border-color:var(--color-blue-200)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-green-200\/50{border-color:#b9f8cf80}@supports (color:color-mix(in lab,red,red)){.border-green-200\/50{border-color:color-mix(in oklab,var(--color-green-200)50%,transparent)}}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-red-200{border-color:var(--color-red-200)}.border-red-200\/50{border-color:#ffcaca80}@supports (color:color-mix(in lab,red,red)){.border-red-200\/50{border-color:color-mix(in oklab,var(--color-red-200)50%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-500{background-color:var(--color-red-500)}.bg-white{background-color:var(--color-white)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-4{padding-right:calc(var(--spacing)*4)}.pl-9{padding-left:calc(var(--spacing)*9)}.pl-10{padding-left:calc(var(--spacing)*10)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-blue-400{color:var(--color-blue-400)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-900{color:var(--color-neutral-900)}.text-orange-600{color:var(--color-orange-600)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-400{color:var(--color-yellow-400)}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-neutral-400::-moz-placeholder{color:var(--color-neutral-400)}.placeholder\:text-neutral-400::placeholder{color:var(--color-neutral-400)}@media(hover:hover){.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-neutral-300:hover{border-color:var(--color-neutral-300)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-neutral-50:hover{background-color:var(--color-neutral-50)}.hover\:bg-neutral-100:hover{background-color:var(--color-neutral-100)}.hover\:bg-neutral-800:hover{background-color:var(--color-neutral-800)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-neutral-700:hover{color:var(--color-neutral-700)}.hover\:text-neutral-900:hover{color:var(--color-neutral-900)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:border-neutral-400:focus{border-color:var(--color-neutral-400)}.focus\:border-transparent:focus{border-color:#0000}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-100:disabled{opacity:1}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(prefers-color-scheme:dark){:where(.dark\:divide-gray-800>:not(:last-child)),.dark\:border-gray-800{border-color:var(--color-gray-800)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:bg-green-900\/30{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-900\/30{background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:text-gray-400{color:var(--color-gray-400)}.dark\:text-green-400{color:var(--color-green-400)}.dark\:text-green-500{color:var(--color-green-500)}.dark\:text-white{color:var(--color-white)}@media(hover:hover){.dark\:hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}}}}:root{font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;font-weight:400;line-height:1.5}body{color:#171717;background-color:#fafafa;min-width:320px;min-height:100vh;margin:0}#root{min-height:100vh}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#d4d4d4;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a3a3a3}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|