groove-dev 0.27.74 → 0.27.75
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/CLAUDE.md +0 -7
- 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 +256 -4
- package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
- package/node_modules/@groove-dev/daemon/src/index.js +41 -1
- package/node_modules/@groove-dev/daemon/src/preview.js +18 -2
- package/node_modules/@groove-dev/daemon/src/process.js +6 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +38 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CAT9SCJi.js +8620 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.css +29 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +40 -7
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +149 -31
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +81 -0
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +263 -0
- package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +203 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +199 -114
- 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 +256 -4
- package/packages/daemon/src/conversations.js +16 -0
- package/packages/daemon/src/index.js +41 -1
- package/packages/daemon/src/preview.js +18 -2
- package/packages/daemon/src/process.js +6 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/codex.js +38 -0
- package/packages/daemon/src/providers/grok.js +156 -0
- package/packages/daemon/src/providers/index.js +5 -1
- package/packages/daemon/src/providers/nano-banana.js +103 -0
- package/packages/gui/dist/assets/index-CAT9SCJi.js +8620 -0
- package/packages/gui/dist/assets/index-CVzz6zyb.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +29 -0
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +16 -5
- package/packages/gui/src/components/chat/chat-input.jsx +40 -7
- package/packages/gui/src/components/chat/chat-messages.jsx +149 -31
- package/packages/gui/src/components/chat/chat-view.jsx +26 -2
- package/packages/gui/src/components/chat/model-picker.jsx +105 -52
- package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/packages/gui/src/components/preview/preview-toolbar.jsx +81 -0
- package/packages/gui/src/components/preview/preview-workspace.jsx +263 -0
- package/packages/gui/src/components/preview/screenshot-overlay.jsx +203 -0
- package/packages/gui/src/components/ui/toast.jsx +6 -2
- package/packages/gui/src/stores/groove.js +149 -9
- package/packages/gui/src/views/preview.jsx +6 -0
- package/packages/gui/src/views/settings.jsx +199 -114
- package/node_modules/@groove-dev/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/node_modules/@groove-dev/gui/dist/assets/index-QR7lyguO.css +0 -1
- package/packages/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/packages/gui/dist/assets/index-QR7lyguO.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useRef, useEffect, useState } from 'react';
|
|
3
|
-
import { Copy, Check, ArrowRight } from 'lucide-react';
|
|
2
|
+
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw } 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';
|
|
@@ -12,7 +12,7 @@ const API_STATUS_MESSAGES = [
|
|
|
12
12
|
'Almost there...',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
function CopyButton({ text }) {
|
|
15
|
+
function CopyButton({ text, className }) {
|
|
16
16
|
const [copied, setCopied] = useState(false);
|
|
17
17
|
function handleCopy() {
|
|
18
18
|
navigator.clipboard.writeText(text).then(() => {
|
|
@@ -23,7 +23,7 @@ function CopyButton({ text }) {
|
|
|
23
23
|
return (
|
|
24
24
|
<button
|
|
25
25
|
onClick={handleCopy}
|
|
26
|
-
className=
|
|
26
|
+
className={cn('flex items-center gap-1 px-2 py-1 text-2xs font-sans text-text-3 hover:text-text-1 transition-colors cursor-pointer', className)}
|
|
27
27
|
>
|
|
28
28
|
{copied ? <Check size={12} className="text-success" /> : <Copy size={12} />}
|
|
29
29
|
{copied ? 'Copied' : 'Copy'}
|
|
@@ -54,7 +54,6 @@ function parseMarkdown(text) {
|
|
|
54
54
|
while (i < lines.length) {
|
|
55
55
|
const line = lines[i];
|
|
56
56
|
|
|
57
|
-
// Fenced code block
|
|
58
57
|
if (line.startsWith('```')) {
|
|
59
58
|
const lang = line.slice(3).trim();
|
|
60
59
|
const codeLines = [];
|
|
@@ -63,12 +62,11 @@ function parseMarkdown(text) {
|
|
|
63
62
|
codeLines.push(lines[i]);
|
|
64
63
|
i++;
|
|
65
64
|
}
|
|
66
|
-
i++;
|
|
65
|
+
i++;
|
|
67
66
|
blocks.push({ type: 'code', language: lang, code: codeLines.join('\n') });
|
|
68
67
|
continue;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
// Heading
|
|
72
70
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
73
71
|
if (headingMatch) {
|
|
74
72
|
blocks.push({ type: 'heading', level: headingMatch[1].length, text: headingMatch[2] });
|
|
@@ -76,14 +74,12 @@ function parseMarkdown(text) {
|
|
|
76
74
|
continue;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
// Horizontal rule
|
|
80
77
|
if (/^(-{3,}|_{3,}|\*{3,})$/.test(line.trim())) {
|
|
81
78
|
blocks.push({ type: 'hr' });
|
|
82
79
|
i++;
|
|
83
80
|
continue;
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
// Blockquote
|
|
87
83
|
if (line.startsWith('> ')) {
|
|
88
84
|
const quoteLines = [line.slice(2)];
|
|
89
85
|
i++;
|
|
@@ -95,7 +91,6 @@ function parseMarkdown(text) {
|
|
|
95
91
|
continue;
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
// Unordered list
|
|
99
94
|
if (/^[-*+]\s/.test(line)) {
|
|
100
95
|
const items = [line.replace(/^[-*+]\s/, '')];
|
|
101
96
|
i++;
|
|
@@ -107,7 +102,6 @@ function parseMarkdown(text) {
|
|
|
107
102
|
continue;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
// Ordered list
|
|
111
105
|
if (/^\d+\.\s/.test(line)) {
|
|
112
106
|
const items = [line.replace(/^\d+\.\s/, '')];
|
|
113
107
|
i++;
|
|
@@ -119,10 +113,9 @@ function parseMarkdown(text) {
|
|
|
119
113
|
continue;
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
// Table
|
|
123
116
|
if (line.includes('|') && i + 1 < lines.length && /^\|?\s*[-:]+/.test(lines[i + 1])) {
|
|
124
117
|
const headerCells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
125
|
-
i += 2;
|
|
118
|
+
i += 2;
|
|
126
119
|
const rows = [];
|
|
127
120
|
while (i < lines.length && lines[i].includes('|')) {
|
|
128
121
|
rows.push(lines[i].split('|').map((c) => c.trim()).filter(Boolean));
|
|
@@ -132,13 +125,11 @@ function parseMarkdown(text) {
|
|
|
132
125
|
continue;
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
// Empty line
|
|
136
128
|
if (line.trim() === '') {
|
|
137
129
|
i++;
|
|
138
130
|
continue;
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
// Paragraph — collect consecutive non-empty lines
|
|
142
133
|
const paraLines = [line];
|
|
143
134
|
i++;
|
|
144
135
|
while (i < lines.length && lines[i].trim() !== '' && !lines[i].startsWith('```') && !lines[i].startsWith('#') && !/^[-*+]\s/.test(lines[i]) && !/^\d+\.\s/.test(lines[i]) && !lines[i].startsWith('> ') && !/^(-{3,}|_{3,}|\*{3,})$/.test(lines[i].trim())) {
|
|
@@ -247,9 +238,9 @@ function RenderedMarkdown({ text }) {
|
|
|
247
238
|
|
|
248
239
|
function UserMessage({ msg }) {
|
|
249
240
|
return (
|
|
250
|
-
<div className="flex justify-end">
|
|
241
|
+
<div className="flex justify-end animate-chat-fade-in">
|
|
251
242
|
<div className="max-w-[75%]">
|
|
252
|
-
<div className="px-4 py-3 rounded-
|
|
243
|
+
<div className="px-4 py-3 rounded-2xl rounded-br-md bg-gradient-to-br from-accent/12 to-accent/6 border border-accent/15 backdrop-blur-sm">
|
|
253
244
|
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
|
|
254
245
|
</div>
|
|
255
246
|
<div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
|
|
@@ -260,9 +251,9 @@ function UserMessage({ msg }) {
|
|
|
260
251
|
|
|
261
252
|
function AssistantMessage({ msg, model }) {
|
|
262
253
|
return (
|
|
263
|
-
<div className="max-w-[85%]">
|
|
264
|
-
{model && <div className="text-2xs text-text-3 font-
|
|
265
|
-
<div className="
|
|
254
|
+
<div className="max-w-[85%] animate-chat-fade-in">
|
|
255
|
+
{model && <div className="text-2xs text-text-3 font-mono mb-1.5 font-medium">{model}</div>}
|
|
256
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle px-4 py-3">
|
|
266
257
|
<RenderedMarkdown text={msg.text} />
|
|
267
258
|
</div>
|
|
268
259
|
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
@@ -270,9 +261,132 @@ function AssistantMessage({ msg, model }) {
|
|
|
270
261
|
);
|
|
271
262
|
}
|
|
272
263
|
|
|
264
|
+
function ImageLoadingMessage({ msg }) {
|
|
265
|
+
return (
|
|
266
|
+
<div className="max-w-[85%] animate-chat-fade-in">
|
|
267
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
268
|
+
<div className="w-80 h-80 image-loading-shimmer flex items-center justify-center">
|
|
269
|
+
<div className="flex flex-col items-center gap-3">
|
|
270
|
+
<div className="w-10 h-10 rounded-full bg-surface-3/80 flex items-center justify-center">
|
|
271
|
+
<ImageIcon size={18} className="text-accent animate-pulse" />
|
|
272
|
+
</div>
|
|
273
|
+
<span className="text-xs text-text-3 font-sans">Generating image...</span>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
{msg.prompt && (
|
|
277
|
+
<div className="px-4 py-2.5 border-t border-border-subtle">
|
|
278
|
+
<p className="text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ImageMessage({ msg, onReply }) {
|
|
287
|
+
const [expanded, setExpanded] = useState(false);
|
|
288
|
+
const [hovering, setHovering] = useState(false);
|
|
289
|
+
|
|
290
|
+
const handleDownload = useCallback(() => {
|
|
291
|
+
if (!msg.imageUrl) return;
|
|
292
|
+
const a = document.createElement('a');
|
|
293
|
+
a.href = msg.imageUrl;
|
|
294
|
+
a.download = `groove-${msg.model || 'image'}-${Date.now()}.png`;
|
|
295
|
+
document.body.appendChild(a);
|
|
296
|
+
a.click();
|
|
297
|
+
document.body.removeChild(a);
|
|
298
|
+
}, [msg.imageUrl, msg.model]);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<>
|
|
302
|
+
<div className="max-w-[85%] animate-chat-fade-in">
|
|
303
|
+
{msg.model && <div className="text-2xs text-text-3 font-mono mb-1.5 font-medium flex items-center gap-1.5"><ImageIcon size={10} /> {msg.model}</div>}
|
|
304
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
305
|
+
<div
|
|
306
|
+
className="relative group"
|
|
307
|
+
onMouseEnter={() => setHovering(true)}
|
|
308
|
+
onMouseLeave={() => setHovering(false)}
|
|
309
|
+
>
|
|
310
|
+
<img
|
|
311
|
+
src={msg.imageUrl}
|
|
312
|
+
alt={msg.prompt || 'Generated image'}
|
|
313
|
+
className="max-w-full max-h-[480px] object-contain cursor-pointer"
|
|
314
|
+
onClick={() => setExpanded(true)}
|
|
315
|
+
/>
|
|
316
|
+
{hovering && (
|
|
317
|
+
<div className="absolute top-2 right-2 flex gap-1.5 animate-chat-fade-in">
|
|
318
|
+
<button
|
|
319
|
+
onClick={handleDownload}
|
|
320
|
+
className="w-8 h-8 rounded-lg bg-surface-0/90 backdrop-blur-sm border border-border-subtle flex items-center justify-center text-text-2 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer"
|
|
321
|
+
title="Download"
|
|
322
|
+
>
|
|
323
|
+
<Download size={14} />
|
|
324
|
+
</button>
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => setExpanded(true)}
|
|
327
|
+
className="w-8 h-8 rounded-lg bg-surface-0/90 backdrop-blur-sm border border-border-subtle flex items-center justify-center text-text-2 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer"
|
|
328
|
+
title="Fullscreen"
|
|
329
|
+
>
|
|
330
|
+
<Maximize2 size={14} />
|
|
331
|
+
</button>
|
|
332
|
+
<CopyButton text={msg.prompt || ''} className="h-8 rounded-lg bg-surface-0/90 backdrop-blur-sm border border-border-subtle text-text-2 hover:text-accent" />
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
<div className="px-4 py-2.5 border-t border-border-subtle flex items-center gap-2">
|
|
337
|
+
<p className="flex-1 text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
338
|
+
{onReply && (
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => onReply(msg)}
|
|
341
|
+
className="text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer flex items-center gap-1 flex-shrink-0"
|
|
342
|
+
>
|
|
343
|
+
<RefreshCw size={10} /> Iterate
|
|
344
|
+
</button>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{expanded && (
|
|
352
|
+
<div
|
|
353
|
+
className="fixed inset-0 z-[200] bg-surface-0/95 backdrop-blur-md flex items-center justify-center animate-chat-fade-in"
|
|
354
|
+
onClick={() => setExpanded(false)}
|
|
355
|
+
>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => setExpanded(false)}
|
|
358
|
+
className="absolute top-4 right-4 w-10 h-10 rounded-full bg-surface-3 border border-border flex items-center justify-center text-text-2 hover:text-text-0 transition-colors cursor-pointer z-10"
|
|
359
|
+
>
|
|
360
|
+
<X size={18} />
|
|
361
|
+
</button>
|
|
362
|
+
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
|
363
|
+
<button
|
|
364
|
+
onClick={(e) => { e.stopPropagation(); handleDownload(); }}
|
|
365
|
+
className="h-9 px-4 rounded-lg bg-surface-3 border border-border flex items-center gap-2 text-xs font-sans text-text-1 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer"
|
|
366
|
+
>
|
|
367
|
+
<Download size={14} /> Download
|
|
368
|
+
</button>
|
|
369
|
+
</div>
|
|
370
|
+
<img
|
|
371
|
+
src={msg.imageUrl}
|
|
372
|
+
alt={msg.prompt || 'Generated image'}
|
|
373
|
+
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg shadow-2xl"
|
|
374
|
+
onClick={(e) => e.stopPropagation()}
|
|
375
|
+
/>
|
|
376
|
+
{msg.prompt && (
|
|
377
|
+
<div className="absolute bottom-4 left-4 max-w-md px-4 py-2 rounded-lg bg-surface-3/90 backdrop-blur-sm border border-border-subtle z-10">
|
|
378
|
+
<p className="text-xs text-text-2 font-sans italic">"{msg.prompt}"</p>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
</>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
273
387
|
function SystemMessage({ msg }) {
|
|
274
388
|
return (
|
|
275
|
-
<div className="flex justify-center py-1">
|
|
389
|
+
<div className="flex justify-center py-1 animate-chat-fade-in">
|
|
276
390
|
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-4/50">
|
|
277
391
|
<ArrowRight size={10} className="text-text-4" />
|
|
278
392
|
<span className="text-2xs text-text-3 font-sans">{msg.text}</span>
|
|
@@ -311,21 +425,23 @@ function ApiTypingIndicator() {
|
|
|
311
425
|
}, []);
|
|
312
426
|
|
|
313
427
|
return (
|
|
314
|
-
<div className="
|
|
315
|
-
<div className="
|
|
316
|
-
<
|
|
428
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle px-4 py-3 max-w-[85%] animate-chat-fade-in">
|
|
429
|
+
<div className="flex items-center gap-2.5">
|
|
430
|
+
<div className="relative w-3.5 h-3.5 flex-shrink-0">
|
|
431
|
+
<span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
|
|
432
|
+
</div>
|
|
433
|
+
<span
|
|
434
|
+
className="text-[12px] font-sans text-text-3 transition-opacity duration-[250ms]"
|
|
435
|
+
style={{ opacity: fade ? 1 : 0 }}
|
|
436
|
+
>
|
|
437
|
+
{API_STATUS_MESSAGES[idx]}
|
|
438
|
+
</span>
|
|
317
439
|
</div>
|
|
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>
|
|
324
440
|
</div>
|
|
325
441
|
);
|
|
326
442
|
}
|
|
327
443
|
|
|
328
|
-
export function ChatMessages({ messages, isStreaming, model, mode }) {
|
|
444
|
+
export function ChatMessages({ messages, isStreaming, model, mode, onImageReply }) {
|
|
329
445
|
const scrollRef = useRef(null);
|
|
330
446
|
const isAtBottomRef = useRef(true);
|
|
331
447
|
|
|
@@ -356,6 +472,8 @@ export function ChatMessages({ messages, isStreaming, model, mode }) {
|
|
|
356
472
|
return (
|
|
357
473
|
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
|
|
358
474
|
{messages.map((msg, i) => {
|
|
475
|
+
if (msg.type === 'image-loading') return <ImageLoadingMessage key={i} msg={msg} />;
|
|
476
|
+
if (msg.type === 'image') return <ImageMessage key={i} msg={msg} onReply={onImageReply} />;
|
|
359
477
|
if (msg.from === 'user') return <UserMessage key={i} msg={msg} />;
|
|
360
478
|
if (msg.from === 'system') return <SystemMessage key={i} msg={msg} />;
|
|
361
479
|
return <AssistantMessage key={i} msg={msg} model={model} />;
|
|
@@ -7,6 +7,7 @@ import { ConversationList } from './conversation-list';
|
|
|
7
7
|
import { ChatHeader } from './chat-header';
|
|
8
8
|
import { ChatMessages } from './chat-messages';
|
|
9
9
|
import { ChatInput } from './chat-input';
|
|
10
|
+
import { isImageModel } from './model-picker';
|
|
10
11
|
|
|
11
12
|
function EmptyState({ onNewChat }) {
|
|
12
13
|
return (
|
|
@@ -35,16 +36,19 @@ export function ChatView() {
|
|
|
35
36
|
const createConversation = useGrooveStore((s) => s.createConversation);
|
|
36
37
|
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
37
38
|
const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
|
|
39
|
+
const sendImageMessage = useGrooveStore((s) => s.sendImageMessage);
|
|
38
40
|
const stopAgent = useGrooveStore((s) => s.stopAgent);
|
|
39
41
|
const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
|
|
40
42
|
const setConversationMode = useGrooveStore((s) => s.setConversationMode);
|
|
41
43
|
const setConversationModel = useGrooveStore((s) => s.setConversationModel);
|
|
42
44
|
|
|
43
45
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
46
|
+
const [replyContext, setReplyContext] = useState(null);
|
|
44
47
|
|
|
45
48
|
const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
|
|
46
49
|
const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
|
|
47
50
|
const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
|
|
51
|
+
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
48
52
|
|
|
49
53
|
const handleNewChat = useCallback(async (provider, model) => {
|
|
50
54
|
const p = provider || 'claude-code';
|
|
@@ -61,8 +65,17 @@ export function ChatView() {
|
|
|
61
65
|
|
|
62
66
|
const handleSend = useCallback((text) => {
|
|
63
67
|
if (!activeConversationId) return;
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
|
|
69
|
+
if (currentModelIsImage) {
|
|
70
|
+
const prompt = replyContext
|
|
71
|
+
? `${text} (iterating on: "${replyContext.prompt}")`
|
|
72
|
+
: text;
|
|
73
|
+
sendImageMessage(activeConversationId, prompt, { model: activeConversation.model });
|
|
74
|
+
setReplyContext(null);
|
|
75
|
+
} else {
|
|
76
|
+
sendChatMessage(activeConversationId, text);
|
|
77
|
+
}
|
|
78
|
+
}, [activeConversationId, activeConversation, currentModelIsImage, replyContext, sendChatMessage, sendImageMessage]);
|
|
66
79
|
|
|
67
80
|
const handleStop = useCallback(() => {
|
|
68
81
|
if (!activeConversation) return;
|
|
@@ -81,6 +94,10 @@ export function ChatView() {
|
|
|
81
94
|
}
|
|
82
95
|
}, [activeConversationId, setConversationModel, handleNewChat]);
|
|
83
96
|
|
|
97
|
+
const handleImageReply = useCallback((msg) => {
|
|
98
|
+
setReplyContext(msg);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
84
101
|
const currentModel = activeConversation
|
|
85
102
|
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
86
103
|
: null;
|
|
@@ -105,6 +122,7 @@ export function ChatView() {
|
|
|
105
122
|
isStreaming={isStreaming}
|
|
106
123
|
model={activeConversation.model}
|
|
107
124
|
mode={activeConversation.mode || 'api'}
|
|
125
|
+
onImageReply={handleImageReply}
|
|
108
126
|
/>
|
|
109
127
|
<ChatInput
|
|
110
128
|
onSend={handleSend}
|
|
@@ -112,6 +130,10 @@ export function ChatView() {
|
|
|
112
130
|
sending={sendingMessage}
|
|
113
131
|
streaming={isStreaming}
|
|
114
132
|
disabled={false}
|
|
133
|
+
isImageModel={currentModelIsImage}
|
|
134
|
+
currentModel={activeConversation.model}
|
|
135
|
+
replyContext={replyContext}
|
|
136
|
+
onClearReply={() => setReplyContext(null)}
|
|
115
137
|
/>
|
|
116
138
|
</>
|
|
117
139
|
) : (
|
|
@@ -130,6 +152,8 @@ export function ChatView() {
|
|
|
130
152
|
sending={false}
|
|
131
153
|
streaming={false}
|
|
132
154
|
disabled={false}
|
|
155
|
+
isImageModel={false}
|
|
156
|
+
currentModel={null}
|
|
133
157
|
/>
|
|
134
158
|
</>
|
|
135
159
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { ChevronDown, Globe, Cpu, Zap, Sparkles } from 'lucide-react';
|
|
3
|
+
import { ChevronDown, Globe, Cpu, Zap, Sparkles, Image as ImageIcon } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { Badge } from '../ui/badge';
|
|
@@ -38,6 +38,12 @@ function getContextSize(model) {
|
|
|
38
38
|
return '128k';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function isImageModel(modelId) {
|
|
42
|
+
if (!modelId) return false;
|
|
43
|
+
const id = modelId.toLowerCase();
|
|
44
|
+
return id.includes('dall-e') || id.includes('imagen') || id.includes('gpt-image') || id.includes('imagine') || id.includes('nano-banana');
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export function ModelPicker({ value, onChange, disabled }) {
|
|
42
48
|
const [open, setOpen] = useState(false);
|
|
43
49
|
const [providers, setProviders] = useState([]);
|
|
@@ -64,6 +70,99 @@ export function ModelPicker({ value, onChange, disabled }) {
|
|
|
64
70
|
const currentModelDisplay = currentModel ? formatModelName(currentModel) : 'Select model';
|
|
65
71
|
const currentProvider = value?.provider || '';
|
|
66
72
|
const isNetwork = currentProvider === 'groove-network';
|
|
73
|
+
const isCurrentImage = isImageModel(currentModel);
|
|
74
|
+
|
|
75
|
+
const chatModels = [];
|
|
76
|
+
const imageModels = [];
|
|
77
|
+
|
|
78
|
+
for (const provider of providers) {
|
|
79
|
+
const models = provider.models || [];
|
|
80
|
+
const chatGroup = [];
|
|
81
|
+
const imgGroup = [];
|
|
82
|
+
for (const model of models) {
|
|
83
|
+
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
84
|
+
const modelType = typeof model === 'object' ? model.type : undefined;
|
|
85
|
+
if (modelType === 'image' || isImageModel(modelId)) {
|
|
86
|
+
imgGroup.push(model);
|
|
87
|
+
} else {
|
|
88
|
+
chatGroup.push(model);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (chatGroup.length > 0) chatModels.push({ ...provider, models: chatGroup });
|
|
92
|
+
if (imgGroup.length > 0) imageModels.push({ ...provider, models: imgGroup });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderModelGroup(providerList, sectionLabel) {
|
|
96
|
+
if (providerList.length === 0) return null;
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
{sectionLabel && (
|
|
100
|
+
<div className="px-3 py-1.5 text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans bg-surface-0 border-b border-border-subtle flex items-center gap-1.5">
|
|
101
|
+
{sectionLabel === 'Image Generation' && <ImageIcon size={10} className="text-purple" />}
|
|
102
|
+
{sectionLabel}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
{providerList.map((provider) => {
|
|
106
|
+
const isNetworkProvider = provider.id === 'groove-network';
|
|
107
|
+
return (
|
|
108
|
+
<div key={provider.id}>
|
|
109
|
+
<div className="px-3 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2 border-b border-border-subtle flex items-center gap-1.5">
|
|
110
|
+
{isNetworkProvider && <Globe size={10} className="text-purple" />}
|
|
111
|
+
{provider.name || provider.id}
|
|
112
|
+
</div>
|
|
113
|
+
{provider.models.map((model) => {
|
|
114
|
+
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
115
|
+
const modelDisplayName = typeof model === 'string' ? model : model.name || model.id;
|
|
116
|
+
const modelType = typeof model === 'object' ? model.type : undefined;
|
|
117
|
+
const isImg = modelType === 'image' || isImageModel(modelId);
|
|
118
|
+
const tier = isImg ? null : getTier(modelId);
|
|
119
|
+
const tierConfig = tier ? TIER_CONFIG[tier] : null;
|
|
120
|
+
const TierIcon = tierConfig?.icon;
|
|
121
|
+
const isActive = currentModel === modelId && currentProvider === provider.id;
|
|
122
|
+
return (
|
|
123
|
+
<button
|
|
124
|
+
key={modelId}
|
|
125
|
+
onClick={() => {
|
|
126
|
+
onChange({ provider: provider.id, model: modelId });
|
|
127
|
+
setOpen(false);
|
|
128
|
+
}}
|
|
129
|
+
className={cn(
|
|
130
|
+
'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer',
|
|
131
|
+
isActive ? 'bg-accent/10 text-text-0' : 'hover:bg-surface-3 text-text-1',
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<div className="flex-1 min-w-0">
|
|
135
|
+
<div className="flex items-center gap-1.5">
|
|
136
|
+
{isImg && <ImageIcon size={11} className="text-purple flex-shrink-0" />}
|
|
137
|
+
<span className="text-xs font-medium font-sans truncate">{modelDisplayName}</span>
|
|
138
|
+
</div>
|
|
139
|
+
{!isImg && <div className="text-2xs text-text-4 font-sans">{getContextSize(modelId)} context</div>}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
142
|
+
{isNetworkProvider && (
|
|
143
|
+
<Badge variant="purple" className="text-[9px]">
|
|
144
|
+
<Globe size={8} /> Decentralized
|
|
145
|
+
</Badge>
|
|
146
|
+
)}
|
|
147
|
+
{isImg ? (
|
|
148
|
+
<Badge variant="purple" className="text-[9px]">
|
|
149
|
+
<ImageIcon size={8} /> Image
|
|
150
|
+
</Badge>
|
|
151
|
+
) : tierConfig && (
|
|
152
|
+
<Badge variant={tierConfig.variant} className="text-[9px]">
|
|
153
|
+
<TierIcon size={8} /> {tierConfig.label}
|
|
154
|
+
</Badge>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</button>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
67
166
|
|
|
68
167
|
return (
|
|
69
168
|
<div ref={ref} className="relative">
|
|
@@ -75,10 +174,11 @@ export function ModelPicker({ value, onChange, disabled }) {
|
|
|
75
174
|
'bg-surface-4 border border-border-subtle hover:bg-surface-5',
|
|
76
175
|
'disabled:opacity-40 disabled:cursor-not-allowed',
|
|
77
176
|
isNetwork && 'border-purple/30 bg-purple/8',
|
|
177
|
+
isCurrentImage && 'border-purple/30 bg-purple/8',
|
|
78
178
|
)}
|
|
79
179
|
>
|
|
80
|
-
{isNetwork ? <Globe size={12} className="text-purple" /> : <Cpu size={12} className="text-text-3" />}
|
|
81
|
-
<span className=
|
|
180
|
+
{isCurrentImage ? <ImageIcon size={12} className="text-purple" /> : isNetwork ? <Globe size={12} className="text-purple" /> : <Cpu size={12} className="text-text-3" />}
|
|
181
|
+
<span className={cn('max-w-[120px] truncate', isCurrentImage ? 'text-purple' : 'text-text-1')}>{currentModelDisplay}</span>
|
|
82
182
|
<ChevronDown size={12} className="text-text-4" />
|
|
83
183
|
</button>
|
|
84
184
|
|
|
@@ -87,55 +187,8 @@ export function ModelPicker({ value, onChange, disabled }) {
|
|
|
87
187
|
{providers.length === 0 && (
|
|
88
188
|
<div className="px-4 py-6 text-center text-xs text-text-3 font-sans">No providers available</div>
|
|
89
189
|
)}
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
if (models.length === 0) return null;
|
|
93
|
-
const isNetworkProvider = provider.id === 'groove-network';
|
|
94
|
-
return (
|
|
95
|
-
<div key={provider.id}>
|
|
96
|
-
<div className="px-3 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2 border-b border-border-subtle flex items-center gap-1.5">
|
|
97
|
-
{isNetworkProvider && <Globe size={10} className="text-purple" />}
|
|
98
|
-
{provider.name || provider.id}
|
|
99
|
-
</div>
|
|
100
|
-
{models.map((model) => {
|
|
101
|
-
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
102
|
-
const modelDisplayName = typeof model === 'string' ? model : model.name || model.id;
|
|
103
|
-
const tier = getTier(modelId);
|
|
104
|
-
const tierConfig = TIER_CONFIG[tier];
|
|
105
|
-
const TierIcon = tierConfig.icon;
|
|
106
|
-
const isActive = currentModel === modelId && currentProvider === provider.id;
|
|
107
|
-
return (
|
|
108
|
-
<button
|
|
109
|
-
key={modelId}
|
|
110
|
-
onClick={() => {
|
|
111
|
-
onChange({ provider: provider.id, model: modelId });
|
|
112
|
-
setOpen(false);
|
|
113
|
-
}}
|
|
114
|
-
className={cn(
|
|
115
|
-
'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer',
|
|
116
|
-
isActive ? 'bg-accent/10 text-text-0' : 'hover:bg-surface-3 text-text-1',
|
|
117
|
-
)}
|
|
118
|
-
>
|
|
119
|
-
<div className="flex-1 min-w-0">
|
|
120
|
-
<div className="text-xs font-medium font-sans truncate">{modelDisplayName}</div>
|
|
121
|
-
<div className="text-2xs text-text-4 font-sans">{getContextSize(modelId)} context</div>
|
|
122
|
-
</div>
|
|
123
|
-
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
124
|
-
{isNetworkProvider && (
|
|
125
|
-
<Badge variant="purple" className="text-[9px]">
|
|
126
|
-
<Globe size={8} /> Decentralized
|
|
127
|
-
</Badge>
|
|
128
|
-
)}
|
|
129
|
-
<Badge variant={tierConfig.variant} className="text-[9px]">
|
|
130
|
-
<TierIcon size={8} /> {tierConfig.label}
|
|
131
|
-
</Badge>
|
|
132
|
-
</div>
|
|
133
|
-
</button>
|
|
134
|
-
);
|
|
135
|
-
})}
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
})}
|
|
190
|
+
{renderModelGroup(chatModels, null)}
|
|
191
|
+
{renderModelGroup(imageModels, 'Image Generation')}
|
|
139
192
|
</div>
|
|
140
193
|
)}
|
|
141
194
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe, MessageCircle } from 'lucide-react';
|
|
2
|
+
import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe, MessageCircle, Eye } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { Tooltip } from '../ui/tooltip';
|
|
5
5
|
import { useGrooveStore } from '../../stores/groove';
|
|
@@ -17,6 +17,7 @@ const BASE_NAV_ITEMS = [
|
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
|
|
20
|
+
const PREVIEW_NAV_ITEM = { id: 'preview', icon: Eye, label: 'Preview' };
|
|
20
21
|
|
|
21
22
|
const UTIL_ITEMS = [
|
|
22
23
|
{ id: 'journalist', icon: Newspaper, label: 'Journalist', panel: true },
|
|
@@ -26,7 +27,9 @@ const UTIL_ITEMS = [
|
|
|
26
27
|
export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel }) {
|
|
27
28
|
const darwinTrafficLights = isElectron() && getPlatform() === 'darwin';
|
|
28
29
|
const networkUnlocked = useGrooveStore((s) => s.networkUnlocked);
|
|
29
|
-
const
|
|
30
|
+
const previewUrl = useGrooveStore((s) => s.previewState.url);
|
|
31
|
+
let NAV_ITEMS = previewUrl ? [...BASE_NAV_ITEMS, PREVIEW_NAV_ITEM] : BASE_NAV_ITEMS;
|
|
32
|
+
if (networkUnlocked) NAV_ITEMS = [...NAV_ITEMS, NETWORK_NAV_ITEM];
|
|
30
33
|
|
|
31
34
|
return (
|
|
32
35
|
<nav className="w-12 flex-shrink-0 flex flex-col bg-surface-3 border-r border-border">
|