groove-dev 0.27.75 → 0.27.78

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.
Files changed (97) hide show
  1. package/MOE_TRAINING_PIPELINE.md +216 -12
  2. package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
  3. package/moe-training/client/consent.js +96 -0
  4. package/moe-training/client/envelope-builder.js +56 -0
  5. package/moe-training/client/index.js +10 -0
  6. package/moe-training/client/parsers/claude-code.js +110 -0
  7. package/moe-training/client/parsers/codex.js +80 -0
  8. package/moe-training/client/parsers/gemini.js +80 -0
  9. package/moe-training/client/parsers/grok.js +16 -0
  10. package/moe-training/client/parsers/index.js +20 -0
  11. package/moe-training/client/scrubber.js +126 -0
  12. package/moe-training/client/session-attestation.js +114 -0
  13. package/moe-training/client/step-classifier.js +51 -0
  14. package/moe-training/client/trajectory-capture.js +227 -0
  15. package/moe-training/client/transmission-queue.js +93 -0
  16. package/moe-training/package-lock.json +1266 -0
  17. package/moe-training/package.json +20 -0
  18. package/moe-training/server/enrichment.js +24 -0
  19. package/moe-training/server/index.js +119 -0
  20. package/moe-training/server/ledger.js +110 -0
  21. package/moe-training/server/routes/ingest.js +96 -0
  22. package/moe-training/server/routes/sessions.js +43 -0
  23. package/moe-training/server/routes/stats.js +31 -0
  24. package/moe-training/server/scoring.js +63 -0
  25. package/moe-training/server/session-registry.js +156 -0
  26. package/moe-training/server/stats.js +129 -0
  27. package/moe-training/server/stitcher.js +69 -0
  28. package/moe-training/server/storage.js +147 -0
  29. package/moe-training/server/verifier.js +102 -0
  30. package/moe-training/shared/constants.js +30 -0
  31. package/moe-training/shared/crypto.js +45 -0
  32. package/moe-training/shared/envelope-schema.js +220 -0
  33. package/moe-training/test/client/consent.test.js +121 -0
  34. package/moe-training/test/client/envelope-builder.test.js +107 -0
  35. package/moe-training/test/client/parsers/claude-code.test.js +119 -0
  36. package/moe-training/test/client/parsers/codex.test.js +83 -0
  37. package/moe-training/test/client/parsers/gemini.test.js +99 -0
  38. package/moe-training/test/client/scrubber.test.js +133 -0
  39. package/moe-training/test/client/session-attestation-security.test.js +95 -0
  40. package/moe-training/test/client/step-classifier.test.js +88 -0
  41. package/moe-training/test/integration/handshake.test.js +260 -0
  42. package/moe-training/test/server/ingest-security.test.js +166 -0
  43. package/moe-training/test/server/ledger.test.js +131 -0
  44. package/moe-training/test/server/scoring.test.js +242 -0
  45. package/moe-training/test/server/session-registry.test.js +125 -0
  46. package/moe-training/test/server/stitcher.test.js +157 -0
  47. package/moe-training/test/server/verifier.test.js +232 -0
  48. package/moe-training/test/shared/crypto.test.js +87 -0
  49. package/moe-training/test/shared/envelope-schema.test.js +351 -0
  50. package/node_modules/@groove-dev/cli/package.json +1 -1
  51. package/node_modules/@groove-dev/daemon/package.json +1 -1
  52. package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
  53. package/node_modules/@groove-dev/daemon/src/api.js +77 -0
  54. package/node_modules/@groove-dev/daemon/src/index.js +61 -0
  55. package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
  56. package/node_modules/@groove-dev/daemon/src/preview.js +14 -0
  57. package/node_modules/@groove-dev/daemon/src/process.js +203 -1
  58. package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
  59. package/node_modules/@groove-dev/daemon/src/state.js +20 -1
  60. package/node_modules/@groove-dev/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
  61. package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +1 -0
  62. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  63. package/node_modules/@groove-dev/gui/package.json +1 -1
  64. package/node_modules/@groove-dev/gui/src/app.css +12 -0
  65. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +32 -27
  66. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +26 -24
  67. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +34 -6
  68. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +19 -4
  69. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  70. package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
  71. package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
  72. package/package.json +1 -1
  73. package/packages/cli/package.json +1 -1
  74. package/packages/daemon/package.json +1 -1
  75. package/packages/daemon/src/agent-loop.js +48 -5
  76. package/packages/daemon/src/api.js +77 -0
  77. package/packages/daemon/src/index.js +61 -0
  78. package/packages/daemon/src/journalist.js +64 -21
  79. package/packages/daemon/src/preview.js +14 -0
  80. package/packages/daemon/src/process.js +203 -1
  81. package/packages/daemon/src/providers/grok.js +15 -0
  82. package/packages/daemon/src/state.js +20 -1
  83. package/packages/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
  84. package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
  85. package/packages/gui/dist/index.html +2 -2
  86. package/packages/gui/package.json +1 -1
  87. package/packages/gui/src/app.css +12 -0
  88. package/packages/gui/src/components/chat/chat-input.jsx +32 -27
  89. package/packages/gui/src/components/chat/chat-messages.jsx +26 -24
  90. package/packages/gui/src/components/preview/preview-toolbar.jsx +34 -6
  91. package/packages/gui/src/components/preview/preview-workspace.jsx +19 -4
  92. package/packages/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  93. package/packages/gui/src/stores/groove.js +32 -0
  94. package/packages/gui/src/views/settings.jsx +167 -1
  95. package/welcome.png +0 -0
  96. package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +0 -1
  97. package/packages/gui/dist/assets/index-CVzz6zyb.css +0 -1
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
- import { Send, Paperclip, MonitorX, Loader2 } from 'lucide-react';
3
+ import { Send, Paperclip, MonitorX, Loader2, MessageCircle, Camera, RefreshCw } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { timeAgo } from '../../lib/format';
@@ -137,9 +137,24 @@ function PreviewChat() {
137
137
  <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
138
138
  {previewChat.length === 0 && (
139
139
  <div className="flex items-center justify-center h-full">
140
- <p className="text-sm text-text-3 font-sans text-center">
141
- Describe changes and they'll be routed to the team planner
142
- </p>
140
+ <div className="max-w-xs w-full px-5 py-5 bg-surface-1 border border-border-subtle rounded-xl text-center">
141
+ <MessageCircle size={24} className="mx-auto text-accent mb-3" />
142
+ <h3 className="text-sm font-semibold text-text-0 font-sans mb-3">Preview is live!</h3>
143
+ <ul className="text-left space-y-2 text-2xs text-text-2 font-sans">
144
+ <li className="flex gap-2">
145
+ <Send size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
146
+ <span>Type a message to request changes — your feedback goes to the team planner who routes it to the right agent</span>
147
+ </li>
148
+ <li className="flex gap-2">
149
+ <Camera size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
150
+ <span>Use the camera icon to screenshot a specific area and annotate it</span>
151
+ </li>
152
+ <li className="flex gap-2">
153
+ <RefreshCw size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
154
+ <span>Changes auto-refresh via hot module reload</span>
155
+ </li>
156
+ </ul>
157
+ </div>
143
158
  </div>
144
159
  )}
145
160
  {previewChat.map((msg, i) => (
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
- import { Send, X } from 'lucide-react';
3
+ import { Send, X, Loader2 } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
 
6
6
  export function ScreenshotOverlay({ iframeRef }) {
@@ -11,8 +11,9 @@ export function ScreenshotOverlay({ iframeRef }) {
11
11
  const [dragging, setDragging] = useState(false);
12
12
  const [start, setStart] = useState(null);
13
13
  const [end, setEnd] = useState(null);
14
- const [captured, setCaptured] = useState(null); // { base64, rect }
14
+ const [captured, setCaptured] = useState(null); // { base64, rect, loading }
15
15
  const [comment, setComment] = useState('');
16
+ const [flashRect, setFlashRect] = useState(null);
16
17
 
17
18
  const handleMouseDown = useCallback((e) => {
18
19
  if (captured) return;
@@ -48,9 +49,37 @@ export function ScreenshotOverlay({ iframeRef }) {
48
49
  return;
49
50
  }
50
51
 
52
+ setCaptured({ base64: null, rect: selRect, loading: true });
53
+
54
+ function finishCapture(base64) {
55
+ setCaptured({ base64, rect: selRect, loading: false });
56
+ setFlashRect(selRect);
57
+ setTimeout(() => setFlashRect(null), 600);
58
+ }
59
+
60
+ function drawPlaceholder() {
61
+ const canvas = document.createElement('canvas');
62
+ const dpr = window.devicePixelRatio || 1;
63
+ canvas.width = selRect.w * dpr;
64
+ canvas.height = selRect.h * dpr;
65
+ const ctx = canvas.getContext('2d');
66
+ ctx.scale(dpr, dpr);
67
+ ctx.fillStyle = '#1e2127';
68
+ ctx.fillRect(0, 0, selRect.w, selRect.h);
69
+ ctx.strokeStyle = '#3e4451';
70
+ ctx.lineWidth = 1;
71
+ ctx.strokeRect(4, 4, selRect.w - 8, selRect.h - 8);
72
+ ctx.fillStyle = '#6e7681';
73
+ ctx.font = '12px Inter, sans-serif';
74
+ ctx.textAlign = 'center';
75
+ ctx.textBaseline = 'middle';
76
+ ctx.fillText(`${Math.round(selRect.w)} × ${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
77
+ return canvas.toDataURL('image/png');
78
+ }
79
+
51
80
  try {
52
81
  const iframe = iframeRef.current;
53
- if (!iframe) { setStart(null); setEnd(null); return; }
82
+ if (!iframe) { finishCapture(drawPlaceholder()); return; }
54
83
 
55
84
  const canvas = document.createElement('canvas');
56
85
  const dpr = window.devicePixelRatio || 1;
@@ -59,53 +88,25 @@ export function ScreenshotOverlay({ iframeRef }) {
59
88
  const ctx = canvas.getContext('2d');
60
89
  ctx.scale(dpr, dpr);
61
90
 
62
- // Same-origin proxy iframe — serialize DOM to SVG foreignObject for capture
63
- const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
64
- if (iframeDoc) {
65
- const serializer = new XMLSerializer();
66
- const html = serializer.serializeToString(iframeDoc.documentElement);
67
- const iframeRect = iframe.getBoundingClientRect();
68
- const overlayRect = overlayRef.current.getBoundingClientRect();
69
- const offsetX = selRect.x - (iframeRect.left - overlayRect.left);
70
- const offsetY = selRect.y - (iframeRect.top - overlayRect.top);
71
-
72
- const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${selRect.w}" height="${selRect.h}">
73
- <foreignObject x="${-offsetX}" y="${-offsetY}" width="${iframeRect.width}" height="${iframeRect.height}">
74
- ${html}
75
- </foreignObject>
76
- </svg>`;
77
- const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
78
- const url = URL.createObjectURL(blob);
79
- const img = new Image();
80
- img.onload = () => {
81
- ctx.drawImage(img, 0, 0, selRect.w, selRect.h);
82
- URL.revokeObjectURL(url);
83
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
84
- };
85
- img.onerror = () => {
86
- URL.revokeObjectURL(url);
87
- // Fallback: region placeholder
88
- ctx.fillStyle = '#1e2127';
89
- ctx.fillRect(0, 0, selRect.w, selRect.h);
90
- ctx.fillStyle = '#6e7681';
91
- ctx.font = '12px Inter, sans-serif';
92
- ctx.textAlign = 'center';
93
- ctx.fillText(`Region ${Math.round(selRect.w)}×${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
94
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
95
- };
96
- img.src = url;
97
- } else {
98
- ctx.fillStyle = '#1e2127';
99
- ctx.fillRect(0, 0, selRect.w, selRect.h);
100
- ctx.fillStyle = '#6e7681';
101
- ctx.font = '12px Inter, sans-serif';
102
- ctx.textAlign = 'center';
103
- ctx.fillText(`Region ${Math.round(selRect.w)}×${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
104
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
91
+ const iframeRect = iframe.getBoundingClientRect();
92
+ const overlayRect = overlayRef.current.getBoundingClientRect();
93
+ const offsetX = selRect.x - (iframeRect.left - overlayRect.left);
94
+ const offsetY = selRect.y - (iframeRect.top - overlayRect.top);
95
+
96
+ try {
97
+ ctx.drawImage(iframe, -offsetX * dpr, -offsetY * dpr, iframeRect.width * dpr, iframeRect.height * dpr, 0, 0, selRect.w, selRect.h);
98
+ const testPixel = ctx.getImageData(0, 0, 1, 1).data;
99
+ if (testPixel[0] === 0 && testPixel[1] === 0 && testPixel[2] === 0 && testPixel[3] === 0) {
100
+ throw new Error('blank');
101
+ }
102
+ finishCapture(canvas.toDataURL('image/png'));
103
+ } catch {
104
+ finishCapture(drawPlaceholder());
105
105
  }
106
106
  } catch {
107
107
  setStart(null);
108
108
  setEnd(null);
109
+ setCaptured(null);
109
110
  }
110
111
  }, [dragging, start, end, iframeRef]);
111
112
 
@@ -118,7 +119,7 @@ export function ScreenshotOverlay({ iframeRef }) {
118
119
  }, [toggleScreenshotMode]);
119
120
 
120
121
  function handleSubmit() {
121
- if (!captured) return;
122
+ if (!captured || captured.loading) return;
122
123
  iteratePreview(comment || 'See screenshot', captured.base64);
123
124
  toggleScreenshotMode();
124
125
  }
@@ -130,6 +131,19 @@ export function ScreenshotOverlay({ iframeRef }) {
130
131
  height: Math.abs(end.y - start.y),
131
132
  } : null;
132
133
 
134
+ const popoverPosition = useCallback((rect) => {
135
+ if (!rect || !overlayRef.current) return {};
136
+ const overlayH = overlayRef.current.clientHeight;
137
+ const overlayW = overlayRef.current.clientWidth;
138
+ const spaceBelow = overlayH - (rect.y + rect.h + 8);
139
+ const popoverH = 200;
140
+ const placeAbove = spaceBelow < popoverH && rect.y > popoverH;
141
+ return {
142
+ left: Math.max(8, Math.min(rect.x, overlayW - 300)),
143
+ top: placeAbove ? rect.y - popoverH - 8 : rect.y + rect.h + 8,
144
+ };
145
+ }, []);
146
+
133
147
  return (
134
148
  <div
135
149
  ref={overlayRef}
@@ -150,21 +164,40 @@ export function ScreenshotOverlay({ iframeRef }) {
150
164
  />
151
165
  )}
152
166
 
167
+ {/* Capture flash */}
168
+ {flashRect && (
169
+ <div
170
+ className="absolute pointer-events-none animate-capture-flash rounded"
171
+ style={{ left: flashRect.x, top: flashRect.y, width: flashRect.w, height: flashRect.h }}
172
+ />
173
+ )}
174
+
175
+ {/* Captured selection outline */}
176
+ {captured && (
177
+ <div
178
+ className="absolute border-2 border-accent rounded pointer-events-none"
179
+ style={{ left: captured.rect.x, top: captured.rect.y, width: captured.rect.w, height: captured.rect.h }}
180
+ />
181
+ )}
182
+
153
183
  {/* Capture popover */}
154
184
  {captured && (
155
185
  <div
156
186
  className="absolute z-40 w-72 bg-surface-2 border border-border rounded-lg shadow-2xl animate-chat-fade-in"
157
- style={{
158
- left: Math.min(captured.rect.x, overlayRef.current?.clientWidth - 300 || 0),
159
- top: captured.rect.y + captured.rect.h + 8,
160
- }}
187
+ style={popoverPosition(captured.rect)}
161
188
  >
162
189
  <div className="p-3 border-b border-border-subtle">
163
- <img
164
- src={captured.base64}
165
- alt="Screenshot"
166
- className="w-full h-auto rounded border border-border-subtle max-h-32 object-contain bg-surface-0"
167
- />
190
+ {captured.loading ? (
191
+ <div className="w-full h-24 rounded border border-border-subtle bg-surface-0 flex items-center justify-center">
192
+ <Loader2 size={20} className="text-accent animate-spin" />
193
+ </div>
194
+ ) : (
195
+ <img
196
+ src={captured.base64}
197
+ alt="Screenshot"
198
+ className="w-full h-auto rounded border border-border-subtle max-h-32 object-contain bg-surface-0"
199
+ />
200
+ )}
168
201
  </div>
169
202
  <div className="p-3 flex items-center gap-2">
170
203
  <input
@@ -178,7 +211,8 @@ export function ScreenshotOverlay({ iframeRef }) {
178
211
  />
179
212
  <button
180
213
  onClick={handleSubmit}
181
- className="w-8 h-8 flex items-center justify-center rounded-md bg-accent/15 text-accent hover:bg-accent/25 transition-colors cursor-pointer"
214
+ disabled={captured.loading}
215
+ className="w-8 h-8 flex items-center justify-center rounded-md bg-accent/15 text-accent hover:bg-accent/25 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
182
216
  >
183
217
  <Send size={14} />
184
218
  </button>
@@ -122,6 +122,10 @@ export const useGrooveStore = create((set, get) => ({
122
122
  networkWallet: { connected: false, address: null, balance: '0.00', token: 'GROOVE', chain: 'base-l2' },
123
123
  networkEarnings: { today: 0, thisWeek: 0, allTime: 0, history: [] },
124
124
 
125
+ // ── Training Data ──────────────────────────────────────────
126
+ trainingOptIn: false,
127
+ trainingStats: null,
128
+
125
129
  // ── Marketplace Auth ───────────────────────────────────────
126
130
  marketplaceUser: null, // { id, displayName, avatar, ... } or null
127
131
  marketplaceAuthenticated: false,
@@ -197,6 +201,7 @@ export const useGrooveStore = create((set, get) => ({
197
201
  get().fetchTunnels();
198
202
  get().fetchBetaStatus();
199
203
  get().fetchNetworkInstallStatus();
204
+ get().fetchTrainingStatus();
200
205
  if (!get().onboardingComplete) get().fetchOnboardingStatus();
201
206
  if (window.groove?.auth?.onSubscriptionStatus) {
202
207
  window.groove.auth.onSubscriptionStatus((data) => {
@@ -853,6 +858,14 @@ export const useGrooveStore = create((set, get) => ({
853
858
  break;
854
859
  }
855
860
 
861
+ case 'training:status': {
862
+ set({
863
+ trainingOptIn: msg.data?.optedIn ?? false,
864
+ trainingStats: msg.data,
865
+ });
866
+ break;
867
+ }
868
+
856
869
  case 'config:updated':
857
870
  get().fetchBetaStatus();
858
871
  get().fetchNetworkInstallStatus();
@@ -2320,6 +2333,25 @@ export const useGrooveStore = create((set, get) => ({
2320
2333
  }
2321
2334
  },
2322
2335
 
2336
+ // ── Training Data ─────────────────────────────────────────
2337
+
2338
+ async setTrainingOptIn(enabled) {
2339
+ try {
2340
+ await api.post('/training/opt-in', { enabled });
2341
+ set({ trainingOptIn: enabled });
2342
+ if (!enabled) set({ trainingStats: null });
2343
+ } catch (e) {
2344
+ get().addToast('error', 'Failed to update training preference');
2345
+ }
2346
+ },
2347
+
2348
+ async fetchTrainingStatus() {
2349
+ try {
2350
+ const data = await api.get('/training/status');
2351
+ set({ trainingOptIn: data.optedIn, trainingStats: data });
2352
+ } catch { /* endpoint may not exist on older daemons */ }
2353
+ },
2354
+
2323
2355
  // ── Network (Early Access) ────────────────────────────────
2324
2356
 
2325
2357
  async fetchBetaStatus() {
@@ -13,10 +13,11 @@ import { Sheet, SheetContent } from '../components/ui/sheet';
13
13
  import { api } from '../lib/api';
14
14
  import { cn } from '../lib/cn';
15
15
  import { fmtUptime } from '../lib/format';
16
+ import { Dialog, DialogContent } from '../components/ui/dialog';
16
17
  import {
17
18
  Key, Eye, EyeOff, Check, Cpu, Download, Loader2, RefreshCw, Terminal, Copy,
18
19
  FolderOpen, FolderSearch, Users, Gauge, ChevronRight,
19
- ShieldCheck, Settings, Lock,
20
+ ShieldCheck, Settings, Lock, Database, Shield,
20
21
  Newspaper, Radio, Send, MessageSquare, MessageCircle,
21
22
  Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
22
23
  } from 'lucide-react';
@@ -1304,6 +1305,168 @@ function AddGatewayCard({ existingTypes, onAdd }) {
1304
1305
  );
1305
1306
  }
1306
1307
 
1308
+ /* ── Training Data Section ────────────────────────────────── */
1309
+
1310
+ function TrainingDataSection() {
1311
+ const trainingOptIn = useGrooveStore((s) => s.trainingOptIn);
1312
+ const trainingStats = useGrooveStore((s) => s.trainingStats);
1313
+ const setTrainingOptIn = useGrooveStore((s) => s.setTrainingOptIn);
1314
+ const fetchTrainingStatus = useGrooveStore((s) => s.fetchTrainingStatus);
1315
+ const addToast = useGrooveStore((s) => s.addToast);
1316
+ const [consentOpen, setConsentOpen] = useState(false);
1317
+ const [deleteOpen, setDeleteOpen] = useState(false);
1318
+ const [deleting, setDeleting] = useState(false);
1319
+
1320
+ useEffect(() => { fetchTrainingStatus(); }, []);
1321
+
1322
+ function handleToggle(next) {
1323
+ if (next) {
1324
+ setConsentOpen(true);
1325
+ } else {
1326
+ setTrainingOptIn(false);
1327
+ }
1328
+ }
1329
+
1330
+ async function handleAgree() {
1331
+ await setTrainingOptIn(true);
1332
+ setConsentOpen(false);
1333
+ }
1334
+
1335
+ async function handleDelete() {
1336
+ setDeleting(true);
1337
+ try {
1338
+ await api.post('/training/opt-in/delete');
1339
+ useGrooveStore.setState({ trainingOptIn: false, trainingStats: null });
1340
+ addToast('info', 'Training data deleted');
1341
+ setDeleteOpen(false);
1342
+ } catch (err) {
1343
+ addToast('error', 'Failed to delete data', err.message);
1344
+ } finally {
1345
+ setDeleting(false);
1346
+ }
1347
+ }
1348
+
1349
+ const sessions = trainingStats?.sessionsCaptured ?? 0;
1350
+ const envelopes = trainingStats?.envelopesSent ?? 0;
1351
+ const active = trainingStats?.captureActive ?? false;
1352
+
1353
+ return (
1354
+ <div>
1355
+ <div className="flex items-center gap-2 mb-2.5 px-0.5">
1356
+ <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Training Data</span>
1357
+ <div className="flex-1 h-px bg-border-subtle" />
1358
+ </div>
1359
+
1360
+ {/* Opt-in card */}
1361
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5 max-w-md space-y-3">
1362
+ <div className="flex items-center gap-2.5">
1363
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
1364
+ <Database size={12} className="text-accent" />
1365
+ </div>
1366
+ <div className="flex-1">
1367
+ <div className="text-[13px] font-medium text-text-0 font-sans leading-tight">Share Usage Data</div>
1368
+ <div className="text-2xs text-text-4 font-sans leading-relaxed mt-0.5">
1369
+ Contribute anonymized session data to train Groove&apos;s MoE models. All data is PII-scrubbed before leaving your machine.
1370
+ </div>
1371
+ </div>
1372
+ <Toggle value={trainingOptIn} onChange={handleToggle} />
1373
+ </div>
1374
+
1375
+ {/* Stats panel — only when opted in */}
1376
+ {trainingOptIn && (
1377
+ <div className="border-t border-border-subtle pt-3 space-y-2.5">
1378
+ {sessions === 0 && envelopes === 0 ? (
1379
+ <div className="text-2xs text-text-3 font-sans">
1380
+ No data captured yet — stats will appear after your first agent session.
1381
+ </div>
1382
+ ) : (
1383
+ <div className="flex items-center gap-4">
1384
+ <div>
1385
+ <div className="text-2xs text-text-2 font-sans">Sessions</div>
1386
+ <div className="text-sm font-semibold text-text-0 font-mono">{sessions}</div>
1387
+ </div>
1388
+ <div>
1389
+ <div className="text-2xs text-text-2 font-sans">Envelopes</div>
1390
+ <div className="text-sm font-semibold text-text-0 font-mono">{envelopes}</div>
1391
+ </div>
1392
+ <div className="flex items-center gap-1.5">
1393
+ <StatusDot status={active ? 'running' : 'crashed'} size="sm" />
1394
+ <span className="text-2xs text-text-2 font-sans">{active ? 'Capturing' : 'Idle'}</span>
1395
+ </div>
1396
+ </div>
1397
+ )}
1398
+
1399
+ {/* Delete data */}
1400
+ <div>
1401
+ <Button variant="ghost" size="sm" onClick={() => setDeleteOpen(true)} className="h-7 text-2xs text-danger hover:text-danger gap-1.5">
1402
+ <Trash2 size={10} />
1403
+ Delete My Data
1404
+ </Button>
1405
+ </div>
1406
+ </div>
1407
+ )}
1408
+ </div>
1409
+
1410
+ {/* Consent dialog */}
1411
+ <Dialog open={consentOpen} onOpenChange={setConsentOpen}>
1412
+ <DialogContent title="Data Collection Consent" description="Review what data is collected and how it is used.">
1413
+ <div className="px-5 py-4 space-y-3">
1414
+ <div className="space-y-2.5 text-xs text-text-1 font-sans leading-relaxed">
1415
+ <div className="flex items-start gap-2.5">
1416
+ <Database size={13} className="text-accent flex-shrink-0 mt-0.5" />
1417
+ <div><span className="font-semibold text-text-0">What is collected:</span> Agent session trajectories — thoughts, tool calls, observations, and outcomes.</div>
1418
+ </div>
1419
+ <div className="flex items-start gap-2.5">
1420
+ <Cpu size={13} className="text-accent flex-shrink-0 mt-0.5" />
1421
+ <div><span className="font-semibold text-text-0">How it is used:</span> Training MoE expert models that become Groove agents.</div>
1422
+ </div>
1423
+ <div className="flex items-start gap-2.5">
1424
+ <Shield size={13} className="text-accent flex-shrink-0 mt-0.5" />
1425
+ <div><span className="font-semibold text-text-0">PII protection:</span> 13 categories of sensitive data (emails, API keys, file paths, etc.) are automatically scrubbed before transmission.</div>
1426
+ </div>
1427
+ <div className="flex items-start gap-2.5">
1428
+ <Settings size={13} className="text-accent flex-shrink-0 mt-0.5" />
1429
+ <div><span className="font-semibold text-text-0">Opt out anytime:</span> Toggle the setting OFF to stop collection immediately.</div>
1430
+ </div>
1431
+ <div className="flex items-start gap-2.5">
1432
+ <Trash2 size={13} className="text-danger flex-shrink-0 mt-0.5" />
1433
+ <div><span className="font-semibold text-text-0">Delete data:</span> Permanently delete all previously collected data at any time.</div>
1434
+ </div>
1435
+ </div>
1436
+ </div>
1437
+ <div className="flex items-center gap-2 px-5 py-4 border-t border-border-subtle">
1438
+ <Button variant="primary" size="sm" onClick={handleAgree} className="flex-1 h-8 text-xs">
1439
+ I Agree — Start Contributing
1440
+ </Button>
1441
+ <Button variant="ghost" size="sm" onClick={() => setConsentOpen(false)} className="h-8 text-xs px-3">
1442
+ Cancel
1443
+ </Button>
1444
+ </div>
1445
+ </DialogContent>
1446
+ </Dialog>
1447
+
1448
+ {/* Delete confirmation dialog */}
1449
+ <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
1450
+ <DialogContent title="Delete Training Data" description="Confirm permanent deletion of all collected training data.">
1451
+ <div className="px-5 py-4">
1452
+ <p className="text-xs text-text-1 font-sans leading-relaxed">
1453
+ This will permanently delete all your collected training data and opt you out. This cannot be undone.
1454
+ </p>
1455
+ </div>
1456
+ <div className="flex items-center gap-2 px-5 py-4 border-t border-border-subtle">
1457
+ <Button variant="danger" size="sm" onClick={handleDelete} disabled={deleting} className="flex-1 h-8 text-xs">
1458
+ {deleting ? 'Deleting...' : 'Delete All Data'}
1459
+ </Button>
1460
+ <Button variant="ghost" size="sm" onClick={() => setDeleteOpen(false)} className="h-8 text-xs px-3">
1461
+ Cancel
1462
+ </Button>
1463
+ </div>
1464
+ </DialogContent>
1465
+ </Dialog>
1466
+ </div>
1467
+ );
1468
+ }
1469
+
1307
1470
  /* ── Early Access Section ─────────────────────────────────── */
1308
1471
 
1309
1472
  function EarlyAccessSection() {
@@ -1619,6 +1782,9 @@ export default function SettingsView() {
1619
1782
  {/* ═══════ EARLY ACCESS ═══════ */}
1620
1783
  <EarlyAccessSection />
1621
1784
 
1785
+ {/* ═══════ TRAINING DATA ═══════ */}
1786
+ <TrainingDataSection />
1787
+
1622
1788
  </div>
1623
1789
  </ScrollArea>
1624
1790
 
package/welcome.png ADDED
Binary file