mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-23 02:16:08 +00:00
Merge branch 'main' into ACT_FEAT_BoltDYI_UI_BUGFIX
This commit is contained in:
commit
7f3b5f6628
@ -1,6 +1,9 @@
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import * as React from 'react';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
model?: string;
|
||||
@ -22,12 +25,118 @@ export const ModelSelector = ({
|
||||
providerList,
|
||||
modelLoading,
|
||||
}: ModelSelectorProps) => {
|
||||
// Load enabled providers from cookies
|
||||
const [modelSearchQuery, setModelSearchQuery] = useState('');
|
||||
const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsModelDropdownOpen(false);
|
||||
setModelSearchQuery('');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Filter models based on search query
|
||||
const filteredModels = [...modelList]
|
||||
.filter((e) => e.provider === provider?.name && e.name)
|
||||
.filter(
|
||||
(model) =>
|
||||
model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
|
||||
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
// Reset focused index when search query changes or dropdown opens/closes
|
||||
useEffect(() => {
|
||||
setFocusedIndex(-1);
|
||||
}, [modelSearchQuery, isModelDropdownOpen]);
|
||||
|
||||
// Focus search input when dropdown opens
|
||||
useEffect(() => {
|
||||
if (isModelDropdownOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isModelDropdownOpen]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isModelDropdownOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) => {
|
||||
const next = prev + 1;
|
||||
|
||||
if (next >= filteredModels.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) => {
|
||||
const next = prev - 1;
|
||||
|
||||
if (next < 0) {
|
||||
return filteredModels.length - 1;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
|
||||
if (focusedIndex >= 0 && focusedIndex < filteredModels.length) {
|
||||
const selectedModel = filteredModels[focusedIndex];
|
||||
setModel?.(selectedModel.name);
|
||||
setIsModelDropdownOpen(false);
|
||||
setModelSearchQuery('');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
setIsModelDropdownOpen(false);
|
||||
setModelSearchQuery('');
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
if (!e.shiftKey && focusedIndex === filteredModels.length - 1) {
|
||||
setIsModelDropdownOpen(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Focus the selected option
|
||||
useEffect(() => {
|
||||
if (focusedIndex >= 0 && optionsRef.current[focusedIndex]) {
|
||||
optionsRef.current[focusedIndex]?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [focusedIndex]);
|
||||
|
||||
// Update enabled providers when cookies change
|
||||
useEffect(() => {
|
||||
// If current provider is disabled, switch to first enabled provider
|
||||
if (providerList.length == 0) {
|
||||
if (providerList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,27 +189,124 @@ export const ModelSelector = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
key={provider?.name}
|
||||
value={model}
|
||||
onChange={(e) => setModel?.(e.target.value)}
|
||||
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%]"
|
||||
disabled={modelLoading === 'all' || modelLoading === provider?.name}
|
||||
>
|
||||
{modelLoading == 'all' || modelLoading == provider?.name ? (
|
||||
<option key={0} value="">
|
||||
Loading...
|
||||
</option>
|
||||
) : (
|
||||
[...modelList]
|
||||
.filter((e) => e.provider == provider?.name && e.name)
|
||||
.map((modelOption, index) => (
|
||||
<option key={index} value={modelOption.name}>
|
||||
{modelOption.label}
|
||||
</option>
|
||||
))
|
||||
|
||||
<div className="relative flex-1 lg:max-w-[70%]" onKeyDown={handleKeyDown} ref={dropdownRef}>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full p-2 rounded-lg border border-bolt-elements-borderColor',
|
||||
'bg-bolt-elements-prompt-background text-bolt-elements-textPrimary',
|
||||
'focus-within:outline-none focus-within:ring-2 focus-within:ring-bolt-elements-focus',
|
||||
'transition-all cursor-pointer',
|
||||
isModelDropdownOpen ? 'ring-2 ring-bolt-elements-focus' : undefined,
|
||||
)}
|
||||
onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setIsModelDropdownOpen(!isModelDropdownOpen);
|
||||
}
|
||||
}}
|
||||
role="combobox"
|
||||
aria-expanded={isModelDropdownOpen}
|
||||
aria-controls="model-listbox"
|
||||
aria-haspopup="listbox"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75',
|
||||
isModelDropdownOpen ? 'rotate-180' : undefined,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isModelDropdownOpen && (
|
||||
<div
|
||||
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"
|
||||
role="listbox"
|
||||
id="model-listbox"
|
||||
>
|
||||
<div className="px-2 pb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={modelSearchQuery}
|
||||
onChange={(e) => setModelSearchQuery(e.target.value)}
|
||||
placeholder="Search models..."
|
||||
className={classNames(
|
||||
'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
|
||||
'transition-all',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="searchbox"
|
||||
aria-label="Search models"
|
||||
/>
|
||||
<div className="absolute left-2.5 top-1/2 -translate-y-1/2">
|
||||
<span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'max-h-60 overflow-y-auto',
|
||||
'sm:scrollbar-none',
|
||||
'[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2',
|
||||
'[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor',
|
||||
'[&::-webkit-scrollbar-thumb]:hover:bg-bolt-elements-borderColorHover',
|
||||
'[&::-webkit-scrollbar-thumb]:rounded-full',
|
||||
'[&::-webkit-scrollbar-track]:bg-bolt-elements-background-depth-2',
|
||||
'[&::-webkit-scrollbar-track]:rounded-full',
|
||||
'sm:[&::-webkit-scrollbar]:w-1.5 sm:[&::-webkit-scrollbar]:h-1.5',
|
||||
'sm:hover:[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor/50',
|
||||
'sm:hover:[&::-webkit-scrollbar-thumb:hover]:bg-bolt-elements-borderColor',
|
||||
'sm:[&::-webkit-scrollbar-track]:bg-transparent',
|
||||
)}
|
||||
>
|
||||
{modelLoading === 'all' || modelLoading === provider?.name ? (
|
||||
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>
|
||||
) : filteredModels.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No models found</div>
|
||||
) : (
|
||||
filteredModels.map((modelOption, index) => (
|
||||
<div
|
||||
ref={(el) => (optionsRef.current[index] = el)}
|
||||
key={index}
|
||||
role="option"
|
||||
aria-selected={model === modelOption.name}
|
||||
className={classNames(
|
||||
'px-3 py-2 text-sm cursor-pointer',
|
||||
'hover:bg-bolt-elements-background-depth-3',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'outline-none',
|
||||
model === modelOption.name || focusedIndex === index
|
||||
? 'bg-bolt-elements-background-depth-2'
|
||||
: undefined,
|
||||
focusedIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setModel?.(modelOption.name);
|
||||
setIsModelDropdownOpen(false);
|
||||
setModelSearchQuery('');
|
||||
}}
|
||||
tabIndex={focusedIndex === index ? 0 : -1}
|
||||
>
|
||||
{modelOption.label}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,12 @@ export default class AmazonBedrockProvider extends BaseProvider {
|
||||
};
|
||||
|
||||
staticModels: ModelInfo[] = [
|
||||
{
|
||||
name: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
label: 'Claude 3.5 Sonnet v2 (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 200000,
|
||||
},
|
||||
{
|
||||
name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
|
||||
label: 'Claude 3.5 Sonnet (Bedrock)',
|
||||
|
Loading…
Reference in New Issue
Block a user