groove-dev 0.27.124 → 0.27.125
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +122 -0
- package/node_modules/@groove-dev/daemon/src/preview.js +28 -5
- package/node_modules/@groove-dev/daemon/src/process.js +21 -0
- package/node_modules/@groove-dev/daemon/src/providers/local.js +19 -20
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +66 -3
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
- package/node_modules/@groove-dev/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +169 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +8 -10
- package/node_modules/@groove-dev/gui/src/views/models.jsx +507 -236
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +122 -0
- package/packages/daemon/src/preview.js +28 -5
- package/packages/daemon/src/process.js +21 -0
- package/packages/daemon/src/providers/local.js +19 -20
- package/packages/daemon/src/providers/ollama.js +66 -3
- package/packages/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
- package/packages/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +66 -6
- package/packages/gui/src/stores/groove.js +169 -0
- package/packages/gui/src/views/agents.jsx +8 -10
- package/packages/gui/src/views/models.jsx +507 -236
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
3
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
4
4
|
import { Badge } from '../components/ui/badge';
|
|
5
5
|
import { Button } from '../components/ui/button';
|
|
6
|
-
import { Input } from '../components/ui/input';
|
|
7
6
|
import { api } from '../lib/api';
|
|
8
7
|
import { useToast } from '../lib/hooks/use-toast';
|
|
9
8
|
import { useGrooveStore } from '../stores/groove';
|
|
10
9
|
import {
|
|
11
10
|
Search, Download, Trash2, HardDrive, Cpu, MemoryStick,
|
|
12
|
-
Check,
|
|
11
|
+
Check, Loader2, Box, ChevronDown, ChevronRight,
|
|
12
|
+
RefreshCw, Play, Square, Zap, AlertCircle, Monitor, Rocket,
|
|
13
13
|
} from 'lucide-react';
|
|
14
14
|
import { cn } from '../lib/cn';
|
|
15
15
|
|
|
16
|
+
const TIER_COLORS = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
|
|
17
|
+
|
|
16
18
|
function formatBytes(bytes) {
|
|
17
19
|
if (!bytes) return '—';
|
|
18
20
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
@@ -26,11 +28,81 @@ function formatSpeed(bytesPerSec) {
|
|
|
26
28
|
return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// ---- Server Status Bar ----
|
|
32
|
+
function ServerStatusBar({ serverRunning, installed, onStart, onStop, onRestart, actionInProgress }) {
|
|
33
|
+
if (!installed) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex items-center gap-2 bg-surface-1 border border-border-subtle rounded-lg px-3 py-2">
|
|
36
|
+
<span className="w-[6px] h-[6px] rounded-full bg-text-4 flex-shrink-0" />
|
|
37
|
+
<span className="text-xs font-sans text-text-3 font-medium">Ollama Not Installed</span>
|
|
38
|
+
<div className="flex-1" />
|
|
39
|
+
<a
|
|
40
|
+
href="https://ollama.ai/download"
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="noopener noreferrer"
|
|
43
|
+
className="text-2xs font-sans text-accent hover:underline"
|
|
44
|
+
>
|
|
45
|
+
Install Ollama
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (serverRunning) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex items-center gap-2 bg-success/8 border border-success/20 rounded-lg px-3 py-2">
|
|
54
|
+
<span className="relative flex-shrink-0 w-[6px] h-[6px]">
|
|
55
|
+
<span className="absolute inset-0 rounded-full bg-success" />
|
|
56
|
+
<span className="absolute inset-[-2px] rounded-full bg-success opacity-20 animate-pulse" />
|
|
57
|
+
</span>
|
|
58
|
+
<span className="text-xs font-sans text-success font-semibold">Server Running</span>
|
|
59
|
+
<span className="text-2xs font-mono text-text-4">:11434</span>
|
|
60
|
+
<div className="flex-1" />
|
|
61
|
+
<button
|
|
62
|
+
onClick={onRestart}
|
|
63
|
+
disabled={!!actionInProgress}
|
|
64
|
+
className="flex items-center gap-1 text-2xs font-sans text-text-3 hover:text-accent cursor-pointer transition-colors disabled:opacity-40"
|
|
65
|
+
>
|
|
66
|
+
<RefreshCw size={10} className={actionInProgress === 'restarting' ? 'animate-spin' : ''} />
|
|
67
|
+
{actionInProgress === 'restarting' ? 'Restarting...' : 'Restart'}
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
onClick={onStop}
|
|
71
|
+
disabled={!!actionInProgress}
|
|
72
|
+
className="flex items-center gap-1 text-2xs font-sans text-text-3 hover:text-danger cursor-pointer transition-colors disabled:opacity-40"
|
|
73
|
+
>
|
|
74
|
+
<Square size={10} />
|
|
75
|
+
{actionInProgress === 'stopping' ? 'Stopping...' : 'Stop'}
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex items-center gap-2 bg-danger/8 border border-danger/20 rounded-lg px-3 py-2">
|
|
83
|
+
<span className="w-[6px] h-[6px] rounded-full bg-danger flex-shrink-0" />
|
|
84
|
+
<span className="text-xs font-sans text-danger font-semibold">Server Stopped</span>
|
|
85
|
+
<span className="text-2xs font-mono text-text-4">:11434</span>
|
|
86
|
+
<div className="flex-1" />
|
|
87
|
+
<Button
|
|
88
|
+
variant="primary"
|
|
89
|
+
size="sm"
|
|
90
|
+
onClick={onStart}
|
|
91
|
+
disabled={!!actionInProgress}
|
|
92
|
+
className="h-6 px-2.5 text-2xs gap-1"
|
|
93
|
+
>
|
|
94
|
+
<Play size={10} />
|
|
95
|
+
{actionInProgress === 'starting' ? 'Starting...' : 'Start Server'}
|
|
96
|
+
</Button>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
29
101
|
// ---- Hardware Info ----
|
|
30
102
|
function HardwareBar({ hardware }) {
|
|
31
103
|
if (!hardware) return null;
|
|
32
104
|
return (
|
|
33
|
-
<div className="flex items-center gap-4 px-
|
|
105
|
+
<div className="flex items-center gap-4 px-3 py-2 bg-surface-1 border border-border-subtle rounded-lg text-xs font-sans text-text-2">
|
|
34
106
|
<div className="flex items-center gap-1.5">
|
|
35
107
|
<MemoryStick size={14} className="text-text-3" />
|
|
36
108
|
<span>{hardware.totalRamGb} GB RAM</span>
|
|
@@ -45,15 +117,109 @@ function HardwareBar({ hardware }) {
|
|
|
45
117
|
<span>{hardware.gpu.name}{hardware.gpu.vram ? ` (${hardware.gpu.vram} GB)` : ''}</span>
|
|
46
118
|
</div>
|
|
47
119
|
)}
|
|
48
|
-
{hardware.
|
|
49
|
-
<
|
|
50
|
-
Recommended: {hardware.recommended.code}
|
|
51
|
-
</div>
|
|
120
|
+
{hardware.isAppleSilicon && (
|
|
121
|
+
<Badge variant="accent" className="text-2xs ml-auto">Unified Memory</Badge>
|
|
52
122
|
)}
|
|
53
123
|
</div>
|
|
54
124
|
);
|
|
55
125
|
}
|
|
56
126
|
|
|
127
|
+
// ---- Running Model Card ----
|
|
128
|
+
function RunningModelCard({ model, onUnload, onSpawn, unloading }) {
|
|
129
|
+
const sizeGb = model.size ? (model.size / (1024 ** 3)).toFixed(1) : '?';
|
|
130
|
+
const vramGb = model.vram ? (model.vram / (1024 ** 3)).toFixed(1) : sizeGb;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex items-center gap-3 px-4 py-3 bg-success/5 border border-success/20 rounded-lg">
|
|
134
|
+
<span className="relative flex-shrink-0 w-2 h-2">
|
|
135
|
+
<span className="absolute inset-0 rounded-full bg-success" />
|
|
136
|
+
<span className="absolute inset-[-2px] rounded-full bg-success opacity-20 animate-pulse" />
|
|
137
|
+
</span>
|
|
138
|
+
<div className="flex-1 min-w-0">
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.name}</span>
|
|
141
|
+
<Badge variant="success" className="text-2xs">Running</Badge>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
144
|
+
{vramGb} GB VRAM · loaded in memory
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => onSpawn(model.name)}
|
|
149
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-2xs font-sans font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors cursor-pointer"
|
|
150
|
+
>
|
|
151
|
+
<Rocket size={11} />
|
|
152
|
+
Spawn Agent
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => onUnload(model.name)}
|
|
156
|
+
disabled={unloading === model.name}
|
|
157
|
+
className="p-1.5 rounded-md text-text-4 hover:text-warning hover:bg-warning/10 transition-colors cursor-pointer disabled:opacity-40"
|
|
158
|
+
title="Unload from memory"
|
|
159
|
+
>
|
|
160
|
+
{unloading === model.name ? <Loader2 size={14} className="animate-spin" /> : <Square size={14} />}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Installed Model Card ----
|
|
167
|
+
function InstalledModelCard({ model, catalogEntry, isRunning, onStart, onSpawn, onDelete, loading, deleting, serverRunning }) {
|
|
168
|
+
return (
|
|
169
|
+
<div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg">
|
|
170
|
+
<Box size={18} className={cn('flex-shrink-0', isRunning ? 'text-success' : 'text-accent')} />
|
|
171
|
+
<div className="flex-1 min-w-0">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.id}</span>
|
|
174
|
+
{model.tier && (
|
|
175
|
+
<span className={cn('text-2xs font-semibold capitalize', TIER_COLORS[model.tier] || 'text-text-3')}>
|
|
176
|
+
{model.tier}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
{model.category && model.category !== 'other' && (
|
|
180
|
+
<Badge variant="subtle" className="text-2xs">{model.category}</Badge>
|
|
181
|
+
)}
|
|
182
|
+
{isRunning && <Badge variant="success" className="text-2xs">Running</Badge>}
|
|
183
|
+
</div>
|
|
184
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
185
|
+
{model.size || '—'}
|
|
186
|
+
{catalogEntry?.ramGb && <> · ~{catalogEntry.ramGb} GB RAM needed</>}
|
|
187
|
+
{catalogEntry?.description && <> · {catalogEntry.description}</>}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="flex items-center gap-1">
|
|
191
|
+
{!isRunning && serverRunning && (
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => onStart(model.id)}
|
|
194
|
+
disabled={!!loading}
|
|
195
|
+
className="flex items-center gap-1 px-2 py-1.5 rounded-md text-2xs font-sans font-medium text-text-2 hover:text-success hover:bg-success/10 transition-colors cursor-pointer disabled:opacity-40"
|
|
196
|
+
title="Load into memory"
|
|
197
|
+
>
|
|
198
|
+
{loading === model.id ? <Loader2 size={11} className="animate-spin" /> : <Play size={11} />}
|
|
199
|
+
Start
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
<button
|
|
203
|
+
onClick={() => onSpawn(model.id)}
|
|
204
|
+
className="flex items-center gap-1 px-2 py-1.5 rounded-md text-2xs font-sans font-medium text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
205
|
+
title="Spawn an agent with this model"
|
|
206
|
+
>
|
|
207
|
+
<Rocket size={11} />
|
|
208
|
+
Spawn
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => onDelete(model.id)}
|
|
212
|
+
disabled={deleting === model.id}
|
|
213
|
+
className="p-1.5 rounded-md text-text-4 hover:text-red-400 hover:bg-red-400/10 transition-colors cursor-pointer disabled:opacity-40"
|
|
214
|
+
title="Delete model"
|
|
215
|
+
>
|
|
216
|
+
{deleting === model.id ? <Loader2 size={13} className="animate-spin" /> : <Trash2 size={13} />}
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
57
223
|
// ---- Download Progress Bar ----
|
|
58
224
|
function DownloadProgress({ download }) {
|
|
59
225
|
const pct = Math.round((download.percent || 0) * 100);
|
|
@@ -73,33 +239,57 @@ function DownloadProgress({ download }) {
|
|
|
73
239
|
);
|
|
74
240
|
}
|
|
75
241
|
|
|
76
|
-
// ----
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
242
|
+
// ---- Pull Progress (Ollama) ----
|
|
243
|
+
function PullProgress({ modelId, progress }) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-accent/5 border border-accent/20 rounded-lg">
|
|
246
|
+
<Loader2 size={14} className="animate-spin text-accent flex-shrink-0" />
|
|
247
|
+
<div className="flex-1 min-w-0">
|
|
248
|
+
<span className="text-xs font-mono text-text-0">{modelId}</span>
|
|
249
|
+
<div className="text-2xs text-text-3 font-sans truncate">{progress.progress || 'Pulling...'}</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- Recommended Model Card ----
|
|
256
|
+
function RecommendedModel({ model, systemRamGb, onPull, pulling, isInstalled }) {
|
|
257
|
+
const categoryIcons = { code: '{}', general: 'AI' };
|
|
258
|
+
const headroom = systemRamGb ? Math.round((1 - model.ramGb / systemRamGb) * 100) : null;
|
|
80
259
|
|
|
81
260
|
return (
|
|
82
|
-
<div className=
|
|
83
|
-
|
|
261
|
+
<div className={cn(
|
|
262
|
+
'flex items-center gap-3 px-4 py-3 border rounded-lg transition-colors',
|
|
263
|
+
isInstalled ? 'bg-success/5 border-success/20' : 'bg-surface-1 border-border-subtle hover:border-accent/20',
|
|
264
|
+
)}>
|
|
265
|
+
<div className="w-9 h-9 rounded-lg bg-surface-3 flex items-center justify-center text-xs font-mono text-text-2 flex-shrink-0">
|
|
266
|
+
{categoryIcons[model.category] || 'AI'}
|
|
267
|
+
</div>
|
|
84
268
|
<div className="flex-1 min-w-0">
|
|
85
269
|
<div className="flex items-center gap-2">
|
|
86
|
-
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
<span className={cn('text-2xs font-medium capitalize', tierColors[model.tier] || 'text-text-3')}>{model.tier}</span>
|
|
270
|
+
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.name}</span>
|
|
271
|
+
<span className={cn('text-2xs font-semibold capitalize', TIER_COLORS[model.tier])}>{model.tier}</span>
|
|
272
|
+
{isInstalled && <Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Installed</Badge>}
|
|
90
273
|
</div>
|
|
91
|
-
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
92
|
-
|
|
93
|
-
|
|
274
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">{model.description}</div>
|
|
275
|
+
<div className="flex items-center gap-3 mt-1 text-2xs font-sans">
|
|
276
|
+
<span className="text-text-2">{model.sizeGb} GB download</span>
|
|
277
|
+
<span className="text-green-400 font-medium">{model.ramGb} GB RAM</span>
|
|
278
|
+
{headroom !== null && <span className="text-text-4">{headroom}% headroom</span>}
|
|
94
279
|
</div>
|
|
95
280
|
</div>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
281
|
+
{isInstalled ? (
|
|
282
|
+
<span className="text-xs text-success font-sans font-medium px-3 py-1.5">Ready</span>
|
|
283
|
+
) : (
|
|
284
|
+
<button
|
|
285
|
+
onClick={() => onPull(model.id)}
|
|
286
|
+
disabled={pulling === model.id}
|
|
287
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-sans font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors cursor-pointer disabled:opacity-40"
|
|
288
|
+
>
|
|
289
|
+
{pulling === model.id ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
|
|
290
|
+
Pull
|
|
291
|
+
</button>
|
|
292
|
+
)}
|
|
103
293
|
</div>
|
|
104
294
|
);
|
|
105
295
|
}
|
|
@@ -199,44 +389,14 @@ function FilePicker({ repoId, onDownload, systemRamGb }) {
|
|
|
199
389
|
);
|
|
200
390
|
}
|
|
201
391
|
|
|
202
|
-
// ----
|
|
203
|
-
function
|
|
204
|
-
const tierColors = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
|
|
205
|
-
const categoryIcons = { code: '{}', general: 'AI' };
|
|
206
|
-
const headroom = systemRamGb ? Math.round((1 - model.ramGb / systemRamGb) * 100) : null;
|
|
207
|
-
|
|
392
|
+
// ---- Section Header ----
|
|
393
|
+
function SectionHeader({ title, count, icon: Icon }) {
|
|
208
394
|
return (
|
|
209
|
-
<div className=
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{categoryIcons[model.category] || 'AI'}
|
|
215
|
-
</div>
|
|
216
|
-
<div className="flex-1 min-w-0">
|
|
217
|
-
<div className="flex items-center gap-2">
|
|
218
|
-
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.name}</span>
|
|
219
|
-
<span className={cn('text-2xs font-semibold capitalize', tierColors[model.tier])}>{model.tier}</span>
|
|
220
|
-
{isInstalled && <Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Installed</Badge>}
|
|
221
|
-
</div>
|
|
222
|
-
<div className="text-2xs text-text-3 font-sans mt-0.5">{model.description}</div>
|
|
223
|
-
<div className="flex items-center gap-3 mt-1 text-2xs font-sans">
|
|
224
|
-
<span className="text-text-2">{model.sizeGb} GB download</span>
|
|
225
|
-
<span className="text-green-400 font-medium">{model.ramGb} GB RAM</span>
|
|
226
|
-
{headroom !== null && <span className="text-text-4">{headroom}% headroom</span>}
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
{isInstalled ? (
|
|
230
|
-
<span className="text-xs text-success font-sans font-medium px-3 py-1.5">Ready</span>
|
|
231
|
-
) : (
|
|
232
|
-
<button
|
|
233
|
-
onClick={() => onPull(model.id)}
|
|
234
|
-
disabled={pulling === model.id}
|
|
235
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-sans font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors cursor-pointer disabled:opacity-40"
|
|
236
|
-
>
|
|
237
|
-
{pulling === model.id ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
|
|
238
|
-
Pull
|
|
239
|
-
</button>
|
|
395
|
+
<div className="flex items-center gap-2 mb-2">
|
|
396
|
+
{Icon && <Icon size={14} className="text-text-3" />}
|
|
397
|
+
<span className="text-xs font-semibold font-sans text-text-2 uppercase tracking-wider">{title}</span>
|
|
398
|
+
{count !== undefined && (
|
|
399
|
+
<Badge variant="subtle" className="text-2xs">{count}</Badge>
|
|
240
400
|
)}
|
|
241
401
|
</div>
|
|
242
402
|
);
|
|
@@ -244,74 +404,59 @@ function RecommendedModel({ model, systemRamGb, onPull, pulling, isInstalled })
|
|
|
244
404
|
|
|
245
405
|
// ---- Main View ----
|
|
246
406
|
export default function ModelsView() {
|
|
247
|
-
const [
|
|
407
|
+
const [discoveryTab, setDiscoveryTab] = useState('recommended');
|
|
248
408
|
const [searchQuery, setSearchQuery] = useState('');
|
|
249
409
|
const [searchResults, setSearchResults] = useState([]);
|
|
250
410
|
const [searching, setSearching] = useState(false);
|
|
251
|
-
const [installed, setInstalled] = useState([]);
|
|
252
411
|
const [recommended, setRecommended] = useState([]);
|
|
253
412
|
const [downloads, setDownloads] = useState([]);
|
|
254
|
-
const [hardware, setHardware] = useState(null);
|
|
255
413
|
const [expandedResult, setExpandedResult] = useState(null);
|
|
256
|
-
const [
|
|
257
|
-
const [
|
|
414
|
+
const [serverAction, setServerAction] = useState(null);
|
|
415
|
+
const [loadingModel, setLoadingModel] = useState(null);
|
|
416
|
+
const [unloadingModel, setUnloadingModel] = useState(null);
|
|
417
|
+
const [deletingModel, setDeletingModel] = useState(null);
|
|
258
418
|
const toast = useToast();
|
|
259
419
|
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
420
|
+
const ollamaStatus = useGrooveStore((s) => s.ollamaStatus);
|
|
421
|
+
const installedModels = useGrooveStore((s) => s.ollamaInstalledModels);
|
|
422
|
+
const runningModels = useGrooveStore((s) => s.ollamaRunningModels);
|
|
423
|
+
const catalog = useGrooveStore((s) => s.ollamaCatalog);
|
|
424
|
+
const pullProgress = useGrooveStore((s) => s.ollamaPullProgress);
|
|
425
|
+
const fetchOllamaStatus = useGrooveStore((s) => s.fetchOllamaStatus);
|
|
426
|
+
const startServer = useGrooveStore((s) => s.startOllamaServer);
|
|
427
|
+
const stopServer = useGrooveStore((s) => s.stopOllamaServer);
|
|
428
|
+
const restartServer = useGrooveStore((s) => s.restartOllamaServer);
|
|
429
|
+
const pullModel = useGrooveStore((s) => s.pullOllamaModel);
|
|
430
|
+
const deleteModel = useGrooveStore((s) => s.deleteOllamaModel);
|
|
431
|
+
const loadModel = useGrooveStore((s) => s.loadOllamaModel);
|
|
432
|
+
const unloadModel = useGrooveStore((s) => s.unloadOllamaModel);
|
|
433
|
+
const spawnFromModel = useGrooveStore((s) => s.spawnFromModel);
|
|
434
|
+
|
|
435
|
+
const pollingRef = useRef(null);
|
|
436
|
+
|
|
437
|
+
// Fetch status on mount and poll every 10s
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
fetchOllamaStatus();
|
|
440
|
+
pollingRef.current = setInterval(fetchOllamaStatus, 10000);
|
|
441
|
+
return () => clearInterval(pollingRef.current);
|
|
442
|
+
}, [fetchOllamaStatus]);
|
|
272
443
|
|
|
273
|
-
// Fetch
|
|
444
|
+
// Fetch recommended models
|
|
274
445
|
useEffect(() => {
|
|
275
|
-
api.get('/providers/ollama/hardware').then(setHardware).catch(() => {});
|
|
276
446
|
api.get('/models/recommended').then((data) => {
|
|
277
447
|
setRecommended(data.models || []);
|
|
278
|
-
if (!hardware && data.hardware) setHardware(data.hardware);
|
|
279
448
|
}).catch(() => {});
|
|
280
|
-
|
|
281
|
-
fetchOllamaModels();
|
|
282
|
-
}, [fetchInstalled, fetchOllamaModels]);
|
|
283
|
-
|
|
284
|
-
async function handlePull(modelId) {
|
|
285
|
-
setPulling(modelId);
|
|
286
|
-
try {
|
|
287
|
-
await api.post('/providers/ollama/pull', { model: modelId });
|
|
288
|
-
toast.success(`${modelId} ready to use`);
|
|
289
|
-
// Refresh all model lists so UI reflects the new install
|
|
290
|
-
fetchInstalled();
|
|
291
|
-
fetchOllamaModels();
|
|
292
|
-
// Also optimistically mark it installed immediately
|
|
293
|
-
setOllamaModels((prev) => [...prev, modelId]);
|
|
294
|
-
} catch (err) {
|
|
295
|
-
toast.error(`Pull failed: ${err.message}`);
|
|
296
|
-
}
|
|
297
|
-
setPulling(null);
|
|
298
|
-
}
|
|
449
|
+
}, []);
|
|
299
450
|
|
|
300
|
-
//
|
|
451
|
+
// Poll active GGUF downloads
|
|
301
452
|
useEffect(() => {
|
|
302
|
-
const unsub = useGrooveStore.subscribe((state, prev) => {
|
|
303
|
-
// Refresh on model events
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Poll active downloads
|
|
307
453
|
const poll = setInterval(() => {
|
|
308
454
|
api.get('/models/downloads').then(setDownloads).catch(() => {});
|
|
309
455
|
}, 2000);
|
|
310
|
-
|
|
311
|
-
return () => { unsub(); clearInterval(poll); };
|
|
456
|
+
return () => clearInterval(poll);
|
|
312
457
|
}, []);
|
|
313
458
|
|
|
314
|
-
// WebSocket events for download progress
|
|
459
|
+
// WebSocket events for GGUF download progress
|
|
315
460
|
useEffect(() => {
|
|
316
461
|
function handleWs(event) {
|
|
317
462
|
try {
|
|
@@ -329,7 +474,6 @@ export default function ModelsView() {
|
|
|
329
474
|
}
|
|
330
475
|
if (msg.type === 'model:download:complete') {
|
|
331
476
|
setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
|
|
332
|
-
fetchInstalled();
|
|
333
477
|
toast.success(`${msg.data.filename} downloaded`);
|
|
334
478
|
}
|
|
335
479
|
if (msg.type === 'model:download:error') {
|
|
@@ -338,15 +482,55 @@ export default function ModelsView() {
|
|
|
338
482
|
}
|
|
339
483
|
} catch {}
|
|
340
484
|
}
|
|
341
|
-
const ws = useGrooveStore.getState().
|
|
485
|
+
const ws = useGrooveStore.getState().ws;
|
|
342
486
|
if (ws) ws.addEventListener('message', handleWs);
|
|
343
487
|
return () => { if (ws) ws.removeEventListener('message', handleWs); };
|
|
344
|
-
}, [
|
|
488
|
+
}, [toast]);
|
|
489
|
+
|
|
490
|
+
async function handleServerStart() {
|
|
491
|
+
setServerAction('starting');
|
|
492
|
+
try { await startServer(); } catch {}
|
|
493
|
+
setServerAction(null);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function handleServerStop() {
|
|
497
|
+
setServerAction('stopping');
|
|
498
|
+
try { await stopServer(); } catch {}
|
|
499
|
+
setServerAction(null);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function handleServerRestart() {
|
|
503
|
+
setServerAction('restarting');
|
|
504
|
+
try { await restartServer(); } catch {}
|
|
505
|
+
setServerAction(null);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function handleLoadModel(modelId) {
|
|
509
|
+
setLoadingModel(modelId);
|
|
510
|
+
try { await loadModel(modelId); } catch {}
|
|
511
|
+
setLoadingModel(null);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function handleUnloadModel(modelId) {
|
|
515
|
+
setUnloadingModel(modelId);
|
|
516
|
+
try { await unloadModel(modelId); } catch {}
|
|
517
|
+
setUnloadingModel(null);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function handleDeleteModel(modelId) {
|
|
521
|
+
setDeletingModel(modelId);
|
|
522
|
+
try { await deleteModel(modelId); } catch {}
|
|
523
|
+
setDeletingModel(null);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function handlePull(modelId) {
|
|
527
|
+
pullModel(modelId);
|
|
528
|
+
}
|
|
345
529
|
|
|
346
530
|
async function handleSearch() {
|
|
347
531
|
if (!searchQuery.trim()) return;
|
|
348
532
|
setSearching(true);
|
|
349
|
-
|
|
533
|
+
setDiscoveryTab('search');
|
|
350
534
|
try {
|
|
351
535
|
const results = await api.get(`/models/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
|
352
536
|
setSearchResults(results);
|
|
@@ -356,14 +540,19 @@ export default function ModelsView() {
|
|
|
356
540
|
setSearching(false);
|
|
357
541
|
}
|
|
358
542
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
543
|
+
const installedIds = new Set(installedModels.map((m) => m.id));
|
|
544
|
+
const runningIds = new Set(runningModels.map((m) => m.name));
|
|
545
|
+
const catalogByBase = {};
|
|
546
|
+
for (const c of catalog) {
|
|
547
|
+
const base = c.id.split(':')[0];
|
|
548
|
+
catalogByBase[base] = c;
|
|
549
|
+
catalogByBase[c.id] = c;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function getCatalogEntry(modelId) {
|
|
553
|
+
if (catalogByBase[modelId]) return catalogByBase[modelId];
|
|
554
|
+
const base = modelId.split(':')[0];
|
|
555
|
+
return catalogByBase[base] || null;
|
|
367
556
|
}
|
|
368
557
|
|
|
369
558
|
return (
|
|
@@ -372,50 +561,29 @@ export default function ModelsView() {
|
|
|
372
561
|
<div className="flex-shrink-0 px-5 pt-4 pb-3 border-b border-border space-y-3">
|
|
373
562
|
<div className="flex items-center justify-between">
|
|
374
563
|
<h1 className="text-base font-bold font-sans text-text-0">Local Models</h1>
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{/* Search */}
|
|
381
|
-
<div className="flex gap-2">
|
|
382
|
-
<div className="relative flex-1">
|
|
383
|
-
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4" />
|
|
384
|
-
<input
|
|
385
|
-
value={searchQuery}
|
|
386
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
387
|
-
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
388
|
-
placeholder="Search HuggingFace for GGUF models..."
|
|
389
|
-
className="w-full h-8 pl-9 pr-3 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
390
|
-
/>
|
|
564
|
+
<div className="flex items-center gap-2">
|
|
565
|
+
<Badge variant="subtle" className="text-2xs">{installedModels.length} installed</Badge>
|
|
566
|
+
{runningModels.length > 0 && (
|
|
567
|
+
<Badge variant="success" className="text-2xs">{runningModels.length} running</Badge>
|
|
568
|
+
)}
|
|
391
569
|
</div>
|
|
392
|
-
<Button onClick={handleSearch} disabled={searching} size="sm" variant="accent">
|
|
393
|
-
{searching ? <Loader2 size={14} className="animate-spin" /> : 'Search'}
|
|
394
|
-
</Button>
|
|
395
570
|
</div>
|
|
396
571
|
|
|
397
|
-
{/*
|
|
398
|
-
<
|
|
399
|
-
{
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
tab === t.id ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
410
|
-
)}
|
|
411
|
-
>
|
|
412
|
-
{t.label}
|
|
413
|
-
</button>
|
|
414
|
-
))}
|
|
415
|
-
</div>
|
|
572
|
+
{/* Server Status Bar */}
|
|
573
|
+
<ServerStatusBar
|
|
574
|
+
serverRunning={ollamaStatus.serverRunning}
|
|
575
|
+
installed={ollamaStatus.installed}
|
|
576
|
+
onStart={handleServerStart}
|
|
577
|
+
onStop={handleServerStop}
|
|
578
|
+
onRestart={handleServerRestart}
|
|
579
|
+
actionInProgress={serverAction}
|
|
580
|
+
/>
|
|
581
|
+
|
|
582
|
+
{/* Hardware Bar */}
|
|
583
|
+
<HardwareBar hardware={ollamaStatus.hardware} />
|
|
416
584
|
</div>
|
|
417
585
|
|
|
418
|
-
{/* Active Downloads */}
|
|
586
|
+
{/* Active Downloads (GGUF) */}
|
|
419
587
|
{downloads.length > 0 && (
|
|
420
588
|
<div className="px-5 py-3 border-b border-border space-y-2">
|
|
421
589
|
<div className="text-xs font-sans font-semibold text-text-2">Downloading</div>
|
|
@@ -423,85 +591,188 @@ export default function ModelsView() {
|
|
|
423
591
|
</div>
|
|
424
592
|
)}
|
|
425
593
|
|
|
594
|
+
{/* Ollama Pull Progress */}
|
|
595
|
+
{Object.keys(pullProgress).length > 0 && (
|
|
596
|
+
<div className="px-5 py-3 border-b border-border space-y-2">
|
|
597
|
+
<div className="text-xs font-sans font-semibold text-text-2">Pulling Models</div>
|
|
598
|
+
{Object.entries(pullProgress).map(([id, prog]) => (
|
|
599
|
+
<PullProgress key={id} modelId={id} progress={prog} />
|
|
600
|
+
))}
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
|
|
426
604
|
{/* Content */}
|
|
427
605
|
<ScrollArea className="flex-1">
|
|
428
|
-
<div className="px-5 py-4 space-y-
|
|
429
|
-
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
606
|
+
<div className="px-5 py-4 space-y-6">
|
|
607
|
+
{/* Running Models Section */}
|
|
608
|
+
<div>
|
|
609
|
+
<SectionHeader title="Running Models" count={runningModels.length} icon={Zap} />
|
|
610
|
+
{runningModels.length === 0 ? (
|
|
611
|
+
<div className="px-4 py-4 bg-surface-1 border border-border-subtle rounded-lg text-center">
|
|
612
|
+
<p className="text-xs text-text-3 font-sans">
|
|
613
|
+
{ollamaStatus.serverRunning
|
|
614
|
+
? 'No models loaded — start one below'
|
|
615
|
+
: 'Start the server to load models'}
|
|
616
|
+
</p>
|
|
617
|
+
</div>
|
|
618
|
+
) : (
|
|
619
|
+
<div className="space-y-2">
|
|
620
|
+
{runningModels.map((m) => (
|
|
621
|
+
<RunningModelCard
|
|
622
|
+
key={m.name}
|
|
623
|
+
model={m}
|
|
624
|
+
onUnload={handleUnloadModel}
|
|
625
|
+
onSpawn={spawnFromModel}
|
|
626
|
+
unloading={unloadingModel}
|
|
627
|
+
/>
|
|
628
|
+
))}
|
|
629
|
+
</div>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
{/* Installed Models Section */}
|
|
634
|
+
<div>
|
|
635
|
+
<SectionHeader title="Installed Models" count={installedModels.length} icon={HardDrive} />
|
|
636
|
+
{installedModels.length === 0 ? (
|
|
637
|
+
<div className="px-4 py-6 bg-surface-1 border border-border-subtle rounded-lg text-center">
|
|
638
|
+
<Box size={32} className="mx-auto text-text-4 mb-2" />
|
|
639
|
+
<p className="text-sm text-text-2 font-sans font-medium">No models installed</p>
|
|
640
|
+
<p className="text-xs text-text-3 font-sans mt-1">
|
|
641
|
+
Pull a model from the Recommended section below, or search HuggingFace.
|
|
642
|
+
</p>
|
|
643
|
+
</div>
|
|
644
|
+
) : (
|
|
645
|
+
<div className="space-y-2">
|
|
646
|
+
{installedModels.map((m) => (
|
|
647
|
+
<InstalledModelCard
|
|
648
|
+
key={m.id}
|
|
649
|
+
model={m}
|
|
650
|
+
catalogEntry={getCatalogEntry(m.id)}
|
|
651
|
+
isRunning={runningIds.has(m.id)}
|
|
652
|
+
onStart={handleLoadModel}
|
|
653
|
+
onSpawn={spawnFromModel}
|
|
654
|
+
onDelete={handleDeleteModel}
|
|
655
|
+
loading={loadingModel}
|
|
656
|
+
deleting={deletingModel}
|
|
657
|
+
serverRunning={ollamaStatus.serverRunning}
|
|
658
|
+
/>
|
|
659
|
+
))}
|
|
660
|
+
</div>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
{/* Divider */}
|
|
665
|
+
<div className="border-t border-border-subtle" />
|
|
666
|
+
|
|
667
|
+
{/* Discovery Section */}
|
|
668
|
+
<div>
|
|
669
|
+
<div className="flex items-center justify-between mb-3">
|
|
670
|
+
<span className="text-xs font-semibold font-sans text-text-2 uppercase tracking-wider">Discover Models</span>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
{/* Search */}
|
|
674
|
+
<div className="flex gap-2 mb-3">
|
|
675
|
+
<div className="relative flex-1">
|
|
676
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4" />
|
|
677
|
+
<input
|
|
678
|
+
value={searchQuery}
|
|
679
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
680
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
681
|
+
placeholder="Search HuggingFace for GGUF models..."
|
|
682
|
+
className="w-full h-8 pl-9 pr-3 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
683
|
+
/>
|
|
684
|
+
</div>
|
|
685
|
+
<Button onClick={handleSearch} disabled={searching} size="sm" variant="accent">
|
|
686
|
+
{searching ? <Loader2 size={14} className="animate-spin" /> : 'Search'}
|
|
687
|
+
</Button>
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
{/* Tabs */}
|
|
691
|
+
<div className="flex gap-1 mb-3">
|
|
692
|
+
{[
|
|
693
|
+
{ id: 'recommended', label: `Recommended (${recommended.length})` },
|
|
694
|
+
{ id: 'search', label: `Search (${searchResults.length})` },
|
|
695
|
+
].map((t) => (
|
|
696
|
+
<button
|
|
697
|
+
key={t.id}
|
|
698
|
+
onClick={() => setDiscoveryTab(t.id)}
|
|
699
|
+
className={cn(
|
|
700
|
+
'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer',
|
|
701
|
+
discoveryTab === t.id ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
702
|
+
)}
|
|
703
|
+
>
|
|
704
|
+
{t.label}
|
|
705
|
+
</button>
|
|
706
|
+
))}
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
{/* Tab content */}
|
|
710
|
+
<div className="space-y-2">
|
|
711
|
+
{discoveryTab === 'recommended' && (
|
|
438
712
|
<>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
<
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
713
|
+
{recommended.length === 0 ? (
|
|
714
|
+
<div className="text-center py-8">
|
|
715
|
+
<Cpu size={32} className="mx-auto text-text-4 mb-2" />
|
|
716
|
+
<p className="text-sm text-text-2 font-sans font-medium">Detecting hardware...</p>
|
|
717
|
+
<p className="text-xs text-text-3 font-sans mt-1">Make sure Ollama is installed so we can check your system.</p>
|
|
718
|
+
</div>
|
|
719
|
+
) : (
|
|
720
|
+
<>
|
|
721
|
+
<div className="text-xs text-text-3 font-sans mb-2">
|
|
722
|
+
Top models for your system ({ollamaStatus.hardware?.totalRamGb || '?'} GB RAM). Click Pull to download via Ollama.
|
|
723
|
+
</div>
|
|
724
|
+
{recommended.map((m) => {
|
|
725
|
+
const baseId = m.id.split(':')[0];
|
|
726
|
+
const isInstalled = installedModels.some((im) =>
|
|
727
|
+
im.id === m.id || im.id.startsWith(baseId + ':') || im.id === baseId
|
|
728
|
+
);
|
|
729
|
+
return (
|
|
730
|
+
<RecommendedModel
|
|
731
|
+
key={m.id}
|
|
732
|
+
model={m}
|
|
733
|
+
systemRamGb={ollamaStatus.hardware?.totalRamGb}
|
|
734
|
+
onPull={handlePull}
|
|
735
|
+
pulling={pullProgress[m.id] ? m.id : null}
|
|
736
|
+
isInstalled={isInstalled}
|
|
737
|
+
/>
|
|
738
|
+
);
|
|
739
|
+
})}
|
|
740
|
+
</>
|
|
741
|
+
)}
|
|
457
742
|
</>
|
|
458
743
|
)}
|
|
459
|
-
</>
|
|
460
|
-
)}
|
|
461
|
-
|
|
462
|
-
{tab === 'installed' && (
|
|
463
|
-
<>
|
|
464
|
-
{installed.length === 0 ? (
|
|
465
|
-
<div className="text-center py-12">
|
|
466
|
-
<Box size={40} className="mx-auto text-text-4 mb-3" />
|
|
467
|
-
<p className="text-sm text-text-2 font-sans font-medium">No local models yet</p>
|
|
468
|
-
<p className="text-xs text-text-3 font-sans mt-1">Search HuggingFace to download GGUF models, or pull models via Ollama.</p>
|
|
469
|
-
</div>
|
|
470
|
-
) : (
|
|
471
|
-
installed.map((m) => <InstalledModel key={m.id} model={m} onDelete={handleDelete} />)
|
|
472
|
-
)}
|
|
473
|
-
</>
|
|
474
|
-
)}
|
|
475
744
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
745
|
+
{discoveryTab === 'search' && (
|
|
746
|
+
<>
|
|
747
|
+
{searching ? (
|
|
748
|
+
<div className="text-center py-8">
|
|
749
|
+
<Loader2 size={24} className="mx-auto text-accent animate-spin mb-2" />
|
|
750
|
+
<p className="text-sm text-text-3 font-sans">Searching HuggingFace...</p>
|
|
751
|
+
</div>
|
|
752
|
+
) : searchResults.length === 0 ? (
|
|
753
|
+
<div className="text-center py-8">
|
|
754
|
+
<Search size={32} className="mx-auto text-text-4 mb-2" />
|
|
755
|
+
<p className="text-sm text-text-2 font-sans font-medium">Search for GGUF models</p>
|
|
756
|
+
<p className="text-xs text-text-3 font-sans mt-1">Try "qwen coder", "deepseek", "codestral", "llama"</p>
|
|
757
|
+
</div>
|
|
758
|
+
) : (
|
|
759
|
+
searchResults.map((r) => (
|
|
760
|
+
<div key={r.id} className="space-y-1">
|
|
761
|
+
<SearchResult
|
|
762
|
+
result={r}
|
|
763
|
+
expanded={expandedResult === r.id}
|
|
764
|
+
onExpand={setExpandedResult}
|
|
765
|
+
/>
|
|
766
|
+
{expandedResult === r.id && (
|
|
767
|
+
<FilePicker repoId={r.id} systemRamGb={ollamaStatus.hardware?.totalRamGb} />
|
|
768
|
+
)}
|
|
769
|
+
</div>
|
|
770
|
+
))
|
|
771
|
+
)}
|
|
772
|
+
</>
|
|
502
773
|
)}
|
|
503
|
-
|
|
504
|
-
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
505
776
|
</div>
|
|
506
777
|
</ScrollArea>
|
|
507
778
|
</div>
|