groove-dev 0.27.142 → 0.27.144
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 +1086 -6532
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- 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 +1086 -6532
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +889 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +24 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +34 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +452 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +227 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +17 -6
- package/packages/gui/src/views/models.jsx +410 -509
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- package/test.py +0 -571
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
3
|
-
import { ScrollArea } from '../components/ui/scroll-area';
|
|
4
|
-
import { Badge } from '../components/ui/badge';
|
|
5
3
|
import { Button } from '../components/ui/button';
|
|
6
4
|
import { api } from '../lib/api';
|
|
7
5
|
import { useToast } from '../lib/hooks/use-toast';
|
|
@@ -10,7 +8,7 @@ import {
|
|
|
10
8
|
Search, Download, Trash2, HardDrive, Cpu, MemoryStick,
|
|
11
9
|
Check, Loader2, Box, ChevronDown, ChevronRight,
|
|
12
10
|
RefreshCw, Play, Square, Rocket, MoreHorizontal,
|
|
13
|
-
|
|
11
|
+
ExternalLink,
|
|
14
12
|
} from 'lucide-react';
|
|
15
13
|
import { cn } from '../lib/cn';
|
|
16
14
|
|
|
@@ -34,198 +32,202 @@ const FILTERS = [
|
|
|
34
32
|
{ id: 'downloaded', label: 'Downloaded' },
|
|
35
33
|
];
|
|
36
34
|
|
|
37
|
-
const
|
|
38
|
-
running:
|
|
39
|
-
ready:
|
|
40
|
-
downloaded:
|
|
41
|
-
downloading:
|
|
35
|
+
const STATUS_LABEL = {
|
|
36
|
+
running: 'running',
|
|
37
|
+
ready: 'ready',
|
|
38
|
+
downloaded: 'downloaded',
|
|
39
|
+
downloading: 'pulling',
|
|
42
40
|
};
|
|
43
41
|
|
|
44
|
-
// ──
|
|
42
|
+
// ── Model Card ─────────────────────────────────────────────────
|
|
45
43
|
|
|
46
|
-
function
|
|
47
|
-
model,
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
function ModelCard({
|
|
45
|
+
model, runtimes,
|
|
46
|
+
onStop, onSpawn, onDelete,
|
|
47
|
+
onStartRuntime, onCreateRuntime,
|
|
48
|
+
isStarting, isUnloading, isDeleting,
|
|
50
49
|
}) {
|
|
51
50
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
51
|
+
const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
|
|
52
52
|
const menuRef = useRef(null);
|
|
53
|
+
const runtimeRef = useRef(null);
|
|
53
54
|
|
|
54
55
|
useEffect(() => {
|
|
55
|
-
if (!menuOpen) return;
|
|
56
|
+
if (!menuOpen && !runtimeMenuOpen) return;
|
|
56
57
|
const close = (e) => {
|
|
57
58
|
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
59
|
+
if (runtimeRef.current && !runtimeRef.current.contains(e.target)) setRuntimeMenuOpen(false);
|
|
58
60
|
};
|
|
59
61
|
document.addEventListener('mousedown', close);
|
|
60
62
|
return () => document.removeEventListener('mousedown', close);
|
|
61
|
-
}, [menuOpen]);
|
|
63
|
+
}, [menuOpen, runtimeMenuOpen]);
|
|
64
|
+
|
|
65
|
+
function handleRuntimeClick() {
|
|
66
|
+
if (runtimes.length === 0) {
|
|
67
|
+
onCreateRuntime();
|
|
68
|
+
} else if (runtimes.length === 1) {
|
|
69
|
+
onStartRuntime(model.id, runtimes[0]);
|
|
70
|
+
} else {
|
|
71
|
+
setRuntimeMenuOpen(!runtimeMenuOpen);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
62
74
|
|
|
63
|
-
const
|
|
75
|
+
const specs = [
|
|
76
|
+
model.parameters,
|
|
77
|
+
model.quantization,
|
|
78
|
+
model.size && model.size !== '—' && model.size,
|
|
79
|
+
].filter(Boolean);
|
|
64
80
|
|
|
65
81
|
return (
|
|
66
82
|
<div className={cn(
|
|
67
|
-
'group rounded-
|
|
68
|
-
|
|
69
|
-
? 'bg-success/5 border-success/20 hover:border-success/40'
|
|
70
|
-
: 'bg-surface-1 border-border-subtle hover:border-accent/30',
|
|
83
|
+
'group flex flex-col p-5 rounded-md border border-border-subtle bg-surface-1',
|
|
84
|
+
'hover:border-accent/30 hover:bg-surface-2 transition-all duration-150 min-h-[180px]',
|
|
71
85
|
)}>
|
|
72
|
-
{/* Header:
|
|
73
|
-
<div className="flex items-
|
|
74
|
-
<div
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</Badge>
|
|
83
|
-
{model.isInLab && (
|
|
84
|
-
<Badge variant="accent" className="text-2xs gap-0.5">
|
|
85
|
-
<FlaskConical size={8} /> Lab
|
|
86
|
-
</Badge>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
86
|
+
{/* Header: icon + name + status */}
|
|
87
|
+
<div className="flex items-center gap-3 mb-3">
|
|
88
|
+
<div
|
|
89
|
+
className="w-9 h-9 rounded-md flex items-center justify-center flex-shrink-0 text-base font-bold font-sans"
|
|
90
|
+
style={{
|
|
91
|
+
background: `hsl(${(model.name || '').charCodeAt(0) * 37 % 360}, 40%, 18%)`,
|
|
92
|
+
color: `hsl(${(model.name || '').charCodeAt(0) * 37 % 360}, 60%, 65%)`,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{(model.name || '?')[0].toUpperCase()}
|
|
89
96
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
<
|
|
101
|
-
{model.source === 'gguf' && (
|
|
102
|
-
<button
|
|
103
|
-
onClick={() => { onImport(model.id); setMenuOpen(false); }}
|
|
104
|
-
disabled={isImporting}
|
|
105
|
-
className="w-full text-left px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-4 transition-colors cursor-pointer flex items-center gap-2 disabled:opacity-40"
|
|
106
|
-
>
|
|
107
|
-
<Rocket size={12} /> Import to Ollama
|
|
108
|
-
</button>
|
|
109
|
-
)}
|
|
110
|
-
<button
|
|
111
|
-
onClick={() => { onDelete(model); setMenuOpen(false); }}
|
|
112
|
-
disabled={isDeleting}
|
|
113
|
-
className="w-full text-left px-3 py-1.5 text-xs font-sans text-danger hover:bg-danger/10 transition-colors cursor-pointer flex items-center gap-2 disabled:opacity-40"
|
|
114
|
-
>
|
|
115
|
-
<Trash2 size={12} /> Delete
|
|
116
|
-
</button>
|
|
117
|
-
</div>
|
|
97
|
+
<div className="flex-1 min-w-0">
|
|
98
|
+
<div className="flex items-center gap-1.5">
|
|
99
|
+
<span className="text-[14px] font-semibold text-text-0 font-sans truncate">{model.name}</span>
|
|
100
|
+
{model.status === 'running' && (
|
|
101
|
+
<span className="relative flex-shrink-0 w-2 h-2">
|
|
102
|
+
<span className="absolute inset-0 rounded-full bg-success" />
|
|
103
|
+
<span className="absolute inset-[-2px] rounded-full bg-success opacity-30 animate-pulse" />
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
{model.status === 'downloading' && (
|
|
107
|
+
<Loader2 size={12} className="animate-spin text-text-3 flex-shrink-0" />
|
|
118
108
|
)}
|
|
119
109
|
</div>
|
|
120
|
-
|
|
110
|
+
<span className="text-2xs text-text-3 font-sans">
|
|
111
|
+
{model.source === 'gguf' ? 'GGUF' : 'Ollama'} · {STATUS_LABEL[model.status]}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
121
114
|
</div>
|
|
122
115
|
|
|
123
116
|
{/* Specs */}
|
|
124
|
-
<
|
|
125
|
-
{
|
|
126
|
-
{model.
|
|
127
|
-
|
|
128
|
-
{(model.parameters || model.quantization) && model.size && model.size !== '—' && (
|
|
129
|
-
<span className="text-text-4">·</span>
|
|
130
|
-
)}
|
|
131
|
-
{model.size && model.size !== '—' && <span>{model.size}</span>}
|
|
132
|
-
{model.vramGb && (
|
|
133
|
-
<>
|
|
134
|
-
<span className="text-text-4">·</span>
|
|
135
|
-
<span className="text-green-400">{model.vramGb} GB VRAM</span>
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
138
|
-
{model.repoId && (
|
|
139
|
-
<>
|
|
140
|
-
<span className="text-text-4">·</span>
|
|
141
|
-
<span className="truncate max-w-[140px]">{model.repoId}</span>
|
|
142
|
-
</>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
117
|
+
<p className="text-xs text-text-2 font-sans leading-relaxed">
|
|
118
|
+
{specs.length > 0 ? specs.join(' · ') : 'Local model'}
|
|
119
|
+
{model.vramGb ? ` · ${model.vramGb} GB VRAM` : ''}
|
|
120
|
+
</p>
|
|
145
121
|
|
|
146
|
-
{/* Download progress
|
|
122
|
+
{/* Download progress */}
|
|
147
123
|
{model.status === 'downloading' && model.download && (
|
|
148
|
-
<div className="
|
|
149
|
-
<div className="flex items-center
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
style={{ width: `${Math.round((model.download.percent || 0) * 100)}%` }}
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
159
|
-
<div className="text-2xs text-text-4">
|
|
160
|
-
{formatBytes(model.download.downloaded)} / {formatBytes(model.download.totalBytes)}
|
|
124
|
+
<div className="mt-2">
|
|
125
|
+
<div className="flex items-center gap-2 mb-1">
|
|
126
|
+
<div className="flex-1 h-1.5 rounded-full overflow-hidden bg-surface-4">
|
|
127
|
+
<div className="h-full rounded-full bg-accent transition-all" style={{ width: `${Math.round((model.download.percent || 0) * 100)}%` }} />
|
|
128
|
+
</div>
|
|
129
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums">
|
|
130
|
+
{Math.round((model.download.percent || 0) * 100)}%
|
|
131
|
+
</span>
|
|
161
132
|
</div>
|
|
133
|
+
{model.download.speed && (
|
|
134
|
+
<div className="text-2xs font-mono text-text-4">{formatSpeed(model.download.speed)}</div>
|
|
135
|
+
)}
|
|
162
136
|
</div>
|
|
163
137
|
)}
|
|
164
138
|
{model.status === 'downloading' && model.pullProgress && (
|
|
165
|
-
<div className="
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
<span className="text-2xs text-text-3 font-sans truncate">
|
|
169
|
-
{model.pullProgress.progress || 'Pulling...'}
|
|
170
|
-
</span>
|
|
171
|
-
</div>
|
|
172
|
-
<div className="mt-1.5 h-1.5 bg-surface-3 rounded-full overflow-hidden">
|
|
173
|
-
<div className="h-full bg-accent rounded-full animate-pulse w-full" />
|
|
174
|
-
</div>
|
|
139
|
+
<div className="flex items-center gap-2 mt-2">
|
|
140
|
+
<Loader2 size={10} className="animate-spin text-text-3" />
|
|
141
|
+
<span className="text-2xs font-sans text-text-3">Pulling…</span>
|
|
175
142
|
</div>
|
|
176
143
|
)}
|
|
177
144
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
145
|
+
<div className="flex-1" />
|
|
146
|
+
|
|
147
|
+
{/* Divider + Actions */}
|
|
148
|
+
{model.status !== 'downloading' && (
|
|
149
|
+
<>
|
|
150
|
+
<div className="h-px bg-border-subtle my-2" />
|
|
151
|
+
<div className="flex items-center justify-end gap-2">
|
|
152
|
+
{model.status === 'running' ? (
|
|
153
|
+
<>
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => onStop(model.name)}
|
|
156
|
+
disabled={isUnloading}
|
|
157
|
+
className="p-1 rounded text-text-4 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-40"
|
|
158
|
+
title="Stop"
|
|
159
|
+
>
|
|
160
|
+
{isUnloading ? <Loader2 size={12} className="animate-spin" /> : <Square size={12} />}
|
|
161
|
+
</button>
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => onSpawn(model.name)}
|
|
164
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-2xs font-sans font-semibold text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
165
|
+
>
|
|
166
|
+
<Rocket size={10} /> Spawn
|
|
167
|
+
</button>
|
|
168
|
+
</>
|
|
169
|
+
) : (
|
|
170
|
+
<>
|
|
171
|
+
{/* Runtime picker */}
|
|
172
|
+
<div ref={runtimeRef} className="relative">
|
|
173
|
+
<button
|
|
174
|
+
onClick={handleRuntimeClick}
|
|
175
|
+
disabled={isStarting}
|
|
176
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-2xs font-sans font-semibold text-accent hover:bg-accent/10 transition-colors cursor-pointer disabled:opacity-40"
|
|
177
|
+
>
|
|
178
|
+
{isStarting
|
|
179
|
+
? <Loader2 size={10} className="animate-spin" />
|
|
180
|
+
: <Play size={10} />}
|
|
181
|
+
{runtimes.length === 0 ? 'Create Runtime' : 'Start Runtime'}
|
|
182
|
+
</button>
|
|
183
|
+
{runtimeMenuOpen && runtimes.length > 1 && (
|
|
184
|
+
<div className="absolute right-0 top-full mt-1 z-50 min-w-[180px] bg-surface-2 border border-border rounded-md shadow-lg py-1">
|
|
185
|
+
{runtimes.map((rt) => (
|
|
186
|
+
<button
|
|
187
|
+
key={rt.id}
|
|
188
|
+
onClick={() => { onStartRuntime(model.id, rt); setRuntimeMenuOpen(false); }}
|
|
189
|
+
className="w-full text-left px-3 py-1.5 text-xs font-sans text-text-2 hover:bg-surface-3 transition-colors cursor-pointer flex items-center gap-2"
|
|
190
|
+
>
|
|
191
|
+
<Play size={10} />
|
|
192
|
+
<span className="truncate flex-1">{rt.name}</span>
|
|
193
|
+
<span className="text-2xs text-text-4 flex-shrink-0">{rt.type}</span>
|
|
194
|
+
</button>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<button
|
|
200
|
+
onClick={() => onSpawn(model.id)}
|
|
201
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-2xs font-sans font-semibold text-text-3 hover:text-text-1 transition-colors cursor-pointer"
|
|
202
|
+
>
|
|
203
|
+
<Rocket size={10} /> Spawn
|
|
204
|
+
</button>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Overflow menu */}
|
|
209
|
+
<div ref={menuRef} className="relative">
|
|
201
210
|
<button
|
|
202
|
-
onClick={() =>
|
|
203
|
-
|
|
204
|
-
className="flex items-center gap-1.5 px-2.5 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"
|
|
211
|
+
onClick={() => setMenuOpen(!menuOpen)}
|
|
212
|
+
className="p-1 rounded text-text-4 hover:text-text-2 transition-colors cursor-pointer"
|
|
205
213
|
>
|
|
206
|
-
|
|
207
|
-
Run
|
|
214
|
+
<MoreHorizontal size={12} />
|
|
208
215
|
</button>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{isImporting ? <Loader2 size={11} className="animate-spin" /> : <Rocket size={11} />}
|
|
225
|
-
{isImporting ? 'Importing...' : 'Launch'}
|
|
226
|
-
</button>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
216
|
+
{menuOpen && (
|
|
217
|
+
<div className="absolute right-0 top-full mt-1 z-50 min-w-[160px] bg-surface-2 border border-border rounded-md shadow-lg py-1">
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => { onDelete(model); setMenuOpen(false); }}
|
|
220
|
+
disabled={isDeleting}
|
|
221
|
+
className="w-full text-left px-3 py-1.5 text-xs font-sans text-danger hover:bg-danger/5 transition-colors cursor-pointer flex items-center gap-2 disabled:opacity-40"
|
|
222
|
+
>
|
|
223
|
+
<Trash2 size={10} /> Delete
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
229
231
|
</div>
|
|
230
232
|
);
|
|
231
233
|
}
|
|
@@ -259,44 +261,34 @@ function FilePicker({ repoId, onDownload, systemRamGb }) {
|
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
if (loading) {
|
|
262
|
-
return <div className="py-
|
|
264
|
+
return <div className="py-2 px-4 text-2xs text-text-4 font-mono">Loading variants...</div>;
|
|
263
265
|
}
|
|
264
266
|
if (!files?.length) {
|
|
265
|
-
return <div className="py-
|
|
267
|
+
return <div className="py-2 px-4 text-2xs text-text-4 font-mono">No GGUF files found.</div>;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
return (
|
|
269
|
-
<div className="pl-
|
|
271
|
+
<div className="pl-8 pr-4 pb-1 space-y-0">
|
|
270
272
|
{files.map((f) => {
|
|
271
273
|
const canRun = !f.estimatedRamGb || !systemRamGb || f.estimatedRamGb <= systemRamGb;
|
|
272
|
-
const tight = f.estimatedRamGb && systemRamGb && f.estimatedRamGb > systemRamGb * 0.8 && canRun;
|
|
273
274
|
return (
|
|
274
275
|
<div key={f.filename} className={cn(
|
|
275
|
-
'flex items-center gap-2 py-1
|
|
276
|
-
canRun
|
|
276
|
+
'flex items-center gap-2 py-1 text-xs font-mono',
|
|
277
|
+
!canRun && 'opacity-40',
|
|
277
278
|
)}>
|
|
278
|
-
<span className="
|
|
279
|
-
{f.quantization && <
|
|
280
|
-
<span className="text-text-
|
|
279
|
+
<span className="text-text-2 truncate flex-1 min-w-0">{f.filename}</span>
|
|
280
|
+
{f.quantization && <span className="text-text-4 flex-shrink-0">{f.quantization}</span>}
|
|
281
|
+
<span className="text-text-3 flex-shrink-0 tabular-nums">{formatBytes(f.size)}</span>
|
|
281
282
|
{f.estimatedRamGb && (
|
|
282
|
-
<span className={
|
|
283
|
-
'text-2xs w-20 text-right font-medium',
|
|
284
|
-
!canRun ? 'text-red-400' : tight ? 'text-yellow-400' : 'text-green-400',
|
|
285
|
-
)}>
|
|
286
|
-
~{f.estimatedRamGb} GB RAM
|
|
287
|
-
</span>
|
|
283
|
+
<span className="text-text-4 flex-shrink-0 tabular-nums">~{f.estimatedRamGb} GB</span>
|
|
288
284
|
)}
|
|
289
|
-
{!canRun && <span className="text-2xs text-
|
|
285
|
+
{!canRun && <span className="text-2xs text-text-4">too large</span>}
|
|
290
286
|
<button
|
|
291
287
|
onClick={() => handleDownload(f)}
|
|
292
288
|
disabled={downloading === f.filename || !canRun}
|
|
293
|
-
className=
|
|
294
|
-
'p-1 rounded transition-colors cursor-pointer',
|
|
295
|
-
canRun ? 'text-accent hover:bg-accent/10' : 'text-text-4 cursor-not-allowed',
|
|
296
|
-
'disabled:opacity-40',
|
|
297
|
-
)}
|
|
289
|
+
className="p-0.5 rounded text-text-3 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
|
298
290
|
>
|
|
299
|
-
{downloading === f.filename ? <Loader2 size={
|
|
291
|
+
{downloading === f.filename ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
|
|
300
292
|
</button>
|
|
301
293
|
</div>
|
|
302
294
|
);
|
|
@@ -315,12 +307,11 @@ export default function ModelsView() {
|
|
|
315
307
|
const [downloads, setDownloads] = useState([]);
|
|
316
308
|
const [expandedResult, setExpandedResult] = useState(null);
|
|
317
309
|
const [serverAction, setServerAction] = useState(null);
|
|
318
|
-
const [loadingModel, setLoadingModel] = useState(null);
|
|
319
310
|
const [unloadingModel, setUnloadingModel] = useState(null);
|
|
320
311
|
const [deletingModel, setDeletingModel] = useState(null);
|
|
321
312
|
const [ggufModels, setGgufModels] = useState([]);
|
|
322
313
|
const [deletingGguf, setDeletingGguf] = useState(null);
|
|
323
|
-
const [
|
|
314
|
+
const [startingModel, setStartingModel] = useState(null);
|
|
324
315
|
const [filter, setFilter] = useState('all');
|
|
325
316
|
const [discoveryOpen, setDiscoveryOpen] = useState(true);
|
|
326
317
|
const [discoveryTab, setDiscoveryTab] = useState('recommended');
|
|
@@ -334,6 +325,10 @@ export default function ModelsView() {
|
|
|
334
325
|
const catalog = useGrooveStore((s) => s.ollamaCatalog);
|
|
335
326
|
const pullProgress = useGrooveStore((s) => s.ollamaPullProgress);
|
|
336
327
|
const labActiveModel = useGrooveStore((s) => s.labActiveModel);
|
|
328
|
+
const labRuntimes = useGrooveStore((s) => s.labRuntimes);
|
|
329
|
+
const fetchLabRuntimes = useGrooveStore((s) => s.fetchLabRuntimes);
|
|
330
|
+
const launchLocalModel = useGrooveStore((s) => s.launchLocalModel);
|
|
331
|
+
const setActiveView = useGrooveStore((s) => s.setActiveView);
|
|
337
332
|
const fetchOllamaStatus = useGrooveStore((s) => s.fetchOllamaStatus);
|
|
338
333
|
const startServer = useGrooveStore((s) => s.startOllamaServer);
|
|
339
334
|
const stopServer = useGrooveStore((s) => s.stopOllamaServer);
|
|
@@ -346,14 +341,13 @@ export default function ModelsView() {
|
|
|
346
341
|
|
|
347
342
|
const pollingRef = useRef(null);
|
|
348
343
|
|
|
349
|
-
// Poll Ollama status
|
|
350
344
|
useEffect(() => {
|
|
351
345
|
fetchOllamaStatus();
|
|
346
|
+
fetchLabRuntimes();
|
|
352
347
|
pollingRef.current = setInterval(fetchOllamaStatus, 10000);
|
|
353
348
|
return () => clearInterval(pollingRef.current);
|
|
354
|
-
}, [fetchOllamaStatus]);
|
|
349
|
+
}, [fetchOllamaStatus, fetchLabRuntimes]);
|
|
355
350
|
|
|
356
|
-
// Fetch recommended + GGUF on mount
|
|
357
351
|
useEffect(() => {
|
|
358
352
|
api.get('/models/recommended').then((data) => {
|
|
359
353
|
setRecommended(data.models || []);
|
|
@@ -363,7 +357,6 @@ export default function ModelsView() {
|
|
|
363
357
|
}).catch(() => {});
|
|
364
358
|
}, []);
|
|
365
359
|
|
|
366
|
-
// Poll active downloads
|
|
367
360
|
useEffect(() => {
|
|
368
361
|
const poll = setInterval(() => {
|
|
369
362
|
api.get('/models/downloads').then(setDownloads).catch(() => {});
|
|
@@ -371,7 +364,6 @@ export default function ModelsView() {
|
|
|
371
364
|
return () => clearInterval(poll);
|
|
372
365
|
}, []);
|
|
373
366
|
|
|
374
|
-
// WebSocket events for GGUF downloads
|
|
375
367
|
useEffect(() => {
|
|
376
368
|
function handleWs(event) {
|
|
377
369
|
try {
|
|
@@ -405,8 +397,6 @@ export default function ModelsView() {
|
|
|
405
397
|
return () => { if (ws) ws.removeEventListener('message', handleWs); };
|
|
406
398
|
}, [toast]);
|
|
407
399
|
|
|
408
|
-
// ── Handlers ──────────────────────────────────────────────────
|
|
409
|
-
|
|
410
400
|
async function handleServerStart() {
|
|
411
401
|
setServerAction('starting');
|
|
412
402
|
try { await startServer(); } catch {}
|
|
@@ -425,12 +415,6 @@ export default function ModelsView() {
|
|
|
425
415
|
setServerAction(null);
|
|
426
416
|
}
|
|
427
417
|
|
|
428
|
-
async function handleLoadModel(modelId) {
|
|
429
|
-
setLoadingModel(modelId);
|
|
430
|
-
try { await loadModel(modelId); } catch {}
|
|
431
|
-
setLoadingModel(null);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
418
|
async function handleUnloadModel(modelId) {
|
|
435
419
|
setUnloadingModel(modelId);
|
|
436
420
|
try { await unloadModel(modelId); } catch {}
|
|
@@ -443,17 +427,21 @@ export default function ModelsView() {
|
|
|
443
427
|
setDeletingModel(null);
|
|
444
428
|
}
|
|
445
429
|
|
|
446
|
-
async function
|
|
447
|
-
|
|
430
|
+
async function handleStartRuntime(modelId, runtime) {
|
|
431
|
+
setStartingModel(modelId);
|
|
448
432
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
433
|
+
if (runtime.type === 'ollama') {
|
|
434
|
+
await loadModel(modelId);
|
|
435
|
+
} else {
|
|
436
|
+
await launchLocalModel(modelId);
|
|
437
|
+
}
|
|
451
438
|
fetchOllamaStatus();
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
439
|
+
} catch {}
|
|
440
|
+
setStartingModel(null);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function handleCreateRuntime() {
|
|
444
|
+
setActiveView('model-lab');
|
|
457
445
|
}
|
|
458
446
|
|
|
459
447
|
async function handleDeleteGguf(modelId) {
|
|
@@ -487,8 +475,6 @@ export default function ModelsView() {
|
|
|
487
475
|
setSearching(false);
|
|
488
476
|
}
|
|
489
477
|
|
|
490
|
-
// ── Computed: catalog lookup ───────────────────────────────────
|
|
491
|
-
|
|
492
478
|
const catalogByBase = useMemo(() => {
|
|
493
479
|
const map = {};
|
|
494
480
|
for (const c of catalog) {
|
|
@@ -504,16 +490,12 @@ export default function ModelsView() {
|
|
|
504
490
|
return catalogByBase[modelId.split(':')[0]] || null;
|
|
505
491
|
}
|
|
506
492
|
|
|
507
|
-
// ── Computed: lab model check ──────────────────────────────────
|
|
508
|
-
|
|
509
493
|
function isModelInLab(modelId) {
|
|
510
494
|
if (!labActiveModel) return false;
|
|
511
495
|
if (typeof labActiveModel === 'string') return labActiveModel === modelId;
|
|
512
496
|
return labActiveModel.name === modelId || labActiveModel.id === modelId;
|
|
513
497
|
}
|
|
514
498
|
|
|
515
|
-
// ── Computed: unified model list ──────────────────────────────
|
|
516
|
-
|
|
517
499
|
const unifiedModels = useMemo(() => {
|
|
518
500
|
const models = [];
|
|
519
501
|
const seen = new Set();
|
|
@@ -596,8 +578,6 @@ export default function ModelsView() {
|
|
|
596
578
|
return models;
|
|
597
579
|
}, [runningModels, installedModels, ggufModels, downloads, pullProgress, labActiveModel, catalog]);
|
|
598
580
|
|
|
599
|
-
// ── Computed: filter + search ──────────────────────────────────
|
|
600
|
-
|
|
601
581
|
const filteredModels = useMemo(() => {
|
|
602
582
|
let list = unifiedModels;
|
|
603
583
|
if (filter === 'running') list = list.filter((m) => m.status === 'running');
|
|
@@ -618,129 +598,91 @@ export default function ModelsView() {
|
|
|
618
598
|
}), [unifiedModels]);
|
|
619
599
|
|
|
620
600
|
const hasNoModels = unifiedModels.length === 0;
|
|
621
|
-
|
|
622
|
-
// ── Render ─────────────────────────────────────────────────────
|
|
601
|
+
const hw = ollamaStatus.hardware;
|
|
623
602
|
|
|
624
603
|
return (
|
|
625
|
-
<div className="h-full flex flex-col
|
|
626
|
-
{/*
|
|
627
|
-
<div className="flex-shrink-0 px-5 pt-
|
|
604
|
+
<div className="h-full flex flex-col">
|
|
605
|
+
{/* Toolbar */}
|
|
606
|
+
<div className="flex-shrink-0 px-5 pt-3 pb-2.5 bg-surface-1 border-b border-border-subtle space-y-2.5">
|
|
628
607
|
|
|
629
|
-
{/* Server status
|
|
608
|
+
{/* Server status */}
|
|
630
609
|
{!ollamaStatus.installed ? (
|
|
631
|
-
<div className="flex items-center gap-2
|
|
610
|
+
<div className="flex items-center gap-2 text-xs font-mono">
|
|
632
611
|
<span className="w-1.5 h-1.5 rounded-full bg-text-4 flex-shrink-0" />
|
|
633
|
-
<span className="text-
|
|
612
|
+
<span className="text-text-3">Ollama not installed</span>
|
|
634
613
|
<div className="flex-1" />
|
|
635
|
-
<a
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
rel="noopener noreferrer"
|
|
639
|
-
className="text-2xs font-sans text-accent hover:underline flex items-center gap-1"
|
|
640
|
-
>
|
|
641
|
-
Install <ExternalLink size={10} />
|
|
614
|
+
<a href="https://ollama.ai/download" target="_blank" rel="noopener noreferrer"
|
|
615
|
+
className="text-2xs text-text-3 hover:text-text-1 flex items-center gap-1 transition-colors">
|
|
616
|
+
Install <ExternalLink size={9} />
|
|
642
617
|
</a>
|
|
643
618
|
</div>
|
|
644
|
-
) :
|
|
645
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
619
|
+
) : (
|
|
620
|
+
<div className="flex items-center gap-2 text-xs font-mono flex-wrap">
|
|
646
621
|
<span className="relative flex-shrink-0 w-1.5 h-1.5">
|
|
647
|
-
<span className=
|
|
648
|
-
<span className="absolute inset-[-2px] rounded-full bg-success opacity-20 animate-pulse" />
|
|
622
|
+
<span className={cn('absolute inset-0 rounded-full', ollamaStatus.serverRunning ? 'bg-success' : 'bg-text-4')} />
|
|
623
|
+
{ollamaStatus.serverRunning && <span className="absolute inset-[-2px] rounded-full bg-success opacity-20 animate-pulse" />}
|
|
649
624
|
</span>
|
|
650
|
-
<span className="text-
|
|
651
|
-
<span className="text-
|
|
652
|
-
|
|
653
|
-
{ollamaStatus.
|
|
654
|
-
<
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
<div className="flex items-center gap-1 px-2 py-0.5 rounded-md bg-surface-2 text-2xs font-sans text-text-2">
|
|
660
|
-
<Cpu size={10} className="text-text-3" />
|
|
661
|
-
{ollamaStatus.hardware.cores} cores
|
|
662
|
-
</div>
|
|
663
|
-
{ollamaStatus.hardware.gpu && (
|
|
664
|
-
<div className="flex items-center gap-1 px-2 py-0.5 rounded-md bg-surface-2 text-2xs font-sans text-text-2">
|
|
665
|
-
<HardDrive size={10} className="text-text-3" />
|
|
666
|
-
{ollamaStatus.hardware.gpu.name}
|
|
667
|
-
{ollamaStatus.hardware.gpu.vram ? ` (${ollamaStatus.hardware.gpu.vram} GB)` : ''}
|
|
668
|
-
</div>
|
|
669
|
-
)}
|
|
670
|
-
{ollamaStatus.hardware.isAppleSilicon && (
|
|
671
|
-
<Badge variant="accent" className="text-2xs">Unified Memory</Badge>
|
|
672
|
-
)}
|
|
673
|
-
</div>
|
|
625
|
+
<span className="text-text-1 font-semibold">Ollama</span>
|
|
626
|
+
<span className="text-text-4">{ollamaStatus.serverRunning ? ':11434' : 'stopped'}</span>
|
|
627
|
+
|
|
628
|
+
{ollamaStatus.serverRunning && hw && (
|
|
629
|
+
<span className="text-text-4 ml-1">
|
|
630
|
+
{hw.totalRamGb} GB · {hw.cores} cores
|
|
631
|
+
{hw.gpu ? ` · ${hw.gpu.name}${hw.gpu.vram ? ` ${hw.gpu.vram} GB` : ''}` : ''}
|
|
632
|
+
{hw.isAppleSilicon ? ' · unified' : ''}
|
|
633
|
+
</span>
|
|
674
634
|
)}
|
|
675
635
|
|
|
676
636
|
<div className="flex-1" />
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
{serverAction
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
<span className="text-xs font-sans text-danger font-semibold">Ollama Stopped</span>
|
|
698
|
-
<span className="text-2xs font-mono text-text-4">:11434</span>
|
|
699
|
-
<div className="flex-1" />
|
|
700
|
-
<Button
|
|
701
|
-
variant="primary"
|
|
702
|
-
size="sm"
|
|
703
|
-
onClick={handleServerStart}
|
|
704
|
-
disabled={!!serverAction}
|
|
705
|
-
className="h-6 px-2.5 text-2xs gap-1"
|
|
706
|
-
>
|
|
707
|
-
<Play size={10} />
|
|
708
|
-
{serverAction === 'starting' ? 'Starting...' : 'Start Server'}
|
|
709
|
-
</Button>
|
|
637
|
+
{ollamaStatus.serverRunning ? (
|
|
638
|
+
<div className="flex items-center gap-2">
|
|
639
|
+
<button onClick={handleServerRestart} disabled={!!serverAction}
|
|
640
|
+
className="text-2xs text-text-4 hover:text-text-2 transition-colors cursor-pointer disabled:opacity-40 flex items-center gap-1">
|
|
641
|
+
<RefreshCw size={9} className={serverAction === 'restarting' ? 'animate-spin' : ''} />
|
|
642
|
+
{serverAction === 'restarting' ? 'Restarting' : 'Restart'}
|
|
643
|
+
</button>
|
|
644
|
+
<button onClick={handleServerStop} disabled={!!serverAction}
|
|
645
|
+
className="text-2xs text-text-4 hover:text-text-2 transition-colors cursor-pointer disabled:opacity-40 flex items-center gap-1">
|
|
646
|
+
<Square size={9} />
|
|
647
|
+
{serverAction === 'stopping' ? 'Stopping' : 'Stop'}
|
|
648
|
+
</button>
|
|
649
|
+
</div>
|
|
650
|
+
) : (
|
|
651
|
+
<button onClick={handleServerStart} disabled={!!serverAction}
|
|
652
|
+
className="text-2xs text-text-2 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-40 flex items-center gap-1">
|
|
653
|
+
{serverAction === 'starting' ? <Loader2 size={9} className="animate-spin" /> : <Play size={9} />}
|
|
654
|
+
{serverAction === 'starting' ? 'Starting' : 'Start'}
|
|
655
|
+
</button>
|
|
656
|
+
)}
|
|
710
657
|
</div>
|
|
711
658
|
)}
|
|
712
659
|
|
|
713
|
-
{/* Search +
|
|
660
|
+
{/* Search + Filters */}
|
|
714
661
|
<div className="flex items-center gap-2">
|
|
715
|
-
<div className="relative flex-1
|
|
716
|
-
<Search size={
|
|
662
|
+
<div className="relative flex-1 min-w-[160px]">
|
|
663
|
+
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
|
|
717
664
|
<input
|
|
718
665
|
ref={searchInputRef}
|
|
719
666
|
value={searchQuery}
|
|
720
667
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
721
668
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
722
669
|
placeholder="Search models or HuggingFace..."
|
|
723
|
-
className="w-full h-
|
|
670
|
+
className="w-full h-7 pl-8 pr-3 text-xs font-mono rounded bg-surface-1 border border-border text-text-1 placeholder:text-text-4 focus:outline-none focus:border-text-3"
|
|
724
671
|
/>
|
|
725
672
|
</div>
|
|
726
|
-
|
|
727
|
-
<div className="flex items-center gap-1">
|
|
673
|
+
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
728
674
|
{FILTERS.map((f) => (
|
|
729
675
|
<button
|
|
730
676
|
key={f.id}
|
|
731
677
|
onClick={() => setFilter(f.id)}
|
|
732
678
|
className={cn(
|
|
733
|
-
'px-2
|
|
734
|
-
filter === f.id
|
|
735
|
-
? 'bg-accent/12 text-accent'
|
|
736
|
-
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
679
|
+
'px-2 py-1 rounded text-2xs font-mono transition-colors cursor-pointer',
|
|
680
|
+
filter === f.id ? 'text-text-1 bg-surface-3' : 'text-text-4 hover:text-text-2',
|
|
737
681
|
)}
|
|
738
682
|
>
|
|
739
683
|
{f.label}
|
|
740
684
|
{filterCounts[f.id] > 0 && (
|
|
741
|
-
<span className=
|
|
742
|
-
{filterCounts[f.id]}
|
|
743
|
-
</span>
|
|
685
|
+
<span className="ml-1 text-text-4 tabular-nums">{filterCounts[f.id]}</span>
|
|
744
686
|
)}
|
|
745
687
|
</button>
|
|
746
688
|
))}
|
|
@@ -748,224 +690,183 @@ export default function ModelsView() {
|
|
|
748
690
|
</div>
|
|
749
691
|
</div>
|
|
750
692
|
|
|
751
|
-
{/*
|
|
752
|
-
<
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
Run AI models locally for privacy, speed, and zero API costs.
|
|
762
|
-
Pull popular models from Ollama or download GGUF files from HuggingFace.
|
|
763
|
-
</p>
|
|
764
|
-
<div className="flex gap-3">
|
|
765
|
-
<Button
|
|
766
|
-
variant="primary"
|
|
767
|
-
onClick={() => {
|
|
768
|
-
setDiscoveryOpen(true);
|
|
769
|
-
setDiscoveryTab('recommended');
|
|
770
|
-
discoveryRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
771
|
-
}}
|
|
772
|
-
className="gap-2"
|
|
773
|
-
>
|
|
774
|
-
<Download size={14} /> Pull from Ollama
|
|
775
|
-
</Button>
|
|
776
|
-
<Button
|
|
777
|
-
variant="secondary"
|
|
778
|
-
onClick={() => {
|
|
779
|
-
searchInputRef.current?.focus();
|
|
780
|
-
}}
|
|
781
|
-
className="gap-2"
|
|
782
|
-
>
|
|
783
|
-
<Search size={14} /> Search HuggingFace
|
|
784
|
-
</Button>
|
|
785
|
-
</div>
|
|
786
|
-
</div>
|
|
787
|
-
) : filteredModels.length === 0 ? (
|
|
788
|
-
<div className="text-center py-12">
|
|
789
|
-
<Search size={32} className="mx-auto text-text-4 mb-2" />
|
|
790
|
-
<p className="text-sm text-text-2 font-sans font-medium">No models match your filter</p>
|
|
791
|
-
<p className="text-xs text-text-3 font-sans mt-1">
|
|
792
|
-
Try changing the filter or clearing your search.
|
|
793
|
-
</p>
|
|
693
|
+
{/* Content */}
|
|
694
|
+
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
|
|
695
|
+
|
|
696
|
+
{/* Empty state */}
|
|
697
|
+
{hasNoModels && !searchQuery.trim() && filter === 'all' ? (
|
|
698
|
+
<div className="flex flex-col items-center justify-center py-16 px-8">
|
|
699
|
+
<Box size={28} className="text-text-4 mb-3" />
|
|
700
|
+
<div className="text-sm font-mono font-semibold text-text-1 mb-1">No local models</div>
|
|
701
|
+
<div className="text-xs font-mono text-text-3 text-center max-w-sm mb-5">
|
|
702
|
+
Pull from Ollama or search HuggingFace for GGUF models to run locally.
|
|
794
703
|
</div>
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
onDelete={handleDeleteUnified}
|
|
807
|
-
onImport={handleImportToOllama}
|
|
808
|
-
isLoading={loadingModel === model.id}
|
|
809
|
-
isUnloading={unloadingModel === model.id || unloadingModel === model.name}
|
|
810
|
-
isDeleting={deletingModel === model.id || deletingGguf === model.id}
|
|
811
|
-
isImporting={importingGguf === model.id}
|
|
812
|
-
/>
|
|
813
|
-
))}
|
|
704
|
+
<div className="flex gap-2">
|
|
705
|
+
<Button variant="primary" onClick={() => {
|
|
706
|
+
setDiscoveryOpen(true);
|
|
707
|
+
setDiscoveryTab('recommended');
|
|
708
|
+
discoveryRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
709
|
+
}} className="gap-1.5 text-xs">
|
|
710
|
+
<Download size={12} /> Pull from Ollama
|
|
711
|
+
</Button>
|
|
712
|
+
<Button variant="secondary" onClick={() => searchInputRef.current?.focus()} className="gap-1.5 text-xs">
|
|
713
|
+
<Search size={12} /> Search HuggingFace
|
|
714
|
+
</Button>
|
|
814
715
|
</div>
|
|
815
|
-
|
|
716
|
+
</div>
|
|
717
|
+
) : filteredModels.length === 0 ? (
|
|
718
|
+
<div className="text-center py-16">
|
|
719
|
+
<div className="text-sm font-sans text-text-4">No models match this filter</div>
|
|
720
|
+
</div>
|
|
721
|
+
) : (
|
|
722
|
+
/* Model grid */
|
|
723
|
+
<div className="px-5 py-4 grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))' }}>
|
|
724
|
+
{filteredModels.map((model) => (
|
|
725
|
+
<ModelCard
|
|
726
|
+
key={model.id}
|
|
727
|
+
model={model}
|
|
728
|
+
runtimes={labRuntimes}
|
|
729
|
+
onStop={handleUnloadModel}
|
|
730
|
+
onSpawn={spawnFromModel}
|
|
731
|
+
onDelete={handleDeleteUnified}
|
|
732
|
+
onStartRuntime={handleStartRuntime}
|
|
733
|
+
onCreateRuntime={handleCreateRuntime}
|
|
734
|
+
isStarting={startingModel === model.id}
|
|
735
|
+
isUnloading={unloadingModel === model.id || unloadingModel === model.name}
|
|
736
|
+
isDeleting={deletingModel === model.id || deletingGguf === model.id}
|
|
737
|
+
/>
|
|
738
|
+
))}
|
|
739
|
+
</div>
|
|
740
|
+
)}
|
|
816
741
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
Discover Models
|
|
829
|
-
</span>
|
|
830
|
-
</button>
|
|
742
|
+
{/* Discovery section */}
|
|
743
|
+
<div ref={discoveryRef} className="border-t border-border mt-2">
|
|
744
|
+
<button
|
|
745
|
+
onClick={() => setDiscoveryOpen(!discoveryOpen)}
|
|
746
|
+
className="flex items-center gap-2 px-5 py-2.5 cursor-pointer group w-full text-left"
|
|
747
|
+
>
|
|
748
|
+
{discoveryOpen
|
|
749
|
+
? <ChevronDown size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
|
|
750
|
+
: <ChevronRight size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />}
|
|
751
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Discover Models</span>
|
|
752
|
+
</button>
|
|
831
753
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
{
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
</div>
|
|
754
|
+
{discoveryOpen && (
|
|
755
|
+
<div className="px-5 pb-4 space-y-3">
|
|
756
|
+
<div className="flex gap-0.5">
|
|
757
|
+
{[
|
|
758
|
+
{ id: 'recommended', label: `Recommended (${recommended.length})` },
|
|
759
|
+
{ id: 'search', label: `Search (${searchResults.length})` },
|
|
760
|
+
].map((t) => (
|
|
761
|
+
<button
|
|
762
|
+
key={t.id}
|
|
763
|
+
onClick={() => setDiscoveryTab(t.id)}
|
|
764
|
+
className={cn(
|
|
765
|
+
'px-2 py-1 rounded text-2xs font-mono transition-colors cursor-pointer',
|
|
766
|
+
discoveryTab === t.id ? 'text-text-1 bg-surface-3' : 'text-text-4 hover:text-text-2',
|
|
767
|
+
)}
|
|
768
|
+
>
|
|
769
|
+
{t.label}
|
|
770
|
+
</button>
|
|
771
|
+
))}
|
|
772
|
+
</div>
|
|
852
773
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
774
|
+
{/* Recommended */}
|
|
775
|
+
{discoveryTab === 'recommended' && (
|
|
776
|
+
<>
|
|
777
|
+
{recommended.length === 0 ? (
|
|
778
|
+
<div className="py-6 text-center">
|
|
779
|
+
<div className="text-xs font-mono text-text-3">Detecting hardware...</div>
|
|
780
|
+
<div className="text-2xs font-mono text-text-4 mt-1">Make sure Ollama is installed.</div>
|
|
781
|
+
</div>
|
|
782
|
+
) : (
|
|
783
|
+
<>
|
|
784
|
+
<div className="text-2xs font-mono text-text-4">
|
|
785
|
+
For your system ({hw?.totalRamGb || '?'} GB RAM)
|
|
861
786
|
</div>
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const isPulling = !!pullProgress[m.id];
|
|
877
|
-
|
|
878
|
-
return (
|
|
879
|
-
<div
|
|
880
|
-
key={m.id}
|
|
881
|
-
className={cn(
|
|
882
|
-
'flex-shrink-0 w-[240px] p-3 rounded-xl border transition-colors',
|
|
883
|
-
isInstalled
|
|
884
|
-
? 'bg-success/5 border-success/20'
|
|
885
|
-
: 'bg-surface-1 border-border-subtle hover:border-accent/30',
|
|
886
|
-
)}
|
|
887
|
-
>
|
|
888
|
-
<div className="flex items-center gap-2 mb-1">
|
|
889
|
-
<span className="text-sm font-mono font-bold text-text-0 truncate">{m.name}</span>
|
|
890
|
-
{isInstalled && <Check size={12} className="text-success flex-shrink-0" />}
|
|
891
|
-
</div>
|
|
892
|
-
<div className="text-2xs text-text-3 font-sans line-clamp-1 mb-2">{m.description}</div>
|
|
893
|
-
<div className="flex items-center gap-2 text-2xs font-sans mb-2">
|
|
894
|
-
<span className="text-text-2">{m.sizeGb} GB</span>
|
|
895
|
-
<span className="text-green-400 font-medium">{m.ramGb} GB RAM</span>
|
|
896
|
-
{headroom !== null && <span className="text-text-4">{headroom}%</span>}
|
|
787
|
+
<div className="space-y-0">
|
|
788
|
+
{recommended.map((m) => {
|
|
789
|
+
const baseId = m.id.split(':')[0];
|
|
790
|
+
const isInstalled = installedModels.some((im) =>
|
|
791
|
+
im.id === m.id || im.id.startsWith(baseId + ':') || im.id === baseId
|
|
792
|
+
);
|
|
793
|
+
const isPulling = !!pullProgress[m.id];
|
|
794
|
+
|
|
795
|
+
return (
|
|
796
|
+
<div key={m.id} className="flex items-center gap-3 py-1.5 border-b border-border last:border-0">
|
|
797
|
+
<div className="flex-1 min-w-0">
|
|
798
|
+
<div className="flex items-center gap-2">
|
|
799
|
+
<span className="text-xs font-mono font-semibold text-text-1 truncate">{m.name}</span>
|
|
800
|
+
{isInstalled && <Check size={10} className="text-success flex-shrink-0" />}
|
|
897
801
|
</div>
|
|
898
|
-
|
|
899
|
-
<Badge variant="success" className="text-2xs">Installed</Badge>
|
|
900
|
-
) : (
|
|
901
|
-
<button
|
|
902
|
-
onClick={() => pullModel(m.id)}
|
|
903
|
-
disabled={isPulling}
|
|
904
|
-
className="w-full flex items-center justify-center gap-1.5 h-7 rounded-md text-xs font-sans font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors cursor-pointer disabled:opacity-40"
|
|
905
|
-
>
|
|
906
|
-
{isPulling ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
|
|
907
|
-
Pull
|
|
908
|
-
</button>
|
|
909
|
-
)}
|
|
802
|
+
<div className="text-2xs font-mono text-text-4 truncate">{m.description}</div>
|
|
910
803
|
</div>
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
<div className="text-center py-8">
|
|
929
|
-
<Search size={32} className="mx-auto text-text-4 mb-2" />
|
|
930
|
-
<p className="text-sm text-text-2 font-sans font-medium">Search for GGUF models</p>
|
|
931
|
-
<p className="text-xs text-text-3 font-sans mt-1">
|
|
932
|
-
Type a query above and press Enter — try "qwen coder", "deepseek", "codestral", "llama"
|
|
933
|
-
</p>
|
|
804
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums flex-shrink-0">{m.sizeGb} GB</span>
|
|
805
|
+
<span className="text-2xs font-mono text-text-4 tabular-nums flex-shrink-0">{m.ramGb} GB RAM</span>
|
|
806
|
+
{isInstalled ? (
|
|
807
|
+
<span className="text-2xs font-mono text-text-4 w-12 text-right">installed</span>
|
|
808
|
+
) : (
|
|
809
|
+
<button
|
|
810
|
+
onClick={() => pullModel(m.id)}
|
|
811
|
+
disabled={isPulling}
|
|
812
|
+
className="text-2xs font-mono text-text-2 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-40 flex items-center gap-1 w-12 justify-end"
|
|
813
|
+
>
|
|
814
|
+
{isPulling ? <Loader2 size={10} className="animate-spin" /> : <Download size={10} />}
|
|
815
|
+
Pull
|
|
816
|
+
</button>
|
|
817
|
+
)}
|
|
818
|
+
</div>
|
|
819
|
+
);
|
|
820
|
+
})}
|
|
934
821
|
</div>
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
</div>
|
|
954
|
-
</button>
|
|
955
|
-
{expandedResult === r.id && (
|
|
956
|
-
<FilePicker repoId={r.id} systemRamGb={ollamaStatus.hardware?.totalRamGb} />
|
|
957
|
-
)}
|
|
958
|
-
</div>
|
|
959
|
-
))}
|
|
822
|
+
</>
|
|
823
|
+
)}
|
|
824
|
+
</>
|
|
825
|
+
)}
|
|
826
|
+
|
|
827
|
+
{/* Search results */}
|
|
828
|
+
{discoveryTab === 'search' && (
|
|
829
|
+
<>
|
|
830
|
+
{searching ? (
|
|
831
|
+
<div className="py-6 text-center">
|
|
832
|
+
<Loader2 size={16} className="mx-auto text-text-3 animate-spin mb-2" />
|
|
833
|
+
<div className="text-xs font-mono text-text-3">Searching HuggingFace...</div>
|
|
834
|
+
</div>
|
|
835
|
+
) : searchResults.length === 0 ? (
|
|
836
|
+
<div className="py-6 text-center">
|
|
837
|
+
<div className="text-xs font-mono text-text-3">Search for GGUF models</div>
|
|
838
|
+
<div className="text-2xs font-mono text-text-4 mt-1">
|
|
839
|
+
Try "qwen coder", "deepseek", "codestral", "llama"
|
|
960
840
|
</div>
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
841
|
+
</div>
|
|
842
|
+
) : (
|
|
843
|
+
<div className="space-y-0">
|
|
844
|
+
{searchResults.map((r) => (
|
|
845
|
+
<div key={r.id}>
|
|
846
|
+
<button
|
|
847
|
+
onClick={() => setExpandedResult(expandedResult === r.id ? null : r.id)}
|
|
848
|
+
className="w-full text-left flex items-center gap-2 py-1.5 border-b border-border hover:bg-surface-2/50 transition-colors cursor-pointer"
|
|
849
|
+
>
|
|
850
|
+
<span className="text-xs font-mono font-semibold text-text-1 truncate flex-1">{r.name}</span>
|
|
851
|
+
<span className="text-2xs font-mono text-text-4">{r.author}</span>
|
|
852
|
+
<span className="text-2xs font-mono text-text-4 tabular-nums">{r.downloads?.toLocaleString()}</span>
|
|
853
|
+
{expandedResult === r.id
|
|
854
|
+
? <ChevronDown size={10} className="text-text-4" />
|
|
855
|
+
: <ChevronRight size={10} className="text-text-4" />}
|
|
856
|
+
</button>
|
|
857
|
+
{expandedResult === r.id && (
|
|
858
|
+
<FilePicker repoId={r.id} systemRamGb={hw?.totalRamGb} />
|
|
859
|
+
)}
|
|
860
|
+
</div>
|
|
861
|
+
))}
|
|
862
|
+
</div>
|
|
863
|
+
)}
|
|
864
|
+
</>
|
|
865
|
+
)}
|
|
866
|
+
</div>
|
|
867
|
+
)}
|
|
967
868
|
</div>
|
|
968
|
-
</
|
|
869
|
+
</div>
|
|
969
870
|
</div>
|
|
970
871
|
);
|
|
971
872
|
}
|