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,198 +0,0 @@
|
|
|
1
|
-
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Pencil, Pin, PinOff, Trash2, Hash, MoreHorizontal, ChevronDown, X } from 'lucide-react';
|
|
4
|
-
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
-
import { fmtNum } from '../../lib/format';
|
|
6
|
-
import { cn } from '../../lib/cn';
|
|
7
|
-
import { roleColor } from '../../lib/status';
|
|
8
|
-
|
|
9
|
-
const CHAT_ROLES = [
|
|
10
|
-
{ id: 'chat', label: 'Chat', desc: 'General conversation' },
|
|
11
|
-
{ id: 'research', label: 'Research Assistant', desc: 'Explore ideas, web search' },
|
|
12
|
-
{ id: 'fullstack', label: 'Fullstack', desc: 'End-to-end engineering' },
|
|
13
|
-
{ id: 'backend', label: 'Backend', desc: 'APIs, services, databases' },
|
|
14
|
-
{ id: 'frontend', label: 'Frontend', desc: 'UI, components, styling' },
|
|
15
|
-
{ id: 'devops', label: 'DevOps', desc: 'CI/CD, infra, deployment' },
|
|
16
|
-
{ id: 'security', label: 'Security', desc: 'Audits, vulnerabilities' },
|
|
17
|
-
{ id: 'database', label: 'Database', desc: 'Schema, migrations, queries' },
|
|
18
|
-
{ id: 'testing', label: 'Testing', desc: 'Tests, coverage, QA' },
|
|
19
|
-
{ id: 'docs', label: 'Docs', desc: 'Documentation, guides' },
|
|
20
|
-
{ id: 'cmo', label: 'CMO', desc: 'Marketing, content, growth' },
|
|
21
|
-
{ id: 'cfo', label: 'CFO', desc: 'Finance, metrics, forecasting' },
|
|
22
|
-
{ id: 'ea', label: 'EA', desc: 'Executive assistant' },
|
|
23
|
-
{ id: 'analyst', label: 'Analyst', desc: 'Data analysis, insights' },
|
|
24
|
-
{ id: 'creative', label: 'Writer', desc: 'Copy, articles, proposals' },
|
|
25
|
-
{ id: 'support', label: 'Support', desc: 'Customer support, FAQs' },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
export function ChatHeader({ conversation, role, onRoleChange, sidebarCollapsed }) {
|
|
29
|
-
const renameConversation = useGrooveStore((s) => s.renameConversation);
|
|
30
|
-
const pinConversation = useGrooveStore((s) => s.pinConversation);
|
|
31
|
-
const deleteConversation = useGrooveStore((s) => s.deleteConversation);
|
|
32
|
-
|
|
33
|
-
const [editing, setEditing] = useState(false);
|
|
34
|
-
const [title, setTitle] = useState(conversation.title || '');
|
|
35
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
36
|
-
const [roleMenuOpen, setRoleMenuOpen] = useState(false);
|
|
37
|
-
const inputRef = useRef(null);
|
|
38
|
-
const menuRef = useRef(null);
|
|
39
|
-
const roleMenuRef = useRef(null);
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
setTitle(conversation.title || '');
|
|
43
|
-
setEditing(false);
|
|
44
|
-
}, [conversation.id, conversation.title]);
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (editing && inputRef.current) inputRef.current.focus();
|
|
48
|
-
}, [editing]);
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!menuOpen) return;
|
|
52
|
-
function handleClick(e) {
|
|
53
|
-
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
54
|
-
}
|
|
55
|
-
document.addEventListener('mousedown', handleClick);
|
|
56
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
57
|
-
}, [menuOpen]);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (!roleMenuOpen) return;
|
|
61
|
-
function handleClick(e) {
|
|
62
|
-
if (roleMenuRef.current && !roleMenuRef.current.contains(e.target)) setRoleMenuOpen(false);
|
|
63
|
-
}
|
|
64
|
-
document.addEventListener('mousedown', handleClick);
|
|
65
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
66
|
-
}, [roleMenuOpen]);
|
|
67
|
-
|
|
68
|
-
function handleRename() {
|
|
69
|
-
const trimmed = title.trim();
|
|
70
|
-
if (trimmed && trimmed !== conversation.title) {
|
|
71
|
-
renameConversation(conversation.id, trimmed);
|
|
72
|
-
} else {
|
|
73
|
-
setTitle(conversation.title || '');
|
|
74
|
-
}
|
|
75
|
-
setEditing(false);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const agent = useGrooveStore((s) => s.agents.find((a) => a.id === conversation.agentId));
|
|
79
|
-
const tokens = agent?.tokensUsed || 0;
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<div className={cn("h-12 flex items-center gap-3 border-b border-border-subtle bg-surface-0/80 flex-shrink-0", sidebarCollapsed ? 'pl-10 pr-4' : 'px-4')}>
|
|
83
|
-
<Hash size={14} className="text-text-4 flex-shrink-0" />
|
|
84
|
-
|
|
85
|
-
{editing ? (
|
|
86
|
-
<input
|
|
87
|
-
ref={inputRef}
|
|
88
|
-
value={title}
|
|
89
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
90
|
-
onKeyDown={(e) => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') { setTitle(conversation.title || ''); setEditing(false); } }}
|
|
91
|
-
onBlur={handleRename}
|
|
92
|
-
className="flex-1 min-w-0 bg-transparent text-sm font-semibold text-text-0 font-sans outline-none border-b border-accent"
|
|
93
|
-
maxLength={100}
|
|
94
|
-
/>
|
|
95
|
-
) : (
|
|
96
|
-
<button
|
|
97
|
-
onClick={() => setEditing(true)}
|
|
98
|
-
className="flex-1 min-w-0 text-left text-sm font-semibold text-text-0 font-sans truncate hover:text-accent transition-colors cursor-pointer"
|
|
99
|
-
>
|
|
100
|
-
{conversation.title || 'New Chat'}
|
|
101
|
-
</button>
|
|
102
|
-
)}
|
|
103
|
-
|
|
104
|
-
<div className="w-px h-5 bg-border-subtle" />
|
|
105
|
-
|
|
106
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
107
|
-
<div ref={roleMenuRef} className="relative">
|
|
108
|
-
<button
|
|
109
|
-
onClick={() => setRoleMenuOpen(!roleMenuOpen)}
|
|
110
|
-
className={cn(
|
|
111
|
-
'flex items-center gap-1.5 h-7 px-2.5 rounded-lg text-2xs font-semibold font-sans transition-colors cursor-pointer border',
|
|
112
|
-
role
|
|
113
|
-
? 'border-current/20'
|
|
114
|
-
: 'bg-surface-3 border-border-subtle text-text-3 hover:text-text-1',
|
|
115
|
-
)}
|
|
116
|
-
style={role ? { background: roleColor(role).bg, color: roleColor(role).text, borderColor: `${roleColor(role).border}33` } : undefined}
|
|
117
|
-
>
|
|
118
|
-
{role ? CHAT_ROLES.find((r) => r.id === role)?.label || role : 'Role'}
|
|
119
|
-
{role ? (
|
|
120
|
-
<X
|
|
121
|
-
size={10}
|
|
122
|
-
className="opacity-60 hover:opacity-100"
|
|
123
|
-
onClick={(e) => { e.stopPropagation(); onRoleChange?.(null); }}
|
|
124
|
-
/>
|
|
125
|
-
) : (
|
|
126
|
-
<ChevronDown size={10} />
|
|
127
|
-
)}
|
|
128
|
-
</button>
|
|
129
|
-
{roleMenuOpen && (
|
|
130
|
-
<div className="absolute left-0 top-full mt-1 w-56 rounded-lg border border-border bg-surface-1 shadow-xl z-50 py-1 max-h-72 overflow-y-auto">
|
|
131
|
-
{CHAT_ROLES.map((r) => {
|
|
132
|
-
const rc = roleColor(r.id);
|
|
133
|
-
const active = role === r.id;
|
|
134
|
-
return (
|
|
135
|
-
<button
|
|
136
|
-
key={r.id}
|
|
137
|
-
onClick={() => { onRoleChange?.(r.id); setRoleMenuOpen(false); }}
|
|
138
|
-
className={cn(
|
|
139
|
-
'w-full flex items-center gap-2.5 px-3 py-2 text-left hover:bg-surface-3 cursor-pointer transition-colors',
|
|
140
|
-
active && 'bg-surface-3',
|
|
141
|
-
)}
|
|
142
|
-
>
|
|
143
|
-
<span
|
|
144
|
-
className="w-2 h-2 rounded-full flex-shrink-0"
|
|
145
|
-
style={{ background: rc.text }}
|
|
146
|
-
/>
|
|
147
|
-
<div className="flex-1 min-w-0">
|
|
148
|
-
<div className="text-xs font-semibold font-sans text-text-0">{r.label}</div>
|
|
149
|
-
<div className="text-2xs text-text-3 font-sans">{r.desc}</div>
|
|
150
|
-
</div>
|
|
151
|
-
{active && <span className="text-2xs text-accent font-mono">active</span>}
|
|
152
|
-
</button>
|
|
153
|
-
);
|
|
154
|
-
})}
|
|
155
|
-
</div>
|
|
156
|
-
)}
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{tokens > 0 && (
|
|
160
|
-
<span className="text-2xs text-text-3 font-mono tabular-nums">{fmtNum(tokens)} tok</span>
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
<div ref={menuRef} className="relative">
|
|
164
|
-
<button
|
|
165
|
-
onClick={() => setMenuOpen(!menuOpen)}
|
|
166
|
-
className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer"
|
|
167
|
-
>
|
|
168
|
-
<MoreHorizontal size={14} />
|
|
169
|
-
</button>
|
|
170
|
-
{menuOpen && (
|
|
171
|
-
<div className="absolute right-0 top-full mt-1 w-40 rounded-md border border-border bg-surface-1 shadow-xl z-50 py-1">
|
|
172
|
-
<button
|
|
173
|
-
onClick={() => { setEditing(true); setMenuOpen(false); }}
|
|
174
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-text-1 hover:bg-surface-5 cursor-pointer font-sans"
|
|
175
|
-
>
|
|
176
|
-
<Pencil size={12} /> Rename
|
|
177
|
-
</button>
|
|
178
|
-
<button
|
|
179
|
-
onClick={() => { pinConversation(conversation.id, !conversation.pinned); setMenuOpen(false); }}
|
|
180
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-text-1 hover:bg-surface-5 cursor-pointer font-sans"
|
|
181
|
-
>
|
|
182
|
-
{conversation.pinned ? <PinOff size={12} /> : <Pin size={12} />}
|
|
183
|
-
{conversation.pinned ? 'Unpin' : 'Pin'}
|
|
184
|
-
</button>
|
|
185
|
-
<div className="h-px my-1 bg-border-subtle" />
|
|
186
|
-
<button
|
|
187
|
-
onClick={() => { deleteConversation(conversation.id); setMenuOpen(false); }}
|
|
188
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-danger hover:bg-danger/10 cursor-pointer font-sans"
|
|
189
|
-
>
|
|
190
|
-
<Trash2 size={12} /> Delete
|
|
191
|
-
</button>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
}
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
-
import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal, ChevronUp } from 'lucide-react';
|
|
4
|
-
import { cn } from '../../lib/cn';
|
|
5
|
-
import { formatModelName, isImageModel as checkImageModel, getTier, getContextSize, TIER_CONFIG } from './model-picker';
|
|
6
|
-
import { useGrooveStore } from '../../stores/groove';
|
|
7
|
-
import { Badge } from '../ui/badge';
|
|
8
|
-
|
|
9
|
-
const EFFORT_OPTIONS = [
|
|
10
|
-
{ value: 'none', label: 'None' },
|
|
11
|
-
{ value: 'low', label: 'Low' },
|
|
12
|
-
{ value: 'medium', label: 'Med' },
|
|
13
|
-
{ value: 'high', label: 'High' },
|
|
14
|
-
{ value: 'xhigh', label: 'XHigh' },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const VERBOSITY_OPTIONS = [
|
|
18
|
-
{ value: 'low', label: 'Concise' },
|
|
19
|
-
{ value: 'medium', label: 'Normal' },
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, currentProvider, onModelChange, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
|
|
23
|
-
const [input, setInput] = useState('');
|
|
24
|
-
const [inputHeight, setInputHeight] = useState(88);
|
|
25
|
-
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
26
|
-
const textareaRef = useRef(null);
|
|
27
|
-
const fileInputRef = useRef(null);
|
|
28
|
-
const modelPickerRef = useRef(null);
|
|
29
|
-
|
|
30
|
-
const [providers, setProviders] = useState([]);
|
|
31
|
-
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
fetchProviders().then((data) => {
|
|
35
|
-
if (Array.isArray(data)) setProviders(data);
|
|
36
|
-
else if (data?.providers) setProviders(data.providers);
|
|
37
|
-
}).catch(() => {});
|
|
38
|
-
}, [fetchProviders]);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (!disabled && textareaRef.current) textareaRef.current.focus();
|
|
42
|
-
}, [disabled]);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!modelPickerOpen) return;
|
|
46
|
-
function handleClick(e) {
|
|
47
|
-
if (modelPickerRef.current && !modelPickerRef.current.contains(e.target)) setModelPickerOpen(false);
|
|
48
|
-
}
|
|
49
|
-
document.addEventListener('mousedown', handleClick);
|
|
50
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
51
|
-
}, [modelPickerOpen]);
|
|
52
|
-
|
|
53
|
-
function handleSend() {
|
|
54
|
-
const text = input.trim();
|
|
55
|
-
if (!text || sending || disabled) return;
|
|
56
|
-
onSend(text);
|
|
57
|
-
setInput('');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function onKeyDown(e) {
|
|
61
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
handleSend();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function handleFileSelect(e) {
|
|
68
|
-
const files = Array.from(e.target.files || []);
|
|
69
|
-
if (files.length === 0) return;
|
|
70
|
-
const pathList = files.map((f) => f.name).join(', ');
|
|
71
|
-
setInput((prev) => prev + (prev ? '\n' : '') + `[Attached: ${pathList}]`);
|
|
72
|
-
e.target.value = '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const onDragStart = useCallback((e) => {
|
|
76
|
-
e.preventDefault();
|
|
77
|
-
const startY = e.clientY;
|
|
78
|
-
const startH = inputHeight;
|
|
79
|
-
const onMove = (ev) => setInputHeight(Math.min(Math.max(56, startH - (ev.clientY - startY)), 400));
|
|
80
|
-
const onUp = () => {
|
|
81
|
-
window.removeEventListener('mousemove', onMove);
|
|
82
|
-
window.removeEventListener('mouseup', onUp);
|
|
83
|
-
};
|
|
84
|
-
window.addEventListener('mousemove', onMove);
|
|
85
|
-
window.addEventListener('mouseup', onUp);
|
|
86
|
-
}, [inputHeight]);
|
|
87
|
-
|
|
88
|
-
const isActive = streaming || sending;
|
|
89
|
-
const canSend = input.trim() && !sending && !disabled;
|
|
90
|
-
const currentMode = mode || 'api';
|
|
91
|
-
|
|
92
|
-
const placeholder = disabled
|
|
93
|
-
? 'Select a model to start chatting...'
|
|
94
|
-
: isImageModel
|
|
95
|
-
? 'Describe the image you want to generate...'
|
|
96
|
-
: role
|
|
97
|
-
? `Ask your ${role}...`
|
|
98
|
-
: 'Send a message...';
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div className="px-4 pb-3">
|
|
102
|
-
<div
|
|
103
|
-
onMouseDown={onDragStart}
|
|
104
|
-
className="flex items-center justify-center h-5 cursor-row-resize group"
|
|
105
|
-
>
|
|
106
|
-
<GripHorizontal size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
{replyContext && (
|
|
110
|
-
<div className="flex items-center gap-2 mb-2 px-3 py-2 rounded-lg bg-accent/5 border border-accent/15">
|
|
111
|
-
<ImageIcon size={12} className="text-accent flex-shrink-0" />
|
|
112
|
-
<span className="flex-1 text-2xs text-text-2 font-sans truncate">Iterating: "{replyContext.prompt}"</span>
|
|
113
|
-
<button onClick={onClearReply} className="text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0">
|
|
114
|
-
<Square size={10} />
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
|
|
119
|
-
<div className="flex flex-col rounded-lg border border-border-subtle bg-surface-0 transition-colors overflow-hidden focus-within:border-text-4/40">
|
|
120
|
-
<input
|
|
121
|
-
ref={fileInputRef}
|
|
122
|
-
type="file"
|
|
123
|
-
multiple
|
|
124
|
-
accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
|
|
125
|
-
onChange={handleFileSelect}
|
|
126
|
-
className="hidden"
|
|
127
|
-
/>
|
|
128
|
-
|
|
129
|
-
<div className="px-1">
|
|
130
|
-
<textarea
|
|
131
|
-
ref={textareaRef}
|
|
132
|
-
value={input}
|
|
133
|
-
onChange={(e) => setInput(e.target.value)}
|
|
134
|
-
onKeyDown={onKeyDown}
|
|
135
|
-
placeholder={placeholder}
|
|
136
|
-
disabled={disabled}
|
|
137
|
-
rows={1}
|
|
138
|
-
className={cn(
|
|
139
|
-
'w-full resize-none px-3 py-2.5 text-[13px]',
|
|
140
|
-
'bg-transparent font-sans text-text-0',
|
|
141
|
-
'placeholder:text-text-4',
|
|
142
|
-
'focus:outline-none',
|
|
143
|
-
'disabled:opacity-40 disabled:cursor-not-allowed',
|
|
144
|
-
)}
|
|
145
|
-
style={{ height: inputHeight }}
|
|
146
|
-
/>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
<div className="flex items-center gap-1.5 px-1.5 pb-1.5 pt-0.5">
|
|
150
|
-
<button
|
|
151
|
-
onClick={() => fileInputRef.current?.click()}
|
|
152
|
-
disabled={disabled}
|
|
153
|
-
className="w-7 h-7 flex items-center justify-center rounded-md text-text-4 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
|
|
154
|
-
title="Attach file"
|
|
155
|
-
>
|
|
156
|
-
<Paperclip size={14} />
|
|
157
|
-
</button>
|
|
158
|
-
|
|
159
|
-
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
160
|
-
<button
|
|
161
|
-
onClick={() => onModeChange?.('api')}
|
|
162
|
-
disabled={modeChanging}
|
|
163
|
-
className={cn(
|
|
164
|
-
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
165
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
166
|
-
currentMode === 'api' ? 'bg-accent/15 text-accent' : 'text-text-3 hover:text-text-1',
|
|
167
|
-
)}
|
|
168
|
-
title="Lightweight — fast and cheap, no tools"
|
|
169
|
-
>
|
|
170
|
-
<Zap size={10} /> Chat
|
|
171
|
-
</button>
|
|
172
|
-
<button
|
|
173
|
-
onClick={() => onModeChange?.('agent')}
|
|
174
|
-
disabled={modeChanging}
|
|
175
|
-
className={cn(
|
|
176
|
-
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
177
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
178
|
-
currentMode === 'agent' ? 'bg-purple/15 text-purple' : 'text-text-3 hover:text-text-1',
|
|
179
|
-
)}
|
|
180
|
-
title="Full agent — tools, files, session resume"
|
|
181
|
-
>
|
|
182
|
-
<Bot size={10} /> Agent
|
|
183
|
-
</button>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<div ref={modelPickerRef} className="relative">
|
|
187
|
-
<button
|
|
188
|
-
onClick={() => setModelPickerOpen(!modelPickerOpen)}
|
|
189
|
-
className={cn(
|
|
190
|
-
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-mono transition-colors cursor-pointer',
|
|
191
|
-
isImageModel
|
|
192
|
-
? 'bg-purple/8 text-purple hover:bg-purple/15'
|
|
193
|
-
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
194
|
-
)}
|
|
195
|
-
>
|
|
196
|
-
{isImageModel && <ImageIcon size={9} />}
|
|
197
|
-
<span className="max-w-[120px] truncate">{currentModel ? formatModelName(currentModel) : 'Select model'}</span>
|
|
198
|
-
<ChevronUp size={10} className="text-text-4 flex-shrink-0" />
|
|
199
|
-
</button>
|
|
200
|
-
|
|
201
|
-
{modelPickerOpen && (() => {
|
|
202
|
-
const chatProviders = [];
|
|
203
|
-
const imageProviders = [];
|
|
204
|
-
for (const provider of providers) {
|
|
205
|
-
const models = provider.models || [];
|
|
206
|
-
const chat = [];
|
|
207
|
-
const img = [];
|
|
208
|
-
for (const m of models) {
|
|
209
|
-
const mid = typeof m === 'string' ? m : m.id || m.name;
|
|
210
|
-
const mtype = typeof m === 'object' ? m.type : undefined;
|
|
211
|
-
if (mtype === 'image' || checkImageModel(mid)) img.push(m);
|
|
212
|
-
else chat.push(m);
|
|
213
|
-
}
|
|
214
|
-
if (chat.length) chatProviders.push({ ...provider, models: chat });
|
|
215
|
-
if (img.length) imageProviders.push({ ...provider, models: img });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const renderModel = (provider, model) => {
|
|
219
|
-
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
220
|
-
const modelName = typeof model === 'string' ? model : model.name || model.id;
|
|
221
|
-
const mtype = typeof model === 'object' ? model.type : undefined;
|
|
222
|
-
const isImg = mtype === 'image' || checkImageModel(modelId);
|
|
223
|
-
const tier = isImg ? null : getTier(modelId);
|
|
224
|
-
const tierCfg = tier ? TIER_CONFIG[tier] : null;
|
|
225
|
-
const TierIcon = tierCfg?.icon;
|
|
226
|
-
const isActive = currentModel === modelId && currentProvider === provider.id;
|
|
227
|
-
return (
|
|
228
|
-
<button
|
|
229
|
-
key={modelId}
|
|
230
|
-
onClick={() => {
|
|
231
|
-
onModelChange?.({ provider: provider.id, model: modelId });
|
|
232
|
-
setModelPickerOpen(false);
|
|
233
|
-
}}
|
|
234
|
-
className={cn(
|
|
235
|
-
'w-full flex items-center gap-3 px-3.5 py-2.5 text-left transition-colors cursor-pointer',
|
|
236
|
-
isActive ? 'bg-accent/10' : 'hover:bg-surface-3',
|
|
237
|
-
)}
|
|
238
|
-
>
|
|
239
|
-
<div className="flex-1 min-w-0">
|
|
240
|
-
<div className="flex items-center gap-1.5">
|
|
241
|
-
{isImg && <ImageIcon size={11} className="text-purple flex-shrink-0" />}
|
|
242
|
-
<span className={cn('text-xs font-medium font-sans truncate', isActive ? 'text-accent' : 'text-text-0')}>{modelName}</span>
|
|
243
|
-
</div>
|
|
244
|
-
{!isImg && (
|
|
245
|
-
<div className="text-2xs text-text-4 font-sans mt-0.5">{getContextSize(modelId)} context</div>
|
|
246
|
-
)}
|
|
247
|
-
</div>
|
|
248
|
-
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
249
|
-
{isImg ? (
|
|
250
|
-
<Badge variant="purple" className="text-[9px]">
|
|
251
|
-
<ImageIcon size={8} /> Image
|
|
252
|
-
</Badge>
|
|
253
|
-
) : tierCfg && (
|
|
254
|
-
<Badge variant={tierCfg.variant} className="text-[9px]">
|
|
255
|
-
<TierIcon size={8} /> {tierCfg.label}
|
|
256
|
-
</Badge>
|
|
257
|
-
)}
|
|
258
|
-
</div>
|
|
259
|
-
</button>
|
|
260
|
-
);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div className="absolute bottom-full left-0 mb-1.5 w-80 max-h-[60vh] overflow-y-auto rounded-xl border border-border bg-surface-1 shadow-2xl z-50">
|
|
265
|
-
{providers.length === 0 && (
|
|
266
|
-
<div className="px-4 py-8 text-center text-xs text-text-3 font-sans">No providers available</div>
|
|
267
|
-
)}
|
|
268
|
-
{chatProviders.map((provider) => (
|
|
269
|
-
<div key={provider.id}>
|
|
270
|
-
<div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle sticky top-0 backdrop-blur-sm">
|
|
271
|
-
{provider.name || provider.id}
|
|
272
|
-
</div>
|
|
273
|
-
{provider.models.map((m) => renderModel(provider, m))}
|
|
274
|
-
</div>
|
|
275
|
-
))}
|
|
276
|
-
{imageProviders.length > 0 && (
|
|
277
|
-
<>
|
|
278
|
-
<div className="px-3.5 py-2 text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans bg-surface-0 border-y border-border-subtle flex items-center gap-1.5 sticky top-0 backdrop-blur-sm">
|
|
279
|
-
<ImageIcon size={10} className="text-purple" />
|
|
280
|
-
Image Generation
|
|
281
|
-
</div>
|
|
282
|
-
{imageProviders.map((provider) => (
|
|
283
|
-
<div key={`img-${provider.id}`}>
|
|
284
|
-
<div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle">
|
|
285
|
-
{provider.name || provider.id}
|
|
286
|
-
</div>
|
|
287
|
-
{provider.models.map((m) => renderModel(provider, m))}
|
|
288
|
-
</div>
|
|
289
|
-
))}
|
|
290
|
-
</>
|
|
291
|
-
)}
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
})()}
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
{isCodex && (
|
|
298
|
-
<>
|
|
299
|
-
<div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
|
|
300
|
-
{EFFORT_OPTIONS.map((opt) => (
|
|
301
|
-
<button
|
|
302
|
-
key={opt.value}
|
|
303
|
-
onClick={() => onReasoningEffortChange?.(opt.value)}
|
|
304
|
-
className={cn(
|
|
305
|
-
'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
306
|
-
reasoningEffort === opt.value
|
|
307
|
-
? 'bg-accent/15 text-accent'
|
|
308
|
-
: 'text-text-4 hover:text-text-1',
|
|
309
|
-
)}
|
|
310
|
-
title={`Reasoning: ${opt.label}`}
|
|
311
|
-
>
|
|
312
|
-
{opt.label}
|
|
313
|
-
</button>
|
|
314
|
-
))}
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
|
|
318
|
-
{VERBOSITY_OPTIONS.map((opt) => (
|
|
319
|
-
<button
|
|
320
|
-
key={opt.value}
|
|
321
|
-
onClick={() => onVerbosityChange?.(opt.value)}
|
|
322
|
-
className={cn(
|
|
323
|
-
'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
324
|
-
verbosity === opt.value
|
|
325
|
-
? 'bg-accent/15 text-accent'
|
|
326
|
-
: 'text-text-4 hover:text-text-1',
|
|
327
|
-
)}
|
|
328
|
-
title={`Verbosity: ${opt.label}`}
|
|
329
|
-
>
|
|
330
|
-
{opt.label}
|
|
331
|
-
</button>
|
|
332
|
-
))}
|
|
333
|
-
</div>
|
|
334
|
-
</>
|
|
335
|
-
)}
|
|
336
|
-
|
|
337
|
-
<div className="flex-1" />
|
|
338
|
-
|
|
339
|
-
{isActive ? (
|
|
340
|
-
<button
|
|
341
|
-
onClick={onStop}
|
|
342
|
-
title="Stop generation"
|
|
343
|
-
className="group w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer"
|
|
344
|
-
>
|
|
345
|
-
<span className="relative flex items-center justify-center w-3.5 h-3.5">
|
|
346
|
-
<span className="absolute inset-0 rounded-full bg-accent/30 group-hover:bg-red-500/30 animate-ping [animation-duration:2s] transition-colors" />
|
|
347
|
-
<span className="relative w-2.5 h-2.5 rounded-full bg-accent group-hover:bg-red-500 transition-colors" />
|
|
348
|
-
</span>
|
|
349
|
-
</button>
|
|
350
|
-
) : (
|
|
351
|
-
<button
|
|
352
|
-
onClick={handleSend}
|
|
353
|
-
disabled={!canSend}
|
|
354
|
-
className={cn(
|
|
355
|
-
'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
|
|
356
|
-
'disabled:opacity-15 disabled:cursor-not-allowed',
|
|
357
|
-
canSend ? 'text-text-0 hover:text-text-1' : 'text-text-4',
|
|
358
|
-
)}
|
|
359
|
-
>
|
|
360
|
-
{sending ? <Loader2 size={15} className="animate-spin" /> : <SendHorizontal size={15} />}
|
|
361
|
-
</button>
|
|
362
|
-
)}
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
367
|
-
}
|