groove-dev 0.27.144 → 0.27.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +7 -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 +12 -6
- package/node_modules/@groove-dev/daemon/src/conversations.js +59 -58
- package/node_modules/@groove-dev/daemon/src/introducer.js +20 -0
- package/node_modules/@groove-dev/daemon/src/process.js +262 -15
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -3
- package/node_modules/@groove-dev/daemon/src/rotator.js +15 -3
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +49 -83
- package/node_modules/@groove-dev/daemon/templates/lab-general.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/llama-cpp-setup.json +12 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.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/components/agents/agent-feed.jsx +80 -95
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -8
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +199 -75
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +21 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +10 -13
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +42 -34
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +66 -65
- package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +124 -127
- package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +24 -1
- package/node_modules/@groove-dev/gui/src/components/ui/question-modal.jsx +107 -0
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +49 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +18 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +14 -14
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +68 -32
- package/node_modules/@groove-dev/gui/src/views/models.jsx +57 -36
- package/node_modules/axios/CHANGELOG.md +260 -0
- package/node_modules/axios/README.md +595 -223
- package/node_modules/axios/dist/axios.js +1460 -1090
- package/node_modules/axios/dist/axios.js.map +1 -1
- package/node_modules/axios/dist/axios.min.js +3 -3
- package/node_modules/axios/dist/axios.min.js.map +1 -1
- package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
- package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
- package/node_modules/axios/dist/esm/axios.js +1557 -1128
- package/node_modules/axios/dist/esm/axios.js.map +1 -1
- package/node_modules/axios/dist/esm/axios.min.js +2 -2
- package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
- package/node_modules/axios/dist/node/axios.cjs +1594 -1057
- package/node_modules/axios/dist/node/axios.cjs.map +1 -1
- package/node_modules/axios/index.d.cts +40 -41
- package/node_modules/axios/index.d.ts +151 -227
- package/node_modules/axios/index.js +2 -0
- package/node_modules/axios/lib/adapters/adapters.js +4 -2
- package/node_modules/axios/lib/adapters/fetch.js +147 -16
- package/node_modules/axios/lib/adapters/http.js +306 -58
- package/node_modules/axios/lib/adapters/xhr.js +6 -2
- package/node_modules/axios/lib/core/Axios.js +7 -3
- package/node_modules/axios/lib/core/AxiosError.js +120 -34
- package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
- package/node_modules/axios/lib/core/buildFullPath.js +1 -1
- package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
- package/node_modules/axios/lib/core/mergeConfig.js +21 -4
- package/node_modules/axios/lib/core/settle.js +7 -11
- package/node_modules/axios/lib/defaults/index.js +14 -9
- package/node_modules/axios/lib/env/data.js +1 -1
- package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
- package/node_modules/axios/lib/helpers/buildURL.js +1 -1
- package/node_modules/axios/lib/helpers/cookies.js +14 -2
- package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
- package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
- package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
- package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
- package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
- package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
- package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
- package/node_modules/axios/lib/helpers/toFormData.js +10 -2
- package/node_modules/axios/lib/helpers/validator.js +3 -1
- package/node_modules/axios/lib/utils.js +33 -21
- package/node_modules/axios/package.json +17 -24
- package/node_modules/follow-redirects/README.md +7 -5
- package/node_modules/follow-redirects/index.js +24 -1
- package/node_modules/follow-redirects/package.json +1 -1
- 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 +12 -6
- package/packages/daemon/src/conversations.js +59 -58
- package/packages/daemon/src/introducer.js +20 -0
- package/packages/daemon/src/process.js +262 -15
- package/packages/daemon/src/providers/groove-network.js +1 -3
- package/packages/daemon/src/rotator.js +15 -3
- package/packages/daemon/src/routes/agents.js +49 -83
- package/packages/daemon/templates/lab-general.json +12 -0
- package/packages/daemon/templates/llama-cpp-setup.json +12 -0
- package/packages/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/packages/gui/dist/assets/index-CEkPsSAm.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +80 -95
- package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
- package/packages/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/packages/gui/src/components/chat/chat-header.jsx +3 -8
- package/packages/gui/src/components/chat/chat-input.jsx +199 -75
- package/packages/gui/src/components/chat/chat-messages.jsx +21 -4
- package/packages/gui/src/components/chat/chat-view.jsx +10 -13
- package/packages/gui/src/components/chat/model-picker.jsx +3 -3
- package/packages/gui/src/components/lab/chat-playground.jsx +42 -34
- package/packages/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/packages/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/packages/gui/src/components/lab/parameter-panel.jsx +66 -65
- package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +124 -127
- package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/status-bar.jsx +24 -1
- package/packages/gui/src/components/ui/question-modal.jsx +107 -0
- package/packages/gui/src/components/ui/sheet.jsx +2 -2
- package/packages/gui/src/components/ui/slider.jsx +8 -8
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +49 -2
- package/packages/gui/src/stores/slices/agents-slice.js +18 -2
- package/packages/gui/src/stores/slices/chat-slice.js +14 -14
- package/packages/gui/src/views/model-lab.jsx +68 -32
- package/packages/gui/src/views/models.jsx +57 -36
- package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +0 -1012
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +0 -1
- package/packages/gui/dist/assets/index-BcoF6_eF.js +0 -1012
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal, ChevronUp } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
|
-
import { formatModelName } from './model-picker';
|
|
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';
|
|
6
8
|
|
|
7
9
|
const EFFORT_OPTIONS = [
|
|
8
10
|
{ value: 'none', label: 'None' },
|
|
@@ -17,22 +19,42 @@ const VERBOSITY_OPTIONS = [
|
|
|
17
19
|
{ value: 'medium', label: 'Normal' },
|
|
18
20
|
];
|
|
19
21
|
|
|
20
|
-
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
|
|
22
|
+
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, currentProvider, onModelChange, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
|
|
21
23
|
const [input, setInput] = useState('');
|
|
22
|
-
const [inputHeight, setInputHeight] = useState(
|
|
24
|
+
const [inputHeight, setInputHeight] = useState(88);
|
|
25
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
23
26
|
const textareaRef = useRef(null);
|
|
24
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]);
|
|
25
39
|
|
|
26
40
|
useEffect(() => {
|
|
27
41
|
if (!disabled && textareaRef.current) textareaRef.current.focus();
|
|
28
42
|
}, [disabled]);
|
|
29
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
|
+
|
|
30
53
|
function handleSend() {
|
|
31
54
|
const text = input.trim();
|
|
32
55
|
if (!text || sending || disabled) return;
|
|
33
56
|
onSend(text);
|
|
34
57
|
setInput('');
|
|
35
|
-
setInputHeight(40);
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
function onKeyDown(e) {
|
|
@@ -54,7 +76,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
54
76
|
e.preventDefault();
|
|
55
77
|
const startY = e.clientY;
|
|
56
78
|
const startH = inputHeight;
|
|
57
|
-
const onMove = (ev) => setInputHeight(Math.min(Math.max(
|
|
79
|
+
const onMove = (ev) => setInputHeight(Math.min(Math.max(56, startH - (ev.clientY - startY)), 400));
|
|
58
80
|
const onUp = () => {
|
|
59
81
|
window.removeEventListener('mousemove', onMove);
|
|
60
82
|
window.removeEventListener('mouseup', onUp);
|
|
@@ -76,110 +98,211 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
76
98
|
: 'Send a message...';
|
|
77
99
|
|
|
78
100
|
return (
|
|
79
|
-
<div className="
|
|
101
|
+
<div className="px-4 pb-3">
|
|
80
102
|
<div
|
|
81
103
|
onMouseDown={onDragStart}
|
|
82
|
-
className="flex items-center justify-center h-
|
|
104
|
+
className="flex items-center justify-center h-5 cursor-row-resize group"
|
|
83
105
|
>
|
|
84
|
-
<GripHorizontal size={
|
|
106
|
+
<GripHorizontal size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
|
|
85
107
|
</div>
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
ref={
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
rows={1}
|
|
106
|
-
style={{ height: inputHeight }}
|
|
107
|
-
className={cn(
|
|
108
|
-
'w-full resize-none rounded-xl px-4 py-2.5 text-sm',
|
|
109
|
-
'bg-surface-0 border text-text-0 font-sans',
|
|
110
|
-
'placeholder:text-text-4',
|
|
111
|
-
'focus:outline-none focus:ring-1',
|
|
112
|
-
'border-border focus:ring-accent/40',
|
|
113
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
114
|
-
)}
|
|
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"
|
|
115
127
|
/>
|
|
116
128
|
|
|
117
|
-
<div className="
|
|
118
|
-
<
|
|
119
|
-
ref={
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 }}
|
|
125
146
|
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="flex items-center gap-1.5 px-1.5 pb-1.5 pt-0.5">
|
|
126
150
|
<button
|
|
127
151
|
onClick={() => fileInputRef.current?.click()}
|
|
128
152
|
disabled={disabled}
|
|
129
|
-
className="w-
|
|
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"
|
|
130
154
|
title="Attach file"
|
|
131
155
|
>
|
|
132
156
|
<Paperclip size={14} />
|
|
133
157
|
</button>
|
|
134
158
|
|
|
135
|
-
<div className="flex items-center h-
|
|
159
|
+
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
136
160
|
<button
|
|
137
161
|
onClick={() => onModeChange?.('api')}
|
|
138
162
|
disabled={modeChanging}
|
|
139
163
|
className={cn(
|
|
140
|
-
'flex items-center gap-1 h-
|
|
164
|
+
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
141
165
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
142
|
-
currentMode === 'api' ? 'bg-accent/15 text-accent
|
|
166
|
+
currentMode === 'api' ? 'bg-accent/15 text-accent' : 'text-text-3 hover:text-text-1',
|
|
143
167
|
)}
|
|
144
168
|
title="Lightweight — fast and cheap, no tools"
|
|
145
169
|
>
|
|
146
|
-
<Zap size={
|
|
170
|
+
<Zap size={10} /> Chat
|
|
147
171
|
</button>
|
|
148
172
|
<button
|
|
149
173
|
onClick={() => onModeChange?.('agent')}
|
|
150
174
|
disabled={modeChanging}
|
|
151
175
|
className={cn(
|
|
152
|
-
'flex items-center gap-1 h-
|
|
176
|
+
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
153
177
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
154
|
-
currentMode === 'agent' ? 'bg-purple/15 text-purple
|
|
178
|
+
currentMode === 'agent' ? 'bg-purple/15 text-purple' : 'text-text-3 hover:text-text-1',
|
|
155
179
|
)}
|
|
156
180
|
title="Full agent — tools, files, session resume"
|
|
157
181
|
>
|
|
158
|
-
<Bot size={
|
|
182
|
+
<Bot size={10} /> Agent
|
|
159
183
|
</button>
|
|
160
184
|
</div>
|
|
161
185
|
|
|
162
|
-
{
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
>
|
|
169
196
|
{isImageModel && <ImageIcon size={9} />}
|
|
170
|
-
<span className="max-w-[
|
|
171
|
-
|
|
172
|
-
|
|
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>
|
|
173
296
|
|
|
174
297
|
{isCodex && (
|
|
175
298
|
<>
|
|
176
|
-
<div className="flex items-center h-
|
|
299
|
+
<div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
|
|
177
300
|
{EFFORT_OPTIONS.map((opt) => (
|
|
178
301
|
<button
|
|
179
302
|
key={opt.value}
|
|
180
303
|
onClick={() => onReasoningEffortChange?.(opt.value)}
|
|
181
304
|
className={cn(
|
|
182
|
-
'h-
|
|
305
|
+
'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
183
306
|
reasoningEffort === opt.value
|
|
184
307
|
? 'bg-accent/15 text-accent'
|
|
185
308
|
: 'text-text-4 hover:text-text-1',
|
|
@@ -191,13 +314,13 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
191
314
|
))}
|
|
192
315
|
</div>
|
|
193
316
|
|
|
194
|
-
<div className="flex items-center h-
|
|
317
|
+
<div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
|
|
195
318
|
{VERBOSITY_OPTIONS.map((opt) => (
|
|
196
319
|
<button
|
|
197
320
|
key={opt.value}
|
|
198
321
|
onClick={() => onVerbosityChange?.(opt.value)}
|
|
199
322
|
className={cn(
|
|
200
|
-
'h-
|
|
323
|
+
'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
201
324
|
verbosity === opt.value
|
|
202
325
|
? 'bg-accent/15 text-accent'
|
|
203
326
|
: 'text-text-4 hover:text-text-1',
|
|
@@ -216,24 +339,25 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
216
339
|
{isActive ? (
|
|
217
340
|
<button
|
|
218
341
|
onClick={onStop}
|
|
219
|
-
className="w-8 h-8 flex items-center justify-center rounded-lg bg-danger/80 text-white hover:bg-danger transition-all cursor-pointer shadow-lg shadow-danger/20 flex-shrink-0"
|
|
220
342
|
title="Stop generation"
|
|
343
|
+
className="group w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer"
|
|
221
344
|
>
|
|
222
|
-
<
|
|
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>
|
|
223
349
|
</button>
|
|
224
350
|
) : (
|
|
225
351
|
<button
|
|
226
352
|
onClick={handleSend}
|
|
227
353
|
disabled={!canSend}
|
|
228
354
|
className={cn(
|
|
229
|
-
'w-
|
|
230
|
-
'disabled:opacity-
|
|
231
|
-
canSend
|
|
232
|
-
? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
|
|
233
|
-
: 'bg-surface-4 text-text-4',
|
|
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',
|
|
234
358
|
)}
|
|
235
359
|
>
|
|
236
|
-
{sending ? <Loader2 size={
|
|
360
|
+
{sending ? <Loader2 size={15} className="animate-spin" /> : <SendHorizontal size={15} />}
|
|
237
361
|
</button>
|
|
238
362
|
)}
|
|
239
363
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw, Loader2 } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { timeAgo } from '../../lib/format';
|
|
6
6
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
@@ -435,7 +435,23 @@ function ApiTypingIndicator() {
|
|
|
435
435
|
);
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
|
|
438
|
+
function ToolCallIndicator({ tool }) {
|
|
439
|
+
return (
|
|
440
|
+
<div className="max-w-[85%] py-1">
|
|
441
|
+
<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">
|
|
442
|
+
<Loader2 size={13} className="text-accent animate-spin flex-shrink-0" />
|
|
443
|
+
<div className="flex-1 min-w-0">
|
|
444
|
+
<span className="text-xs font-semibold font-sans text-text-2">{tool.name}</span>
|
|
445
|
+
{tool.summary && (
|
|
446
|
+
<span className="text-xs font-mono text-text-4 ml-1.5 truncate">{tool.summary}</span>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function ChatMessages({ messages, isStreaming, model, mode, onImageReply, role, activeTool }) {
|
|
439
455
|
const scrollRef = useRef(null);
|
|
440
456
|
const isAtBottomRef = useRef(true);
|
|
441
457
|
|
|
@@ -453,7 +469,7 @@ export function ChatMessages({ messages, isStreaming, model, mode, onImageReply,
|
|
|
453
469
|
if (isAtBottomRef.current && scrollRef.current) {
|
|
454
470
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
455
471
|
}
|
|
456
|
-
}, [messages?.length, isStreaming]);
|
|
472
|
+
}, [messages?.length, isStreaming, activeTool]);
|
|
457
473
|
|
|
458
474
|
if (!messages || messages.length === 0) {
|
|
459
475
|
return (
|
|
@@ -472,7 +488,8 @@ export function ChatMessages({ messages, isStreaming, model, mode, onImageReply,
|
|
|
472
488
|
if (msg.from === 'system') return <SystemMessage key={i} msg={msg} />;
|
|
473
489
|
return <AssistantMessage key={i} msg={msg} model={model} role={role} />;
|
|
474
490
|
})}
|
|
475
|
-
{
|
|
491
|
+
{activeTool && <ToolCallIndicator tool={activeTool} />}
|
|
492
|
+
{isStreaming && !activeTool && (
|
|
476
493
|
mode === 'agent' ? (
|
|
477
494
|
<ThinkingIndicator className="py-1" />
|
|
478
495
|
) : (
|
|
@@ -33,11 +33,12 @@ export function ChatView() {
|
|
|
33
33
|
const conversationMessages = useGrooveStore((s) => s.conversationMessages);
|
|
34
34
|
const sendingMessage = useGrooveStore((s) => s.sendingMessage);
|
|
35
35
|
const streamingConversationId = useGrooveStore((s) => s.streamingConversationId);
|
|
36
|
+
const conversationActiveTools = useGrooveStore((s) => s.conversationActiveTools);
|
|
36
37
|
const createConversation = useGrooveStore((s) => s.createConversation);
|
|
37
38
|
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
38
39
|
const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
|
|
39
40
|
const sendImageMessage = useGrooveStore((s) => s.sendImageMessage);
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
|
|
42
43
|
const setConversationMode = useGrooveStore((s) => s.setConversationMode);
|
|
43
44
|
const setConversationModel = useGrooveStore((s) => s.setConversationModel);
|
|
@@ -54,7 +55,7 @@ export function ChatView() {
|
|
|
54
55
|
const [replyContext, setReplyContext] = useState(null);
|
|
55
56
|
const [modeChanging, setModeChanging] = useState(false);
|
|
56
57
|
|
|
57
|
-
const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] ||
|
|
58
|
+
const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || 'chat') : 'chat';
|
|
58
59
|
const activeReasoningEffort = activeConversationId ? (conversationReasoningEffort?.[activeConversationId] || 'medium') : 'medium';
|
|
59
60
|
const activeVerbosity = activeConversationId ? (conversationVerbosity?.[activeConversationId] || 'medium') : 'medium';
|
|
60
61
|
|
|
@@ -62,6 +63,7 @@ export function ChatView() {
|
|
|
62
63
|
const isCodexProvider = activeConversation?.provider === 'codex';
|
|
63
64
|
const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
|
|
64
65
|
const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
|
|
66
|
+
const activeTool = activeConversationId ? (conversationActiveTools?.[activeConversationId] || null) : null;
|
|
65
67
|
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
66
68
|
|
|
67
69
|
const handleNewChat = useCallback(async (provider, model) => {
|
|
@@ -101,12 +103,8 @@ export function ChatView() {
|
|
|
101
103
|
|
|
102
104
|
const handleStop = useCallback(() => {
|
|
103
105
|
if (!activeConversation) return;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} else {
|
|
107
|
-
stopChatStreaming(activeConversationId);
|
|
108
|
-
}
|
|
109
|
-
}, [activeConversation, activeConversationId, stopAgent, stopChatStreaming]);
|
|
106
|
+
stopChatStreaming(activeConversationId);
|
|
107
|
+
}, [activeConversation, activeConversationId, stopChatStreaming]);
|
|
110
108
|
|
|
111
109
|
const handleModelChange = useCallback(async (selection) => {
|
|
112
110
|
if (activeConversationId) {
|
|
@@ -130,10 +128,6 @@ export function ChatView() {
|
|
|
130
128
|
setConversationVerbosity(activeConversationId, verbosity);
|
|
131
129
|
}, [activeConversationId, setConversationVerbosity]);
|
|
132
130
|
|
|
133
|
-
const currentModel = activeConversation
|
|
134
|
-
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
135
|
-
: null;
|
|
136
|
-
|
|
137
131
|
return (
|
|
138
132
|
<div className="flex h-full bg-surface-0">
|
|
139
133
|
{/* Conversation sidebar */}
|
|
@@ -158,7 +152,7 @@ export function ChatView() {
|
|
|
158
152
|
|
|
159
153
|
{activeConversation ? (
|
|
160
154
|
<>
|
|
161
|
-
<ChatHeader conversation={activeConversation}
|
|
155
|
+
<ChatHeader conversation={activeConversation} role={activeRole} onRoleChange={handleRoleChange} sidebarCollapsed={sidebarCollapsed} />
|
|
162
156
|
<ChatMessages
|
|
163
157
|
messages={messages}
|
|
164
158
|
isStreaming={isStreaming}
|
|
@@ -166,6 +160,7 @@ export function ChatView() {
|
|
|
166
160
|
mode={activeConversation.mode || 'api'}
|
|
167
161
|
onImageReply={handleImageReply}
|
|
168
162
|
role={activeRole}
|
|
163
|
+
activeTool={activeTool}
|
|
169
164
|
/>
|
|
170
165
|
<ChatInput
|
|
171
166
|
onSend={handleSend}
|
|
@@ -175,6 +170,8 @@ export function ChatView() {
|
|
|
175
170
|
disabled={false}
|
|
176
171
|
isImageModel={currentModelIsImage}
|
|
177
172
|
currentModel={activeConversation.model}
|
|
173
|
+
currentProvider={activeConversation.provider}
|
|
174
|
+
onModelChange={handleModelChange}
|
|
178
175
|
replyContext={replyContext}
|
|
179
176
|
onClearReply={() => setReplyContext(null)}
|
|
180
177
|
role={activeRole}
|
|
@@ -15,13 +15,13 @@ export function formatModelName(id) {
|
|
|
15
15
|
.join(' ');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const TIER_CONFIG = {
|
|
18
|
+
export const TIER_CONFIG = {
|
|
19
19
|
frontier: { label: 'Frontier', variant: 'purple', icon: Sparkles },
|
|
20
20
|
mid: { label: 'Mid', variant: 'accent', icon: Zap },
|
|
21
21
|
fast: { label: 'Fast', variant: 'success', icon: Zap },
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
function getTier(model) {
|
|
24
|
+
export function getTier(model) {
|
|
25
25
|
const name = (model || '').toLowerCase();
|
|
26
26
|
if (name.includes('gpt-5.5') || name.includes('gpt-5.4-pro')) return 'frontier';
|
|
27
27
|
if (name.includes('gpt-5.4-mini') || name.includes('gpt-5-mini')) return 'mid';
|
|
@@ -32,7 +32,7 @@ function getTier(model) {
|
|
|
32
32
|
return 'fast';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function getContextSize(model) {
|
|
35
|
+
export function getContextSize(model) {
|
|
36
36
|
const name = (model || '').toLowerCase();
|
|
37
37
|
if (name.startsWith('gpt-5')) return '200k';
|
|
38
38
|
if (name.includes('opus') || name.includes('sonnet')) return '200k';
|