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