groove-dev 0.27.74 → 0.27.77
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 +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 +32 -2
- package/node_modules/@groove-dev/daemon/src/process.js +9 -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-BbmPDhuW.js +8616 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.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 +41 -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 +49 -11
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +144 -24
- 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 +109 -0
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +278 -0
- package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +237 -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 +32 -2
- package/packages/daemon/src/process.js +9 -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-BbmPDhuW.js +8616 -0
- package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +41 -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 +49 -11
- package/packages/gui/src/components/chat/chat-messages.jsx +144 -24
- 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 +109 -0
- package/packages/gui/src/components/preview/preview-workspace.jsx +278 -0
- package/packages/gui/src/components/preview/screenshot-overlay.jsx +237 -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/welcome.png +0 -0
- 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,8 +1,9 @@
|
|
|
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
|
+
import { Avatar } from '../ui/avatar';
|
|
6
7
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
7
8
|
|
|
8
9
|
const API_STATUS_MESSAGES = [
|
|
@@ -12,7 +13,7 @@ const API_STATUS_MESSAGES = [
|
|
|
12
13
|
'Almost there...',
|
|
13
14
|
];
|
|
14
15
|
|
|
15
|
-
function CopyButton({ text }) {
|
|
16
|
+
function CopyButton({ text, className }) {
|
|
16
17
|
const [copied, setCopied] = useState(false);
|
|
17
18
|
function handleCopy() {
|
|
18
19
|
navigator.clipboard.writeText(text).then(() => {
|
|
@@ -23,7 +24,7 @@ function CopyButton({ text }) {
|
|
|
23
24
|
return (
|
|
24
25
|
<button
|
|
25
26
|
onClick={handleCopy}
|
|
26
|
-
className=
|
|
27
|
+
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
28
|
>
|
|
28
29
|
{copied ? <Check size={12} className="text-success" /> : <Copy size={12} />}
|
|
29
30
|
{copied ? 'Copied' : 'Copy'}
|
|
@@ -54,7 +55,6 @@ function parseMarkdown(text) {
|
|
|
54
55
|
while (i < lines.length) {
|
|
55
56
|
const line = lines[i];
|
|
56
57
|
|
|
57
|
-
// Fenced code block
|
|
58
58
|
if (line.startsWith('```')) {
|
|
59
59
|
const lang = line.slice(3).trim();
|
|
60
60
|
const codeLines = [];
|
|
@@ -63,12 +63,11 @@ function parseMarkdown(text) {
|
|
|
63
63
|
codeLines.push(lines[i]);
|
|
64
64
|
i++;
|
|
65
65
|
}
|
|
66
|
-
i++;
|
|
66
|
+
i++;
|
|
67
67
|
blocks.push({ type: 'code', language: lang, code: codeLines.join('\n') });
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Heading
|
|
72
71
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
73
72
|
if (headingMatch) {
|
|
74
73
|
blocks.push({ type: 'heading', level: headingMatch[1].length, text: headingMatch[2] });
|
|
@@ -76,14 +75,12 @@ function parseMarkdown(text) {
|
|
|
76
75
|
continue;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
// Horizontal rule
|
|
80
78
|
if (/^(-{3,}|_{3,}|\*{3,})$/.test(line.trim())) {
|
|
81
79
|
blocks.push({ type: 'hr' });
|
|
82
80
|
i++;
|
|
83
81
|
continue;
|
|
84
82
|
}
|
|
85
83
|
|
|
86
|
-
// Blockquote
|
|
87
84
|
if (line.startsWith('> ')) {
|
|
88
85
|
const quoteLines = [line.slice(2)];
|
|
89
86
|
i++;
|
|
@@ -95,7 +92,6 @@ function parseMarkdown(text) {
|
|
|
95
92
|
continue;
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
// Unordered list
|
|
99
95
|
if (/^[-*+]\s/.test(line)) {
|
|
100
96
|
const items = [line.replace(/^[-*+]\s/, '')];
|
|
101
97
|
i++;
|
|
@@ -107,7 +103,6 @@ function parseMarkdown(text) {
|
|
|
107
103
|
continue;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
// Ordered list
|
|
111
106
|
if (/^\d+\.\s/.test(line)) {
|
|
112
107
|
const items = [line.replace(/^\d+\.\s/, '')];
|
|
113
108
|
i++;
|
|
@@ -119,10 +114,9 @@ function parseMarkdown(text) {
|
|
|
119
114
|
continue;
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
// Table
|
|
123
117
|
if (line.includes('|') && i + 1 < lines.length && /^\|?\s*[-:]+/.test(lines[i + 1])) {
|
|
124
118
|
const headerCells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
125
|
-
i += 2;
|
|
119
|
+
i += 2;
|
|
126
120
|
const rows = [];
|
|
127
121
|
while (i < lines.length && lines[i].includes('|')) {
|
|
128
122
|
rows.push(lines[i].split('|').map((c) => c.trim()).filter(Boolean));
|
|
@@ -132,13 +126,11 @@ function parseMarkdown(text) {
|
|
|
132
126
|
continue;
|
|
133
127
|
}
|
|
134
128
|
|
|
135
|
-
// Empty line
|
|
136
129
|
if (line.trim() === '') {
|
|
137
130
|
i++;
|
|
138
131
|
continue;
|
|
139
132
|
}
|
|
140
133
|
|
|
141
|
-
// Paragraph — collect consecutive non-empty lines
|
|
142
134
|
const paraLines = [line];
|
|
143
135
|
i++;
|
|
144
136
|
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())) {
|
|
@@ -248,8 +240,8 @@ function RenderedMarkdown({ text }) {
|
|
|
248
240
|
function UserMessage({ msg }) {
|
|
249
241
|
return (
|
|
250
242
|
<div className="flex justify-end">
|
|
251
|
-
<div className="max-w-[
|
|
252
|
-
<div className="px-
|
|
243
|
+
<div className="max-w-[85%]">
|
|
244
|
+
<div className="px-3.5 py-2.5 rounded-2xl rounded-br-md bg-accent/10 border border-accent/15">
|
|
253
245
|
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
|
|
254
246
|
</div>
|
|
255
247
|
<div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
|
|
@@ -259,17 +251,143 @@ function UserMessage({ msg }) {
|
|
|
259
251
|
}
|
|
260
252
|
|
|
261
253
|
function AssistantMessage({ msg, model }) {
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex gap-2.5">
|
|
256
|
+
<Avatar name={model || 'assistant'} role="assistant" size="sm" className="mt-1 flex-shrink-0" />
|
|
257
|
+
<div className="max-w-[85%]">
|
|
258
|
+
{model && <div className="text-2xs text-text-3 font-sans mb-1 font-medium">{model}</div>}
|
|
259
|
+
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
260
|
+
<RenderedMarkdown text={msg.text} />
|
|
261
|
+
</div>
|
|
262
|
+
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ImageLoadingMessage({ msg }) {
|
|
262
269
|
return (
|
|
263
270
|
<div className="max-w-[85%]">
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
272
|
+
<div className="w-80 h-80 image-loading-shimmer flex items-center justify-center">
|
|
273
|
+
<div className="flex flex-col items-center gap-3">
|
|
274
|
+
<div className="w-10 h-10 rounded-full bg-surface-3/80 flex items-center justify-center">
|
|
275
|
+
<ImageIcon size={18} className="text-accent animate-pulse" />
|
|
276
|
+
</div>
|
|
277
|
+
<span className="text-xs text-text-3 font-sans">Generating image...</span>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{msg.prompt && (
|
|
281
|
+
<div className="px-4 py-2.5 border-t border-border-subtle">
|
|
282
|
+
<p className="text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
267
285
|
</div>
|
|
268
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
269
286
|
</div>
|
|
270
287
|
);
|
|
271
288
|
}
|
|
272
289
|
|
|
290
|
+
function ImageMessage({ msg, onReply }) {
|
|
291
|
+
const [expanded, setExpanded] = useState(false);
|
|
292
|
+
const [hovering, setHovering] = useState(false);
|
|
293
|
+
|
|
294
|
+
const handleDownload = useCallback(() => {
|
|
295
|
+
if (!msg.imageUrl) return;
|
|
296
|
+
const a = document.createElement('a');
|
|
297
|
+
a.href = msg.imageUrl;
|
|
298
|
+
a.download = `groove-${msg.model || 'image'}-${Date.now()}.png`;
|
|
299
|
+
document.body.appendChild(a);
|
|
300
|
+
a.click();
|
|
301
|
+
document.body.removeChild(a);
|
|
302
|
+
}, [msg.imageUrl, msg.model]);
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<>
|
|
306
|
+
<div className="max-w-[85%]">
|
|
307
|
+
{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>}
|
|
308
|
+
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
309
|
+
<div
|
|
310
|
+
className="relative group"
|
|
311
|
+
onMouseEnter={() => setHovering(true)}
|
|
312
|
+
onMouseLeave={() => setHovering(false)}
|
|
313
|
+
>
|
|
314
|
+
<img
|
|
315
|
+
src={msg.imageUrl}
|
|
316
|
+
alt={msg.prompt || 'Generated image'}
|
|
317
|
+
className="max-w-full max-h-[480px] object-contain cursor-pointer"
|
|
318
|
+
onClick={() => setExpanded(true)}
|
|
319
|
+
/>
|
|
320
|
+
{hovering && (
|
|
321
|
+
<div className="absolute top-2 right-2 flex gap-1.5">
|
|
322
|
+
<button
|
|
323
|
+
onClick={handleDownload}
|
|
324
|
+
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"
|
|
325
|
+
title="Download"
|
|
326
|
+
>
|
|
327
|
+
<Download size={14} />
|
|
328
|
+
</button>
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => setExpanded(true)}
|
|
331
|
+
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"
|
|
332
|
+
title="Fullscreen"
|
|
333
|
+
>
|
|
334
|
+
<Maximize2 size={14} />
|
|
335
|
+
</button>
|
|
336
|
+
<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" />
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
<div className="px-4 py-2.5 border-t border-border-subtle flex items-center gap-2">
|
|
341
|
+
<p className="flex-1 text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
342
|
+
{onReply && (
|
|
343
|
+
<button
|
|
344
|
+
onClick={() => onReply(msg)}
|
|
345
|
+
className="text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer flex items-center gap-1 flex-shrink-0"
|
|
346
|
+
>
|
|
347
|
+
<RefreshCw size={10} /> Iterate
|
|
348
|
+
</button>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{expanded && (
|
|
356
|
+
<div
|
|
357
|
+
className="fixed inset-0 z-[200] bg-surface-0/95 backdrop-blur-md flex items-center justify-center"
|
|
358
|
+
onClick={() => setExpanded(false)}
|
|
359
|
+
>
|
|
360
|
+
<button
|
|
361
|
+
onClick={() => setExpanded(false)}
|
|
362
|
+
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"
|
|
363
|
+
>
|
|
364
|
+
<X size={18} />
|
|
365
|
+
</button>
|
|
366
|
+
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
|
367
|
+
<button
|
|
368
|
+
onClick={(e) => { e.stopPropagation(); handleDownload(); }}
|
|
369
|
+
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"
|
|
370
|
+
>
|
|
371
|
+
<Download size={14} /> Download
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
<img
|
|
375
|
+
src={msg.imageUrl}
|
|
376
|
+
alt={msg.prompt || 'Generated image'}
|
|
377
|
+
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg shadow-2xl"
|
|
378
|
+
onClick={(e) => e.stopPropagation()}
|
|
379
|
+
/>
|
|
380
|
+
{msg.prompt && (
|
|
381
|
+
<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">
|
|
382
|
+
<p className="text-xs text-text-2 font-sans italic">"{msg.prompt}"</p>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
</>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
273
391
|
function SystemMessage({ msg }) {
|
|
274
392
|
return (
|
|
275
393
|
<div className="flex justify-center py-1">
|
|
@@ -311,12 +429,12 @@ function ApiTypingIndicator() {
|
|
|
311
429
|
}, []);
|
|
312
430
|
|
|
313
431
|
return (
|
|
314
|
-
<div className="
|
|
432
|
+
<div className="flex items-center gap-2.5 ml-8 py-1">
|
|
315
433
|
<div className="relative w-3.5 h-3.5 flex-shrink-0">
|
|
316
434
|
<span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
|
|
317
435
|
</div>
|
|
318
436
|
<span
|
|
319
|
-
className="text-
|
|
437
|
+
className="text-2xs font-sans text-text-3 transition-opacity duration-[250ms]"
|
|
320
438
|
style={{ opacity: fade ? 1 : 0 }}
|
|
321
439
|
>
|
|
322
440
|
{API_STATUS_MESSAGES[idx]}
|
|
@@ -325,7 +443,7 @@ function ApiTypingIndicator() {
|
|
|
325
443
|
);
|
|
326
444
|
}
|
|
327
445
|
|
|
328
|
-
export function ChatMessages({ messages, isStreaming, model, mode }) {
|
|
446
|
+
export function ChatMessages({ messages, isStreaming, model, mode, onImageReply }) {
|
|
329
447
|
const scrollRef = useRef(null);
|
|
330
448
|
const isAtBottomRef = useRef(true);
|
|
331
449
|
|
|
@@ -356,6 +474,8 @@ export function ChatMessages({ messages, isStreaming, model, mode }) {
|
|
|
356
474
|
return (
|
|
357
475
|
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
|
|
358
476
|
{messages.map((msg, i) => {
|
|
477
|
+
if (msg.type === 'image-loading') return <ImageLoadingMessage key={i} msg={msg} />;
|
|
478
|
+
if (msg.type === 'image') return <ImageMessage key={i} msg={msg} onReply={onImageReply} />;
|
|
359
479
|
if (msg.from === 'user') return <UserMessage key={i} msg={msg} />;
|
|
360
480
|
if (msg.from === 'system') return <SystemMessage key={i} msg={msg} />;
|
|
361
481
|
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">
|