groove-dev 0.27.142 → 0.27.144
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 +1086 -6532
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.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/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- 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 +1086 -6532
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +889 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +24 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +34 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +452 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +227 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +17 -6
- package/packages/gui/src/views/models.jsx +410 -509
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- package/test.py +0 -571
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { api } from '../../lib/api';
|
|
4
|
+
import { loadJSON, persistJSON } from '../helpers.js';
|
|
5
|
+
|
|
6
|
+
export const createProvidersSlice = (set, get) => ({
|
|
7
|
+
// ── Providers ────────────────────────────────────────────
|
|
8
|
+
_providerRefreshTick: 0,
|
|
9
|
+
|
|
10
|
+
// ── Local Models (Ollama) ─────────────────────────────────
|
|
11
|
+
ollamaStatus: { installed: false, serverRunning: false, hardware: null },
|
|
12
|
+
ollamaInstalledModels: [],
|
|
13
|
+
ollamaRunningModels: [],
|
|
14
|
+
ollamaCatalog: [],
|
|
15
|
+
ollamaPullProgress: {},
|
|
16
|
+
|
|
17
|
+
// ── Provider Setup (Settings) ──────────────────────────────
|
|
18
|
+
providerInstallProgress: {},
|
|
19
|
+
|
|
20
|
+
// ── Model Lab ──────────────────────────────────────────────
|
|
21
|
+
labRuntimes: loadJSON('groove:labRuntimes', []),
|
|
22
|
+
labActiveRuntime: null,
|
|
23
|
+
labModels: [],
|
|
24
|
+
labActiveModel: null,
|
|
25
|
+
labPresets: loadJSON('groove:labPresets', []),
|
|
26
|
+
labActivePreset: null,
|
|
27
|
+
labSessions: [],
|
|
28
|
+
labActiveSession: null,
|
|
29
|
+
labMetrics: {
|
|
30
|
+
ttft: null, tokensPerSec: null, tokensPerSecHistory: [], ttftHistory: [],
|
|
31
|
+
memory: null, peakMemory: null, totalTokens: 0, promptTokens: 0, completionTokens: 0,
|
|
32
|
+
generationTime: null, generationCount: 0, sessionStartTime: null,
|
|
33
|
+
},
|
|
34
|
+
labParameters: loadJSON('groove:labParameters', {
|
|
35
|
+
temperature: 0.7, topP: 0.9, topK: 40, minP: 0, repeatPenalty: 1.1,
|
|
36
|
+
maxTokens: 2048, frequencyPenalty: 0, presencePenalty: 0,
|
|
37
|
+
thinking: false, seed: null, stopSequences: [], jsonMode: false,
|
|
38
|
+
}),
|
|
39
|
+
labSystemPrompt: localStorage.getItem('groove:labSystemPrompt') || '',
|
|
40
|
+
labStreaming: false,
|
|
41
|
+
labAbortController: null,
|
|
42
|
+
labLocalModels: [],
|
|
43
|
+
labLaunching: null,
|
|
44
|
+
labLlamaInstalled: null,
|
|
45
|
+
labLaunchPhase: null,
|
|
46
|
+
labLaunchError: null,
|
|
47
|
+
labAssistantAgentId: localStorage.getItem('groove:labAssistantAgentId') || null,
|
|
48
|
+
labAssistantMode: false,
|
|
49
|
+
labAssistantBackend: localStorage.getItem('groove:labAssistantBackend') || null,
|
|
50
|
+
|
|
51
|
+
// ── Provider Actions ──────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async fetchProviders() {
|
|
54
|
+
return api.get('/providers');
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ── Local Models (Ollama) ─────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async fetchOllamaStatus() {
|
|
60
|
+
try {
|
|
61
|
+
const check = await api.post('/providers/ollama/check');
|
|
62
|
+
const updates = {
|
|
63
|
+
ollamaStatus: { installed: check.installed, serverRunning: check.serverRunning, hardware: check.hardware },
|
|
64
|
+
};
|
|
65
|
+
if (check.installed) {
|
|
66
|
+
try {
|
|
67
|
+
const models = await api.get('/providers/ollama/models');
|
|
68
|
+
updates.ollamaInstalledModels = models.installed || [];
|
|
69
|
+
updates.ollamaCatalog = models.catalog || [];
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
if (check.serverRunning) {
|
|
73
|
+
try {
|
|
74
|
+
const running = await api.get('/providers/ollama/running');
|
|
75
|
+
updates.ollamaRunningModels = running.models || [];
|
|
76
|
+
} catch {
|
|
77
|
+
updates.ollamaRunningModels = [];
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
updates.ollamaRunningModels = [];
|
|
81
|
+
}
|
|
82
|
+
set(updates);
|
|
83
|
+
return updates.ollamaStatus;
|
|
84
|
+
} catch {
|
|
85
|
+
return get().ollamaStatus;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async startOllamaServer() {
|
|
90
|
+
try {
|
|
91
|
+
const result = await api.post('/providers/ollama/serve');
|
|
92
|
+
if (result.ok) {
|
|
93
|
+
get().addToast('success', 'Ollama server started');
|
|
94
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
95
|
+
await get().fetchOllamaStatus();
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
get().addToast('error', 'Could not start server', err.message);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async stopOllamaServer() {
|
|
105
|
+
try {
|
|
106
|
+
const result = await api.post('/providers/ollama/stop');
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
get().addToast('info', 'Ollama server stopped');
|
|
109
|
+
set((s) => ({
|
|
110
|
+
ollamaStatus: { ...s.ollamaStatus, serverRunning: false },
|
|
111
|
+
ollamaRunningModels: [],
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
get().addToast('error', 'Stop failed', err.message);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async restartOllamaServer() {
|
|
122
|
+
try {
|
|
123
|
+
const result = await api.post('/providers/ollama/restart');
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
get().addToast('success', 'Ollama server restarted');
|
|
126
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
127
|
+
await get().fetchOllamaStatus();
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
get().addToast('error', 'Restart failed', err.message);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async pullOllamaModel(modelId) {
|
|
137
|
+
try {
|
|
138
|
+
set((s) => ({ ollamaPullProgress: { ...s.ollamaPullProgress, [modelId]: { status: 'pulling', progress: '' } } }));
|
|
139
|
+
await api.post('/providers/ollama/pull', { model: modelId });
|
|
140
|
+
set((s) => {
|
|
141
|
+
const progress = { ...s.ollamaPullProgress };
|
|
142
|
+
delete progress[modelId];
|
|
143
|
+
return { ollamaPullProgress: progress };
|
|
144
|
+
});
|
|
145
|
+
get().addToast('success', `${modelId} ready to use`);
|
|
146
|
+
get().fetchOllamaStatus();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
set((s) => {
|
|
149
|
+
const progress = { ...s.ollamaPullProgress };
|
|
150
|
+
delete progress[modelId];
|
|
151
|
+
return { ollamaPullProgress: progress };
|
|
152
|
+
});
|
|
153
|
+
get().addToast('error', `Pull failed: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async deleteOllamaModel(modelId) {
|
|
158
|
+
try {
|
|
159
|
+
await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
|
|
160
|
+
set((s) => ({ ollamaInstalledModels: s.ollamaInstalledModels.filter((m) => m.id !== modelId) }));
|
|
161
|
+
get().addToast('success', `Removed ${modelId}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
get().addToast('error', `Delete failed: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async loadOllamaModel(modelId) {
|
|
168
|
+
try {
|
|
169
|
+
await api.post('/providers/ollama/load', { model: modelId });
|
|
170
|
+
get().addToast('success', `${modelId} loaded into memory`);
|
|
171
|
+
get().fetchOllamaStatus();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
get().addToast('error', `Could not load model: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async unloadOllamaModel(modelId) {
|
|
178
|
+
try {
|
|
179
|
+
await api.post('/providers/ollama/unload', { model: modelId });
|
|
180
|
+
set((s) => ({ ollamaRunningModels: s.ollamaRunningModels.filter((m) => m.name !== modelId) }));
|
|
181
|
+
get().addToast('info', `${modelId} unloaded`);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
get().addToast('error', `Unload failed: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
spawnFromModel(modelId) {
|
|
188
|
+
get().openDetail({ type: 'spawn', presetProvider: 'ollama', presetModel: modelId });
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// ── Onboarding ────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
async fetchOnboardingStatus() {
|
|
194
|
+
try {
|
|
195
|
+
const data = await api.get('/onboarding/status');
|
|
196
|
+
if (data?.complete) {
|
|
197
|
+
set({ onboardingComplete: true });
|
|
198
|
+
localStorage.setItem('groove:onboardingComplete', 'true');
|
|
199
|
+
}
|
|
200
|
+
return data;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
dismissOnboarding() {
|
|
207
|
+
set({ onboardingComplete: true });
|
|
208
|
+
localStorage.setItem('groove:onboardingComplete', 'true');
|
|
209
|
+
api.post('/onboarding/dismiss').catch(() => {});
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// ── Provider Setup (Settings) ──────────────────────────────
|
|
213
|
+
|
|
214
|
+
async installProvider(providerId) {
|
|
215
|
+
const update = (patch) => set((s) => ({
|
|
216
|
+
providerInstallProgress: {
|
|
217
|
+
...s.providerInstallProgress,
|
|
218
|
+
[providerId]: { ...s.providerInstallProgress[providerId], ...patch },
|
|
219
|
+
},
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
update({ installing: true, percent: 0, message: 'Starting install...', error: null, done: false });
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch(`/api/providers/${encodeURIComponent(providerId)}/install`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
const err = await res.text();
|
|
231
|
+
throw new Error(err || `Install failed (${res.status})`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let body;
|
|
235
|
+
try {
|
|
236
|
+
body = await res.text();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
throw new Error(`Failed to read response: ${e.message}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let lastError = null;
|
|
242
|
+
let completed = false;
|
|
243
|
+
for (const line of body.split('\n')) {
|
|
244
|
+
if (!line.trim()) continue;
|
|
245
|
+
try {
|
|
246
|
+
const ev = JSON.parse(line);
|
|
247
|
+
const isError = ev.status === 'error';
|
|
248
|
+
const isDone = ev.status === 'complete';
|
|
249
|
+
if (isError) lastError = ev.output || 'Install failed';
|
|
250
|
+
if (isDone) completed = true;
|
|
251
|
+
update({
|
|
252
|
+
percent: ev.progress ?? get().providerInstallProgress[providerId]?.percent ?? 0,
|
|
253
|
+
message: ev.output || get().providerInstallProgress[providerId]?.message,
|
|
254
|
+
error: isError ? (ev.output || 'Install failed') : null,
|
|
255
|
+
done: isDone,
|
|
256
|
+
installing: !isDone && !isError,
|
|
257
|
+
});
|
|
258
|
+
} catch { /* skip malformed line */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (lastError) throw new Error(lastError);
|
|
262
|
+
if (!completed) throw new Error(body.slice(0, 500) || 'Install ended without confirmation');
|
|
263
|
+
|
|
264
|
+
update({ installing: false, percent: 100, message: 'Installed', error: null, done: true });
|
|
265
|
+
set({ _providerRefreshTick: Date.now() });
|
|
266
|
+
get().addToast('success', `${providerId} installed`);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
update({ installing: false, percent: 0, message: null, error: err.message, done: false });
|
|
269
|
+
get().addToast('error', `Install failed: ${providerId}`, err.message);
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async loginProvider(providerId, body) {
|
|
275
|
+
try {
|
|
276
|
+
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/login`, body);
|
|
277
|
+
if (data?.url && !data?.browserOpened) window.open(data.url, '_blank');
|
|
278
|
+
return data;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
get().addToast('error', `Login failed`, err.message);
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async setProviderPath(providerId, path) {
|
|
286
|
+
try {
|
|
287
|
+
await api.post(`/providers/${encodeURIComponent(providerId)}/set-path`, { path });
|
|
288
|
+
get().addToast('success', `Custom path set for ${providerId}`);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
get().addToast('error', 'Failed to set path', err.message);
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async verifyProvider(providerId) {
|
|
296
|
+
try {
|
|
297
|
+
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/verify`);
|
|
298
|
+
return data;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
get().addToast('error', `Verification failed`, err.message);
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async setDefaultProvider(provider, model) {
|
|
306
|
+
try {
|
|
307
|
+
await api.post('/onboarding/set-default', { provider, model });
|
|
308
|
+
get().addToast('success', `Default set to ${provider} (${model})`);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
get().addToast('error', 'Failed to set default', err.message);
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
// ── Model Lab Actions ──────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
setLabParameter(key, value) {
|
|
318
|
+
const params = { ...get().labParameters, [key]: value };
|
|
319
|
+
set({ labParameters: params });
|
|
320
|
+
persistJSON('groove:labParameters', params);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
setLabSystemPrompt(text) {
|
|
324
|
+
set({ labSystemPrompt: text });
|
|
325
|
+
localStorage.setItem('groove:labSystemPrompt', text);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
async fetchLabRuntimes() {
|
|
329
|
+
try {
|
|
330
|
+
const raw = await api.get('/lab/runtimes');
|
|
331
|
+
const data = raw.map((rt) => ({
|
|
332
|
+
...rt,
|
|
333
|
+
status: rt.online === true ? 'connected' : rt.online === false ? 'error' : rt.status,
|
|
334
|
+
}));
|
|
335
|
+
set({ labRuntimes: data });
|
|
336
|
+
persistJSON('groove:labRuntimes', data);
|
|
337
|
+
if (data.length > 0 && !get().labActiveRuntime) {
|
|
338
|
+
get().setLabActiveRuntime(data[0].id);
|
|
339
|
+
} else if (get().labActiveRuntime) {
|
|
340
|
+
get().fetchLabModels(get().labActiveRuntime);
|
|
341
|
+
}
|
|
342
|
+
} catch { /* backend may not have lab endpoints yet */ }
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
async fetchLabLocalModels() {
|
|
346
|
+
try {
|
|
347
|
+
const data = await api.get('/lab/local-models');
|
|
348
|
+
set({ labLocalModels: data });
|
|
349
|
+
} catch { set({ labLocalModels: [] }); }
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
async checkLlamaStatus() {
|
|
353
|
+
try {
|
|
354
|
+
const data = await api.get('/llama/status');
|
|
355
|
+
set({ labLlamaInstalled: !!data.installed });
|
|
356
|
+
} catch { set({ labLlamaInstalled: false }); }
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async launchLocalModel(modelId) {
|
|
360
|
+
set({ labLaunching: modelId, labLaunchPhase: 'starting', labLaunchError: null });
|
|
361
|
+
try {
|
|
362
|
+
const result = await api.post('/lab/launch-local', { modelId });
|
|
363
|
+
const raw = await api.get('/lab/runtimes');
|
|
364
|
+
const runtimes = raw.map((rt) => ({
|
|
365
|
+
...rt,
|
|
366
|
+
status: rt.online === true ? 'connected' : rt.online === false ? 'error' : rt.status,
|
|
367
|
+
}));
|
|
368
|
+
set({ labRuntimes: runtimes });
|
|
369
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
370
|
+
get().setLabActiveRuntime(result.runtime.id);
|
|
371
|
+
set({ labActiveModel: result.model, labLaunching: null, labLaunchPhase: 'ready' });
|
|
372
|
+
get().addToast('success', `Launched ${result.model}`);
|
|
373
|
+
setTimeout(() => { if (get().labLaunchPhase === 'ready') set({ labLaunchPhase: null }); }, 3000);
|
|
374
|
+
return result;
|
|
375
|
+
} catch (err) {
|
|
376
|
+
set({ labLaunching: null, labLaunchPhase: 'error', labLaunchError: err.message });
|
|
377
|
+
get().addToast('error', 'Failed to launch model', err.message);
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
async addLabRuntime(runtime) {
|
|
383
|
+
try {
|
|
384
|
+
const created = await api.post('/lab/runtimes', runtime);
|
|
385
|
+
const runtimes = [...get().labRuntimes, created];
|
|
386
|
+
set({ labRuntimes: runtimes });
|
|
387
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
388
|
+
get().setLabActiveRuntime(created.id);
|
|
389
|
+
get().addToast('success', `Runtime "${runtime.name}" added`);
|
|
390
|
+
return created;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
get().addToast('error', 'Failed to add runtime', err.message);
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async startLabRuntime(id) {
|
|
398
|
+
try {
|
|
399
|
+
get().addToast('info', 'Starting server...');
|
|
400
|
+
await api.post(`/lab/runtimes/${id}/start`);
|
|
401
|
+
await get().fetchLabRuntimes();
|
|
402
|
+
get().setLabActiveRuntime(id);
|
|
403
|
+
get().addToast('success', 'Server started');
|
|
404
|
+
} catch (err) {
|
|
405
|
+
get().addToast('error', 'Failed to start server', err.message);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
async stopLabRuntime(id) {
|
|
410
|
+
try {
|
|
411
|
+
await api.post(`/lab/runtimes/${id}/stop`);
|
|
412
|
+
const runtimes = get().labRuntimes.map((r) =>
|
|
413
|
+
r.id === id ? { ...r, status: 'error', latency: null } : r,
|
|
414
|
+
);
|
|
415
|
+
set({ labRuntimes: runtimes });
|
|
416
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
417
|
+
get().addToast('success', 'Server stopped');
|
|
418
|
+
} catch (err) {
|
|
419
|
+
get().addToast('error', 'Failed to stop server', err.message);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
async removeLabRuntime(id) {
|
|
424
|
+
try {
|
|
425
|
+
await api.delete(`/lab/runtimes/${id}`);
|
|
426
|
+
const runtimes = get().labRuntimes.filter((r) => r.id !== id);
|
|
427
|
+
const active = get().labActiveRuntime === id ? null : get().labActiveRuntime;
|
|
428
|
+
set({ labRuntimes: runtimes, labActiveRuntime: active, labModels: active ? get().labModels : [] });
|
|
429
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
430
|
+
get().addToast('success', 'Runtime removed');
|
|
431
|
+
} catch (err) {
|
|
432
|
+
get().addToast('error', 'Failed to remove runtime', err.message);
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
async testLabRuntime(id) {
|
|
437
|
+
try {
|
|
438
|
+
const result = await api.post(`/lab/runtimes/${id}/test`);
|
|
439
|
+
const runtimes = get().labRuntimes.map((r) =>
|
|
440
|
+
r.id === id ? { ...r, status: result.ok ? 'connected' : 'error', latency: result.latency } : r,
|
|
441
|
+
);
|
|
442
|
+
const updates = { labRuntimes: runtimes };
|
|
443
|
+
if (result.ok && result.models && get().labActiveRuntime === id) {
|
|
444
|
+
updates.labModels = result.models;
|
|
445
|
+
}
|
|
446
|
+
set(updates);
|
|
447
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
448
|
+
return result;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const runtimes = get().labRuntimes.map((r) =>
|
|
451
|
+
r.id === id ? { ...r, status: 'error' } : r,
|
|
452
|
+
);
|
|
453
|
+
set({ labRuntimes: runtimes });
|
|
454
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
455
|
+
return { ok: false, error: err.message };
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
setLabActiveRuntime(id) {
|
|
460
|
+
set({ labActiveRuntime: id, labModels: [], labActiveModel: null });
|
|
461
|
+
if (id) get().fetchLabModels(id);
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
setLabActiveModel(model) {
|
|
465
|
+
set({ labActiveModel: model });
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
async fetchLabModels(runtimeId) {
|
|
469
|
+
try {
|
|
470
|
+
const data = await api.get(`/lab/runtimes/${runtimeId}/models`);
|
|
471
|
+
const updates = { labModels: data };
|
|
472
|
+
if (data.length === 1 && !get().labActiveModel) {
|
|
473
|
+
updates.labActiveModel = data[0].id || data[0].name;
|
|
474
|
+
}
|
|
475
|
+
set(updates);
|
|
476
|
+
} catch { set({ labModels: [] }); }
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
newLabSession() {
|
|
480
|
+
const id = `lab-${Date.now()}`;
|
|
481
|
+
const session = { id, messages: [], createdAt: Date.now() };
|
|
482
|
+
set((s) => ({
|
|
483
|
+
labSessions: [session, ...s.labSessions],
|
|
484
|
+
labActiveSession: id,
|
|
485
|
+
labMetrics: {
|
|
486
|
+
ttft: null, tokensPerSec: null, tokensPerSecHistory: [], ttftHistory: [],
|
|
487
|
+
memory: null, peakMemory: null, totalTokens: 0, promptTokens: 0, completionTokens: 0,
|
|
488
|
+
generationTime: null, generationCount: 0, sessionStartTime: null,
|
|
489
|
+
},
|
|
490
|
+
}));
|
|
491
|
+
return id;
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
loadLabSession(id) {
|
|
495
|
+
set({ labActiveSession: id });
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
async sendLabMessage(text) {
|
|
499
|
+
const st = get();
|
|
500
|
+
if (st.labStreaming) return;
|
|
501
|
+
let sessionId = st.labActiveSession;
|
|
502
|
+
if (!sessionId) sessionId = get().newLabSession();
|
|
503
|
+
|
|
504
|
+
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
505
|
+
set((s) => {
|
|
506
|
+
const sessions = s.labSessions.map((sess) =>
|
|
507
|
+
sess.id === sessionId ? { ...sess, messages: [...sess.messages, userMsg] } : sess,
|
|
508
|
+
);
|
|
509
|
+
return { labSessions: sessions, labStreaming: true };
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const assistantMsg = { role: 'assistant', content: '', timestamp: Date.now(), metrics: null };
|
|
513
|
+
set((s) => {
|
|
514
|
+
const sessions = s.labSessions.map((sess) =>
|
|
515
|
+
sess.id === sessionId ? { ...sess, messages: [...sess.messages, assistantMsg] } : sess,
|
|
516
|
+
);
|
|
517
|
+
return { labSessions: sessions };
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const abortController = new AbortController();
|
|
521
|
+
set({ labAbortController: abortController });
|
|
522
|
+
|
|
523
|
+
const startTime = performance.now();
|
|
524
|
+
let firstTokenTime = null;
|
|
525
|
+
let tokenCount = 0;
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const p = st.labParameters;
|
|
529
|
+
const parameters = {};
|
|
530
|
+
if (p.temperature !== undefined) parameters.temperature = p.temperature;
|
|
531
|
+
if (p.topP !== undefined) parameters.top_p = p.topP;
|
|
532
|
+
if (p.topK !== undefined) parameters.top_k = p.topK;
|
|
533
|
+
if (p.minP !== undefined && p.minP > 0) parameters.min_p = p.minP;
|
|
534
|
+
if (p.repeatPenalty !== undefined) parameters.repeat_penalty = p.repeatPenalty;
|
|
535
|
+
if (p.maxTokens !== undefined) parameters.max_tokens = p.maxTokens;
|
|
536
|
+
if (p.frequencyPenalty !== undefined) parameters.frequency_penalty = p.frequencyPenalty;
|
|
537
|
+
if (p.presencePenalty !== undefined) parameters.presence_penalty = p.presencePenalty;
|
|
538
|
+
parameters.enable_thinking = !!p.thinking;
|
|
539
|
+
if (p.seed != null) parameters.seed = p.seed;
|
|
540
|
+
if (p.stopSequences?.length) parameters.stop = p.stopSequences;
|
|
541
|
+
if (p.jsonMode) parameters.response_format = { type: 'json_object' };
|
|
542
|
+
|
|
543
|
+
const messages = [];
|
|
544
|
+
if (st.labSystemPrompt) messages.push({ role: 'system', content: st.labSystemPrompt });
|
|
545
|
+
const sessionMsgs = get().labSessions.find((s) => s.id === sessionId)?.messages || [];
|
|
546
|
+
for (const m of sessionMsgs) {
|
|
547
|
+
if (m.role === 'assistant' && !m.content) continue;
|
|
548
|
+
messages.push({ role: m.role, content: m.content });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const body = {
|
|
552
|
+
runtimeId: st.labActiveRuntime,
|
|
553
|
+
model: st.labActiveModel,
|
|
554
|
+
messages,
|
|
555
|
+
parameters,
|
|
556
|
+
sessionId,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const res = await fetch('/api/lab/inference', {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: { 'Content-Type': 'application/json' },
|
|
562
|
+
body: JSON.stringify(body),
|
|
563
|
+
signal: abortController.signal,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (!res.ok) {
|
|
567
|
+
let errMsg;
|
|
568
|
+
try { errMsg = (await res.json()).error || `HTTP ${res.status}`; } catch { errMsg = `HTTP ${res.status}`; }
|
|
569
|
+
throw new Error(errMsg);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const reader = res.body.getReader();
|
|
573
|
+
const decoder = new TextDecoder();
|
|
574
|
+
let buffer = '';
|
|
575
|
+
let fullContent = '';
|
|
576
|
+
let fullReasoning = '';
|
|
577
|
+
|
|
578
|
+
while (true) {
|
|
579
|
+
const { done, value } = await reader.read();
|
|
580
|
+
if (done) break;
|
|
581
|
+
buffer += decoder.decode(value, { stream: true });
|
|
582
|
+
const lines = buffer.split('\n');
|
|
583
|
+
buffer = lines.pop() || '';
|
|
584
|
+
|
|
585
|
+
for (const line of lines) {
|
|
586
|
+
if (!line.startsWith('data: ')) continue;
|
|
587
|
+
const payload = line.slice(6);
|
|
588
|
+
if (payload === '[DONE]') continue;
|
|
589
|
+
try {
|
|
590
|
+
const parsed = JSON.parse(payload);
|
|
591
|
+
|
|
592
|
+
// Support both raw OpenAI format (piped) and legacy wrapper format
|
|
593
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
594
|
+
const reasoningText = delta?.reasoning_content || delta?.reasoning || (parsed.type === 'reasoning' ? parsed.content : null);
|
|
595
|
+
let contentText = delta?.content || (parsed.type === 'token' ? parsed.content : null);
|
|
596
|
+
|
|
597
|
+
// Raw fallback: if no known field matched, extract any string from delta
|
|
598
|
+
if (!reasoningText && !contentText && delta) {
|
|
599
|
+
for (const v of Object.values(delta)) {
|
|
600
|
+
if (typeof v === 'string' && v) { contentText = v; break; }
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Last resort: if the parsed object itself has text but no choices wrapper
|
|
604
|
+
if (!reasoningText && !contentText && !delta && parsed.response) {
|
|
605
|
+
contentText = typeof parsed.response === 'string' ? parsed.response : null;
|
|
606
|
+
}
|
|
607
|
+
if (!reasoningText && !contentText && !delta && typeof parsed.text === 'string') {
|
|
608
|
+
contentText = parsed.text;
|
|
609
|
+
}
|
|
610
|
+
if (!reasoningText && !contentText && !delta && typeof parsed.output === 'string') {
|
|
611
|
+
contentText = parsed.output;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (reasoningText) {
|
|
615
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
616
|
+
tokenCount++;
|
|
617
|
+
fullReasoning += reasoningText;
|
|
618
|
+
set((s) => {
|
|
619
|
+
const sessions = s.labSessions.map((sess) => {
|
|
620
|
+
if (sess.id !== sessionId) return sess;
|
|
621
|
+
const msgs = [...sess.messages];
|
|
622
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], reasoning: fullReasoning };
|
|
623
|
+
return { ...sess, messages: msgs };
|
|
624
|
+
});
|
|
625
|
+
return { labSessions: sessions };
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (contentText) {
|
|
629
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
630
|
+
tokenCount++;
|
|
631
|
+
fullContent += contentText;
|
|
632
|
+
set((s) => {
|
|
633
|
+
const sessions = s.labSessions.map((sess) => {
|
|
634
|
+
if (sess.id !== sessionId) return sess;
|
|
635
|
+
const msgs = [...sess.messages];
|
|
636
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: fullContent };
|
|
637
|
+
return { ...sess, messages: msgs };
|
|
638
|
+
});
|
|
639
|
+
return { labSessions: sessions };
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Handle done event (legacy wrapper) or finish_reason (raw OpenAI)
|
|
644
|
+
if (parsed.type === 'done' && parsed.metrics) {
|
|
645
|
+
const elapsed = performance.now() - startTime;
|
|
646
|
+
const ttft = firstTokenTime ? firstTokenTime - startTime : null;
|
|
647
|
+
const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
|
|
648
|
+
const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...parsed.metrics };
|
|
649
|
+
|
|
650
|
+
set((s) => {
|
|
651
|
+
const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-20);
|
|
652
|
+
const ttftHist = [...s.labMetrics.ttftHistory, ttft].filter((v) => v != null).slice(-20);
|
|
653
|
+
const mem = parsed.metrics.memoryUsage || s.labMetrics.memory;
|
|
654
|
+
const sessions = s.labSessions.map((sess) => {
|
|
655
|
+
if (sess.id !== sessionId) return sess;
|
|
656
|
+
const msgs = [...sess.messages];
|
|
657
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], metrics: msgMetrics };
|
|
658
|
+
return { ...sess, messages: msgs };
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
labSessions: sessions,
|
|
662
|
+
labMetrics: {
|
|
663
|
+
...s.labMetrics,
|
|
664
|
+
ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist, ttftHistory: ttftHist,
|
|
665
|
+
memory: mem, peakMemory: Math.max(mem || 0, s.labMetrics.peakMemory || 0) || null,
|
|
666
|
+
totalTokens: s.labMetrics.totalTokens + (parsed.metrics.totalTokens || tokenCount),
|
|
667
|
+
promptTokens: s.labMetrics.promptTokens + (parsed.metrics.promptTokens || 0),
|
|
668
|
+
completionTokens: s.labMetrics.completionTokens + (parsed.metrics.completionTokens || tokenCount),
|
|
669
|
+
generationTime: parsed.metrics.generationTime || elapsed,
|
|
670
|
+
generationCount: s.labMetrics.generationCount + 1,
|
|
671
|
+
sessionStartTime: s.labMetrics.sessionStartTime || Date.now(),
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (parsed.type === 'error') {
|
|
677
|
+
throw new Error(parsed.error || 'Inference error');
|
|
678
|
+
}
|
|
679
|
+
} catch (e) {
|
|
680
|
+
if (e.message && e.message !== 'Inference error' && !e.message.startsWith('HTTP ')) continue;
|
|
681
|
+
throw e;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Strip <think> tags from content — some models embed reasoning in content field
|
|
687
|
+
if (fullContent && fullContent.includes('<think>')) {
|
|
688
|
+
const stripped = fullContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
689
|
+
const thinkMatch = fullContent.match(/<think>([\s\S]*?)<\/think>/);
|
|
690
|
+
if (thinkMatch && !fullReasoning) fullReasoning = thinkMatch[1].trim();
|
|
691
|
+
// If stripping left nothing, check for unclosed <think> (model still mid-thought)
|
|
692
|
+
if (stripped) {
|
|
693
|
+
fullContent = stripped;
|
|
694
|
+
} else if (fullContent.includes('<think>') && !fullContent.includes('</think>')) {
|
|
695
|
+
const afterTag = fullContent.replace(/<think>/, '').trim();
|
|
696
|
+
if (!fullReasoning) fullReasoning = afterTag;
|
|
697
|
+
fullContent = '';
|
|
698
|
+
}
|
|
699
|
+
set((s) => {
|
|
700
|
+
const sessions = s.labSessions.map((sess) => {
|
|
701
|
+
if (sess.id !== sessionId) return sess;
|
|
702
|
+
const msgs = [...sess.messages];
|
|
703
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: fullContent || undefined, reasoning: fullReasoning || undefined };
|
|
704
|
+
return { ...sess, messages: msgs };
|
|
705
|
+
});
|
|
706
|
+
return { labSessions: sessions };
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// If stream ended with reasoning but no content, promote reasoning as the response
|
|
711
|
+
if (fullReasoning && !fullContent) {
|
|
712
|
+
fullContent = fullReasoning;
|
|
713
|
+
set((s) => {
|
|
714
|
+
const sessions = s.labSessions.map((sess) => {
|
|
715
|
+
if (sess.id !== sessionId) return sess;
|
|
716
|
+
const msgs = [...sess.messages];
|
|
717
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: fullContent };
|
|
718
|
+
return { ...sess, messages: msgs };
|
|
719
|
+
});
|
|
720
|
+
return { labSessions: sessions };
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// If stream ended with zero content at all, surface a fallback message
|
|
725
|
+
if (!fullContent && !fullReasoning) {
|
|
726
|
+
fullContent = '[Model returned an empty response — try a different prompt or check server logs]';
|
|
727
|
+
set((s) => {
|
|
728
|
+
const sessions = s.labSessions.map((sess) => {
|
|
729
|
+
if (sess.id !== sessionId) return sess;
|
|
730
|
+
const msgs = [...sess.messages];
|
|
731
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: fullContent };
|
|
732
|
+
return { ...sess, messages: msgs };
|
|
733
|
+
});
|
|
734
|
+
return { labSessions: sessions };
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Compute final metrics from client-side timing
|
|
739
|
+
const elapsed = performance.now() - startTime;
|
|
740
|
+
const ttft = firstTokenTime ? firstTokenTime - startTime : null;
|
|
741
|
+
const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
|
|
742
|
+
if (tokenCount > 0) {
|
|
743
|
+
set((s) => {
|
|
744
|
+
const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-20);
|
|
745
|
+
const ttftHist = [...s.labMetrics.ttftHistory, ttft].filter((v) => v != null).slice(-20);
|
|
746
|
+
const sessions = s.labSessions.map((sess) => {
|
|
747
|
+
if (sess.id !== sessionId) return sess;
|
|
748
|
+
const msgs = [...sess.messages];
|
|
749
|
+
const last = msgs[msgs.length - 1];
|
|
750
|
+
if (!last?.metrics) {
|
|
751
|
+
msgs[msgs.length - 1] = { ...last, metrics: { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed } };
|
|
752
|
+
}
|
|
753
|
+
return { ...sess, messages: msgs };
|
|
754
|
+
});
|
|
755
|
+
return {
|
|
756
|
+
labSessions: sessions,
|
|
757
|
+
labMetrics: {
|
|
758
|
+
...s.labMetrics, ttft, tokensPerSec: tps,
|
|
759
|
+
tokensPerSecHistory: tpsHist, ttftHistory: ttftHist,
|
|
760
|
+
totalTokens: s.labMetrics.totalTokens + tokenCount,
|
|
761
|
+
completionTokens: s.labMetrics.completionTokens + tokenCount,
|
|
762
|
+
generationTime: elapsed,
|
|
763
|
+
generationCount: s.labMetrics.generationCount + 1,
|
|
764
|
+
sessionStartTime: s.labMetrics.sessionStartTime || Date.now(),
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
} catch (err) {
|
|
770
|
+
if (err.name === 'AbortError') {
|
|
771
|
+
// User cancelled — keep whatever content was already streamed
|
|
772
|
+
} else {
|
|
773
|
+
set((s) => {
|
|
774
|
+
const sessions = s.labSessions.map((sess) => {
|
|
775
|
+
if (sess.id !== sessionId) return sess;
|
|
776
|
+
const msgs = [...sess.messages];
|
|
777
|
+
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: `Error: ${err.message}`, error: true };
|
|
778
|
+
return { ...sess, messages: msgs };
|
|
779
|
+
});
|
|
780
|
+
return { labSessions: sessions };
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
} finally {
|
|
784
|
+
set({ labStreaming: false, labAbortController: null });
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
stopLabInference() {
|
|
789
|
+
const ctrl = get().labAbortController;
|
|
790
|
+
if (ctrl) ctrl.abort();
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
saveLabPreset(name) {
|
|
794
|
+
const st = get();
|
|
795
|
+
const preset = {
|
|
796
|
+
id: `preset-${Date.now()}`,
|
|
797
|
+
name,
|
|
798
|
+
parameters: { ...st.labParameters },
|
|
799
|
+
systemPrompt: st.labSystemPrompt,
|
|
800
|
+
runtimeId: st.labActiveRuntime,
|
|
801
|
+
model: st.labActiveModel,
|
|
802
|
+
createdAt: Date.now(),
|
|
803
|
+
};
|
|
804
|
+
const presets = [...st.labPresets.filter((p) => p.name !== name), preset];
|
|
805
|
+
set({ labPresets: presets, labActivePreset: preset.id });
|
|
806
|
+
persistJSON('groove:labPresets', presets);
|
|
807
|
+
get().addToast('success', `Preset "${name}" saved`);
|
|
808
|
+
return preset;
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
loadLabPreset(id) {
|
|
812
|
+
const preset = get().labPresets.find((p) => p.id === id);
|
|
813
|
+
if (!preset) return;
|
|
814
|
+
const defaults = {
|
|
815
|
+
temperature: 0.7, topP: 0.9, topK: 40, minP: 0, repeatPenalty: 1.1,
|
|
816
|
+
maxTokens: 2048, frequencyPenalty: 0, presencePenalty: 0,
|
|
817
|
+
thinking: false, seed: null, stopSequences: [], jsonMode: false,
|
|
818
|
+
};
|
|
819
|
+
const merged = { ...defaults, ...preset.parameters };
|
|
820
|
+
const updates = {
|
|
821
|
+
labParameters: merged,
|
|
822
|
+
labSystemPrompt: preset.systemPrompt || '',
|
|
823
|
+
labActivePreset: id,
|
|
824
|
+
};
|
|
825
|
+
if (preset.model) updates.labActiveModel = preset.model;
|
|
826
|
+
set(updates);
|
|
827
|
+
persistJSON('groove:labParameters', merged);
|
|
828
|
+
if (preset.systemPrompt !== undefined) localStorage.setItem('groove:labSystemPrompt', preset.systemPrompt);
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
deleteLabPreset(id) {
|
|
832
|
+
const presets = get().labPresets.filter((p) => p.id !== id);
|
|
833
|
+
set({ labPresets: presets, labActivePreset: get().labActivePreset === id ? null : get().labActivePreset });
|
|
834
|
+
persistJSON('groove:labPresets', presets);
|
|
835
|
+
get().addToast('success', 'Preset deleted');
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
async launchLabAssistant(backend, model) {
|
|
839
|
+
const existing = get().labAssistantAgentId;
|
|
840
|
+
if (existing) {
|
|
841
|
+
const agent = get().agents.find((a) => a.id === existing);
|
|
842
|
+
if (agent && agent.status === 'running') {
|
|
843
|
+
set({ labAssistantMode: true });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const body = { backend };
|
|
849
|
+
if (model) body.model = { id: model.id, filename: model.filename, parameters: model.parameters, quantization: model.quantization };
|
|
850
|
+
const data = await api.post('/lab/assistant', body);
|
|
851
|
+
localStorage.setItem('groove:labAssistantAgentId', data.agentId);
|
|
852
|
+
localStorage.setItem('groove:labAssistantBackend', backend);
|
|
853
|
+
set({ labAssistantAgentId: data.agentId, labAssistantMode: true, labAssistantBackend: backend });
|
|
854
|
+
get().addToast('info', `Lab Assistant started for ${backend}`);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
get().addToast('error', 'Failed to start assistant', err.message);
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
dismissLabAssistant() {
|
|
861
|
+
set({ labAssistantMode: false });
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
clearLabAssistant() {
|
|
865
|
+
const id = get().labAssistantAgentId;
|
|
866
|
+
if (id) api.delete(`/agents/${encodeURIComponent(id)}`).catch(() => {});
|
|
867
|
+
localStorage.removeItem('groove:labAssistantAgentId');
|
|
868
|
+
localStorage.removeItem('groove:labAssistantBackend');
|
|
869
|
+
set({ labAssistantAgentId: null, labAssistantMode: false, labAssistantBackend: null });
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
setLabAssistantMode(mode) {
|
|
873
|
+
set({ labAssistantMode: mode });
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
async onLabAssistantComplete() {
|
|
877
|
+
const prevIds = new Set(get().labRuntimes.map((r) => r.id));
|
|
878
|
+
try {
|
|
879
|
+
const raw = await api.get('/lab/runtimes');
|
|
880
|
+
const data = raw.map((rt) => ({
|
|
881
|
+
...rt,
|
|
882
|
+
status: rt.online === true ? 'connected' : rt.online === false ? 'error' : rt.status,
|
|
883
|
+
}));
|
|
884
|
+
set({ labRuntimes: data });
|
|
885
|
+
persistJSON('groove:labRuntimes', data);
|
|
886
|
+
const newRuntime = data.find((r) => !prevIds.has(r.id));
|
|
887
|
+
if (newRuntime) {
|
|
888
|
+
set({ labActiveRuntime: newRuntime.id, labModels: [], labActiveModel: null });
|
|
889
|
+
try {
|
|
890
|
+
const models = await api.get(`/lab/runtimes/${newRuntime.id}/models`);
|
|
891
|
+
set({ labModels: models });
|
|
892
|
+
if (models.length > 0) set({ labActiveModel: models[0].id || models[0].name });
|
|
893
|
+
} catch { /* models may not be available yet */ }
|
|
894
|
+
}
|
|
895
|
+
} catch { /* ignore */ }
|
|
896
|
+
},
|
|
897
|
+
});
|