groove-dev 0.27.196 → 0.27.198
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/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
- package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
- package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
- 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 +5 -5
- package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
- package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
- package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
- package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
- package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
- package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
- package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
- package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
- package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
- package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
- package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
- package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
- package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -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.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +53 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
- package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
- package/package.json +2 -2
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +5 -5
- package/packages/daemon/src/axom-connector.js +340 -0
- package/packages/daemon/src/axom-install.js +140 -0
- package/packages/daemon/src/axom-server.js +229 -0
- package/packages/daemon/src/chatstore.js +104 -0
- package/packages/daemon/src/federation.js +6 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/innerchat-docs.js +8 -1
- package/packages/daemon/src/innerchat-relay.js +89 -0
- package/packages/daemon/src/innerchat.js +261 -1
- package/packages/daemon/src/introducer.js +1 -1
- package/packages/daemon/src/network-guard.js +56 -0
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/axom.js +66 -0
- package/packages/daemon/src/providers/index.js +2 -0
- package/packages/daemon/src/routes/agents.js +1 -175
- package/packages/daemon/src/routes/axom.js +196 -0
- package/packages/daemon/src/routes/chat-history.js +34 -0
- package/packages/daemon/src/routes/innerchat.js +152 -20
- package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
- package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +2 -2
- package/packages/gui/src/app.css +53 -0
- package/packages/gui/src/components/agents/agent-config.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
- package/packages/gui/src/components/layout/command-palette.jsx +1 -1
- package/packages/gui/src/stores/groove.js +24 -163
- package/packages/gui/src/stores/slices/agents-slice.js +52 -2
- package/packages/gui/src/stores/slices/axom-slice.js +197 -0
- package/packages/gui/src/views/axom.jsx +1060 -0
- package/packages/gui/src/views/marketplace.jsx +1 -1
- package/packages/gui/src/views/settings.jsx +178 -64
- package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
- package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
- package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
- package/packages/daemon/src/conversations.js +0 -576
- package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
- package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
- package/packages/gui/src/components/chat/chat-header.jsx +0 -198
- package/packages/gui/src/components/chat/chat-input.jsx +0 -367
- package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
- package/packages/gui/src/components/chat/chat-view.jsx +0 -212
- package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
- package/packages/gui/src/components/chat/model-picker.jsx +0 -201
- package/packages/gui/src/stores/slices/chat-slice.js +0 -227
- package/packages/gui/src/views/chat.jsx +0 -6
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw, Loader2, MessageSquare } from 'lucide-react';
|
|
4
|
-
import { cn } from '../../lib/cn';
|
|
5
|
-
import { timeAgo } from '../../lib/format';
|
|
6
|
-
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
7
|
-
import { TableTree } from '../ui/table-tree';
|
|
8
|
-
|
|
9
|
-
const API_STATUS_MESSAGES = [
|
|
10
|
-
'Generating response...',
|
|
11
|
-
'Processing...',
|
|
12
|
-
'Thinking...',
|
|
13
|
-
'Almost there...',
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
function stripEmojis(text) {
|
|
17
|
-
if (!text) return '';
|
|
18
|
-
return text
|
|
19
|
-
.replace(/\p{Extended_Pictographic}/gu, '')
|
|
20
|
-
.replace(/[️⃣]/g, '')
|
|
21
|
-
.replace(/\s{2,}/g, ' ');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function CopyButton({ text, className }) {
|
|
25
|
-
const [copied, setCopied] = useState(false);
|
|
26
|
-
function handleCopy() {
|
|
27
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
28
|
-
setCopied(true);
|
|
29
|
-
setTimeout(() => setCopied(false), 2000);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return (
|
|
33
|
-
<button
|
|
34
|
-
onClick={handleCopy}
|
|
35
|
-
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)}
|
|
36
|
-
>
|
|
37
|
-
{copied ? <Check size={12} className="text-success" /> : <Copy size={12} />}
|
|
38
|
-
{copied ? 'Copied' : 'Copy'}
|
|
39
|
-
</button>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function CodeBlock({ language, code }) {
|
|
44
|
-
return (
|
|
45
|
-
<div className="my-3 rounded-lg border border-border-subtle overflow-hidden bg-surface-0">
|
|
46
|
-
<div className="flex items-center justify-between px-3 py-1.5 bg-surface-3 border-b border-border-subtle">
|
|
47
|
-
<span className="text-2xs font-mono text-text-3">{language || 'code'}</span>
|
|
48
|
-
<CopyButton text={code} />
|
|
49
|
-
</div>
|
|
50
|
-
<pre className="px-4 py-3 overflow-x-auto">
|
|
51
|
-
<code className="text-xs font-mono text-text-1 leading-relaxed whitespace-pre">{code}</code>
|
|
52
|
-
</pre>
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseMarkdown(text) {
|
|
58
|
-
if (!text) return [];
|
|
59
|
-
const lines = text.split('\n');
|
|
60
|
-
const blocks = [];
|
|
61
|
-
let i = 0;
|
|
62
|
-
|
|
63
|
-
while (i < lines.length) {
|
|
64
|
-
const line = lines[i];
|
|
65
|
-
|
|
66
|
-
if (line.startsWith('```')) {
|
|
67
|
-
const lang = line.slice(3).trim();
|
|
68
|
-
const codeLines = [];
|
|
69
|
-
i++;
|
|
70
|
-
while (i < lines.length && !lines[i].startsWith('```')) {
|
|
71
|
-
codeLines.push(lines[i]);
|
|
72
|
-
i++;
|
|
73
|
-
}
|
|
74
|
-
i++;
|
|
75
|
-
blocks.push({ type: 'code', language: lang, code: codeLines.join('\n') });
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
80
|
-
if (headingMatch) {
|
|
81
|
-
blocks.push({ type: 'heading', level: headingMatch[1].length, text: headingMatch[2] });
|
|
82
|
-
i++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (/^(-{3,}|_{3,}|\*{3,})$/.test(line.trim())) {
|
|
87
|
-
blocks.push({ type: 'hr' });
|
|
88
|
-
i++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (line.startsWith('> ')) {
|
|
93
|
-
const quoteLines = [line.slice(2)];
|
|
94
|
-
i++;
|
|
95
|
-
while (i < lines.length && lines[i].startsWith('> ')) {
|
|
96
|
-
quoteLines.push(lines[i].slice(2));
|
|
97
|
-
i++;
|
|
98
|
-
}
|
|
99
|
-
blocks.push({ type: 'blockquote', text: quoteLines.join('\n') });
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (/^[-*+]\s/.test(line)) {
|
|
104
|
-
const items = [line.replace(/^[-*+]\s/, '')];
|
|
105
|
-
i++;
|
|
106
|
-
while (i < lines.length && /^[-*+]\s/.test(lines[i])) {
|
|
107
|
-
items.push(lines[i].replace(/^[-*+]\s/, ''));
|
|
108
|
-
i++;
|
|
109
|
-
}
|
|
110
|
-
blocks.push({ type: 'ul', items });
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (/^\d+\.\s/.test(line)) {
|
|
115
|
-
const items = [line.replace(/^\d+\.\s/, '')];
|
|
116
|
-
i++;
|
|
117
|
-
while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
|
|
118
|
-
items.push(lines[i].replace(/^\d+\.\s/, ''));
|
|
119
|
-
i++;
|
|
120
|
-
}
|
|
121
|
-
blocks.push({ type: 'ol', items });
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (line.includes('|') && i + 1 < lines.length && /^\|?\s*[-:]+/.test(lines[i + 1])) {
|
|
126
|
-
const headerCells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
127
|
-
i += 2;
|
|
128
|
-
const rows = [];
|
|
129
|
-
while (i < lines.length && lines[i].includes('|')) {
|
|
130
|
-
rows.push(lines[i].split('|').map((c) => c.trim()).filter(Boolean));
|
|
131
|
-
i++;
|
|
132
|
-
}
|
|
133
|
-
blocks.push({ type: 'table', headers: headerCells, rows });
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (line.trim() === '') {
|
|
138
|
-
i++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const paraLines = [line];
|
|
143
|
-
i++;
|
|
144
|
-
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())) {
|
|
145
|
-
paraLines.push(lines[i]);
|
|
146
|
-
i++;
|
|
147
|
-
}
|
|
148
|
-
blocks.push({ type: 'paragraph', text: paraLines.join('\n') });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return blocks;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function InlineMarkdown({ text }) {
|
|
155
|
-
if (!text) return null;
|
|
156
|
-
const parts = text.split(/(```[\s\S]*?```|`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|~~[^~]+~~|\[([^\]]+)\]\(([^)]+)\))/g);
|
|
157
|
-
return (
|
|
158
|
-
<>
|
|
159
|
-
{parts.map((part, i) => {
|
|
160
|
-
if (!part) return null;
|
|
161
|
-
if (part.startsWith('`') && part.endsWith('`') && !part.startsWith('``')) {
|
|
162
|
-
return <code key={i} className="px-1.5 py-0.5 rounded bg-surface-0 text-xs font-mono text-accent">{part.slice(1, -1)}</code>;
|
|
163
|
-
}
|
|
164
|
-
if (part.startsWith('**') && part.endsWith('**')) {
|
|
165
|
-
return <strong key={i} className="font-semibold text-text-0">{part.slice(2, -2)}</strong>;
|
|
166
|
-
}
|
|
167
|
-
if (part.startsWith('*') && part.endsWith('*') && !part.startsWith('**')) {
|
|
168
|
-
return <em key={i} className="italic">{part.slice(1, -1)}</em>;
|
|
169
|
-
}
|
|
170
|
-
if (part.startsWith('~~') && part.endsWith('~~')) {
|
|
171
|
-
return <del key={i} className="line-through text-text-3">{part.slice(2, -2)}</del>;
|
|
172
|
-
}
|
|
173
|
-
const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
174
|
-
if (linkMatch) {
|
|
175
|
-
return <a key={i} href={linkMatch[2]} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">{linkMatch[1]}</a>;
|
|
176
|
-
}
|
|
177
|
-
return <span key={i}>{part}</span>;
|
|
178
|
-
})}
|
|
179
|
-
</>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function RenderedMarkdown({ text }) {
|
|
184
|
-
const blocks = parseMarkdown(text);
|
|
185
|
-
return (
|
|
186
|
-
<div className="space-y-2">
|
|
187
|
-
{blocks.map((block, i) => {
|
|
188
|
-
switch (block.type) {
|
|
189
|
-
case 'code':
|
|
190
|
-
return <CodeBlock key={i} language={block.language} code={block.code} />;
|
|
191
|
-
case 'heading': {
|
|
192
|
-
const sizes = ['text-lg font-bold', 'text-base font-bold', 'text-sm font-semibold', 'text-sm font-semibold', 'text-xs font-semibold', 'text-xs font-semibold'];
|
|
193
|
-
return <div key={i} className={cn(sizes[block.level - 1] || sizes[0], 'text-text-0 font-sans mt-3 mb-1')}><InlineMarkdown text={block.text} /></div>;
|
|
194
|
-
}
|
|
195
|
-
case 'hr':
|
|
196
|
-
return <hr key={i} className="border-border-subtle my-3" />;
|
|
197
|
-
case 'blockquote':
|
|
198
|
-
return (
|
|
199
|
-
<div key={i} className="border-l-2 border-accent/40 pl-3 py-1 text-sm text-text-2 italic font-sans">
|
|
200
|
-
<InlineMarkdown text={block.text} />
|
|
201
|
-
</div>
|
|
202
|
-
);
|
|
203
|
-
case 'ul':
|
|
204
|
-
return (
|
|
205
|
-
<ul key={i} className="list-disc list-inside space-y-0.5 text-sm text-text-0 font-sans">
|
|
206
|
-
{block.items.map((item, j) => <li key={j}><InlineMarkdown text={item} /></li>)}
|
|
207
|
-
</ul>
|
|
208
|
-
);
|
|
209
|
-
case 'ol':
|
|
210
|
-
return (
|
|
211
|
-
<ol key={i} className="list-decimal list-inside space-y-0.5 text-sm text-text-0 font-sans">
|
|
212
|
-
{block.items.map((item, j) => <li key={j}><InlineMarkdown text={item} /></li>)}
|
|
213
|
-
</ol>
|
|
214
|
-
);
|
|
215
|
-
case 'table':
|
|
216
|
-
return <TableTree key={i} headers={block.headers} rows={block.rows} />;
|
|
217
|
-
case 'paragraph':
|
|
218
|
-
return <p key={i} className="text-sm text-text-0 font-sans leading-relaxed whitespace-pre-wrap break-words"><InlineMarkdown text={block.text} /></p>;
|
|
219
|
-
default:
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
})}
|
|
223
|
-
</div>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function UserMessage({ msg }) {
|
|
228
|
-
return (
|
|
229
|
-
<div className="flex justify-end">
|
|
230
|
-
<div className="max-w-[85%]">
|
|
231
|
-
<div className="px-3.5 py-2.5 rounded-2xl rounded-br-md bg-info/10 border border-info/15">
|
|
232
|
-
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
|
|
233
|
-
</div>
|
|
234
|
-
<div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function AssistantMessage({ msg, model, role }) {
|
|
241
|
-
const cleanText = stripEmojis(msg.text);
|
|
242
|
-
const modelName = model || 'Assistant';
|
|
243
|
-
const roleLabel = role ? role.charAt(0).toUpperCase() + role.slice(1) : null;
|
|
244
|
-
return (
|
|
245
|
-
<div className="max-w-[85%]">
|
|
246
|
-
<div className="text-2xs font-sans mb-1 font-medium">
|
|
247
|
-
<span className="text-text-1">{modelName}</span>
|
|
248
|
-
{roleLabel && <span className="text-text-3 ml-1">{roleLabel}</span>}
|
|
249
|
-
</div>
|
|
250
|
-
<div className="border-l-2 border-accent pl-3.5 py-1">
|
|
251
|
-
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
252
|
-
<RenderedMarkdown text={cleanText} />
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
256
|
-
</div>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function ImageLoadingMessage({ msg }) {
|
|
261
|
-
return (
|
|
262
|
-
<div className="max-w-[85%]">
|
|
263
|
-
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
264
|
-
<div className="w-80 h-80 image-loading-shimmer flex items-center justify-center">
|
|
265
|
-
<div className="flex flex-col items-center gap-3">
|
|
266
|
-
<div className="w-10 h-10 rounded-full bg-surface-3/80 flex items-center justify-center">
|
|
267
|
-
<ImageIcon size={18} className="text-accent animate-pulse" />
|
|
268
|
-
</div>
|
|
269
|
-
<span className="text-xs text-text-3 font-sans">Generating image...</span>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
{msg.prompt && (
|
|
273
|
-
<div className="px-4 py-2.5 border-t border-border-subtle">
|
|
274
|
-
<p className="text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function ImageMessage({ msg, onReply }) {
|
|
283
|
-
const [expanded, setExpanded] = useState(false);
|
|
284
|
-
const [hovering, setHovering] = useState(false);
|
|
285
|
-
|
|
286
|
-
const handleDownload = useCallback(() => {
|
|
287
|
-
if (!msg.imageUrl) return;
|
|
288
|
-
const a = document.createElement('a');
|
|
289
|
-
a.href = msg.imageUrl;
|
|
290
|
-
a.download = `groove-${msg.model || 'image'}-${Date.now()}.png`;
|
|
291
|
-
document.body.appendChild(a);
|
|
292
|
-
a.click();
|
|
293
|
-
document.body.removeChild(a);
|
|
294
|
-
}, [msg.imageUrl, msg.model]);
|
|
295
|
-
|
|
296
|
-
return (
|
|
297
|
-
<>
|
|
298
|
-
<div className="max-w-[85%]">
|
|
299
|
-
{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>}
|
|
300
|
-
<div className="rounded-2xl rounded-tl-md bg-surface-1/80 border border-border-subtle overflow-hidden">
|
|
301
|
-
<div
|
|
302
|
-
className="relative group"
|
|
303
|
-
onMouseEnter={() => setHovering(true)}
|
|
304
|
-
onMouseLeave={() => setHovering(false)}
|
|
305
|
-
>
|
|
306
|
-
<img
|
|
307
|
-
src={msg.imageUrl}
|
|
308
|
-
alt={msg.prompt || 'Generated image'}
|
|
309
|
-
className="max-w-full max-h-[480px] object-contain cursor-pointer"
|
|
310
|
-
onClick={() => setExpanded(true)}
|
|
311
|
-
/>
|
|
312
|
-
{hovering && (
|
|
313
|
-
<div className="absolute top-2 right-2 flex gap-1.5">
|
|
314
|
-
<button
|
|
315
|
-
onClick={handleDownload}
|
|
316
|
-
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"
|
|
317
|
-
title="Download"
|
|
318
|
-
>
|
|
319
|
-
<Download size={14} />
|
|
320
|
-
</button>
|
|
321
|
-
<button
|
|
322
|
-
onClick={() => setExpanded(true)}
|
|
323
|
-
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"
|
|
324
|
-
title="Fullscreen"
|
|
325
|
-
>
|
|
326
|
-
<Maximize2 size={14} />
|
|
327
|
-
</button>
|
|
328
|
-
<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" />
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
|
-
</div>
|
|
332
|
-
<div className="px-4 py-2.5 border-t border-border-subtle flex items-center gap-2">
|
|
333
|
-
<p className="flex-1 text-2xs text-text-3 font-sans italic truncate">"{msg.prompt}"</p>
|
|
334
|
-
{onReply && (
|
|
335
|
-
<button
|
|
336
|
-
onClick={() => onReply(msg)}
|
|
337
|
-
className="text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer flex items-center gap-1 flex-shrink-0"
|
|
338
|
-
>
|
|
339
|
-
<RefreshCw size={10} /> Iterate
|
|
340
|
-
</button>
|
|
341
|
-
)}
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
|
-
{expanded && (
|
|
348
|
-
<div
|
|
349
|
-
className="fixed inset-0 z-[200] bg-surface-0/95 backdrop-blur-md flex items-center justify-center"
|
|
350
|
-
onClick={() => setExpanded(false)}
|
|
351
|
-
>
|
|
352
|
-
<button
|
|
353
|
-
onClick={() => setExpanded(false)}
|
|
354
|
-
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"
|
|
355
|
-
>
|
|
356
|
-
<X size={18} />
|
|
357
|
-
</button>
|
|
358
|
-
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
|
359
|
-
<button
|
|
360
|
-
onClick={(e) => { e.stopPropagation(); handleDownload(); }}
|
|
361
|
-
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"
|
|
362
|
-
>
|
|
363
|
-
<Download size={14} /> Download
|
|
364
|
-
</button>
|
|
365
|
-
</div>
|
|
366
|
-
<img
|
|
367
|
-
src={msg.imageUrl}
|
|
368
|
-
alt={msg.prompt || 'Generated image'}
|
|
369
|
-
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg shadow-2xl"
|
|
370
|
-
onClick={(e) => e.stopPropagation()}
|
|
371
|
-
/>
|
|
372
|
-
{msg.prompt && (
|
|
373
|
-
<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">
|
|
374
|
-
<p className="text-xs text-text-2 font-sans italic">"{msg.prompt}"</p>
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
</div>
|
|
378
|
-
)}
|
|
379
|
-
</>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function SystemMessage({ msg }) {
|
|
384
|
-
return (
|
|
385
|
-
<div className="flex justify-center py-1">
|
|
386
|
-
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-4/50">
|
|
387
|
-
<ArrowRight size={10} className="text-text-4" />
|
|
388
|
-
<span className="text-2xs text-text-3 font-sans">{msg.text}</span>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function InnerChatMessage({ msg }) {
|
|
395
|
-
const senderName = msg.innerchat?.fromAgent?.name || 'Agent';
|
|
396
|
-
const senderRole = msg.innerchat?.fromAgent?.role;
|
|
397
|
-
const cleanText = stripEmojis(msg.text);
|
|
398
|
-
return (
|
|
399
|
-
<div className="max-w-[85%]">
|
|
400
|
-
<div className="flex items-center gap-1.5 mb-1">
|
|
401
|
-
<MessageSquare size={11} className="text-warning" />
|
|
402
|
-
<span className="text-2xs font-sans font-medium text-warning">
|
|
403
|
-
{msg.innerchat?.kind === 'reply' ? 'InnerChat reply' : 'InnerChat'}
|
|
404
|
-
</span>
|
|
405
|
-
<span className="text-2xs font-sans text-text-3">from</span>
|
|
406
|
-
<span className="text-2xs font-sans font-medium text-text-1">{senderName}</span>
|
|
407
|
-
{senderRole && <span className="text-2xs font-sans text-text-3">{senderRole}</span>}
|
|
408
|
-
</div>
|
|
409
|
-
<div className="border-l-2 border-warning/40 pl-3.5 py-1">
|
|
410
|
-
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
411
|
-
<RenderedMarkdown text={cleanText} />
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
415
|
-
</div>
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function StreamingCursor() {
|
|
420
|
-
return (
|
|
421
|
-
<span className="inline-block w-2 h-4 bg-accent/60 ml-0.5 animate-pulse rounded-sm" />
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function WelcomeMessage() {
|
|
426
|
-
return (
|
|
427
|
-
<div className="flex flex-col items-center justify-center h-full text-center py-16">
|
|
428
|
-
<p className="text-sm text-text-3 font-sans">Send a message to start</p>
|
|
429
|
-
</div>
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function ApiTypingIndicator() {
|
|
434
|
-
const [idx, setIdx] = useState(0);
|
|
435
|
-
const [fade, setFade] = useState(true);
|
|
436
|
-
|
|
437
|
-
useEffect(() => {
|
|
438
|
-
const t = setInterval(() => {
|
|
439
|
-
setFade(false);
|
|
440
|
-
setTimeout(() => {
|
|
441
|
-
setIdx((i) => (i + 1) % API_STATUS_MESSAGES.length);
|
|
442
|
-
setFade(true);
|
|
443
|
-
}, 250);
|
|
444
|
-
}, 2800);
|
|
445
|
-
return () => clearInterval(t);
|
|
446
|
-
}, []);
|
|
447
|
-
|
|
448
|
-
return (
|
|
449
|
-
<div className="flex items-center gap-2.5 ml-8 py-1">
|
|
450
|
-
<div className="relative w-3.5 h-3.5 flex-shrink-0">
|
|
451
|
-
<span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
|
|
452
|
-
</div>
|
|
453
|
-
<span
|
|
454
|
-
className="text-2xs font-sans text-text-3 transition-opacity duration-[250ms]"
|
|
455
|
-
style={{ opacity: fade ? 1 : 0 }}
|
|
456
|
-
>
|
|
457
|
-
{API_STATUS_MESSAGES[idx]}
|
|
458
|
-
</span>
|
|
459
|
-
</div>
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function ToolCallIndicator({ tool }) {
|
|
464
|
-
return (
|
|
465
|
-
<div className="max-w-[85%] py-1">
|
|
466
|
-
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg bg-surface-3/50 border border-border-subtle/30">
|
|
467
|
-
<Loader2 size={13} className="text-accent animate-spin flex-shrink-0" />
|
|
468
|
-
<div className="flex-1 min-w-0">
|
|
469
|
-
<span className="text-xs font-semibold font-sans text-text-2">{tool.name}</span>
|
|
470
|
-
{tool.summary && (
|
|
471
|
-
<span className="text-xs font-mono text-text-4 ml-1.5 truncate">{tool.summary}</span>
|
|
472
|
-
)}
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
</div>
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export function ChatMessages({ messages, isStreaming, model, mode, onImageReply, role, activeTool }) {
|
|
480
|
-
const scrollRef = useRef(null);
|
|
481
|
-
const isAtBottomRef = useRef(true);
|
|
482
|
-
|
|
483
|
-
useEffect(() => {
|
|
484
|
-
const el = scrollRef.current;
|
|
485
|
-
if (!el) return;
|
|
486
|
-
function handleScroll() {
|
|
487
|
-
isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
|
|
488
|
-
}
|
|
489
|
-
el.addEventListener('scroll', handleScroll);
|
|
490
|
-
return () => el.removeEventListener('scroll', handleScroll);
|
|
491
|
-
}, []);
|
|
492
|
-
|
|
493
|
-
useEffect(() => {
|
|
494
|
-
if (isAtBottomRef.current && scrollRef.current) {
|
|
495
|
-
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
496
|
-
}
|
|
497
|
-
}, [messages?.length, isStreaming, activeTool]);
|
|
498
|
-
|
|
499
|
-
if (!messages || messages.length === 0) {
|
|
500
|
-
return (
|
|
501
|
-
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
|
502
|
-
<WelcomeMessage />
|
|
503
|
-
</div>
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return (
|
|
508
|
-
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
|
|
509
|
-
{messages.map((msg, i) => {
|
|
510
|
-
if (msg.type === 'image-loading') return <ImageLoadingMessage key={i} msg={msg} />;
|
|
511
|
-
if (msg.type === 'image') return <ImageMessage key={i} msg={msg} onReply={onImageReply} />;
|
|
512
|
-
if (msg.from === 'innerchat') return <InnerChatMessage key={i} msg={msg} />;
|
|
513
|
-
if (msg.from === 'user') return <UserMessage key={i} msg={msg} />;
|
|
514
|
-
if (msg.from === 'system') return <SystemMessage key={i} msg={msg} />;
|
|
515
|
-
return <AssistantMessage key={i} msg={msg} model={model} role={role} />;
|
|
516
|
-
})}
|
|
517
|
-
{activeTool && <ToolCallIndicator tool={activeTool} />}
|
|
518
|
-
{isStreaming && !activeTool && (
|
|
519
|
-
mode === 'agent' ? (
|
|
520
|
-
<ThinkingIndicator className="py-1" />
|
|
521
|
-
) : (
|
|
522
|
-
<ApiTypingIndicator />
|
|
523
|
-
)
|
|
524
|
-
)}
|
|
525
|
-
</div>
|
|
526
|
-
);
|
|
527
|
-
}
|