groove-dev 0.25.20 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/daemon/src/agent-loop.js +479 -0
- package/node_modules/@groove-dev/daemon/src/api.js +104 -5
- package/node_modules/@groove-dev/daemon/src/index.js +6 -1
- package/node_modules/@groove-dev/daemon/src/llama-server.js +268 -0
- package/node_modules/@groove-dev/daemon/src/model-manager.js +411 -0
- package/node_modules/@groove-dev/daemon/src/process.js +179 -11
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +51 -1
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +3 -2
- package/node_modules/@groove-dev/daemon/src/providers/index.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/local.js +183 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +367 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BQnZrh4f.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BqL4GcgZ.js +633 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +7 -2
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +6 -2
- package/node_modules/@groove-dev/gui/src/views/models.jsx +380 -0
- package/package.json +2 -2
- package/packages/daemon/src/agent-loop.js +479 -0
- package/packages/daemon/src/api.js +104 -5
- package/packages/daemon/src/index.js +6 -1
- package/packages/daemon/src/llama-server.js +268 -0
- package/packages/daemon/src/model-manager.js +411 -0
- package/packages/daemon/src/process.js +179 -11
- package/packages/daemon/src/providers/codex.js +51 -1
- package/packages/daemon/src/providers/gemini.js +3 -2
- package/packages/daemon/src/providers/index.js +4 -0
- package/packages/daemon/src/providers/local.js +183 -0
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/tool-executor.js +367 -0
- package/packages/gui/dist/assets/index-BQnZrh4f.css +1 -0
- package/packages/gui/dist/assets/index-BqL4GcgZ.js +633 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/agents/agent-config.jsx +7 -2
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/groove.js +6 -2
- package/packages/gui/src/views/models.jsx +380 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-GYcMwmjs.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-H_e3KvZp.js +0 -623
- package/packages/gui/dist/assets/index-GYcMwmjs.css +0 -1
- package/packages/gui/dist/assets/index-H_e3KvZp.js +0 -623
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BqL4GcgZ.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BQnZrh4f.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -12,6 +12,7 @@ import DashboardView from './views/dashboard';
|
|
|
12
12
|
import MarketplaceView from './views/marketplace';
|
|
13
13
|
import TeamsView from './views/teams';
|
|
14
14
|
import SettingsView from './views/settings';
|
|
15
|
+
import ModelsView from './views/models';
|
|
15
16
|
|
|
16
17
|
// Agent components
|
|
17
18
|
import { AgentPanel } from './components/agents/agent-panel';
|
|
@@ -57,6 +58,7 @@ function ViewRouter() {
|
|
|
57
58
|
case 'dashboard': content = <DashboardView />; break;
|
|
58
59
|
case 'marketplace': content = <MarketplaceView />; break;
|
|
59
60
|
case 'teams': content = <TeamsView />; break;
|
|
61
|
+
case 'models': content = <ModelsView />; break;
|
|
60
62
|
case 'settings': content = <SettingsView />; break;
|
|
61
63
|
default: content = <AgentsView />;
|
|
62
64
|
}
|
|
@@ -186,8 +186,13 @@ export function AgentConfig({ agent }) {
|
|
|
186
186
|
async function handleModelSwap(providerId, modelId) {
|
|
187
187
|
setSelectedModel(modelId);
|
|
188
188
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
const updates = { model: modelId };
|
|
190
|
+
// Switch provider if selecting a model from a different provider
|
|
191
|
+
if (providerId && providerId !== agent.provider) {
|
|
192
|
+
updates.provider = providerId;
|
|
193
|
+
}
|
|
194
|
+
await api.patch(`/agents/${agent.id}`, updates);
|
|
195
|
+
addToast('success', `Model → ${modelId}${updates.provider ? ` (${providerId})` : ''}`);
|
|
191
196
|
} catch (err) {
|
|
192
197
|
addToast('error', 'Model swap failed', err.message);
|
|
193
198
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Network, Code2, BarChart3, Puzzle, Users, Newspaper, Settings } from 'lucide-react';
|
|
2
|
+
import { Network, Code2, BarChart3, Puzzle, Users, Box, Newspaper, Settings } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { Tooltip } from '../ui/tooltip';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ const NAV_ITEMS = [
|
|
|
8
8
|
{ id: 'editor', icon: Code2, label: 'Editor' },
|
|
9
9
|
{ id: 'dashboard', icon: BarChart3, label: 'Dashboard' },
|
|
10
10
|
{ id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
|
|
11
|
+
{ id: 'models', icon: Box, label: 'Models' },
|
|
11
12
|
{ id: 'teams', icon: Users, label: 'Teams' },
|
|
12
13
|
];
|
|
13
14
|
|
|
@@ -197,13 +197,17 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
197
197
|
case 'agent:exit': {
|
|
198
198
|
const agent = get().agents.find((a) => a.id === msg.agentId);
|
|
199
199
|
const name = agent?.name || msg.agentId;
|
|
200
|
-
// Exit 143 = SIGTERM (kill), exit 137 = SIGKILL — treat as intentional kill
|
|
201
200
|
const isKill = msg.status === 'killed' || msg.code === 143 || msg.code === 137;
|
|
202
201
|
const text = msg.status === 'completed' ? `${name} completed`
|
|
203
202
|
: isKill ? `${name} stopped`
|
|
204
203
|
: `${name} crashed (exit ${msg.code})`;
|
|
205
204
|
const type = msg.status === 'completed' ? 'success' : isKill ? 'info' : 'warning';
|
|
206
|
-
get().addToast(type, text);
|
|
205
|
+
get().addToast(type, text, msg.error ? msg.error.slice(0, 200) : undefined);
|
|
206
|
+
|
|
207
|
+
// Log crash error to agent chat so user can see what happened
|
|
208
|
+
if (msg.error && msg.agentId) {
|
|
209
|
+
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
210
|
+
}
|
|
207
211
|
// Check for recommended team when planner completes
|
|
208
212
|
if (agent?.role === 'planner' && msg.status === 'completed') {
|
|
209
213
|
setTimeout(() => get().checkRecommendedTeam(), 1000);
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
4
|
+
import { Badge } from '../components/ui/badge';
|
|
5
|
+
import { Button } from '../components/ui/button';
|
|
6
|
+
import { Input } from '../components/ui/input';
|
|
7
|
+
import { api } from '../lib/api';
|
|
8
|
+
import { useToast } from '../lib/hooks/use-toast';
|
|
9
|
+
import { useGrooveStore } from '../stores/groove';
|
|
10
|
+
import {
|
|
11
|
+
Search, Download, Trash2, HardDrive, Cpu, MemoryStick,
|
|
12
|
+
Check, X, Loader2, ExternalLink, Box, ChevronDown, ChevronRight,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
import { cn } from '../lib/cn';
|
|
15
|
+
|
|
16
|
+
function formatBytes(bytes) {
|
|
17
|
+
if (!bytes) return '—';
|
|
18
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
19
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
20
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatSpeed(bytesPerSec) {
|
|
24
|
+
if (!bytesPerSec) return '';
|
|
25
|
+
if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(0)} KB/s`;
|
|
26
|
+
return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- Hardware Info ----
|
|
30
|
+
function HardwareBar({ hardware }) {
|
|
31
|
+
if (!hardware) return null;
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border border-border-subtle rounded-lg text-xs font-sans text-text-2">
|
|
34
|
+
<div className="flex items-center gap-1.5">
|
|
35
|
+
<MemoryStick size={14} className="text-text-3" />
|
|
36
|
+
<span>{hardware.totalRamGb} GB RAM</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex items-center gap-1.5">
|
|
39
|
+
<Cpu size={14} className="text-text-3" />
|
|
40
|
+
<span>{hardware.cores} cores</span>
|
|
41
|
+
</div>
|
|
42
|
+
{hardware.gpu && (
|
|
43
|
+
<div className="flex items-center gap-1.5">
|
|
44
|
+
<HardDrive size={14} className="text-text-3" />
|
|
45
|
+
<span>{hardware.gpu.name}{hardware.gpu.vram ? ` (${hardware.gpu.vram} GB)` : ''}</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
{hardware.recommended?.code && (
|
|
49
|
+
<div className="ml-auto text-accent">
|
|
50
|
+
Recommended: {hardware.recommended.code}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- Download Progress Bar ----
|
|
58
|
+
function DownloadProgress({ download }) {
|
|
59
|
+
const pct = Math.round((download.percent || 0) * 100);
|
|
60
|
+
return (
|
|
61
|
+
<div className="space-y-1">
|
|
62
|
+
<div className="flex items-center justify-between text-2xs font-sans text-text-3">
|
|
63
|
+
<span>{download.filename}</span>
|
|
64
|
+
<span>{pct}% {formatSpeed(download.speed)}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="h-1.5 bg-surface-3 rounded-full overflow-hidden">
|
|
67
|
+
<div className="h-full bg-accent rounded-full transition-all" style={{ width: `${pct}%` }} />
|
|
68
|
+
</div>
|
|
69
|
+
<div className="text-2xs text-text-4">
|
|
70
|
+
{formatBytes(download.downloaded)} / {formatBytes(download.totalBytes)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- Installed Model Card ----
|
|
77
|
+
function InstalledModel({ model, onDelete }) {
|
|
78
|
+
const [deleting, setDeleting] = useState(false);
|
|
79
|
+
const tierColors = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg">
|
|
83
|
+
<Box size={18} className="text-accent flex-shrink-0" />
|
|
84
|
+
<div className="flex-1 min-w-0">
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<span className="text-sm font-mono font-bold text-text-0 truncate">{model.id}</span>
|
|
87
|
+
{model.quantization && <Badge variant="subtle" className="text-2xs">{model.quantization}</Badge>}
|
|
88
|
+
{model.parameters && <Badge variant="subtle" className="text-2xs">{model.parameters}</Badge>}
|
|
89
|
+
<span className={cn('text-2xs font-medium capitalize', tierColors[model.tier] || 'text-text-3')}>{model.tier}</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
92
|
+
{formatBytes(model.sizeBytes)} · ctx {(model.contextWindow || 0).toLocaleString()} · {model.category}
|
|
93
|
+
{model.repoId && <span className="text-text-4"> · {model.repoId}</span>}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<button
|
|
97
|
+
onClick={async () => { setDeleting(true); await onDelete(model.id); setDeleting(false); }}
|
|
98
|
+
disabled={deleting}
|
|
99
|
+
className="p-1.5 rounded-md text-text-4 hover:text-red-400 hover:bg-red-400/10 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
{deleting ? <Loader2 size={14} className="animate-spin" /> : <Trash2 size={14} />}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- Search Result Card (HuggingFace) ----
|
|
108
|
+
function SearchResult({ result, onExpand, expanded }) {
|
|
109
|
+
return (
|
|
110
|
+
<button
|
|
111
|
+
onClick={() => onExpand(expanded ? null : result.id)}
|
|
112
|
+
className="w-full text-left px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg hover:border-accent/30 transition-colors cursor-pointer"
|
|
113
|
+
>
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<span className="text-sm font-mono font-bold text-text-0 truncate flex-1">{result.name}</span>
|
|
116
|
+
<span className="text-2xs text-text-4 font-sans">{result.author}</span>
|
|
117
|
+
{expanded ? <ChevronDown size={14} className="text-text-3" /> : <ChevronRight size={14} className="text-text-3" />}
|
|
118
|
+
</div>
|
|
119
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5 flex gap-3">
|
|
120
|
+
<span>{result.downloads?.toLocaleString()} downloads</span>
|
|
121
|
+
<span>{result.likes} likes</span>
|
|
122
|
+
</div>
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- File Picker (quantization variants) ----
|
|
128
|
+
function FilePicker({ repoId, onDownload }) {
|
|
129
|
+
const [files, setFiles] = useState(null);
|
|
130
|
+
const [loading, setLoading] = useState(true);
|
|
131
|
+
const [downloading, setDownloading] = useState(null);
|
|
132
|
+
const toast = useToast();
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setLoading(true);
|
|
136
|
+
api.get(`/models/${repoId}/files`)
|
|
137
|
+
.then((data) => setFiles(data.files || []))
|
|
138
|
+
.catch(() => toast.error('Failed to load model files'))
|
|
139
|
+
.finally(() => setLoading(false));
|
|
140
|
+
}, [repoId]);
|
|
141
|
+
|
|
142
|
+
async function handleDownload(file) {
|
|
143
|
+
setDownloading(file.filename);
|
|
144
|
+
try {
|
|
145
|
+
await api.post('/models/download', { repoId, filename: file.filename });
|
|
146
|
+
toast.success(`Downloading ${file.filename}`);
|
|
147
|
+
onDownload?.(file.filename);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
toast.error(err.message);
|
|
150
|
+
}
|
|
151
|
+
setDownloading(null);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (loading) {
|
|
155
|
+
return <div className="py-3 px-4 text-2xs text-text-4 font-sans">Loading quantization variants...</div>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!files?.length) {
|
|
159
|
+
return <div className="py-3 px-4 text-2xs text-text-4 font-sans">No GGUF files found in this repo.</div>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="pl-6 pr-4 pb-2 space-y-1.5">
|
|
164
|
+
{files.map((f) => (
|
|
165
|
+
<div key={f.filename} className="flex items-center gap-2 py-1.5 px-3 rounded-md bg-surface-2 text-xs font-sans">
|
|
166
|
+
<span className="font-mono text-text-1 truncate flex-1">{f.filename}</span>
|
|
167
|
+
{f.quantization && <Badge variant="subtle" className="text-2xs">{f.quantization}</Badge>}
|
|
168
|
+
<span className="text-text-3 text-2xs w-16 text-right">{formatBytes(f.size)}</span>
|
|
169
|
+
{f.estimatedRamGb && <span className="text-text-4 text-2xs w-14 text-right">~{f.estimatedRamGb}GB</span>}
|
|
170
|
+
<button
|
|
171
|
+
onClick={() => handleDownload(f)}
|
|
172
|
+
disabled={downloading === f.filename}
|
|
173
|
+
className="p-1 rounded text-accent hover:bg-accent/10 transition-colors disabled:opacity-40"
|
|
174
|
+
>
|
|
175
|
+
{downloading === f.filename ? <Loader2 size={13} className="animate-spin" /> : <Download size={13} />}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---- Main View ----
|
|
184
|
+
export default function ModelsView() {
|
|
185
|
+
const [tab, setTab] = useState('installed'); // installed | search
|
|
186
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
187
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
188
|
+
const [searching, setSearching] = useState(false);
|
|
189
|
+
const [installed, setInstalled] = useState([]);
|
|
190
|
+
const [downloads, setDownloads] = useState([]);
|
|
191
|
+
const [hardware, setHardware] = useState(null);
|
|
192
|
+
const [expandedResult, setExpandedResult] = useState(null);
|
|
193
|
+
const toast = useToast();
|
|
194
|
+
|
|
195
|
+
// Fetch installed models
|
|
196
|
+
const fetchInstalled = useCallback(() => {
|
|
197
|
+
api.get('/models/installed').then((data) => {
|
|
198
|
+
setInstalled(data.models || []);
|
|
199
|
+
}).catch(() => {});
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
// Fetch hardware info
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
api.get('/providers/ollama/hardware').then(setHardware).catch(() => {});
|
|
205
|
+
fetchInstalled();
|
|
206
|
+
}, [fetchInstalled]);
|
|
207
|
+
|
|
208
|
+
// Listen for download progress via WebSocket
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const unsub = useGrooveStore.subscribe((state, prev) => {
|
|
211
|
+
// Refresh on model events
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Poll active downloads
|
|
215
|
+
const poll = setInterval(() => {
|
|
216
|
+
api.get('/models/downloads').then(setDownloads).catch(() => {});
|
|
217
|
+
}, 2000);
|
|
218
|
+
|
|
219
|
+
return () => { unsub(); clearInterval(poll); };
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
// WebSocket events for download progress
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
function handleWs(event) {
|
|
225
|
+
try {
|
|
226
|
+
const msg = JSON.parse(event.data);
|
|
227
|
+
if (msg.type === 'model:download:progress') {
|
|
228
|
+
setDownloads((prev) => {
|
|
229
|
+
const idx = prev.findIndex((d) => d.filename === msg.data.filename);
|
|
230
|
+
if (idx >= 0) {
|
|
231
|
+
const next = [...prev];
|
|
232
|
+
next[idx] = msg.data;
|
|
233
|
+
return next;
|
|
234
|
+
}
|
|
235
|
+
return [...prev, msg.data];
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (msg.type === 'model:download:complete') {
|
|
239
|
+
setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
|
|
240
|
+
fetchInstalled();
|
|
241
|
+
toast.success(`${msg.data.filename} downloaded`);
|
|
242
|
+
}
|
|
243
|
+
if (msg.type === 'model:download:error') {
|
|
244
|
+
setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
|
|
245
|
+
toast.error(`Download failed: ${msg.data.error}`);
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
const ws = useGrooveStore.getState()._ws;
|
|
250
|
+
if (ws) ws.addEventListener('message', handleWs);
|
|
251
|
+
return () => { if (ws) ws.removeEventListener('message', handleWs); };
|
|
252
|
+
}, [fetchInstalled, toast]);
|
|
253
|
+
|
|
254
|
+
async function handleSearch() {
|
|
255
|
+
if (!searchQuery.trim()) return;
|
|
256
|
+
setSearching(true);
|
|
257
|
+
setTab('search');
|
|
258
|
+
try {
|
|
259
|
+
const results = await api.get(`/models/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
|
260
|
+
setSearchResults(results);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
toast.error(err.message);
|
|
263
|
+
}
|
|
264
|
+
setSearching(false);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleDelete(modelId) {
|
|
268
|
+
try {
|
|
269
|
+
await api.delete(`/models/${modelId}`);
|
|
270
|
+
setInstalled((prev) => prev.filter((m) => m.id !== modelId));
|
|
271
|
+
toast.success('Model deleted');
|
|
272
|
+
} catch (err) {
|
|
273
|
+
toast.error(err.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="h-full flex flex-col bg-surface-0">
|
|
279
|
+
{/* Header */}
|
|
280
|
+
<div className="flex-shrink-0 px-5 pt-4 pb-3 border-b border-border space-y-3">
|
|
281
|
+
<div className="flex items-center justify-between">
|
|
282
|
+
<h1 className="text-base font-bold font-sans text-text-0">Local Models</h1>
|
|
283
|
+
<Badge variant="subtle" className="text-2xs">{installed.length} installed</Badge>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<HardwareBar hardware={hardware} />
|
|
287
|
+
|
|
288
|
+
{/* Search */}
|
|
289
|
+
<div className="flex gap-2">
|
|
290
|
+
<div className="relative flex-1">
|
|
291
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4" />
|
|
292
|
+
<input
|
|
293
|
+
value={searchQuery}
|
|
294
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
295
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
296
|
+
placeholder="Search HuggingFace for GGUF models..."
|
|
297
|
+
className="w-full h-8 pl-9 pr-3 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
<Button onClick={handleSearch} disabled={searching} size="sm" variant="accent">
|
|
301
|
+
{searching ? <Loader2 size={14} className="animate-spin" /> : 'Search'}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Tabs */}
|
|
306
|
+
<div className="flex gap-1">
|
|
307
|
+
{['installed', 'search'].map((t) => (
|
|
308
|
+
<button
|
|
309
|
+
key={t}
|
|
310
|
+
onClick={() => setTab(t)}
|
|
311
|
+
className={cn(
|
|
312
|
+
'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer capitalize',
|
|
313
|
+
tab === t ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
314
|
+
)}
|
|
315
|
+
>
|
|
316
|
+
{t === 'installed' ? `Installed (${installed.length})` : `Search Results (${searchResults.length})`}
|
|
317
|
+
</button>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{/* Active Downloads */}
|
|
323
|
+
{downloads.length > 0 && (
|
|
324
|
+
<div className="px-5 py-3 border-b border-border space-y-2">
|
|
325
|
+
<div className="text-xs font-sans font-semibold text-text-2">Downloading</div>
|
|
326
|
+
{downloads.map((d) => <DownloadProgress key={d.filename} download={d} />)}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* Content */}
|
|
331
|
+
<ScrollArea className="flex-1">
|
|
332
|
+
<div className="px-5 py-4 space-y-2">
|
|
333
|
+
{tab === 'installed' && (
|
|
334
|
+
<>
|
|
335
|
+
{installed.length === 0 ? (
|
|
336
|
+
<div className="text-center py-12">
|
|
337
|
+
<Box size={40} className="mx-auto text-text-4 mb-3" />
|
|
338
|
+
<p className="text-sm text-text-2 font-sans font-medium">No local models yet</p>
|
|
339
|
+
<p className="text-xs text-text-3 font-sans mt-1">Search HuggingFace to download GGUF models, or pull models via Ollama.</p>
|
|
340
|
+
</div>
|
|
341
|
+
) : (
|
|
342
|
+
installed.map((m) => <InstalledModel key={m.id} model={m} onDelete={handleDelete} />)
|
|
343
|
+
)}
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{tab === 'search' && (
|
|
348
|
+
<>
|
|
349
|
+
{searching ? (
|
|
350
|
+
<div className="text-center py-12">
|
|
351
|
+
<Loader2 size={24} className="mx-auto text-accent animate-spin mb-3" />
|
|
352
|
+
<p className="text-sm text-text-3 font-sans">Searching HuggingFace...</p>
|
|
353
|
+
</div>
|
|
354
|
+
) : searchResults.length === 0 ? (
|
|
355
|
+
<div className="text-center py-12">
|
|
356
|
+
<Search size={40} className="mx-auto text-text-4 mb-3" />
|
|
357
|
+
<p className="text-sm text-text-2 font-sans font-medium">Search for GGUF models</p>
|
|
358
|
+
<p className="text-xs text-text-3 font-sans mt-1">Try "qwen coder", "deepseek", "codestral", "llama"</p>
|
|
359
|
+
</div>
|
|
360
|
+
) : (
|
|
361
|
+
searchResults.map((r) => (
|
|
362
|
+
<div key={r.id} className="space-y-1">
|
|
363
|
+
<SearchResult
|
|
364
|
+
result={r}
|
|
365
|
+
expanded={expandedResult === r.id}
|
|
366
|
+
onExpand={setExpandedResult}
|
|
367
|
+
/>
|
|
368
|
+
{expandedResult === r.id && (
|
|
369
|
+
<FilePicker repoId={r.id} onDownload={() => fetchInstalled()} />
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
))
|
|
373
|
+
)}
|
|
374
|
+
</>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
</ScrollArea>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
7
7
|
"homepage": "https://groovedev.ai",
|