groove-dev 0.27.59 → 0.27.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +73 -56
- package/node_modules/@groove-dev/daemon/src/conversations.js +78 -35
- package/node_modules/@groove-dev/daemon/src/journalist.js +1 -0
- package/node_modules/@groove-dev/daemon/src/process.js +17 -7
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +63 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +55 -0
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +53 -0
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/index.js +16 -1
- package/node_modules/@groove-dev/daemon/src/providers/local.js +44 -0
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +44 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +4 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B3AqeyS4.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DWao9glo.js +8614 -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/components/chat/chat-view.jsx +3 -2
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +13 -7
- package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +245 -0
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/ui/update-modal.jsx +70 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +66 -6
- package/node_modules/@groove-dev/gui/src/views/network.jsx +99 -17
- 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 +73 -56
- package/packages/daemon/src/conversations.js +78 -35
- package/packages/daemon/src/journalist.js +1 -0
- package/packages/daemon/src/process.js +17 -7
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +63 -0
- package/packages/daemon/src/providers/codex.js +55 -0
- package/packages/daemon/src/providers/gemini.js +53 -0
- package/packages/daemon/src/providers/groove-network.js +1 -1
- package/packages/daemon/src/providers/index.js +16 -1
- package/packages/daemon/src/providers/local.js +44 -0
- package/packages/daemon/src/providers/ollama.js +44 -0
- package/packages/daemon/src/rotator.js +4 -0
- package/packages/gui/dist/assets/index-B3AqeyS4.css +1 -0
- package/packages/gui/dist/assets/index-DWao9glo.js +8614 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/chat/chat-view.jsx +3 -2
- package/packages/gui/src/components/chat/model-picker.jsx +1 -1
- package/packages/gui/src/components/layout/status-bar.jsx +13 -7
- package/packages/gui/src/components/network/activity-chart.jsx +245 -0
- package/packages/gui/src/components/network/compute-header.jsx +1 -1
- package/packages/gui/src/components/network/network-health.jsx +1 -1
- package/packages/gui/src/components/network/network-status.jsx +5 -5
- package/packages/gui/src/components/network/node-details.jsx +1 -1
- package/packages/gui/src/components/ui/update-modal.jsx +70 -0
- package/packages/gui/src/stores/groove.js +66 -6
- package/packages/gui/src/views/network.jsx +99 -17
- package/default/fix-beta-endpoint-deployment.md +0 -68
- package/default/groovedev-beta-auth-endpoint.md +0 -166
- package/default/security-review-prompt.md +0 -98
- package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BycOlqLx.js +0 -8614
- package/packages/gui/dist/assets/index-BrfCzrxJ.css +0 -1
- package/packages/gui/dist/assets/index-BycOlqLx.js +0 -8614
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DWao9glo.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B3AqeyS4.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -38,6 +38,7 @@ export function ChatView() {
|
|
|
38
38
|
const stopAgent = useGrooveStore((s) => s.stopAgent);
|
|
39
39
|
const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
|
|
40
40
|
const setConversationMode = useGrooveStore((s) => s.setConversationMode);
|
|
41
|
+
const setConversationModel = useGrooveStore((s) => s.setConversationModel);
|
|
41
42
|
|
|
42
43
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
43
44
|
|
|
@@ -74,11 +75,11 @@ export function ChatView() {
|
|
|
74
75
|
|
|
75
76
|
const handleModelChange = useCallback(async (selection) => {
|
|
76
77
|
if (activeConversationId) {
|
|
77
|
-
|
|
78
|
+
await setConversationModel(activeConversationId, selection.provider, selection.model);
|
|
78
79
|
} else {
|
|
79
80
|
await handleNewChat(selection.provider, selection.model);
|
|
80
81
|
}
|
|
81
|
-
}, [activeConversationId, handleNewChat]);
|
|
82
|
+
}, [activeConversationId, setConversationModel, handleNewChat]);
|
|
82
83
|
|
|
83
84
|
const currentModel = activeConversation
|
|
84
85
|
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
@@ -83,7 +83,7 @@ export function ModelPicker({ value, onChange, disabled }) {
|
|
|
83
83
|
</button>
|
|
84
84
|
|
|
85
85
|
{open && (
|
|
86
|
-
<div className="absolute top-full
|
|
86
|
+
<div className="absolute top-full right-0 mt-1 w-72 max-h-80 overflow-y-auto rounded-lg border border-border bg-surface-1 shadow-xl z-50">
|
|
87
87
|
{providers.length === 0 && (
|
|
88
88
|
<div className="px-4 py-6 text-center text-xs text-text-3 font-sans">No providers available</div>
|
|
89
89
|
)}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { StatusDot } from '../ui/status-dot';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
5
6
|
import { fmtUptime } from '../../lib/format';
|
|
6
7
|
import { useGrooveStore } from '../../stores/groove';
|
|
7
8
|
import { isElectron, openExternal } from '../../lib/electron';
|
|
9
|
+
import { UpdateModal } from '../ui/update-modal';
|
|
8
10
|
|
|
9
11
|
export function StatusBar({
|
|
10
12
|
connected,
|
|
@@ -18,7 +20,8 @@ export function StatusBar({
|
|
|
18
20
|
const tunneled = useGrooveStore((s) => s.tunneled);
|
|
19
21
|
const version = useGrooveStore((s) => s.version);
|
|
20
22
|
const updateReady = useGrooveStore((s) => s.updateReady);
|
|
21
|
-
const
|
|
23
|
+
const updateProgress = useGrooveStore((s) => s.updateProgress);
|
|
24
|
+
const setUpdateModalOpen = useGrooveStore((s) => s.setUpdateModalOpen);
|
|
22
25
|
const subscription = useGrooveStore((s) => s.subscription);
|
|
23
26
|
const navigate = useGrooveStore((s) => s.setActiveView);
|
|
24
27
|
const activeTunnel = savedTunnels.find((t) => t.active);
|
|
@@ -110,14 +113,16 @@ export function StatusBar({
|
|
|
110
113
|
<div className="flex-1" />
|
|
111
114
|
|
|
112
115
|
{/* Right: version + docs + terminal toggle */}
|
|
113
|
-
{updateReady ? (
|
|
116
|
+
{updateReady || updateProgress ? (
|
|
114
117
|
<button
|
|
115
|
-
onClick={
|
|
116
|
-
className="flex items-center gap-1
|
|
117
|
-
title={`Update to v${updateReady}`}
|
|
118
|
+
onClick={() => setUpdateModalOpen(true)}
|
|
119
|
+
className="flex items-center gap-1 px-2 h-full cursor-pointer"
|
|
120
|
+
title={updateReady ? `Update to v${updateReady}` : 'Downloading update\u2026'}
|
|
118
121
|
>
|
|
119
|
-
<
|
|
120
|
-
|
|
122
|
+
<Badge variant="warning" className="cursor-pointer">
|
|
123
|
+
<ArrowUpCircle size={10} />
|
|
124
|
+
{updateReady ? 'Update Available' : 'Downloading\u2026'}
|
|
125
|
+
</Badge>
|
|
121
126
|
</button>
|
|
122
127
|
) : version ? (
|
|
123
128
|
<span className="text-text-4 px-2">v{version}</span>
|
|
@@ -146,6 +151,7 @@ export function StatusBar({
|
|
|
146
151
|
<span>Terminal</span>
|
|
147
152
|
<kbd className="font-mono text-text-4 ml-0.5">Cmd+J</kbd>
|
|
148
153
|
</button>
|
|
154
|
+
<UpdateModal />
|
|
149
155
|
</footer>
|
|
150
156
|
);
|
|
151
157
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useRef, useEffect, useState, useCallback, useMemo, memo } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { HEX, hexAlpha } from '../../lib/theme-hex';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
6
|
+
|
|
7
|
+
function shortAddr(addr) {
|
|
8
|
+
if (!addr || typeof addr !== 'string') return '—';
|
|
9
|
+
if (addr.length < 14) return addr;
|
|
10
|
+
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ActivityChart = memo(function ActivityChart() {
|
|
14
|
+
const snapshots = useGrooveStore((s) => s.networkSnapshots);
|
|
15
|
+
const nodes = useGrooveStore((s) => s.networkStatus.nodes || []);
|
|
16
|
+
const ownNodeId = useGrooveStore((s) => s.networkNode.nodeId);
|
|
17
|
+
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const canvasRef = useRef(null);
|
|
20
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
21
|
+
const [hover, setHover] = useState(null);
|
|
22
|
+
|
|
23
|
+
const { width, height } = size;
|
|
24
|
+
const pad = { top: 28, right: 12, bottom: 8, left: 12 };
|
|
25
|
+
const w = Math.max(width - pad.left - pad.right, 0);
|
|
26
|
+
const h = Math.max(height - pad.top - pad.bottom, 0);
|
|
27
|
+
|
|
28
|
+
const chartData = useMemo(() => {
|
|
29
|
+
if (!snapshots || snapshots.length < 2) return [];
|
|
30
|
+
return snapshots;
|
|
31
|
+
}, [snapshots]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const el = containerRef.current;
|
|
35
|
+
if (!el) return;
|
|
36
|
+
const observer = new ResizeObserver((entries) => {
|
|
37
|
+
const { width: cw, height: ch } = entries[0].contentRect;
|
|
38
|
+
if (cw > 0 && ch > 0) setSize({ width: Math.floor(cw), height: Math.floor(ch) });
|
|
39
|
+
});
|
|
40
|
+
observer.observe(el);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const onMouseMove = useCallback((e) => {
|
|
45
|
+
const canvas = canvasRef.current;
|
|
46
|
+
if (!canvas || !chartData.length || w <= 0) return;
|
|
47
|
+
const rect = canvas.getBoundingClientRect();
|
|
48
|
+
const x = e.clientX - rect.left - pad.left;
|
|
49
|
+
if (x < 0 || x > w) { setHover(null); return; }
|
|
50
|
+
const index = Math.round((x / w) * (chartData.length - 1));
|
|
51
|
+
setHover({ x: pad.left + (index / Math.max(chartData.length - 1, 1)) * w, index });
|
|
52
|
+
}, [chartData, w, pad.left]);
|
|
53
|
+
|
|
54
|
+
const onMouseLeave = useCallback(() => setHover(null), []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const canvas = canvasRef.current;
|
|
58
|
+
if (!canvas || !chartData.length || width <= 0 || height <= 0 || w <= 0 || h <= 0) return;
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
const dpr = window.devicePixelRatio || 1;
|
|
61
|
+
|
|
62
|
+
canvas.width = width * dpr;
|
|
63
|
+
canvas.height = height * dpr;
|
|
64
|
+
ctx.scale(dpr, dpr);
|
|
65
|
+
ctx.clearRect(0, 0, width, height);
|
|
66
|
+
|
|
67
|
+
const globalSessions = chartData.map((d) => d.globalSessions);
|
|
68
|
+
const mySessions = chartData.map((d) => d.mySessions);
|
|
69
|
+
const maxVal = Math.max(...globalSessions, ...mySessions, 1);
|
|
70
|
+
|
|
71
|
+
const xAt = (i) => pad.left + (i / Math.max(chartData.length - 1, 1)) * w;
|
|
72
|
+
const yAt = (v) => pad.top + h - (v / maxVal) * h;
|
|
73
|
+
|
|
74
|
+
// Horizontal grid lines
|
|
75
|
+
ctx.setLineDash([2, 4]);
|
|
76
|
+
ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
|
|
77
|
+
ctx.lineWidth = 1;
|
|
78
|
+
for (let i = 1; i <= 3; i++) {
|
|
79
|
+
const y = pad.top + (h / 4) * i;
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.moveTo(pad.left, y);
|
|
82
|
+
ctx.lineTo(pad.left + w, y);
|
|
83
|
+
ctx.stroke();
|
|
84
|
+
}
|
|
85
|
+
ctx.setLineDash([]);
|
|
86
|
+
|
|
87
|
+
// Y-axis labels
|
|
88
|
+
ctx.font = "9px 'JetBrains Mono Variable', monospace";
|
|
89
|
+
ctx.textAlign = 'left';
|
|
90
|
+
ctx.fillStyle = hexAlpha(HEX.text3, 0.5);
|
|
91
|
+
ctx.fillText(String(maxVal), pad.left + 4, pad.top + 10);
|
|
92
|
+
ctx.fillText(String(Math.round(maxVal / 2)), pad.left + 4, pad.top + h / 2 + 4);
|
|
93
|
+
|
|
94
|
+
// Network line — gradient fill
|
|
95
|
+
ctx.beginPath();
|
|
96
|
+
ctx.moveTo(pad.left, pad.top + h);
|
|
97
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
98
|
+
ctx.lineTo(xAt(i), yAt(globalSessions[i]));
|
|
99
|
+
}
|
|
100
|
+
ctx.lineTo(xAt(chartData.length - 1), pad.top + h);
|
|
101
|
+
ctx.closePath();
|
|
102
|
+
const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + h);
|
|
103
|
+
grad.addColorStop(0, hexAlpha(HEX.purple, 0.2));
|
|
104
|
+
grad.addColorStop(0.7, hexAlpha(HEX.purple, 0.04));
|
|
105
|
+
grad.addColorStop(1, hexAlpha(HEX.purple, 0));
|
|
106
|
+
ctx.fillStyle = grad;
|
|
107
|
+
ctx.fill();
|
|
108
|
+
|
|
109
|
+
// Network line — stroke
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.strokeStyle = HEX.purple;
|
|
112
|
+
ctx.lineWidth = 1.5;
|
|
113
|
+
ctx.lineJoin = 'round';
|
|
114
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
115
|
+
const x = xAt(i);
|
|
116
|
+
const y = yAt(globalSessions[i]);
|
|
117
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
118
|
+
}
|
|
119
|
+
ctx.stroke();
|
|
120
|
+
|
|
121
|
+
// Your Node line — stroke only
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.strokeStyle = HEX.accent;
|
|
124
|
+
ctx.lineWidth = 1.5;
|
|
125
|
+
ctx.lineJoin = 'round';
|
|
126
|
+
for (let i = 0; i < chartData.length; i++) {
|
|
127
|
+
const x = xAt(i);
|
|
128
|
+
const y = yAt(mySessions[i]);
|
|
129
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
130
|
+
}
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
|
|
133
|
+
// Inline legend (top-right)
|
|
134
|
+
ctx.font = "9px 'Inter Variable', sans-serif";
|
|
135
|
+
ctx.textAlign = 'right';
|
|
136
|
+
let rx = width - pad.right - 4;
|
|
137
|
+
const ly = 14;
|
|
138
|
+
|
|
139
|
+
ctx.fillStyle = HEX.accent;
|
|
140
|
+
ctx.fillText('Your Node', rx, ly);
|
|
141
|
+
rx -= ctx.measureText('Your Node').width + 4;
|
|
142
|
+
ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
|
|
143
|
+
rx -= 14;
|
|
144
|
+
|
|
145
|
+
ctx.fillStyle = HEX.purple;
|
|
146
|
+
ctx.fillText('Network', rx, ly);
|
|
147
|
+
rx -= ctx.measureText('Network').width + 4;
|
|
148
|
+
ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
|
|
149
|
+
|
|
150
|
+
// Hover
|
|
151
|
+
if (hover && hover.index >= 0 && hover.index < chartData.length) {
|
|
152
|
+
const hx = hover.x;
|
|
153
|
+
const d = chartData[hover.index];
|
|
154
|
+
|
|
155
|
+
// Crosshair
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
ctx.moveTo(hx, pad.top);
|
|
158
|
+
ctx.lineTo(hx, pad.top + h);
|
|
159
|
+
ctx.strokeStyle = hexAlpha(HEX.text1, 0.15);
|
|
160
|
+
ctx.lineWidth = 1;
|
|
161
|
+
ctx.stroke();
|
|
162
|
+
|
|
163
|
+
// Dots
|
|
164
|
+
ctx.beginPath(); ctx.arc(hx, yAt(d.globalSessions), 3, 0, Math.PI * 2);
|
|
165
|
+
ctx.fillStyle = HEX.purple; ctx.fill();
|
|
166
|
+
ctx.beginPath(); ctx.arc(hx, yAt(d.mySessions), 3, 0, Math.PI * 2);
|
|
167
|
+
ctx.fillStyle = HEX.accent; ctx.fill();
|
|
168
|
+
|
|
169
|
+
// Tooltip
|
|
170
|
+
const lines = [
|
|
171
|
+
{ label: 'Network', value: String(d.globalSessions), color: HEX.purple },
|
|
172
|
+
{ label: 'Your Node', value: String(d.mySessions), color: HEX.accent },
|
|
173
|
+
{ label: 'Nodes', value: String(d.nodeCount), color: HEX.text2 },
|
|
174
|
+
];
|
|
175
|
+
const tooltipW = 104;
|
|
176
|
+
const tooltipH = lines.length * 16 + 12;
|
|
177
|
+
let tx = hx + 12;
|
|
178
|
+
if (tx + tooltipW > width - 8) tx = hx - tooltipW - 12;
|
|
179
|
+
const ty = Math.max(pad.top, yAt(d.globalSessions) - tooltipH / 2);
|
|
180
|
+
|
|
181
|
+
ctx.fillStyle = hexAlpha(HEX.surface0, 0.92);
|
|
182
|
+
ctx.beginPath(); ctx.roundRect(tx, ty, tooltipW, tooltipH, 4); ctx.fill();
|
|
183
|
+
ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
|
|
184
|
+
ctx.lineWidth = 1; ctx.stroke();
|
|
185
|
+
|
|
186
|
+
ctx.textAlign = 'left';
|
|
187
|
+
lines.forEach((line, i) => {
|
|
188
|
+
const rowY = ty + 14 + i * 16;
|
|
189
|
+
ctx.beginPath(); ctx.arc(tx + 8, rowY - 3, 2, 0, Math.PI * 2);
|
|
190
|
+
ctx.fillStyle = line.color; ctx.fill();
|
|
191
|
+
ctx.font = "8px 'Inter Variable', sans-serif";
|
|
192
|
+
ctx.fillStyle = HEX.text3; ctx.fillText(line.label, tx + 14, rowY);
|
|
193
|
+
ctx.font = "9px 'JetBrains Mono Variable', monospace";
|
|
194
|
+
ctx.fillStyle = HEX.text0; ctx.textAlign = 'right';
|
|
195
|
+
ctx.fillText(line.value, tx + tooltipW - 8, rowY);
|
|
196
|
+
ctx.textAlign = 'left';
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}, [chartData, width, height, hover, w, h, pad]);
|
|
200
|
+
|
|
201
|
+
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="flex flex-col h-full">
|
|
205
|
+
<div className="px-3 pt-2.5 pb-1 flex-shrink-0 flex items-center justify-between">
|
|
206
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Network Activity</span>
|
|
207
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums">{activeNodes.length} nodes</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="relative flex-1 min-h-0">
|
|
211
|
+
{chartData.length < 2 ? (
|
|
212
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
213
|
+
<span className="text-xs font-mono text-text-3">Collecting network data…</span>
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<div ref={containerRef} className="absolute inset-0">
|
|
217
|
+
{width > 0 && height > 0 && (
|
|
218
|
+
<canvas
|
|
219
|
+
ref={canvasRef}
|
|
220
|
+
style={{ width, height }}
|
|
221
|
+
className="block cursor-crosshair"
|
|
222
|
+
onMouseMove={onMouseMove}
|
|
223
|
+
onMouseLeave={onMouseLeave}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="px-3 py-1.5 border-t border-border-subtle flex items-center gap-2 flex-shrink-0 font-mono text-2xs" style={{ background: hexAlpha(HEX.accent, 0.04) }}>
|
|
231
|
+
<span className="text-text-3">{activeNodes.length} node{activeNodes.length !== 1 ? 's' : ''} online</span>
|
|
232
|
+
{activeNodes.map((n) => {
|
|
233
|
+
const id = n.node_id || n.nodeId || '';
|
|
234
|
+
const isSelf = ownNodeId && id === ownNodeId;
|
|
235
|
+
const layers = Array.isArray(n.layers) ? `${n.layers[0]}-${n.layers[1]}` : '';
|
|
236
|
+
return (
|
|
237
|
+
<span key={id} className={isSelf ? 'text-accent' : 'text-text-2'}>
|
|
238
|
+
{shortAddr(id)}{isSelf ? ' (You' : '('}{layers ? ` · ${layers}` : ''})
|
|
239
|
+
</span>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
});
|
|
@@ -121,7 +121,7 @@ export const ComputeHeader = memo(function ComputeHeader() {
|
|
|
121
121
|
const loadColor = compute.avgLoad > 2.0 ? HEX.danger : compute.avgLoad > 1.0 ? HEX.warning : HEX.success;
|
|
122
122
|
const activeModel = models.length > 0
|
|
123
123
|
? (typeof models[0] === 'string' ? models[0] : models[0].name)
|
|
124
|
-
: '
|
|
124
|
+
: 'Qwen/Qwen3-4B';
|
|
125
125
|
|
|
126
126
|
const kpis = [
|
|
127
127
|
{ label: 'RAM', value: `${fmtMbToGb(compute.totalRamMb)} GB`, color: HEX.accent, hint: 'Total RAM across all network nodes.' },
|
|
@@ -22,7 +22,7 @@ export const NetworkHealth = memo(function NetworkHealth() {
|
|
|
22
22
|
const node = useGrooveStore((s) => s.networkNode);
|
|
23
23
|
|
|
24
24
|
const nodes = Array.isArray(status.nodes) ? status.nodes : [];
|
|
25
|
-
const totalLayers = status.totalLayers ||
|
|
25
|
+
const totalLayers = status.totalLayers || 36;
|
|
26
26
|
const covered = status.coverage || 0;
|
|
27
27
|
const coverage = coverageState(covered, totalLayers);
|
|
28
28
|
const coveragePct = totalLayers ? Math.min(100, (covered / totalLayers) * 100) : 0;
|
|
@@ -24,7 +24,7 @@ function coverageState(covered, total) {
|
|
|
24
24
|
|
|
25
25
|
function KpiTile({ icon: Icon, label, value, sub }) {
|
|
26
26
|
return (
|
|
27
|
-
<div className="rounded-
|
|
27
|
+
<div className="rounded-sm border border-border bg-surface-1 px-3 py-2.5 flex items-center gap-2.5 min-w-0">
|
|
28
28
|
<div className="w-8 h-8 rounded-md bg-accent/10 text-accent flex items-center justify-center flex-shrink-0">
|
|
29
29
|
<Icon size={14} />
|
|
30
30
|
</div>
|
|
@@ -53,7 +53,7 @@ export function NetworkStatus() {
|
|
|
53
53
|
return (
|
|
54
54
|
<div className="flex flex-col gap-3">
|
|
55
55
|
{/* Signal connection indicator */}
|
|
56
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-
|
|
56
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-sm border border-border bg-surface-1">
|
|
57
57
|
<StatusDot status={signalReachable ? 'running' : 'crashed'} size="sm" />
|
|
58
58
|
<span className="text-2xs font-sans text-text-3">Signal:</span>
|
|
59
59
|
<span className="text-2xs font-mono text-text-1">signal.groovedev.ai</span>
|
|
@@ -71,7 +71,7 @@ export function NetworkStatus() {
|
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
{/* Coverage bar */}
|
|
74
|
-
<div className="rounded-
|
|
74
|
+
<div className="rounded-sm border border-border bg-surface-1 px-4 py-3">
|
|
75
75
|
<div className="flex items-center justify-between mb-2">
|
|
76
76
|
<span className="text-xs font-semibold text-text-1 font-sans">Layer Coverage</span>
|
|
77
77
|
<Badge variant={coverage.tone}>{coverage.label}</Badge>
|
|
@@ -86,7 +86,7 @@ export function NetworkStatus() {
|
|
|
86
86
|
</div>
|
|
87
87
|
|
|
88
88
|
{/* Models */}
|
|
89
|
-
<div className="rounded-
|
|
89
|
+
<div className="rounded-sm border border-border bg-surface-1 overflow-hidden">
|
|
90
90
|
<div className="px-4 py-2.5 border-b border-border-subtle">
|
|
91
91
|
<span className="text-xs font-semibold text-text-1 font-sans">Models</span>
|
|
92
92
|
</div>
|
|
@@ -111,7 +111,7 @@ export function NetworkStatus() {
|
|
|
111
111
|
</div>
|
|
112
112
|
|
|
113
113
|
{/* Nodes table */}
|
|
114
|
-
<div className="rounded-
|
|
114
|
+
<div className="rounded-sm border border-border bg-surface-1 overflow-hidden">
|
|
115
115
|
<button
|
|
116
116
|
onClick={() => setNodesOpen((v) => !v)}
|
|
117
117
|
className="w-full flex items-center gap-2 px-4 py-2.5 border-b border-border-subtle cursor-pointer hover:bg-surface-2/40 transition-colors"
|
|
@@ -29,7 +29,7 @@ export function NodeDetails() {
|
|
|
29
29
|
const events = useGrooveStore((s) => s.networkEvents);
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
|
-
<div className="flex flex-col rounded-
|
|
32
|
+
<div className="flex flex-col rounded-sm border border-border bg-surface-1 overflow-hidden min-h-0">
|
|
33
33
|
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border-subtle">
|
|
34
34
|
<Activity size={12} className="text-text-3" />
|
|
35
35
|
<span className="text-xs font-semibold text-text-1 font-sans">Node Activity</span>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { ArrowUpCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import { Dialog, DialogContent } from './dialog';
|
|
4
|
+
import { Button } from './button';
|
|
5
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
6
|
+
|
|
7
|
+
export function UpdateModal() {
|
|
8
|
+
const open = useGrooveStore((s) => s.updateModalOpen);
|
|
9
|
+
const setOpen = useGrooveStore((s) => s.setUpdateModalOpen);
|
|
10
|
+
const version = useGrooveStore((s) => s.version);
|
|
11
|
+
const updateReady = useGrooveStore((s) => s.updateReady);
|
|
12
|
+
const updateProgress = useGrooveStore((s) => s.updateProgress);
|
|
13
|
+
const installUpdate = useGrooveStore((s) => s.installUpdate);
|
|
14
|
+
|
|
15
|
+
const downloading = updateProgress && !updateReady;
|
|
16
|
+
const percent = downloading ? Math.max(0, Math.min(100, updateProgress.percent || 0)) : 100;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
20
|
+
<DialogContent title="Update Available" description="Desktop app update">
|
|
21
|
+
<div className="px-5 py-4 flex flex-col gap-3">
|
|
22
|
+
<div className="flex items-center gap-3">
|
|
23
|
+
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-accent/12">
|
|
24
|
+
<ArrowUpCircle size={20} className="text-accent" />
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<p className="text-sm text-text-1 font-sans font-medium">
|
|
28
|
+
{downloading ? 'Downloading update\u2026' : `Ready to update`}
|
|
29
|
+
</p>
|
|
30
|
+
<p className="text-xs text-text-3 font-sans mt-0.5">
|
|
31
|
+
{version && <span className="font-mono">{version}</span>}
|
|
32
|
+
{version && updateReady && ' \u2192 '}
|
|
33
|
+
{updateReady && <span className="font-mono text-accent">{updateReady}</span>}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
{downloading && (
|
|
38
|
+
<div className="flex items-center gap-2 mt-1">
|
|
39
|
+
<Loader2 size={12} className="animate-spin text-accent flex-shrink-0" />
|
|
40
|
+
<div className="flex-1 h-1.5 rounded-full bg-surface-3 overflow-hidden">
|
|
41
|
+
<div
|
|
42
|
+
className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
|
|
43
|
+
style={{ width: `${percent}%` }}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<span className="text-2xs font-mono text-text-3 tabular-nums">{percent}%</span>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
{!downloading && (
|
|
50
|
+
<p className="text-xs text-text-3 font-sans leading-relaxed">
|
|
51
|
+
The app will restart to apply the update. Your work is saved automatically.
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border-subtle bg-surface-0">
|
|
56
|
+
<Button variant="ghost" size="sm" onClick={() => setOpen(false)}>Later</Button>
|
|
57
|
+
<Button
|
|
58
|
+
variant="primary"
|
|
59
|
+
size="sm"
|
|
60
|
+
disabled={downloading}
|
|
61
|
+
onClick={() => { installUpdate(); setOpen(false); }}
|
|
62
|
+
>
|
|
63
|
+
<ArrowUpCircle size={12} />
|
|
64
|
+
Update & Restart
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
</DialogContent>
|
|
68
|
+
</Dialog>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -104,6 +104,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
104
104
|
networkVersion: { installed: null, latest: null, updateAvailable: false },
|
|
105
105
|
networkUpdateProgress: { updating: false, step: null, message: null, percent: 0, error: null },
|
|
106
106
|
networkCompute: { totalRamMb: 0, totalVramMb: 0, totalCpuCores: 0, totalBandwidthMbps: 0, activeNodes: 0, totalNodes: 0, avgLoad: 0 },
|
|
107
|
+
networkSnapshots: [],
|
|
107
108
|
|
|
108
109
|
// ── Marketplace Auth ───────────────────────────────────────
|
|
109
110
|
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
@@ -122,6 +123,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
122
123
|
// ── Version / Auto-Update ──────────────────────────────────
|
|
123
124
|
version: null,
|
|
124
125
|
updateReady: null,
|
|
126
|
+
updateProgress: null,
|
|
127
|
+
updateModalOpen: false,
|
|
125
128
|
|
|
126
129
|
// ── Toasts ────────────────────────────────────────────────
|
|
127
130
|
toasts: [],
|
|
@@ -183,10 +186,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
183
186
|
if (data) set({ subscription: { ...get().subscription, ...data } });
|
|
184
187
|
});
|
|
185
188
|
}
|
|
189
|
+
if (window.groove?.update?.onUpdateProgress) {
|
|
190
|
+
window.groove.update.onUpdateProgress((data) => {
|
|
191
|
+
set({ updateProgress: data });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
186
194
|
if (window.groove?.update?.onUpdateDownloaded) {
|
|
187
195
|
window.groove.update.onUpdateDownloaded((data) => {
|
|
188
|
-
set({ updateReady: data.version });
|
|
189
|
-
get().addToast('info', 'Update available', `v${data.version} downloaded — restart to apply`);
|
|
196
|
+
set({ updateReady: data.version, updateModalOpen: true, updateProgress: null });
|
|
190
197
|
});
|
|
191
198
|
}
|
|
192
199
|
};
|
|
@@ -712,6 +719,24 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
712
719
|
};
|
|
713
720
|
}
|
|
714
721
|
set(nsUpdate);
|
|
722
|
+
|
|
723
|
+
// Push snapshot for activity chart
|
|
724
|
+
const wsNodes = nsData.nodes || [];
|
|
725
|
+
const wsOwnId = get().networkNode.nodeId;
|
|
726
|
+
const wsOwn = wsOwnId ? wsNodes.find((n) => (n.node_id || n.nodeId) === wsOwnId) : null;
|
|
727
|
+
const wsActive = wsNodes.filter((n) => n.status === 'active');
|
|
728
|
+
const wsSnap = {
|
|
729
|
+
t: Date.now(),
|
|
730
|
+
globalSessions: nsData.activeSessions || 0,
|
|
731
|
+
mySessions: wsOwn?.active_sessions ?? wsOwn?.sessions ?? 0,
|
|
732
|
+
nodeCount: wsActive.length,
|
|
733
|
+
avgLoad: wsActive.length > 0 ? wsActive.reduce((s, n) => s + (n.load || 0), 0) / wsActive.length : 0,
|
|
734
|
+
myLoad: wsOwn?.load ?? 0,
|
|
735
|
+
};
|
|
736
|
+
let wsSnapshots = [...get().networkSnapshots, wsSnap];
|
|
737
|
+
if (wsSnapshots.length > 100) wsSnapshots = wsSnapshots.slice(-100);
|
|
738
|
+
set({ networkSnapshots: wsSnapshots });
|
|
739
|
+
|
|
715
740
|
break;
|
|
716
741
|
}
|
|
717
742
|
|
|
@@ -842,7 +867,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
842
867
|
arr.push({ from: 'assistant', text, timestamp: Date.now() });
|
|
843
868
|
}
|
|
844
869
|
msgs[conversationId] = arr.slice(-200);
|
|
845
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
846
870
|
return { conversationMessages: msgs, streamingConversationId: conversationId };
|
|
847
871
|
});
|
|
848
872
|
break;
|
|
@@ -850,10 +874,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
850
874
|
|
|
851
875
|
case 'conversation:complete': {
|
|
852
876
|
const { conversationId } = msg.data || msg;
|
|
853
|
-
if (conversationId) {
|
|
877
|
+
if (conversationId && get().streamingConversationId === conversationId) {
|
|
854
878
|
set({ sendingMessage: false, streamingConversationId: null });
|
|
855
|
-
persistJSON('groove:conversationMessages', get().conversationMessages);
|
|
856
879
|
}
|
|
880
|
+
if (conversationId) persistJSON('groove:conversationMessages', get().conversationMessages);
|
|
857
881
|
break;
|
|
858
882
|
}
|
|
859
883
|
|
|
@@ -865,7 +889,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
865
889
|
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
866
890
|
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Error: ${error || 'Unknown error'}`, timestamp: Date.now() }];
|
|
867
891
|
persistJSON('groove:conversationMessages', msgs);
|
|
868
|
-
|
|
892
|
+
const isActive = s.streamingConversationId === conversationId;
|
|
893
|
+
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
869
894
|
});
|
|
870
895
|
}
|
|
871
896
|
break;
|
|
@@ -1052,6 +1077,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1052
1077
|
installUpdate() {
|
|
1053
1078
|
window.groove?.update?.installUpdate();
|
|
1054
1079
|
},
|
|
1080
|
+
setUpdateModalOpen(open) {
|
|
1081
|
+
set({ updateModalOpen: open });
|
|
1082
|
+
},
|
|
1083
|
+
checkForUpdate() {
|
|
1084
|
+
window.groove?.update?.checkForUpdate();
|
|
1085
|
+
},
|
|
1055
1086
|
|
|
1056
1087
|
// ── Marketplace Auth ────────────────────────────────────────
|
|
1057
1088
|
|
|
@@ -1699,6 +1730,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1699
1730
|
}
|
|
1700
1731
|
},
|
|
1701
1732
|
|
|
1733
|
+
async setConversationModel(id, provider, model) {
|
|
1734
|
+
try {
|
|
1735
|
+
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { provider, model });
|
|
1736
|
+
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
get().addToast('error', 'Model change failed', err.message);
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
|
|
1702
1742
|
async stopChatStreaming(conversationId) {
|
|
1703
1743
|
try {
|
|
1704
1744
|
await api.post(`/conversations/${encodeURIComponent(conversationId)}/stop`);
|
|
@@ -2106,6 +2146,26 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2106
2146
|
};
|
|
2107
2147
|
}
|
|
2108
2148
|
set(update);
|
|
2149
|
+
|
|
2150
|
+
// Push snapshot for activity chart
|
|
2151
|
+
if (data) {
|
|
2152
|
+
const ownId = get().networkNode.nodeId;
|
|
2153
|
+
const nodes = data.nodes || [];
|
|
2154
|
+
const ownNode = ownId ? nodes.find((n) => (n.node_id || n.nodeId) === ownId) : null;
|
|
2155
|
+
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
2156
|
+
const snap = {
|
|
2157
|
+
t: Date.now(),
|
|
2158
|
+
globalSessions: data.activeSessions || 0,
|
|
2159
|
+
mySessions: ownNode?.active_sessions ?? ownNode?.sessions ?? 0,
|
|
2160
|
+
nodeCount: activeNodes.length,
|
|
2161
|
+
avgLoad: activeNodes.length > 0 ? activeNodes.reduce((s, n) => s + (n.load || 0), 0) / activeNodes.length : 0,
|
|
2162
|
+
myLoad: ownNode?.load ?? 0,
|
|
2163
|
+
};
|
|
2164
|
+
let snapshots = [...get().networkSnapshots, snap];
|
|
2165
|
+
if (snapshots.length > 100) snapshots = snapshots.slice(-100);
|
|
2166
|
+
set({ networkSnapshots: snapshots });
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2109
2169
|
return data;
|
|
2110
2170
|
} catch {
|
|
2111
2171
|
set({ networkStatusReachable: false });
|