groove-dev 0.27.57 → 0.27.59
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 +126 -7
- package/node_modules/@groove-dev/daemon/src/conversations.js +2 -5
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +1 -0
- package/{packages/gui/dist/assets/index-X58BAjGp.js → node_modules/@groove-dev/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
- 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-header.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +49 -24
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +9 -36
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/activity-stream.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +166 -0
- package/node_modules/@groove-dev/gui/src/components/network/fleet-table.jsx +190 -0
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +135 -0
- package/node_modules/@groove-dev/gui/src/components/network/node-toggle.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -4
- package/node_modules/@groove-dev/gui/src/views/network.jsx +128 -55
- 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 +126 -7
- package/packages/daemon/src/conversations.js +2 -5
- package/packages/daemon/src/providers/groove-network.js +1 -1
- package/packages/gui/dist/assets/index-BrfCzrxJ.css +1 -0
- package/{node_modules/@groove-dev/gui/dist/assets/index-X58BAjGp.js → packages/gui/dist/assets/index-BycOlqLx.js} +1742 -1742
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/chat/chat-header.jsx +1 -1
- package/packages/gui/src/components/chat/chat-input.jsx +1 -1
- package/packages/gui/src/components/chat/chat-messages.jsx +49 -24
- package/packages/gui/src/components/chat/chat-view.jsx +9 -36
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -2
- package/packages/gui/src/components/network/activity-stream.jsx +105 -0
- package/packages/gui/src/components/network/compute-header.jsx +166 -0
- package/packages/gui/src/components/network/fleet-table.jsx +190 -0
- package/packages/gui/src/components/network/network-health.jsx +135 -0
- package/packages/gui/src/components/network/node-toggle.jsx +1 -1
- package/packages/gui/src/stores/groove.js +57 -4
- package/packages/gui/src/views/network.jsx +128 -55
- package/ai-chat/CHAT_MASTER_PLAN.md +0 -184
- package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +0 -1
- package/packages/gui/dist/assets/index-C5WTeZO4.css +0 -1
|
@@ -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-BycOlqLx.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-BrfCzrxJ.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -50,7 +50,7 @@ export function ChatHeader({ conversation, model, onModelChange, onModeChange })
|
|
|
50
50
|
const mode = conversation.mode || 'api';
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<div className="h-11 flex items-center gap-3 px-4 border-b border-border bg-surface-
|
|
53
|
+
<div className="h-11 flex items-center gap-3 px-4 border-b border-border-subtle bg-surface-0/80 flex-shrink-0">
|
|
54
54
|
<Hash size={14} className="text-text-4 flex-shrink-0" />
|
|
55
55
|
|
|
56
56
|
{editing ? (
|
|
@@ -53,7 +53,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled }) {
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="px-4 py-3">
|
|
56
|
-
<div className="flex items-end gap-2 rounded-2xl bg-surface-
|
|
56
|
+
<div className="flex items-end gap-2 rounded-2xl bg-surface-1/80 border border-accent/8 px-3 py-2 focus-within:border-accent/30 transition-colors">
|
|
57
57
|
<input
|
|
58
58
|
ref={fileInputRef}
|
|
59
59
|
type="file"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useRef, useEffect, useState } from 'react';
|
|
3
|
-
import { Copy, Check, ArrowRight
|
|
3
|
+
import { Copy, Check, ArrowRight } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { timeAgo } from '../../lib/format';
|
|
6
6
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
7
7
|
|
|
8
|
+
const API_STATUS_MESSAGES = [
|
|
9
|
+
'Generating response...',
|
|
10
|
+
'Processing...',
|
|
11
|
+
'Thinking...',
|
|
12
|
+
'Almost there...',
|
|
13
|
+
];
|
|
14
|
+
|
|
8
15
|
function CopyButton({ text }) {
|
|
9
16
|
const [copied, setCopied] = useState(false);
|
|
10
17
|
function handleCopy() {
|
|
@@ -242,7 +249,7 @@ function UserMessage({ msg }) {
|
|
|
242
249
|
return (
|
|
243
250
|
<div className="flex justify-end">
|
|
244
251
|
<div className="max-w-[75%]">
|
|
245
|
-
<div className="px-4 py-3 rounded-
|
|
252
|
+
<div className="px-4 py-3 rounded-xl bg-surface-3/80 border border-border-subtle">
|
|
246
253
|
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
|
|
247
254
|
</div>
|
|
248
255
|
<div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
|
|
@@ -253,17 +260,12 @@ function UserMessage({ msg }) {
|
|
|
253
260
|
|
|
254
261
|
function AssistantMessage({ msg, model }) {
|
|
255
262
|
return (
|
|
256
|
-
<div className="
|
|
257
|
-
<div className="
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<div className="flex-1 min-w-0 max-w-[85%]">
|
|
261
|
-
{model && <div className="text-2xs text-text-3 font-sans mb-1 font-medium">{model}</div>}
|
|
262
|
-
<div className="px-4 py-3 rounded-2xl rounded-bl-md bg-surface-4 border border-border-subtle">
|
|
263
|
-
<RenderedMarkdown text={msg.text} />
|
|
264
|
-
</div>
|
|
265
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
263
|
+
<div className="max-w-[85%]">
|
|
264
|
+
{model && <div className="text-2xs text-text-3 font-sans mb-1 font-medium">{model}</div>}
|
|
265
|
+
<div className="border-l-2 border-accent/30 pl-3.5">
|
|
266
|
+
<RenderedMarkdown text={msg.text} />
|
|
266
267
|
</div>
|
|
268
|
+
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
267
269
|
</div>
|
|
268
270
|
);
|
|
269
271
|
}
|
|
@@ -288,18 +290,42 @@ function StreamingCursor() {
|
|
|
288
290
|
function WelcomeMessage() {
|
|
289
291
|
return (
|
|
290
292
|
<div className="flex flex-col items-center justify-center h-full text-center py-16">
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
+
<p className="text-sm text-text-3 font-sans">Send a message to start</p>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function ApiTypingIndicator() {
|
|
299
|
+
const [idx, setIdx] = useState(0);
|
|
300
|
+
const [fade, setFade] = useState(true);
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
const t = setInterval(() => {
|
|
304
|
+
setFade(false);
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
setIdx((i) => (i + 1) % API_STATUS_MESSAGES.length);
|
|
307
|
+
setFade(true);
|
|
308
|
+
}, 250);
|
|
309
|
+
}, 2800);
|
|
310
|
+
return () => clearInterval(t);
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div className="border-l-2 border-accent/30 pl-3.5 py-1 flex items-center gap-2.5">
|
|
315
|
+
<div className="relative w-3.5 h-3.5 flex-shrink-0">
|
|
316
|
+
<span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
|
|
293
317
|
</div>
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
<span
|
|
319
|
+
className="text-[12px] font-sans text-text-3 transition-opacity duration-[250ms]"
|
|
320
|
+
style={{ opacity: fade ? 1 : 0 }}
|
|
321
|
+
>
|
|
322
|
+
{API_STATUS_MESSAGES[idx]}
|
|
323
|
+
</span>
|
|
298
324
|
</div>
|
|
299
325
|
);
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
export function ChatMessages({ messages, isStreaming, model }) {
|
|
328
|
+
export function ChatMessages({ messages, isStreaming, model, mode }) {
|
|
303
329
|
const scrollRef = useRef(null);
|
|
304
330
|
const isAtBottomRef = useRef(true);
|
|
305
331
|
|
|
@@ -335,12 +361,11 @@ export function ChatMessages({ messages, isStreaming, model }) {
|
|
|
335
361
|
return <AssistantMessage key={i} msg={msg} model={model} />;
|
|
336
362
|
})}
|
|
337
363
|
{isStreaming && (
|
|
338
|
-
|
|
339
|
-
<div className="w-7 h-7 rounded-full bg-surface-4 border border-border-subtle flex items-center justify-center flex-shrink-0">
|
|
340
|
-
<Sparkles size={13} className="text-accent" />
|
|
341
|
-
</div>
|
|
364
|
+
mode === 'agent' ? (
|
|
342
365
|
<ThinkingIndicator className="py-1" />
|
|
343
|
-
|
|
366
|
+
) : (
|
|
367
|
+
<ApiTypingIndicator />
|
|
368
|
+
)
|
|
344
369
|
)}
|
|
345
370
|
</div>
|
|
346
371
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Plus } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { ConversationList } from './conversation-list';
|
|
@@ -11,44 +11,16 @@ import { ChatInput } from './chat-input';
|
|
|
11
11
|
function EmptyState({ onNewChat }) {
|
|
12
12
|
return (
|
|
13
13
|
<div className="flex-1 flex items-center justify-center">
|
|
14
|
-
<div className="
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
<div className="absolute inset-1 rounded-full bg-surface-3 border border-border-subtle flex items-center justify-center shadow-lg shadow-accent/5">
|
|
18
|
-
<MessageCircle size={32} className="text-accent" />
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div className="space-y-3">
|
|
23
|
-
<h1 className="text-2xl font-bold text-text-0 font-sans tracking-tight">Groove Chat</h1>
|
|
24
|
-
<p className="text-sm text-text-2 font-sans max-w-sm mx-auto leading-relaxed">
|
|
25
|
-
A command center disguised as a conversation. Every provider, every model, full project context.
|
|
26
|
-
</p>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
14
|
+
<div className="text-center space-y-4">
|
|
15
|
+
<h1 className="text-lg font-semibold text-text-1 font-sans">Groove Chat</h1>
|
|
16
|
+
<p className="text-sm text-text-3 font-sans">Every provider, every model, full project context.</p>
|
|
29
17
|
<button
|
|
30
18
|
onClick={onNewChat}
|
|
31
|
-
className="inline-flex items-center gap-2 h-
|
|
19
|
+
className="inline-flex items-center gap-2 h-9 px-5 rounded-lg bg-accent/15 text-accent text-sm font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
|
|
32
20
|
>
|
|
33
|
-
<Plus size={
|
|
21
|
+
<Plus size={14} />
|
|
34
22
|
New Chat
|
|
35
23
|
</button>
|
|
36
|
-
|
|
37
|
-
<div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
|
|
38
|
-
<div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
|
|
39
|
-
<Sparkles size={14} className="text-purple flex-shrink-0" />
|
|
40
|
-
<span className="text-2xs text-text-2 font-sans">Multi-model routing</span>
|
|
41
|
-
</div>
|
|
42
|
-
<div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
|
|
43
|
-
<Zap size={14} className="text-warning flex-shrink-0" />
|
|
44
|
-
<span className="text-2xs text-text-2 font-sans">Streaming responses</span>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<p className="text-xs text-text-4 font-sans">
|
|
49
|
-
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+Shift+N</kbd>
|
|
50
|
-
<span className="mx-1.5">new chat</span>
|
|
51
|
-
</p>
|
|
52
24
|
</div>
|
|
53
25
|
</div>
|
|
54
26
|
);
|
|
@@ -113,10 +85,10 @@ export function ChatView() {
|
|
|
113
85
|
: null;
|
|
114
86
|
|
|
115
87
|
return (
|
|
116
|
-
<div className="flex h-full bg-surface-
|
|
88
|
+
<div className="flex h-full bg-surface-0">
|
|
117
89
|
{/* Conversation sidebar */}
|
|
118
90
|
<div className={cn(
|
|
119
|
-
'flex-shrink-0 border-r border-
|
|
91
|
+
'flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
|
|
120
92
|
sidebarCollapsed ? 'w-0' : 'w-64',
|
|
121
93
|
)}>
|
|
122
94
|
<ConversationList onNewChat={() => handleNewChat()} />
|
|
@@ -131,6 +103,7 @@ export function ChatView() {
|
|
|
131
103
|
messages={messages}
|
|
132
104
|
isStreaming={isStreaming}
|
|
133
105
|
model={activeConversation.model}
|
|
106
|
+
mode={activeConversation.mode || 'api'}
|
|
134
107
|
/>
|
|
135
108
|
<ChatInput
|
|
136
109
|
onSend={handleSend}
|
|
@@ -7,13 +7,13 @@ import { isElectron, getPlatform } from '../../lib/electron';
|
|
|
7
7
|
|
|
8
8
|
const BASE_NAV_ITEMS = [
|
|
9
9
|
{ id: 'agents', icon: Network, label: 'Agents' },
|
|
10
|
+
{ id: 'chat', icon: MessageCircle, label: 'Chat' },
|
|
10
11
|
{ id: 'editor', icon: Code2, label: 'Editor' },
|
|
11
12
|
{ id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
|
|
13
|
+
{ id: 'teams', icon: Users, label: 'Teams' },
|
|
12
14
|
{ id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
|
|
13
15
|
{ id: 'toys', icon: Gamepad2, label: 'Toys' },
|
|
14
16
|
{ id: 'models', icon: Box, label: 'Models' },
|
|
15
|
-
{ id: 'teams', icon: Users, label: 'Teams' },
|
|
16
|
-
{ id: 'chat', icon: MessageCircle, label: 'Chat' },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo, useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { HEX } from '../../lib/theme-hex';
|
|
6
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
7
|
+
|
|
8
|
+
const FILTERS = ['All', 'Sessions', 'Errors', 'Connections'];
|
|
9
|
+
|
|
10
|
+
const LEVEL_FILTER = {
|
|
11
|
+
Sessions: ['session'],
|
|
12
|
+
Errors: ['error', 'warning'],
|
|
13
|
+
Connections: ['connected', 'disconnected'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LEVEL_COLOR = {
|
|
17
|
+
info: HEX.text3,
|
|
18
|
+
success: HEX.success,
|
|
19
|
+
warning: HEX.warning,
|
|
20
|
+
error: HEX.danger,
|
|
21
|
+
connected: HEX.success,
|
|
22
|
+
disconnected: HEX.warning,
|
|
23
|
+
session: HEX.accent,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function fmtTime(ts) {
|
|
27
|
+
if (!ts) return '--:--:--';
|
|
28
|
+
const d = new Date(ts);
|
|
29
|
+
return d.toLocaleTimeString('en-GB', { hour12: false });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function levelTag(level) {
|
|
33
|
+
const tags = {
|
|
34
|
+
info: 'info', success: ' ok ', warning: 'warn',
|
|
35
|
+
error: 'ERR!', connected: 'conn', disconnected: 'disc', session: 'sess',
|
|
36
|
+
};
|
|
37
|
+
return tags[level] || level || 'info';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ActivityStream = memo(function ActivityStream() {
|
|
41
|
+
const events = useGrooveStore((s) => s.networkEvents);
|
|
42
|
+
const [filter, setFilter] = useState('All');
|
|
43
|
+
const bottomRef = useRef(null);
|
|
44
|
+
|
|
45
|
+
const filtered = filter === 'All'
|
|
46
|
+
? events
|
|
47
|
+
: events.filter((ev) => {
|
|
48
|
+
const level = ev.level || ev.type || 'info';
|
|
49
|
+
return (LEVEL_FILTER[filter] || []).includes(level);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const display = filtered.slice(-200);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
56
|
+
}, [display.length]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-col h-full">
|
|
60
|
+
<div className="flex items-center gap-2 px-3 pt-2.5 pb-1 flex-shrink-0">
|
|
61
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Activity</span>
|
|
62
|
+
<div className="flex-1" />
|
|
63
|
+
<div className="flex items-center gap-0.5">
|
|
64
|
+
{FILTERS.map((f) => (
|
|
65
|
+
<button
|
|
66
|
+
key={f}
|
|
67
|
+
onClick={() => setFilter(f)}
|
|
68
|
+
className={cn(
|
|
69
|
+
'px-2 py-0.5 text-2xs font-mono rounded-sm transition-colors cursor-pointer',
|
|
70
|
+
filter === f
|
|
71
|
+
? 'bg-[rgba(51,175,188,0.15)] text-accent'
|
|
72
|
+
: 'bg-surface-4 text-text-3 hover:text-text-2',
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{f}
|
|
76
|
+
</button>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
82
|
+
{display.length === 0 ? (
|
|
83
|
+
<div className="px-3 py-6 text-2xs font-mono text-text-4 text-center">
|
|
84
|
+
No events yet — toggle your node on to start.
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<div className="px-2 py-1">
|
|
88
|
+
{display.map((ev, i) => {
|
|
89
|
+
const level = ev.level || ev.type || 'info';
|
|
90
|
+
const color = LEVEL_COLOR[level] || HEX.text3;
|
|
91
|
+
return (
|
|
92
|
+
<div key={i} className="flex items-start gap-0 font-mono text-2xs leading-relaxed">
|
|
93
|
+
<span className="text-text-4 flex-shrink-0 w-[62px]">[{fmtTime(ev.timestamp || ev.ts)}]</span>
|
|
94
|
+
<span className="flex-shrink-0 w-[36px]" style={{ color }}>{levelTag(level)}</span>
|
|
95
|
+
<span className="text-text-2 break-words min-w-0">{ev.msg || ev.message || ev.text || 'event'}</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
<div ref={bottomRef} />
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</ScrollArea>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { HEX } from '../../lib/theme-hex';
|
|
6
|
+
import { Tooltip } from '../ui/tooltip';
|
|
7
|
+
import { HelpCircle } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
const BAR_WIDTH = 28;
|
|
10
|
+
|
|
11
|
+
function gaugeColor(ratio) {
|
|
12
|
+
if (ratio > 0.9) return HEX.danger;
|
|
13
|
+
if (ratio > 0.7) return HEX.warning;
|
|
14
|
+
return HEX.success;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fmtMbToGb(mb) {
|
|
18
|
+
if (!mb) return '0';
|
|
19
|
+
return (mb / 1024).toFixed(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function AsciiBar({ label, value, max, unit, nodeCount }) {
|
|
23
|
+
const ratio = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0;
|
|
24
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
25
|
+
const empty = BAR_WIDTH - filled;
|
|
26
|
+
const bar = '\u2502'.repeat(filled) + '\u2500'.repeat(empty);
|
|
27
|
+
const color = gaugeColor(ratio);
|
|
28
|
+
|
|
29
|
+
let displayVal, displayMax;
|
|
30
|
+
if (unit === 'GB') {
|
|
31
|
+
displayVal = fmtMbToGb(value);
|
|
32
|
+
displayMax = fmtMbToGb(max);
|
|
33
|
+
} else if (unit === 'cores' || unit === 'Mbps') {
|
|
34
|
+
displayVal = Math.round(value);
|
|
35
|
+
displayMax = Math.round(max);
|
|
36
|
+
} else {
|
|
37
|
+
displayVal = value.toFixed(1);
|
|
38
|
+
displayMax = max.toFixed(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center gap-2 font-mono text-xs leading-tight">
|
|
43
|
+
<span className="w-[40px] text-right text-text-3 uppercase text-2xs tracking-wider flex-shrink-0">
|
|
44
|
+
{label}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="text-text-4">[</span>
|
|
47
|
+
<span style={{ color: ratio > 0 ? color : undefined }} className={cn('whitespace-pre', !ratio && 'text-text-4')}>
|
|
48
|
+
{bar}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-text-4">]</span>
|
|
51
|
+
<span className="text-text-1 tabular-nums whitespace-nowrap text-2xs">
|
|
52
|
+
{displayVal} / {displayMax} {unit}
|
|
53
|
+
</span>
|
|
54
|
+
{nodeCount != null && (
|
|
55
|
+
<span className="text-text-4 text-2xs whitespace-nowrap">({nodeCount} nodes)</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
|
|
62
|
+
if (!data || data.length < 2) return <div style={{ width, height }} />;
|
|
63
|
+
const vals = data.map((d) => (typeof d === 'number' ? d : d.v));
|
|
64
|
+
const min = Math.min(...vals);
|
|
65
|
+
const max = Math.max(...vals);
|
|
66
|
+
const range = max - min || 1;
|
|
67
|
+
const points = vals.map((v, i) => {
|
|
68
|
+
const x = (i / (vals.length - 1)) * width;
|
|
69
|
+
const y = height - ((v - min) / range) * (height - 2) - 1;
|
|
70
|
+
return `${x},${y}`;
|
|
71
|
+
}).join(' ');
|
|
72
|
+
const gradId = `net-${color.replace('#', '')}`;
|
|
73
|
+
return (
|
|
74
|
+
<svg width={width} height={height} className="flex-shrink-0">
|
|
75
|
+
<defs>
|
|
76
|
+
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
77
|
+
<stop offset="0%" stopColor={color} stopOpacity="0.2" />
|
|
78
|
+
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
|
79
|
+
</linearGradient>
|
|
80
|
+
</defs>
|
|
81
|
+
<polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#${gradId})`} />
|
|
82
|
+
<polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeOpacity="0.8" />
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function KpiCard({ label, value, color = HEX.accent, hint, className }) {
|
|
88
|
+
return (
|
|
89
|
+
<div className={cn('flex items-center gap-2.5 px-3 py-2.5 min-w-0 bg-surface-1', className)}>
|
|
90
|
+
<div className="flex-1 min-w-0">
|
|
91
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5 truncate flex items-center gap-1">
|
|
92
|
+
{label}
|
|
93
|
+
{hint && (
|
|
94
|
+
<Tooltip content={<span className="max-w-[220px] block leading-relaxed">{hint}</span>} side="bottom">
|
|
95
|
+
<HelpCircle size={10} className="text-text-4 hover:text-text-2 cursor-help flex-shrink-0 transition-colors" />
|
|
96
|
+
</Tooltip>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="text-base font-semibold font-mono text-text-0 tabular-nums leading-none">{value}</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MAX_RAM_MB = 256 * 1024;
|
|
106
|
+
const MAX_VRAM_MB = 128 * 1024;
|
|
107
|
+
const MAX_CPU = 128;
|
|
108
|
+
const MAX_LOAD = 4.0;
|
|
109
|
+
|
|
110
|
+
export const ComputeHeader = memo(function ComputeHeader() {
|
|
111
|
+
const compute = useGrooveStore((s) => s.networkCompute);
|
|
112
|
+
const nodes = useGrooveStore((s) => s.networkStatus.nodes || []);
|
|
113
|
+
const models = useGrooveStore((s) => s.networkStatus.models || []);
|
|
114
|
+
const allZero = !compute.totalRamMb && !compute.totalVramMb && !compute.totalCpuCores;
|
|
115
|
+
|
|
116
|
+
const activeNodes = nodes.filter((n) => n.status === 'active');
|
|
117
|
+
const avgGpuUtil = activeNodes.length > 0
|
|
118
|
+
? activeNodes.reduce((s, n) => s + (n.gpu_utilization_pct || 0), 0) / activeNodes.length
|
|
119
|
+
: 0;
|
|
120
|
+
const gpuColor = avgGpuUtil > 80 ? HEX.danger : avgGpuUtil > 50 ? HEX.warning : HEX.success;
|
|
121
|
+
const loadColor = compute.avgLoad > 2.0 ? HEX.danger : compute.avgLoad > 1.0 ? HEX.warning : HEX.success;
|
|
122
|
+
const activeModel = models.length > 0
|
|
123
|
+
? (typeof models[0] === 'string' ? models[0] : models[0].name)
|
|
124
|
+
: 'google/gemma-3-4b';
|
|
125
|
+
|
|
126
|
+
const kpis = [
|
|
127
|
+
{ label: 'RAM', value: `${fmtMbToGb(compute.totalRamMb)} GB`, color: HEX.accent, hint: 'Total RAM across all network nodes.' },
|
|
128
|
+
{ label: 'VRAM', value: `${fmtMbToGb(compute.totalVramMb)} GB`, color: HEX.info, hint: 'Total GPU VRAM across all network nodes.' },
|
|
129
|
+
{ label: 'CPU Cores', value: `${compute.totalCpuCores}`, color: HEX.purple, hint: 'Total CPU cores across all network nodes.' },
|
|
130
|
+
{ label: 'GPU Util', value: avgGpuUtil > 0 ? `${Math.round(avgGpuUtil)}%` : '--', color: gpuColor, hint: 'Average GPU utilization across active nodes. Green <50%, yellow 50-80%, red >80%.' },
|
|
131
|
+
{ label: 'Nodes', value: `${compute.activeNodes}/${compute.totalNodes}`, color: HEX.accent, hint: 'Active nodes out of total registered.' },
|
|
132
|
+
{ label: 'Load', value: compute.avgLoad > 0 ? compute.avgLoad.toFixed(2) : '0.00', color: loadColor, hint: 'Average load across active nodes. Green <1.0, yellow 1.0-2.0, red >2.0.' },
|
|
133
|
+
{ label: 'Model', value: activeModel, color: HEX.info, hint: 'Active inference model on the network.' },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex-shrink-0">
|
|
138
|
+
<div className="flex flex-wrap border-b border-border" style={{ background: 'var(--color-surface-0)' }}>
|
|
139
|
+
{kpis.map((kpi) => (
|
|
140
|
+
<KpiCard
|
|
141
|
+
key={kpi.label}
|
|
142
|
+
label={kpi.label}
|
|
143
|
+
value={kpi.value}
|
|
144
|
+
color={kpi.color}
|
|
145
|
+
hint={kpi.hint}
|
|
146
|
+
className={cn('flex-1 basis-[14.2%] min-w-[110px]', 'border-b border-r border-border')}
|
|
147
|
+
/>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="bg-surface-1 border-b border-border px-4 py-2.5">
|
|
152
|
+
{allZero ? (
|
|
153
|
+
<div className="text-2xs font-mono text-text-4">Waiting for network data...</div>
|
|
154
|
+
) : (
|
|
155
|
+
<div className="flex flex-col gap-0.5">
|
|
156
|
+
<AsciiBar label="RAM" value={compute.totalRamMb} max={MAX_RAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
157
|
+
<AsciiBar label="VRAM" value={compute.totalVramMb} max={MAX_VRAM_MB} unit="GB" nodeCount={compute.totalNodes} />
|
|
158
|
+
<AsciiBar label="CPU" value={compute.totalCpuCores} max={MAX_CPU} unit="cores" />
|
|
159
|
+
<AsciiBar label="GPU%" value={avgGpuUtil} max={100} unit="%" />
|
|
160
|
+
<AsciiBar label="LOAD" value={compute.avgLoad} max={MAX_LOAD} unit="" />
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
});
|