groove-dev 0.27.44 → 0.27.45
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/default/groovedev-beta-auth-endpoint.md +166 -0
- 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 +619 -0
- package/node_modules/@groove-dev/daemon/src/firstrun.js +11 -0
- package/node_modules/@groove-dev/daemon/src/index.js +28 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +114 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BoIbnaqa.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.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 +3 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +7 -2
- package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +164 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +66 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +172 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +190 -0
- package/node_modules/@groove-dev/gui/src/views/network.jsx +227 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +88 -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 +619 -0
- package/packages/daemon/src/firstrun.js +11 -0
- package/packages/daemon/src/index.js +28 -0
- package/packages/daemon/src/providers/claude-code.js +1 -1
- package/packages/daemon/src/providers/groove-network.js +114 -0
- package/packages/daemon/src/providers/index.js +2 -0
- package/packages/gui/dist/assets/index-BoIbnaqa.js +8607 -0
- package/packages/gui/dist/assets/index-CyVj0fHl.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +3 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +7 -2
- package/packages/gui/src/components/network/network-status.jsx +164 -0
- package/packages/gui/src/components/network/node-details.jsx +66 -0
- package/packages/gui/src/components/network/node-toggle.jsx +172 -0
- package/packages/gui/src/stores/groove.js +190 -0
- package/packages/gui/src/views/network.jsx +227 -0
- package/packages/gui/src/views/settings.jsx +88 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B5Uor698.js +0 -8607
- package/node_modules/@groove-dev/gui/dist/assets/index-VGmIZurO.css +0 -1
- package/packages/gui/dist/assets/index-B5Uor698.js +0 -8607
- package/packages/gui/dist/assets/index-VGmIZurO.css +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
6
|
+
import { StatusDot } from '../ui/status-dot';
|
|
7
|
+
import { Tooltip } from '../ui/tooltip';
|
|
8
|
+
import { Copy, Check, Cpu } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
function shortAddress(addr) {
|
|
11
|
+
if (!addr || typeof addr !== 'string') return '—';
|
|
12
|
+
if (addr.length < 14) return addr;
|
|
13
|
+
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nodeStatus(node) {
|
|
17
|
+
if (node.active && node.status === 'connected') return 'running';
|
|
18
|
+
if (node.status === 'connecting') return 'starting';
|
|
19
|
+
return 'crashed';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Toggle({ value, onChange, disabled }) {
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => !disabled && onChange(!value)}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
className={cn(
|
|
28
|
+
'relative inline-flex h-7 w-12 rounded-full p-0.5 transition-colors',
|
|
29
|
+
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
|
30
|
+
value ? 'bg-accent' : 'bg-surface-5',
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
className={cn(
|
|
35
|
+
'inline-block h-6 w-6 rounded-full bg-white shadow-sm transition-transform',
|
|
36
|
+
value ? 'translate-x-5' : 'translate-x-0',
|
|
37
|
+
)}
|
|
38
|
+
/>
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function NodeToggle() {
|
|
44
|
+
const node = useGrooveStore((s) => s.networkNode);
|
|
45
|
+
const startNetworkNode = useGrooveStore((s) => s.startNetworkNode);
|
|
46
|
+
const stopNetworkNode = useGrooveStore((s) => s.stopNetworkNode);
|
|
47
|
+
const [pending, setPending] = useState(false);
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
|
|
50
|
+
async function handleToggle(next) {
|
|
51
|
+
setPending(true);
|
|
52
|
+
try {
|
|
53
|
+
if (next) await startNetworkNode();
|
|
54
|
+
else await stopNetworkNode();
|
|
55
|
+
} catch { /* toasted in store */ }
|
|
56
|
+
setPending(false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleCopy() {
|
|
60
|
+
if (!node.nodeId) return;
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(node.nodeId);
|
|
63
|
+
setCopied(true);
|
|
64
|
+
setTimeout(() => setCopied(false), 1500);
|
|
65
|
+
} catch { /* clipboard blocked */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const active = !!node.active;
|
|
69
|
+
const hardware = node.hardware || {};
|
|
70
|
+
const layersLabel = Array.isArray(node.layers)
|
|
71
|
+
? `layers ${node.layers[0]}-${node.layers[1]}`
|
|
72
|
+
: (node.layers && node.layers.start != null)
|
|
73
|
+
? `layers ${node.layers.start}-${node.layers.end}`
|
|
74
|
+
: 'unassigned';
|
|
75
|
+
const modelLabel = node.model || 'No model assigned';
|
|
76
|
+
const memPct = Number.isFinite(node.memoryPct) ? node.memoryPct : null;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="rounded-lg border border-border bg-surface-1 overflow-hidden">
|
|
80
|
+
{/* Hero toggle */}
|
|
81
|
+
<div className="flex items-center gap-4 px-5 py-4 border-b border-border-subtle">
|
|
82
|
+
<div className="flex-1 min-w-0">
|
|
83
|
+
<div className="text-sm font-semibold text-text-0 font-sans">Lend Compute</div>
|
|
84
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
85
|
+
{active ? 'Contributing to the Groove network' : 'Your machine can contribute to the Groove network'}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<Toggle value={active} onChange={handleToggle} disabled={pending} />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Body */}
|
|
92
|
+
<div className="px-5 py-4 space-y-4">
|
|
93
|
+
{!active ? (
|
|
94
|
+
<div>
|
|
95
|
+
<div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2">Detected hardware</div>
|
|
96
|
+
<div className="grid grid-cols-3 gap-2">
|
|
97
|
+
<div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
|
|
98
|
+
<div className="text-2xs text-text-4 font-sans">Device</div>
|
|
99
|
+
<div className="text-xs font-mono text-text-1 truncate">{hardware.device || 'auto'}</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
|
|
102
|
+
<div className="text-2xs text-text-4 font-sans">Memory</div>
|
|
103
|
+
<div className="text-xs font-mono text-text-1 truncate">{hardware.memory || '—'}</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
|
|
106
|
+
<div className="text-2xs text-text-4 font-sans">GPU</div>
|
|
107
|
+
<div className="text-xs font-mono text-text-1 truncate">{hardware.gpu || 'None'}</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<>
|
|
113
|
+
{/* Status row */}
|
|
114
|
+
<div className="flex items-center gap-3">
|
|
115
|
+
<StatusDot status={nodeStatus(node)} />
|
|
116
|
+
<div className="text-xs font-sans text-text-1 capitalize">{node.status || 'disconnected'}</div>
|
|
117
|
+
<div className="flex-1" />
|
|
118
|
+
<Badge variant="accent" className="font-mono">
|
|
119
|
+
<Cpu size={9} /> {modelLabel} · {layersLabel}
|
|
120
|
+
</Badge>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Identity */}
|
|
124
|
+
<div>
|
|
125
|
+
<div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">Node Identity</div>
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<code className="flex-1 h-8 px-2.5 flex items-center bg-surface-0 border border-border-subtle rounded-md text-xs font-mono text-text-1">
|
|
128
|
+
{shortAddress(node.nodeId)}
|
|
129
|
+
</code>
|
|
130
|
+
<Tooltip content={copied ? 'Copied' : 'Copy full address'} side="top">
|
|
131
|
+
<button
|
|
132
|
+
onClick={handleCopy}
|
|
133
|
+
className="h-8 w-8 inline-flex items-center justify-center rounded-md border border-border-subtle bg-surface-0 text-text-3 hover:text-accent hover:border-accent/40 cursor-pointer transition-colors"
|
|
134
|
+
>
|
|
135
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
136
|
+
</button>
|
|
137
|
+
</Tooltip>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Sessions + memory */}
|
|
142
|
+
<div className="grid grid-cols-2 gap-2">
|
|
143
|
+
<div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
|
|
144
|
+
<div className="text-2xs text-text-4 font-sans uppercase tracking-wider">Active Sessions</div>
|
|
145
|
+
<div className="text-lg font-mono text-text-0 tabular-nums leading-tight">{node.sessions || 0}</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2">
|
|
148
|
+
<div className="text-2xs text-text-4 font-sans uppercase tracking-wider mb-1">Memory</div>
|
|
149
|
+
{memPct != null ? (
|
|
150
|
+
<>
|
|
151
|
+
<div className="h-1.5 w-full rounded-full bg-surface-3 overflow-hidden">
|
|
152
|
+
<div
|
|
153
|
+
className={cn(
|
|
154
|
+
'h-full rounded-full transition-all',
|
|
155
|
+
memPct >= 90 ? 'bg-danger' : memPct >= 70 ? 'bg-warning' : 'bg-accent',
|
|
156
|
+
)}
|
|
157
|
+
style={{ width: `${Math.min(100, Math.max(0, memPct))}%` }}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="mt-1 text-2xs font-mono text-text-3 tabular-nums">{Math.round(memPct)}%</div>
|
|
161
|
+
</>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="text-xs font-mono text-text-3">—</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -86,6 +86,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
86
86
|
// ── Journalist ────────────────────────────────────────────
|
|
87
87
|
journalistStatus: null, // { cycleCount, lastCycleTime, history, lastSynthesis }
|
|
88
88
|
|
|
89
|
+
// ── Network (Early Access) ────────────────────────────────
|
|
90
|
+
networkUnlocked: false,
|
|
91
|
+
networkInstalled: false,
|
|
92
|
+
networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
|
|
93
|
+
networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
|
|
94
|
+
networkStatus: { nodes: [], coverage: 0, totalLayers: 0, models: [], activeSessions: 0 },
|
|
95
|
+
networkEvents: [],
|
|
96
|
+
|
|
89
97
|
// ── Marketplace Auth ───────────────────────────────────────
|
|
90
98
|
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
91
99
|
marketplaceAuthenticated: false,
|
|
@@ -155,6 +163,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
155
163
|
get().fetchApprovals();
|
|
156
164
|
get().checkMarketplaceAuth();
|
|
157
165
|
get().fetchTunnels();
|
|
166
|
+
get().fetchBetaStatus();
|
|
167
|
+
get().fetchNetworkInstallStatus();
|
|
158
168
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
159
169
|
if (window.groove?.auth?.onSubscriptionStatus) {
|
|
160
170
|
window.groove.auth.onSubscriptionStatus((data) => {
|
|
@@ -593,6 +603,59 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
593
603
|
set({ marketplaceAuthenticated: false, marketplaceUser: null });
|
|
594
604
|
get().addToast('warning', 'Session expired', 'Please sign in again');
|
|
595
605
|
break;
|
|
606
|
+
|
|
607
|
+
case 'network:node:status':
|
|
608
|
+
set({ networkNode: { ...get().networkNode, ...(msg.data || {}) } });
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'network:node:event': {
|
|
612
|
+
const ev = msg.data || {};
|
|
613
|
+
set((s) => ({
|
|
614
|
+
networkEvents: [...s.networkEvents, { ...ev, timestamp: ev.timestamp || Date.now() }].slice(-100),
|
|
615
|
+
}));
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
case 'network:status':
|
|
620
|
+
set({ networkStatus: { ...get().networkStatus, ...(msg.data || {}) } });
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
case 'network:install:progress': {
|
|
624
|
+
const { step, message, percent } = msg.data || {};
|
|
625
|
+
if (step === 'done') {
|
|
626
|
+
set({
|
|
627
|
+
networkInstalled: true,
|
|
628
|
+
networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
|
|
629
|
+
});
|
|
630
|
+
get().addToast('success', 'Network package installed');
|
|
631
|
+
} else if (step === 'error') {
|
|
632
|
+
set({
|
|
633
|
+
networkInstallProgress: {
|
|
634
|
+
installing: false,
|
|
635
|
+
step: 'error',
|
|
636
|
+
message: message || 'Install failed',
|
|
637
|
+
percent: 0,
|
|
638
|
+
error: message || 'Install failed',
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
set({
|
|
643
|
+
networkInstallProgress: {
|
|
644
|
+
installing: true,
|
|
645
|
+
step: step || 'progress',
|
|
646
|
+
message: message || '',
|
|
647
|
+
percent: Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : get().networkInstallProgress.percent,
|
|
648
|
+
error: null,
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
case 'config:updated':
|
|
656
|
+
get().fetchBetaStatus();
|
|
657
|
+
get().fetchNetworkInstallStatus();
|
|
658
|
+
break;
|
|
596
659
|
}
|
|
597
660
|
};
|
|
598
661
|
|
|
@@ -1589,6 +1652,133 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1589
1652
|
}
|
|
1590
1653
|
},
|
|
1591
1654
|
|
|
1655
|
+
// ── Network (Early Access) ────────────────────────────────
|
|
1656
|
+
|
|
1657
|
+
async fetchBetaStatus() {
|
|
1658
|
+
try {
|
|
1659
|
+
const data = await api.get('/beta/status');
|
|
1660
|
+
set({ networkUnlocked: !!data?.unlocked });
|
|
1661
|
+
} catch { /* endpoint may not exist yet */ }
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1664
|
+
async activateBeta(code) {
|
|
1665
|
+
const data = await api.post('/beta/activate', { code });
|
|
1666
|
+
if (!data?.unlocked) {
|
|
1667
|
+
throw new Error(data?.message || 'Invalid invite code');
|
|
1668
|
+
}
|
|
1669
|
+
set({ networkUnlocked: true });
|
|
1670
|
+
return data;
|
|
1671
|
+
},
|
|
1672
|
+
|
|
1673
|
+
async deactivateBeta() {
|
|
1674
|
+
try {
|
|
1675
|
+
await api.post('/beta/deactivate');
|
|
1676
|
+
set({
|
|
1677
|
+
networkUnlocked: false,
|
|
1678
|
+
activeView: get().activeView === 'network' ? 'agents' : get().activeView,
|
|
1679
|
+
});
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
get().addToast('error', 'Deactivate failed', err.message);
|
|
1682
|
+
throw err;
|
|
1683
|
+
}
|
|
1684
|
+
},
|
|
1685
|
+
|
|
1686
|
+
async fetchNetworkNodeStatus() {
|
|
1687
|
+
try {
|
|
1688
|
+
const data = await api.get('/network/node/status');
|
|
1689
|
+
const update = { networkNode: { ...get().networkNode, ...(data || {}) } };
|
|
1690
|
+
if (data && typeof data.installed === 'boolean') {
|
|
1691
|
+
update.networkInstalled = data.installed;
|
|
1692
|
+
}
|
|
1693
|
+
set(update);
|
|
1694
|
+
return data;
|
|
1695
|
+
} catch { return null; }
|
|
1696
|
+
},
|
|
1697
|
+
|
|
1698
|
+
async fetchNetworkInstallStatus() {
|
|
1699
|
+
try {
|
|
1700
|
+
const data = await api.get('/network/install/status');
|
|
1701
|
+
if (data && typeof data.installed === 'boolean') {
|
|
1702
|
+
set({ networkInstalled: data.installed });
|
|
1703
|
+
}
|
|
1704
|
+
return data;
|
|
1705
|
+
} catch { return null; }
|
|
1706
|
+
},
|
|
1707
|
+
|
|
1708
|
+
async installNetworkPackage() {
|
|
1709
|
+
set({
|
|
1710
|
+
networkInstallProgress: {
|
|
1711
|
+
installing: true,
|
|
1712
|
+
step: 'starting',
|
|
1713
|
+
message: 'Starting install…',
|
|
1714
|
+
percent: 0,
|
|
1715
|
+
error: null,
|
|
1716
|
+
},
|
|
1717
|
+
});
|
|
1718
|
+
try {
|
|
1719
|
+
await api.post('/network/install');
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
set({
|
|
1722
|
+
networkInstallProgress: {
|
|
1723
|
+
installing: false,
|
|
1724
|
+
step: 'error',
|
|
1725
|
+
message: err.message,
|
|
1726
|
+
percent: 0,
|
|
1727
|
+
error: err.message,
|
|
1728
|
+
},
|
|
1729
|
+
});
|
|
1730
|
+
get().addToast('error', 'Install failed', err.message);
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
|
|
1734
|
+
async uninstallNetworkPackage() {
|
|
1735
|
+
try {
|
|
1736
|
+
await api.post('/network/uninstall');
|
|
1737
|
+
set({
|
|
1738
|
+
networkInstalled: false,
|
|
1739
|
+
networkNode: { active: false, status: 'disconnected', nodeId: null, layers: null, model: null, sessions: 0, hardware: null },
|
|
1740
|
+
networkInstallProgress: { installing: false, step: null, message: null, percent: 0, error: null },
|
|
1741
|
+
});
|
|
1742
|
+
get().addToast('success', 'Network package uninstalled');
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
get().addToast('error', 'Uninstall failed', err.message);
|
|
1745
|
+
throw err;
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
|
|
1749
|
+
async fetchNetworkStatus() {
|
|
1750
|
+
try {
|
|
1751
|
+
const data = await api.get('/network/status');
|
|
1752
|
+
set({ networkStatus: { ...get().networkStatus, ...(data || {}) } });
|
|
1753
|
+
return data;
|
|
1754
|
+
} catch { return null; }
|
|
1755
|
+
},
|
|
1756
|
+
|
|
1757
|
+
async startNetworkNode() {
|
|
1758
|
+
set({ networkNode: { ...get().networkNode, status: 'connecting' } });
|
|
1759
|
+
try {
|
|
1760
|
+
const data = await api.post('/network/node/start');
|
|
1761
|
+
set({ networkNode: { ...get().networkNode, active: true, ...(data || {}) } });
|
|
1762
|
+
get().addToast('success', 'Node started', 'Connecting to the Groove network');
|
|
1763
|
+
return data;
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
set({ networkNode: { ...get().networkNode, status: 'disconnected', active: false } });
|
|
1766
|
+
get().addToast('error', 'Node start failed', err.message);
|
|
1767
|
+
throw err;
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
|
|
1771
|
+
async stopNetworkNode() {
|
|
1772
|
+
try {
|
|
1773
|
+
await api.post('/network/node/stop');
|
|
1774
|
+
set({ networkNode: { ...get().networkNode, active: false, status: 'disconnected' } });
|
|
1775
|
+
get().addToast('info', 'Node stopped');
|
|
1776
|
+
} catch (err) {
|
|
1777
|
+
get().addToast('error', 'Node stop failed', err.message);
|
|
1778
|
+
throw err;
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1592
1782
|
async renameFile(oldPath, newPath) {
|
|
1593
1783
|
try {
|
|
1594
1784
|
await api.post('/files/rename', { oldPath, newPath });
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
4
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
5
|
+
import { StatusDot } from '../components/ui/status-dot';
|
|
6
|
+
import { Badge } from '../components/ui/badge';
|
|
7
|
+
import { Button } from '../components/ui/button';
|
|
8
|
+
import { Dialog, DialogContent, DialogTrigger } from '../components/ui/dialog';
|
|
9
|
+
import { NodeToggle } from '../components/network/node-toggle';
|
|
10
|
+
import { NodeDetails } from '../components/network/node-details';
|
|
11
|
+
import { NetworkStatus } from '../components/network/network-status';
|
|
12
|
+
import { Globe, Download, Check, AlertCircle, Loader2, Trash2 } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
const REQUIREMENTS = [
|
|
15
|
+
'Python 3.10 or higher',
|
|
16
|
+
'~2 GB disk space for model shards',
|
|
17
|
+
'8 GB+ RAM recommended',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function InstallProgress({ progress }) {
|
|
21
|
+
const percent = Math.max(0, Math.min(100, Number.isFinite(progress.percent) ? progress.percent : 0));
|
|
22
|
+
return (
|
|
23
|
+
<div className="w-full flex flex-col gap-3">
|
|
24
|
+
<div className="h-2 w-full rounded-full bg-surface-3 overflow-hidden">
|
|
25
|
+
<div
|
|
26
|
+
className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
|
|
27
|
+
style={{ width: `${percent}%` }}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="flex items-center justify-between text-2xs font-mono text-text-3 tabular-nums">
|
|
31
|
+
<div className="flex items-center gap-2 text-text-2 font-sans">
|
|
32
|
+
<Loader2 size={12} className="animate-spin text-accent" />
|
|
33
|
+
<span className="truncate">{progress.message || 'Installing…'}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<span>{percent}%</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function InstallError({ message, onRetry }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="w-full flex flex-col gap-3">
|
|
44
|
+
<div className="rounded-md border border-danger/40 bg-danger/10 px-4 py-3 flex items-start gap-2.5 text-left">
|
|
45
|
+
<AlertCircle size={14} className="text-danger flex-shrink-0 mt-0.5" />
|
|
46
|
+
<div className="flex-1 min-w-0">
|
|
47
|
+
<div className="text-xs font-semibold text-danger font-sans mb-0.5">Install failed</div>
|
|
48
|
+
<div className="text-xs text-text-1 font-sans break-words">{message}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<Button variant="primary" size="lg" onClick={onRetry} className="w-full">
|
|
52
|
+
<Download size={14} />
|
|
53
|
+
Retry Install
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function InstallGate() {
|
|
60
|
+
const installNetworkPackage = useGrooveStore((s) => s.installNetworkPackage);
|
|
61
|
+
const progress = useGrooveStore((s) => s.networkInstallProgress);
|
|
62
|
+
const { installing, error } = progress;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col items-center justify-center min-h-full px-6 py-12">
|
|
66
|
+
<div className="w-full max-w-md flex flex-col items-center text-center">
|
|
67
|
+
<div className="mb-5 rounded-full bg-surface-2 border border-border-subtle p-5">
|
|
68
|
+
<Globe size={48} className="text-text-3" strokeWidth={1.25} />
|
|
69
|
+
</div>
|
|
70
|
+
<h3 className="text-base font-semibold text-text-0 font-sans mb-2">
|
|
71
|
+
Install Groove Network
|
|
72
|
+
</h3>
|
|
73
|
+
<p className="text-sm text-text-2 font-sans leading-relaxed mb-6">
|
|
74
|
+
The network package enables decentralized LLM inference. Contribute your compute power or run models across the Groove network.
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
<div className="w-full rounded-md border border-border-subtle bg-surface-1 px-4 py-3 mb-6">
|
|
78
|
+
<div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2 text-left">
|
|
79
|
+
Requirements
|
|
80
|
+
</div>
|
|
81
|
+
<ul className="flex flex-col gap-1.5">
|
|
82
|
+
{REQUIREMENTS.map((req) => (
|
|
83
|
+
<li key={req} className="flex items-center gap-2 text-xs font-sans text-text-1 text-left">
|
|
84
|
+
<Check size={12} className="text-accent flex-shrink-0" />
|
|
85
|
+
<span>{req}</span>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{installing ? (
|
|
92
|
+
<InstallProgress progress={progress} />
|
|
93
|
+
) : error ? (
|
|
94
|
+
<InstallError message={error} onRetry={() => installNetworkPackage()} />
|
|
95
|
+
) : (
|
|
96
|
+
<>
|
|
97
|
+
<Button
|
|
98
|
+
variant="primary"
|
|
99
|
+
size="lg"
|
|
100
|
+
onClick={() => installNetworkPackage()}
|
|
101
|
+
className="w-full"
|
|
102
|
+
>
|
|
103
|
+
<Download size={14} />
|
|
104
|
+
Install Network Package
|
|
105
|
+
</Button>
|
|
106
|
+
<p className="text-2xs font-sans text-text-3 mt-3">
|
|
107
|
+
This will download and set up the Groove Network runtime (~500 MB)
|
|
108
|
+
</p>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function UninstallButton() {
|
|
117
|
+
const [open, setOpen] = useState(false);
|
|
118
|
+
const uninstallNetworkPackage = useGrooveStore((s) => s.uninstallNetworkPackage);
|
|
119
|
+
const [busy, setBusy] = useState(false);
|
|
120
|
+
|
|
121
|
+
const confirm = async () => {
|
|
122
|
+
setBusy(true);
|
|
123
|
+
try {
|
|
124
|
+
await uninstallNetworkPackage();
|
|
125
|
+
setOpen(false);
|
|
126
|
+
} catch { /* toast already shown */ }
|
|
127
|
+
finally { setBusy(false); }
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
132
|
+
<DialogTrigger asChild>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className="inline-flex items-center gap-1.5 text-2xs font-sans text-text-3 hover:text-danger transition-colors"
|
|
136
|
+
>
|
|
137
|
+
<Trash2 size={11} />
|
|
138
|
+
Uninstall Network Package
|
|
139
|
+
</button>
|
|
140
|
+
</DialogTrigger>
|
|
141
|
+
<DialogContent title="Uninstall Network Package" description="Confirm uninstall">
|
|
142
|
+
<div className="px-5 py-4 flex flex-col gap-3">
|
|
143
|
+
<p className="text-sm text-text-1 font-sans leading-relaxed">
|
|
144
|
+
This will stop your node and remove the network package from <span className="font-mono text-text-2">~/.groove/network</span>.
|
|
145
|
+
</p>
|
|
146
|
+
<p className="text-xs text-text-3 font-sans leading-relaxed">
|
|
147
|
+
Your identity (<span className="font-mono">~/.groove/node_key.json</span>) will be preserved — you can reinstall later without losing your wallet.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border-subtle bg-surface-0">
|
|
151
|
+
<Button variant="ghost" size="sm" onClick={() => setOpen(false)} disabled={busy}>Cancel</Button>
|
|
152
|
+
<Button variant="danger" size="sm" onClick={confirm} disabled={busy}>
|
|
153
|
+
{busy ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
154
|
+
Uninstall
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
</DialogContent>
|
|
158
|
+
</Dialog>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default function NetworkView() {
|
|
163
|
+
const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
|
|
164
|
+
const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
|
|
165
|
+
const node = useGrooveStore((s) => s.networkNode);
|
|
166
|
+
const installed = useGrooveStore((s) => s.networkInstalled);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
fetchNetworkNodeStatus();
|
|
170
|
+
if (installed) {
|
|
171
|
+
fetchNetworkStatus();
|
|
172
|
+
const interval = setInterval(() => { fetchNetworkStatus(); }, 10000);
|
|
173
|
+
return () => clearInterval(interval);
|
|
174
|
+
}
|
|
175
|
+
}, [fetchNetworkNodeStatus, fetchNetworkStatus, installed]);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex flex-col h-full">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div className="flex items-center gap-3 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
|
|
181
|
+
<Globe size={14} className="text-accent" />
|
|
182
|
+
<h2 className="text-sm font-semibold text-text-0 font-sans">Groove Network</h2>
|
|
183
|
+
<Badge variant="purple">Early Access</Badge>
|
|
184
|
+
<div className="flex-1" />
|
|
185
|
+
{installed && (
|
|
186
|
+
<>
|
|
187
|
+
<UninstallButton />
|
|
188
|
+
<div className="flex items-center gap-1.5 text-2xs font-sans text-text-3">
|
|
189
|
+
<StatusDot status={node.active ? 'running' : 'crashed'} size="sm" />
|
|
190
|
+
{node.active ? 'Contributing' : 'Idle'}
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Body */}
|
|
197
|
+
<ScrollArea className="flex-1">
|
|
198
|
+
{!installed ? (
|
|
199
|
+
<InstallGate />
|
|
200
|
+
) : (
|
|
201
|
+
<div className="p-4 grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
202
|
+
{/* Left column — node operator */}
|
|
203
|
+
<div className="flex flex-col gap-3 min-w-0">
|
|
204
|
+
<div>
|
|
205
|
+
<div className="flex items-center gap-2 mb-2 px-0.5">
|
|
206
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Node Operator</span>
|
|
207
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
208
|
+
</div>
|
|
209
|
+
<NodeToggle />
|
|
210
|
+
</div>
|
|
211
|
+
<NodeDetails />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Right column — network status */}
|
|
215
|
+
<div className="flex flex-col gap-3 min-w-0">
|
|
216
|
+
<div className="flex items-center gap-2 px-0.5">
|
|
217
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Network Status</span>
|
|
218
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
219
|
+
</div>
|
|
220
|
+
<NetworkStatus />
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</ScrollArea>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|