Merge branch 'main' into ACT_FEAT_BoltDYI_UI_BUGFIX
Browse files
app/components/chat/ModelSelector.tsx
CHANGED
@@ -1,6 +1,9 @@
|
|
1 |
import type { ProviderInfo } from '~/types/model';
|
2 |
-
import { useEffect } from 'react';
|
|
|
3 |
import type { ModelInfo } from '~/lib/modules/llm/types';
|
|
|
|
|
4 |
|
5 |
interface ModelSelectorProps {
|
6 |
model?: string;
|
@@ -22,12 +25,118 @@ export const ModelSelector = ({
|
|
22 |
providerList,
|
23 |
modelLoading,
|
24 |
}: ModelSelectorProps) => {
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
// Update enabled providers when cookies change
|
28 |
useEffect(() => {
|
29 |
// If current provider is disabled, switch to first enabled provider
|
30 |
-
if (providerList.length
|
31 |
return;
|
32 |
}
|
33 |
|
@@ -80,27 +189,124 @@ export const ModelSelector = ({
|
|
80 |
</option>
|
81 |
))}
|
82 |
</select>
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
)}
|
103 |
-
</
|
104 |
</div>
|
105 |
);
|
106 |
};
|
|
|
1 |
import type { ProviderInfo } from '~/types/model';
|
2 |
+
import { useEffect, useState, useRef } from 'react';
|
3 |
+
import type { KeyboardEvent } from 'react';
|
4 |
import type { ModelInfo } from '~/lib/modules/llm/types';
|
5 |
+
import { classNames } from '~/utils/classNames';
|
6 |
+
import * as React from 'react';
|
7 |
|
8 |
interface ModelSelectorProps {
|
9 |
model?: string;
|
|
|
25 |
providerList,
|
26 |
modelLoading,
|
27 |
}: ModelSelectorProps) => {
|
28 |
+
const [modelSearchQuery, setModelSearchQuery] = useState('');
|
29 |
+
const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
|
30 |
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
31 |
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
32 |
+
const optionsRef = useRef<(HTMLDivElement | null)[]>([]);
|
33 |
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
34 |
+
|
35 |
+
useEffect(() => {
|
36 |
+
const handleClickOutside = (event: MouseEvent) => {
|
37 |
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
38 |
+
setIsModelDropdownOpen(false);
|
39 |
+
setModelSearchQuery('');
|
40 |
+
}
|
41 |
+
};
|
42 |
+
|
43 |
+
document.addEventListener('mousedown', handleClickOutside);
|
44 |
+
|
45 |
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
46 |
+
}, []);
|
47 |
+
|
48 |
+
// Filter models based on search query
|
49 |
+
const filteredModels = [...modelList]
|
50 |
+
.filter((e) => e.provider === provider?.name && e.name)
|
51 |
+
.filter(
|
52 |
+
(model) =>
|
53 |
+
model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
|
54 |
+
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()),
|
55 |
+
);
|
56 |
+
|
57 |
+
// Reset focused index when search query changes or dropdown opens/closes
|
58 |
+
useEffect(() => {
|
59 |
+
setFocusedIndex(-1);
|
60 |
+
}, [modelSearchQuery, isModelDropdownOpen]);
|
61 |
+
|
62 |
+
// Focus search input when dropdown opens
|
63 |
+
useEffect(() => {
|
64 |
+
if (isModelDropdownOpen && searchInputRef.current) {
|
65 |
+
searchInputRef.current.focus();
|
66 |
+
}
|
67 |
+
}, [isModelDropdownOpen]);
|
68 |
+
|
69 |
+
// Handle keyboard navigation
|
70 |
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
71 |
+
if (!isModelDropdownOpen) {
|
72 |
+
return;
|
73 |
+
}
|
74 |
+
|
75 |
+
switch (e.key) {
|
76 |
+
case 'ArrowDown':
|
77 |
+
e.preventDefault();
|
78 |
+
setFocusedIndex((prev) => {
|
79 |
+
const next = prev + 1;
|
80 |
+
|
81 |
+
if (next >= filteredModels.length) {
|
82 |
+
return 0;
|
83 |
+
}
|
84 |
+
|
85 |
+
return next;
|
86 |
+
});
|
87 |
+
break;
|
88 |
+
|
89 |
+
case 'ArrowUp':
|
90 |
+
e.preventDefault();
|
91 |
+
setFocusedIndex((prev) => {
|
92 |
+
const next = prev - 1;
|
93 |
+
|
94 |
+
if (next < 0) {
|
95 |
+
return filteredModels.length - 1;
|
96 |
+
}
|
97 |
+
|
98 |
+
return next;
|
99 |
+
});
|
100 |
+
break;
|
101 |
+
|
102 |
+
case 'Enter':
|
103 |
+
e.preventDefault();
|
104 |
+
|
105 |
+
if (focusedIndex >= 0 && focusedIndex < filteredModels.length) {
|
106 |
+
const selectedModel = filteredModels[focusedIndex];
|
107 |
+
setModel?.(selectedModel.name);
|
108 |
+
setIsModelDropdownOpen(false);
|
109 |
+
setModelSearchQuery('');
|
110 |
+
}
|
111 |
+
|
112 |
+
break;
|
113 |
+
|
114 |
+
case 'Escape':
|
115 |
+
e.preventDefault();
|
116 |
+
setIsModelDropdownOpen(false);
|
117 |
+
setModelSearchQuery('');
|
118 |
+
break;
|
119 |
+
|
120 |
+
case 'Tab':
|
121 |
+
if (!e.shiftKey && focusedIndex === filteredModels.length - 1) {
|
122 |
+
setIsModelDropdownOpen(false);
|
123 |
+
}
|
124 |
+
|
125 |
+
break;
|
126 |
+
}
|
127 |
+
};
|
128 |
+
|
129 |
+
// Focus the selected option
|
130 |
+
useEffect(() => {
|
131 |
+
if (focusedIndex >= 0 && optionsRef.current[focusedIndex]) {
|
132 |
+
optionsRef.current[focusedIndex]?.scrollIntoView({ block: 'nearest' });
|
133 |
+
}
|
134 |
+
}, [focusedIndex]);
|
135 |
|
136 |
// Update enabled providers when cookies change
|
137 |
useEffect(() => {
|
138 |
// If current provider is disabled, switch to first enabled provider
|
139 |
+
if (providerList.length === 0) {
|
140 |
return;
|
141 |
}
|
142 |
|
|
|
189 |
</option>
|
190 |
))}
|
191 |
</select>
|
192 |
+
|
193 |
+
<div className="relative flex-1 lg:max-w-[70%]" onKeyDown={handleKeyDown} ref={dropdownRef}>
|
194 |
+
<div
|
195 |
+
className={classNames(
|
196 |
+
'w-full p-2 rounded-lg border border-bolt-elements-borderColor',
|
197 |
+
'bg-bolt-elements-prompt-background text-bolt-elements-textPrimary',
|
198 |
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-bolt-elements-focus',
|
199 |
+
'transition-all cursor-pointer',
|
200 |
+
isModelDropdownOpen ? 'ring-2 ring-bolt-elements-focus' : undefined,
|
201 |
+
)}
|
202 |
+
onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}
|
203 |
+
onKeyDown={(e) => {
|
204 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
205 |
+
e.preventDefault();
|
206 |
+
setIsModelDropdownOpen(!isModelDropdownOpen);
|
207 |
+
}
|
208 |
+
}}
|
209 |
+
role="combobox"
|
210 |
+
aria-expanded={isModelDropdownOpen}
|
211 |
+
aria-controls="model-listbox"
|
212 |
+
aria-haspopup="listbox"
|
213 |
+
tabIndex={0}
|
214 |
+
>
|
215 |
+
<div className="flex items-center justify-between">
|
216 |
+
<div className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</div>
|
217 |
+
<div
|
218 |
+
className={classNames(
|
219 |
+
'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75',
|
220 |
+
isModelDropdownOpen ? 'rotate-180' : undefined,
|
221 |
+
)}
|
222 |
+
/>
|
223 |
+
</div>
|
224 |
+
</div>
|
225 |
+
|
226 |
+
{isModelDropdownOpen && (
|
227 |
+
<div
|
228 |
+
className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 shadow-lg"
|
229 |
+
role="listbox"
|
230 |
+
id="model-listbox"
|
231 |
+
>
|
232 |
+
<div className="px-2 pb-2">
|
233 |
+
<div className="relative">
|
234 |
+
<input
|
235 |
+
ref={searchInputRef}
|
236 |
+
type="text"
|
237 |
+
value={modelSearchQuery}
|
238 |
+
onChange={(e) => setModelSearchQuery(e.target.value)}
|
239 |
+
placeholder="Search models..."
|
240 |
+
className={classNames(
|
241 |
+
'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
|
242 |
+
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
243 |
+
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
|
244 |
+
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
|
245 |
+
'transition-all',
|
246 |
+
)}
|
247 |
+
onClick={(e) => e.stopPropagation()}
|
248 |
+
role="searchbox"
|
249 |
+
aria-label="Search models"
|
250 |
+
/>
|
251 |
+
<div className="absolute left-2.5 top-1/2 -translate-y-1/2">
|
252 |
+
<span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
|
253 |
+
</div>
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
|
257 |
+
<div
|
258 |
+
className={classNames(
|
259 |
+
'max-h-60 overflow-y-auto',
|
260 |
+
'sm:scrollbar-none',
|
261 |
+
'[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2',
|
262 |
+
'[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor',
|
263 |
+
'[&::-webkit-scrollbar-thumb]:hover:bg-bolt-elements-borderColorHover',
|
264 |
+
'[&::-webkit-scrollbar-thumb]:rounded-full',
|
265 |
+
'[&::-webkit-scrollbar-track]:bg-bolt-elements-background-depth-2',
|
266 |
+
'[&::-webkit-scrollbar-track]:rounded-full',
|
267 |
+
'sm:[&::-webkit-scrollbar]:w-1.5 sm:[&::-webkit-scrollbar]:h-1.5',
|
268 |
+
'sm:hover:[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor/50',
|
269 |
+
'sm:hover:[&::-webkit-scrollbar-thumb:hover]:bg-bolt-elements-borderColor',
|
270 |
+
'sm:[&::-webkit-scrollbar-track]:bg-transparent',
|
271 |
+
)}
|
272 |
+
>
|
273 |
+
{modelLoading === 'all' || modelLoading === provider?.name ? (
|
274 |
+
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>
|
275 |
+
) : filteredModels.length === 0 ? (
|
276 |
+
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No models found</div>
|
277 |
+
) : (
|
278 |
+
filteredModels.map((modelOption, index) => (
|
279 |
+
<div
|
280 |
+
ref={(el) => (optionsRef.current[index] = el)}
|
281 |
+
key={index}
|
282 |
+
role="option"
|
283 |
+
aria-selected={model === modelOption.name}
|
284 |
+
className={classNames(
|
285 |
+
'px-3 py-2 text-sm cursor-pointer',
|
286 |
+
'hover:bg-bolt-elements-background-depth-3',
|
287 |
+
'text-bolt-elements-textPrimary',
|
288 |
+
'outline-none',
|
289 |
+
model === modelOption.name || focusedIndex === index
|
290 |
+
? 'bg-bolt-elements-background-depth-2'
|
291 |
+
: undefined,
|
292 |
+
focusedIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined,
|
293 |
+
)}
|
294 |
+
onClick={(e) => {
|
295 |
+
e.stopPropagation();
|
296 |
+
setModel?.(modelOption.name);
|
297 |
+
setIsModelDropdownOpen(false);
|
298 |
+
setModelSearchQuery('');
|
299 |
+
}}
|
300 |
+
tabIndex={focusedIndex === index ? 0 : -1}
|
301 |
+
>
|
302 |
+
{modelOption.label}
|
303 |
+
</div>
|
304 |
+
))
|
305 |
+
)}
|
306 |
+
</div>
|
307 |
+
</div>
|
308 |
)}
|
309 |
+
</div>
|
310 |
</div>
|
311 |
);
|
312 |
};
|
app/lib/modules/llm/providers/amazon-bedrock.ts
CHANGED
@@ -20,6 +20,12 @@ export default class AmazonBedrockProvider extends BaseProvider {
|
|
20 |
};
|
21 |
|
22 |
staticModels: ModelInfo[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
{
|
24 |
name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
|
25 |
label: 'Claude 3.5 Sonnet (Bedrock)',
|
|
|
20 |
};
|
21 |
|
22 |
staticModels: ModelInfo[] = [
|
23 |
+
{
|
24 |
+
name: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
25 |
+
label: 'Claude 3.5 Sonnet v2 (Bedrock)',
|
26 |
+
provider: 'AmazonBedrock',
|
27 |
+
maxTokenAllowed: 200000,
|
28 |
+
},
|
29 |
{
|
30 |
name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
|
31 |
label: 'Claude 3.5 Sonnet (Bedrock)',
|