groove-dev 0.26.5 → 0.26.7

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.
@@ -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-BiwUzEwA.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-PPbrScja.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-DvNB4K83.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-DVRmIjTA.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -162,8 +162,8 @@ function FilePicker({ repoId, onDownload, systemRamGb }) {
162
162
  return (
163
163
  <div className="pl-6 pr-4 pb-2 space-y-1.5">
164
164
  {files.map((f) => {
165
- const canRun = !f.estimatedRamGb || !systemRamGb || f.estimatedRamGb <= systemRamGb * 0.85;
166
- const tight = f.estimatedRamGb && systemRamGb && f.estimatedRamGb > systemRamGb * 0.7 && canRun;
165
+ const canRun = !f.estimatedRamGb || !systemRamGb || f.estimatedRamGb <= systemRamGb;
166
+ const tight = f.estimatedRamGb && systemRamGb && f.estimatedRamGb > systemRamGb * 0.8 && canRun;
167
167
  return (
168
168
  <div key={f.filename} className={cn(
169
169
  'flex items-center gap-2 py-1.5 px-3 rounded-md text-xs font-sans',
@@ -199,16 +199,53 @@ function FilePicker({ repoId, onDownload, systemRamGb }) {
199
199
  );
200
200
  }
201
201
 
202
+ // ---- Recommended Model Card ----
203
+ function RecommendedModel({ model, systemRamGb, onPull, pulling }) {
204
+ const tierColors = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
205
+ const categoryIcons = { code: '{}', general: 'AI' };
206
+ const headroom = systemRamGb ? Math.round((1 - model.ramGb / systemRamGb) * 100) : null;
207
+
208
+ return (
209
+ <div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg hover:border-accent/20 transition-colors">
210
+ <div className="w-9 h-9 rounded-lg bg-surface-3 flex items-center justify-center text-xs font-mono text-text-2 flex-shrink-0">
211
+ {categoryIcons[model.category] || 'AI'}
212
+ </div>
213
+ <div className="flex-1 min-w-0">
214
+ <div className="flex items-center gap-2">
215
+ <span className="text-sm font-mono font-bold text-text-0 truncate">{model.name}</span>
216
+ <span className={cn('text-2xs font-semibold capitalize', tierColors[model.tier])}>{model.tier}</span>
217
+ </div>
218
+ <div className="text-2xs text-text-3 font-sans mt-0.5">{model.description}</div>
219
+ <div className="flex items-center gap-3 mt-1 text-2xs font-sans">
220
+ <span className="text-text-2">{model.sizeGb} GB download</span>
221
+ <span className="text-green-400 font-medium">{model.ramGb} GB RAM</span>
222
+ {headroom !== null && <span className="text-text-4">{headroom}% headroom</span>}
223
+ </div>
224
+ </div>
225
+ <button
226
+ onClick={() => onPull(model.id)}
227
+ disabled={pulling === model.id}
228
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-sans font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors cursor-pointer disabled:opacity-40"
229
+ >
230
+ {pulling === model.id ? <Loader2 size={12} className="animate-spin" /> : <Download size={12} />}
231
+ Pull
232
+ </button>
233
+ </div>
234
+ );
235
+ }
236
+
202
237
  // ---- Main View ----
203
238
  export default function ModelsView() {
204
- const [tab, setTab] = useState('installed'); // installed | search
239
+ const [tab, setTab] = useState('recommended'); // recommended | installed | search
205
240
  const [searchQuery, setSearchQuery] = useState('');
206
241
  const [searchResults, setSearchResults] = useState([]);
207
242
  const [searching, setSearching] = useState(false);
208
243
  const [installed, setInstalled] = useState([]);
244
+ const [recommended, setRecommended] = useState([]);
209
245
  const [downloads, setDownloads] = useState([]);
210
246
  const [hardware, setHardware] = useState(null);
211
247
  const [expandedResult, setExpandedResult] = useState(null);
248
+ const [pulling, setPulling] = useState(null);
212
249
  const toast = useToast();
213
250
 
214
251
  // Fetch installed models
@@ -218,12 +255,28 @@ export default function ModelsView() {
218
255
  }).catch(() => {});
219
256
  }, []);
220
257
 
221
- // Fetch hardware info
258
+ // Fetch hardware info + recommended models
222
259
  useEffect(() => {
223
260
  api.get('/providers/ollama/hardware').then(setHardware).catch(() => {});
261
+ api.get('/models/recommended').then((data) => {
262
+ setRecommended(data.models || []);
263
+ if (!hardware && data.hardware) setHardware(data.hardware);
264
+ }).catch(() => {});
224
265
  fetchInstalled();
225
266
  }, [fetchInstalled]);
226
267
 
268
+ async function handlePull(modelId) {
269
+ setPulling(modelId);
270
+ try {
271
+ await api.post('/providers/ollama/pull', { model: modelId });
272
+ toast.success(`${modelId} pulled successfully`);
273
+ fetchInstalled();
274
+ } catch (err) {
275
+ toast.error(`Pull failed: ${err.message}`);
276
+ }
277
+ setPulling(null);
278
+ }
279
+
227
280
  // Listen for download progress via WebSocket
228
281
  useEffect(() => {
229
282
  const unsub = useGrooveStore.subscribe((state, prev) => {
@@ -323,16 +376,20 @@ export default function ModelsView() {
323
376
 
324
377
  {/* Tabs */}
325
378
  <div className="flex gap-1">
326
- {['installed', 'search'].map((t) => (
379
+ {[
380
+ { id: 'recommended', label: `Recommended (${recommended.length})` },
381
+ { id: 'installed', label: `Installed (${installed.length})` },
382
+ { id: 'search', label: `Search (${searchResults.length})` },
383
+ ].map((t) => (
327
384
  <button
328
- key={t}
329
- onClick={() => setTab(t)}
385
+ key={t.id}
386
+ onClick={() => setTab(t.id)}
330
387
  className={cn(
331
- 'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer capitalize',
332
- tab === t ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
388
+ 'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer',
389
+ tab === t.id ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
333
390
  )}
334
391
  >
335
- {t === 'installed' ? `Installed (${installed.length})` : `Search Results (${searchResults.length})`}
392
+ {t.label}
336
393
  </button>
337
394
  ))}
338
395
  </div>
@@ -349,6 +406,33 @@ export default function ModelsView() {
349
406
  {/* Content */}
350
407
  <ScrollArea className="flex-1">
351
408
  <div className="px-5 py-4 space-y-2">
409
+ {tab === 'recommended' && (
410
+ <>
411
+ {recommended.length === 0 ? (
412
+ <div className="text-center py-12">
413
+ <Cpu size={40} className="mx-auto text-text-4 mb-3" />
414
+ <p className="text-sm text-text-2 font-sans font-medium">Detecting hardware...</p>
415
+ <p className="text-xs text-text-3 font-sans mt-1">Make sure Ollama is installed so we can check your system.</p>
416
+ </div>
417
+ ) : (
418
+ <>
419
+ <div className="text-xs text-text-3 font-sans mb-2">
420
+ Top models for your system ({hardware?.totalRamGb || '?'} GB RAM). Click Pull to download via Ollama.
421
+ </div>
422
+ {recommended.map((m) => (
423
+ <RecommendedModel
424
+ key={m.id}
425
+ model={m}
426
+ systemRamGb={hardware?.totalRamGb}
427
+ onPull={handlePull}
428
+ pulling={pulling}
429
+ />
430
+ ))}
431
+ </>
432
+ )}
433
+ </>
434
+ )}
435
+
352
436
  {tab === 'installed' && (
353
437
  <>
354
438
  {installed.length === 0 ? (