groove-dev 0.27.55 → 0.27.57
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/ai-chat/CHAT_MASTER_PLAN.md +184 -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 +169 -0
- package/node_modules/@groove-dev/daemon/src/conversations.js +423 -0
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- 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 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +138 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +112 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +347 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +165 -0
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +154 -0
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +143 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +220 -0
- package/node_modules/@groove-dev/gui/src/views/chat.jsx +6 -0
- 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 +169 -0
- package/packages/daemon/src/conversations.js +423 -0
- package/packages/daemon/src/index.js +2 -0
- package/packages/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/packages/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +138 -0
- package/packages/gui/src/components/chat/chat-input.jsx +112 -0
- package/packages/gui/src/components/chat/chat-messages.jsx +347 -0
- package/packages/gui/src/components/chat/chat-view.jsx +165 -0
- package/packages/gui/src/components/chat/conversation-list.jsx +154 -0
- package/packages/gui/src/components/chat/model-picker.jsx +143 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/groove.js +220 -0
- package/packages/gui/src/views/chat.jsx +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +0 -1
- package/packages/gui/dist/assets/index-CyVj0fHl.css +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { MessageCircle, Plus, Sparkles, Zap } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { ConversationList } from './conversation-list';
|
|
7
|
+
import { ChatHeader } from './chat-header';
|
|
8
|
+
import { ChatMessages } from './chat-messages';
|
|
9
|
+
import { ChatInput } from './chat-input';
|
|
10
|
+
|
|
11
|
+
function EmptyState({ onNewChat }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex-1 flex items-center justify-center">
|
|
14
|
+
<div className="max-w-md w-full text-center space-y-8 px-8">
|
|
15
|
+
<div className="relative mx-auto w-20 h-20">
|
|
16
|
+
<div className="absolute inset-0 rounded-full bg-accent/8 animate-pulse" />
|
|
17
|
+
<div className="absolute inset-1 rounded-full bg-surface-3 border border-border-subtle flex items-center justify-center shadow-lg shadow-accent/5">
|
|
18
|
+
<MessageCircle size={32} className="text-accent" />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div className="space-y-3">
|
|
23
|
+
<h1 className="text-2xl font-bold text-text-0 font-sans tracking-tight">Groove Chat</h1>
|
|
24
|
+
<p className="text-sm text-text-2 font-sans max-w-sm mx-auto leading-relaxed">
|
|
25
|
+
A command center disguised as a conversation. Every provider, every model, full project context.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<button
|
|
30
|
+
onClick={onNewChat}
|
|
31
|
+
className="inline-flex items-center gap-2 h-10 px-6 rounded-lg bg-accent/15 text-accent text-sm font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
|
|
32
|
+
>
|
|
33
|
+
<Plus size={16} />
|
|
34
|
+
New Chat
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
|
|
38
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
|
|
39
|
+
<Sparkles size={14} className="text-purple flex-shrink-0" />
|
|
40
|
+
<span className="text-2xs text-text-2 font-sans">Multi-model routing</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
|
|
43
|
+
<Zap size={14} className="text-warning flex-shrink-0" />
|
|
44
|
+
<span className="text-2xs text-text-2 font-sans">Streaming responses</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<p className="text-xs text-text-4 font-sans">
|
|
49
|
+
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+Shift+N</kbd>
|
|
50
|
+
<span className="mx-1.5">new chat</span>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ChatView() {
|
|
58
|
+
const conversations = useGrooveStore((s) => s.conversations);
|
|
59
|
+
const activeConversationId = useGrooveStore((s) => s.activeConversationId);
|
|
60
|
+
const conversationMessages = useGrooveStore((s) => s.conversationMessages);
|
|
61
|
+
const sendingMessage = useGrooveStore((s) => s.sendingMessage);
|
|
62
|
+
const streamingConversationId = useGrooveStore((s) => s.streamingConversationId);
|
|
63
|
+
const createConversation = useGrooveStore((s) => s.createConversation);
|
|
64
|
+
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
65
|
+
const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
|
|
66
|
+
const stopAgent = useGrooveStore((s) => s.stopAgent);
|
|
67
|
+
const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
|
|
68
|
+
const setConversationMode = useGrooveStore((s) => s.setConversationMode);
|
|
69
|
+
|
|
70
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
71
|
+
|
|
72
|
+
const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
|
|
73
|
+
const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
|
|
74
|
+
const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
|
|
75
|
+
|
|
76
|
+
const handleNewChat = useCallback(async (provider, model) => {
|
|
77
|
+
const p = provider || 'claude-code';
|
|
78
|
+
const m = model || 'claude-sonnet-4-6';
|
|
79
|
+
try {
|
|
80
|
+
await createConversation(p, m, 'api');
|
|
81
|
+
} catch { /* toast handles */ }
|
|
82
|
+
}, [createConversation]);
|
|
83
|
+
|
|
84
|
+
const handleModeChange = useCallback((mode) => {
|
|
85
|
+
if (!activeConversationId) return;
|
|
86
|
+
setConversationMode(activeConversationId, mode);
|
|
87
|
+
}, [activeConversationId, setConversationMode]);
|
|
88
|
+
|
|
89
|
+
const handleSend = useCallback((text) => {
|
|
90
|
+
if (!activeConversationId) return;
|
|
91
|
+
sendChatMessage(activeConversationId, text);
|
|
92
|
+
}, [activeConversationId, sendChatMessage]);
|
|
93
|
+
|
|
94
|
+
const handleStop = useCallback(() => {
|
|
95
|
+
if (!activeConversation) return;
|
|
96
|
+
if (activeConversation.mode === 'agent' && activeConversation.agentId) {
|
|
97
|
+
stopAgent(activeConversation.agentId);
|
|
98
|
+
} else {
|
|
99
|
+
stopChatStreaming(activeConversationId);
|
|
100
|
+
}
|
|
101
|
+
}, [activeConversation, activeConversationId, stopAgent, stopChatStreaming]);
|
|
102
|
+
|
|
103
|
+
const handleModelChange = useCallback(async (selection) => {
|
|
104
|
+
if (activeConversationId) {
|
|
105
|
+
// TODO: Update conversation model via API
|
|
106
|
+
} else {
|
|
107
|
+
await handleNewChat(selection.provider, selection.model);
|
|
108
|
+
}
|
|
109
|
+
}, [activeConversationId, handleNewChat]);
|
|
110
|
+
|
|
111
|
+
const currentModel = activeConversation
|
|
112
|
+
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
113
|
+
: null;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex h-full bg-surface-2">
|
|
117
|
+
{/* Conversation sidebar */}
|
|
118
|
+
<div className={cn(
|
|
119
|
+
'flex-shrink-0 border-r border-border bg-surface-1 transition-all duration-200 overflow-hidden',
|
|
120
|
+
sidebarCollapsed ? 'w-0' : 'w-64',
|
|
121
|
+
)}>
|
|
122
|
+
<ConversationList onNewChat={() => handleNewChat()} />
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Main chat area */}
|
|
126
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
127
|
+
{activeConversation ? (
|
|
128
|
+
<>
|
|
129
|
+
<ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange} onModeChange={handleModeChange} />
|
|
130
|
+
<ChatMessages
|
|
131
|
+
messages={messages}
|
|
132
|
+
isStreaming={isStreaming}
|
|
133
|
+
model={activeConversation.model}
|
|
134
|
+
/>
|
|
135
|
+
<ChatInput
|
|
136
|
+
onSend={handleSend}
|
|
137
|
+
onStop={handleStop}
|
|
138
|
+
sending={sendingMessage}
|
|
139
|
+
streaming={isStreaming}
|
|
140
|
+
disabled={false}
|
|
141
|
+
/>
|
|
142
|
+
</>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<EmptyState onNewChat={() => handleNewChat()} />
|
|
146
|
+
<ChatInput
|
|
147
|
+
onSend={(text) => {
|
|
148
|
+
handleNewChat().then(() => {
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
const id = useGrooveStore.getState().activeConversationId;
|
|
151
|
+
if (id) sendChatMessage(id, text);
|
|
152
|
+
}, 500);
|
|
153
|
+
});
|
|
154
|
+
}}
|
|
155
|
+
onStop={() => {}}
|
|
156
|
+
sending={false}
|
|
157
|
+
streaming={false}
|
|
158
|
+
disabled={false}
|
|
159
|
+
/>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Plus, MessageCircle, Pin, Pencil, PinOff, Trash2, Zap, Bot } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { Badge } from '../ui/badge';
|
|
7
|
+
import { timeAgo } from '../../lib/format';
|
|
8
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../ui/context-menu';
|
|
9
|
+
import { formatModelName } from './model-picker';
|
|
10
|
+
|
|
11
|
+
function groupByDate(conversations) {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
14
|
+
const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
|
|
15
|
+
const weekAgo = new Date(today); weekAgo.setDate(today.getDate() - 7);
|
|
16
|
+
|
|
17
|
+
const groups = { pinned: [], today: [], yesterday: [], week: [], older: [] };
|
|
18
|
+
|
|
19
|
+
for (const conv of conversations) {
|
|
20
|
+
if (conv.pinned) { groups.pinned.push(conv); continue; }
|
|
21
|
+
const d = new Date(conv.updatedAt || conv.createdAt);
|
|
22
|
+
if (d >= today) groups.today.push(conv);
|
|
23
|
+
else if (d >= yesterday) groups.yesterday.push(conv);
|
|
24
|
+
else if (d >= weekAgo) groups.week.push(conv);
|
|
25
|
+
else groups.older.push(conv);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return groups;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function GroupLabel({ label }) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="px-3 pt-4 pb-1.5">
|
|
34
|
+
<span className="text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans">{label}</span>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete }) {
|
|
40
|
+
return (
|
|
41
|
+
<ContextMenu>
|
|
42
|
+
<ContextMenuTrigger asChild>
|
|
43
|
+
<button
|
|
44
|
+
onClick={() => onSelect(conv.id)}
|
|
45
|
+
className={cn(
|
|
46
|
+
'w-full flex items-center gap-2 px-3 py-2 text-left rounded-md transition-colors cursor-pointer group',
|
|
47
|
+
isActive
|
|
48
|
+
? 'bg-accent/10 text-text-0'
|
|
49
|
+
: 'text-text-2 hover:bg-surface-4 hover:text-text-1',
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-accent' : 'text-text-4 group-hover:text-text-3')} />
|
|
53
|
+
<div className="flex-1 min-w-0">
|
|
54
|
+
<div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
|
|
55
|
+
<div className="flex items-center gap-1.5 mt-0.5">
|
|
56
|
+
{conv.mode === 'agent'
|
|
57
|
+
? <Bot size={9} className="text-purple flex-shrink-0" />
|
|
58
|
+
: <Zap size={9} className="text-accent flex-shrink-0" />
|
|
59
|
+
}
|
|
60
|
+
{conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
|
|
61
|
+
<span className="text-2xs text-text-4 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{conv.pinned && <Pin size={10} className="text-accent flex-shrink-0" />}
|
|
65
|
+
</button>
|
|
66
|
+
</ContextMenuTrigger>
|
|
67
|
+
<ContextMenuContent>
|
|
68
|
+
<ContextMenuItem onSelect={() => onRename(conv)}>
|
|
69
|
+
<Pencil size={12} /> Rename
|
|
70
|
+
</ContextMenuItem>
|
|
71
|
+
<ContextMenuItem onSelect={() => onPin(conv)}>
|
|
72
|
+
{conv.pinned ? <PinOff size={12} /> : <Pin size={12} />}
|
|
73
|
+
{conv.pinned ? 'Unpin' : 'Pin'}
|
|
74
|
+
</ContextMenuItem>
|
|
75
|
+
<ContextMenuSeparator />
|
|
76
|
+
<ContextMenuItem danger onSelect={() => onDelete(conv.id)}>
|
|
77
|
+
<Trash2 size={12} /> Delete
|
|
78
|
+
</ContextMenuItem>
|
|
79
|
+
</ContextMenuContent>
|
|
80
|
+
</ContextMenu>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function ConversationList({ onNewChat }) {
|
|
85
|
+
const conversations = useGrooveStore((s) => s.conversations);
|
|
86
|
+
const activeConversationId = useGrooveStore((s) => s.activeConversationId);
|
|
87
|
+
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
88
|
+
const renameConversation = useGrooveStore((s) => s.renameConversation);
|
|
89
|
+
const pinConversation = useGrooveStore((s) => s.pinConversation);
|
|
90
|
+
const deleteConversation = useGrooveStore((s) => s.deleteConversation);
|
|
91
|
+
|
|
92
|
+
const groups = useMemo(() => groupByDate(conversations), [conversations]);
|
|
93
|
+
|
|
94
|
+
function handleRename(conv) {
|
|
95
|
+
const name = prompt('Rename conversation:', conv.title || '');
|
|
96
|
+
if (name && name.trim()) renameConversation(conv.id, name.trim());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handlePin(conv) {
|
|
100
|
+
pinConversation(conv.id, !conv.pinned);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const renderGroup = (label, items) => {
|
|
104
|
+
if (items.length === 0) return null;
|
|
105
|
+
return (
|
|
106
|
+
<div key={label}>
|
|
107
|
+
<GroupLabel label={label} />
|
|
108
|
+
{items.map((conv) => (
|
|
109
|
+
<ConversationItem
|
|
110
|
+
key={conv.id}
|
|
111
|
+
conv={conv}
|
|
112
|
+
isActive={conv.id === activeConversationId}
|
|
113
|
+
onSelect={setActiveConversation}
|
|
114
|
+
onRename={handleRename}
|
|
115
|
+
onPin={handlePin}
|
|
116
|
+
onDelete={deleteConversation}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex flex-col h-full">
|
|
125
|
+
<div className="flex-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
|
|
126
|
+
{conversations.length === 0 ? (
|
|
127
|
+
<div className="flex flex-col items-center justify-center py-16 text-center px-4">
|
|
128
|
+
<MessageCircle size={24} className="text-text-4 mb-3" />
|
|
129
|
+
<p className="text-xs text-text-3 font-sans">No conversations yet</p>
|
|
130
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Start a new chat to begin</p>
|
|
131
|
+
</div>
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
{renderGroup('Pinned', groups.pinned)}
|
|
135
|
+
{renderGroup('Today', groups.today)}
|
|
136
|
+
{renderGroup('Yesterday', groups.yesterday)}
|
|
137
|
+
{renderGroup('Previous 7 Days', groups.week)}
|
|
138
|
+
{renderGroup('Older', groups.older)}
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="p-3 border-t border-border-subtle">
|
|
144
|
+
<button
|
|
145
|
+
onClick={onNewChat}
|
|
146
|
+
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
|
|
147
|
+
>
|
|
148
|
+
<Plus size={14} />
|
|
149
|
+
New Chat
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { ChevronDown, Globe, Cpu, Zap, Sparkles } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { Badge } from '../ui/badge';
|
|
7
|
+
|
|
8
|
+
export function formatModelName(id) {
|
|
9
|
+
if (!id) return '';
|
|
10
|
+
return id
|
|
11
|
+
.replace(/^claude-/, '')
|
|
12
|
+
.replace(/-(\d)/, ' $1')
|
|
13
|
+
.split('-')
|
|
14
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
15
|
+
.join(' ');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TIER_CONFIG = {
|
|
19
|
+
frontier: { label: 'Frontier', variant: 'purple', icon: Sparkles },
|
|
20
|
+
mid: { label: 'Mid', variant: 'accent', icon: Zap },
|
|
21
|
+
fast: { label: 'Fast', variant: 'success', icon: Zap },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getTier(model) {
|
|
25
|
+
const name = (model || '').toLowerCase();
|
|
26
|
+
if (name.includes('opus') || name.includes('pro') || name.includes('o3') || name.includes('gpt-4o')) return 'frontier';
|
|
27
|
+
if (name.includes('sonnet') || name.includes('flash') || name.includes('o4-mini')) return 'mid';
|
|
28
|
+
return 'fast';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getContextSize(model) {
|
|
32
|
+
const name = (model || '').toLowerCase();
|
|
33
|
+
if (name.includes('opus') || name.includes('sonnet')) return '200k';
|
|
34
|
+
if (name.includes('haiku')) return '200k';
|
|
35
|
+
if (name.includes('pro')) return '1M';
|
|
36
|
+
if (name.includes('flash')) return '1M';
|
|
37
|
+
if (name.includes('o3') || name.includes('o4')) return '128k';
|
|
38
|
+
return '128k';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ModelPicker({ value, onChange, disabled }) {
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const [providers, setProviders] = useState([]);
|
|
44
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
45
|
+
const ref = useRef(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
fetchProviders().then((data) => {
|
|
49
|
+
if (Array.isArray(data)) setProviders(data);
|
|
50
|
+
else if (data?.providers) setProviders(data.providers);
|
|
51
|
+
}).catch(() => {});
|
|
52
|
+
}, [fetchProviders]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!open) return;
|
|
56
|
+
function handleClick(e) {
|
|
57
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
58
|
+
}
|
|
59
|
+
document.addEventListener('mousedown', handleClick);
|
|
60
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
61
|
+
}, [open]);
|
|
62
|
+
|
|
63
|
+
const currentModel = value?.model || '';
|
|
64
|
+
const currentModelDisplay = currentModel ? formatModelName(currentModel) : 'Select model';
|
|
65
|
+
const currentProvider = value?.provider || '';
|
|
66
|
+
const isNetwork = currentProvider === 'groove-network';
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div ref={ref} className="relative">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
className={cn(
|
|
74
|
+
'flex items-center gap-1.5 h-8 px-2.5 rounded-lg text-xs font-medium font-sans transition-colors cursor-pointer',
|
|
75
|
+
'bg-surface-4 border border-border-subtle hover:bg-surface-5',
|
|
76
|
+
'disabled:opacity-40 disabled:cursor-not-allowed',
|
|
77
|
+
isNetwork && 'border-purple/30 bg-purple/8',
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{isNetwork ? <Globe size={12} className="text-purple" /> : <Cpu size={12} className="text-text-3" />}
|
|
81
|
+
<span className="text-text-1 max-w-[120px] truncate">{currentModelDisplay}</span>
|
|
82
|
+
<ChevronDown size={12} className="text-text-4" />
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
{open && (
|
|
86
|
+
<div className="absolute top-full left-0 mt-1 w-72 max-h-80 overflow-y-auto rounded-lg border border-border bg-surface-1 shadow-xl z-50">
|
|
87
|
+
{providers.length === 0 && (
|
|
88
|
+
<div className="px-4 py-6 text-center text-xs text-text-3 font-sans">No providers available</div>
|
|
89
|
+
)}
|
|
90
|
+
{providers.map((provider) => {
|
|
91
|
+
const models = provider.models || [];
|
|
92
|
+
if (models.length === 0) return null;
|
|
93
|
+
const isNetworkProvider = provider.id === 'groove-network';
|
|
94
|
+
return (
|
|
95
|
+
<div key={provider.id}>
|
|
96
|
+
<div className="px-3 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2 border-b border-border-subtle flex items-center gap-1.5">
|
|
97
|
+
{isNetworkProvider && <Globe size={10} className="text-purple" />}
|
|
98
|
+
{provider.name || provider.id}
|
|
99
|
+
</div>
|
|
100
|
+
{models.map((model) => {
|
|
101
|
+
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
102
|
+
const modelDisplayName = typeof model === 'string' ? model : model.name || model.id;
|
|
103
|
+
const tier = getTier(modelId);
|
|
104
|
+
const tierConfig = TIER_CONFIG[tier];
|
|
105
|
+
const TierIcon = tierConfig.icon;
|
|
106
|
+
const isActive = currentModel === modelId && currentProvider === provider.id;
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
key={modelId}
|
|
110
|
+
onClick={() => {
|
|
111
|
+
onChange({ provider: provider.id, model: modelId });
|
|
112
|
+
setOpen(false);
|
|
113
|
+
}}
|
|
114
|
+
className={cn(
|
|
115
|
+
'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer',
|
|
116
|
+
isActive ? 'bg-accent/10 text-text-0' : 'hover:bg-surface-3 text-text-1',
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<div className="flex-1 min-w-0">
|
|
120
|
+
<div className="text-xs font-medium font-sans truncate">{modelDisplayName}</div>
|
|
121
|
+
<div className="text-2xs text-text-4 font-sans">{getContextSize(modelId)} context</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
124
|
+
{isNetworkProvider && (
|
|
125
|
+
<Badge variant="purple" className="text-[9px]">
|
|
126
|
+
<Globe size={8} /> Decentralized
|
|
127
|
+
</Badge>
|
|
128
|
+
)}
|
|
129
|
+
<Badge variant={tierConfig.variant} className="text-[9px]">
|
|
130
|
+
<TierIcon size={8} /> {tierConfig.label}
|
|
131
|
+
</Badge>
|
|
132
|
+
</div>
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe } from 'lucide-react';
|
|
2
|
+
import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe, MessageCircle } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { Tooltip } from '../ui/tooltip';
|
|
5
5
|
import { useGrooveStore } from '../../stores/groove';
|
|
@@ -13,6 +13,7 @@ const BASE_NAV_ITEMS = [
|
|
|
13
13
|
{ id: 'toys', icon: Gamepad2, label: 'Toys' },
|
|
14
14
|
{ id: 'models', icon: Box, label: 'Models' },
|
|
15
15
|
{ id: 'teams', icon: Users, label: 'Teams' },
|
|
16
|
+
{ id: 'chat', icon: MessageCircle, label: 'Chat' },
|
|
16
17
|
];
|
|
17
18
|
|
|
18
19
|
const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
|