diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c4706e1764..954b89deb9 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -8,7 +8,7 @@ import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks'; import { description, useChatHistory } from '~/lib/persistence'; import { chatStore } from '~/lib/stores/chat'; import { workbenchStore } from '~/lib/stores/workbench'; -import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants'; +import { DEFAULT_MODEL, PROMPT_COOKIE_KEY } from '~/utils/constants'; import { cubicEasingFn } from '~/utils/easings'; import { createScopedLogger, renderLogger } from '~/utils/logger'; import { BaseChat } from './BaseChat'; @@ -102,14 +102,8 @@ export const ChatImpl = memo( const supabaseAlert = useStore(workbenchStore.supabaseAlert); const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); const [llmErrorAlert, setLlmErrorAlert] = useState(undefined); - const [model, setModel] = useState(() => { - const savedModel = Cookies.get('selectedModel'); - return savedModel || DEFAULT_MODEL; - }); - const [provider, setProvider] = useState(() => { - const savedProvider = Cookies.get('selectedProvider'); - return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo; - }); + const model = DEFAULT_MODEL; + const provider = { name: 'Anthropic' } as ProviderInfo; const { showChat } = useStore(chatStore); const [animationScope, animate] = useAnimate(); const [apiKeys, setApiKeys] = useState>({}); @@ -584,15 +578,8 @@ export const ChatImpl = memo( } }, []); - const handleModelChange = (newModel: string) => { - setModel(newModel); - Cookies.set('selectedModel', newModel, { expires: 30 }); - }; - - const handleProviderChange = (newProvider: ProviderInfo) => { - setProvider(newProvider); - Cookies.set('selectedProvider', newProvider.name, { expires: 30 }); - }; + const handleModelChange = (_model: string) => {}; + const handleProviderChange = (_provider: ProviderInfo) => {}; return ( = (props) => { apiKeys={props.apiKeys} modelLoading={props.isModelLoading} /> - {(props.providerList || []).length > 0 && - props.provider && - !LOCAL_PROVIDERS.includes(props.provider.name) && ( - { - props.onApiKeysChange(props.provider.name, key); - }} - /> - )} )} diff --git a/app/components/chat/ModelSelector.tsx b/app/components/chat/ModelSelector.tsx index 2ccb9a5277..e98273c6d8 100644 --- a/app/components/chat/ModelSelector.tsx +++ b/app/components/chat/ModelSelector.tsx @@ -1,83 +1,5 @@ import type { ProviderInfo } from '~/types/model'; -import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; -import type { KeyboardEvent } from 'react'; import type { ModelInfo } from '~/lib/modules/llm/types'; -import { classNames } from '~/utils/classNames'; - -// Fuzzy search utilities -const levenshteinDistance = (str1: string, str2: string): number => { - const matrix = []; - - for (let i = 0; i <= str2.length; i++) { - matrix[i] = [i]; - } - - for (let j = 0; j <= str1.length; j++) { - matrix[0][j] = j; - } - - for (let i = 1; i <= str2.length; i++) { - for (let j = 1; j <= str1.length; j++) { - if (str2.charAt(i - 1) === str1.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); - } - } - } - - return matrix[str2.length][str1.length]; -}; - -const fuzzyMatch = (query: string, text: string): { score: number; matches: boolean } => { - if (!query) { - return { score: 0, matches: true }; - } - - if (!text) { - return { score: 0, matches: false }; - } - - const queryLower = query.toLowerCase(); - const textLower = text.toLowerCase(); - - // Exact substring match gets highest score - if (textLower.includes(queryLower)) { - return { score: 100 - (textLower.indexOf(queryLower) / textLower.length) * 20, matches: true }; - } - - // Fuzzy match with reasonable threshold - const distance = levenshteinDistance(queryLower, textLower); - const maxLen = Math.max(queryLower.length, textLower.length); - const similarity = 1 - distance / maxLen; - - return { - score: similarity > 0.6 ? similarity * 80 : 0, - matches: similarity > 0.6, - }; -}; - -const highlightText = (text: string, query: string): string => { - if (!query) { - return text; - } - - const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); - - return text.replace(regex, '$1'); -}; - -const formatContextSize = (tokens: number): string => { - if (tokens >= 1000000) { - return `${(tokens / 1000000).toFixed(1)}M`; - } - - if (tokens >= 1000) { - return `${(tokens / 1000).toFixed(0)}K`; - } - - return tokens.toString(); -}; interface ModelSelectorProps { model?: string; @@ -90,707 +12,13 @@ interface ModelSelectorProps { modelLoading?: string; } -// Helper function to determine if a model is likely free -const isModelLikelyFree = (model: ModelInfo, providerName?: string): boolean => { - // OpenRouter models with zero pricing in the label - if (providerName === 'OpenRouter' && model.label.includes('in:$0.00') && model.label.includes('out:$0.00')) { - return true; - } - - // Models with "free" in the name or label - if (model.name.toLowerCase().includes('free') || model.label.toLowerCase().includes('free')) { - return true; - } - - return false; -}; - -export const ModelSelector = ({ - model, - setModel, - provider, - setProvider, - modelList, - providerList, - modelLoading, -}: ModelSelectorProps) => { - const [modelSearchQuery, setModelSearchQuery] = useState(''); - const [debouncedModelSearchQuery, setDebouncedModelSearchQuery] = useState(''); - const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); - const [focusedModelIndex, setFocusedModelIndex] = useState(-1); - const modelSearchInputRef = useRef(null); - const modelOptionsRef = useRef<(HTMLDivElement | null)[]>([]); - const modelDropdownRef = useRef(null); - const [providerSearchQuery, setProviderSearchQuery] = useState(''); - const [debouncedProviderSearchQuery, setDebouncedProviderSearchQuery] = useState(''); - const [isProviderDropdownOpen, setIsProviderDropdownOpen] = useState(false); - const [focusedProviderIndex, setFocusedProviderIndex] = useState(-1); - const providerSearchInputRef = useRef(null); - const providerOptionsRef = useRef<(HTMLDivElement | null)[]>([]); - const providerDropdownRef = useRef(null); - const [showFreeModelsOnly, setShowFreeModelsOnly] = useState(false); - - // Debounce search queries - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedModelSearchQuery(modelSearchQuery); - }, 150); - - return () => clearTimeout(timer); - }, [modelSearchQuery]); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedProviderSearchQuery(providerSearchQuery); - }, 150); - - return () => clearTimeout(timer); - }, [providerSearchQuery]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) { - setIsModelDropdownOpen(false); - setModelSearchQuery(''); - } - - if (providerDropdownRef.current && !providerDropdownRef.current.contains(event.target as Node)) { - setIsProviderDropdownOpen(false); - setProviderSearchQuery(''); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const filteredModels = useMemo(() => { - const baseModels = [...modelList].filter((e) => e.provider === provider?.name && e.name); - - return baseModels - .filter((model) => { - // Apply free models filter - if (showFreeModelsOnly && !isModelLikelyFree(model, provider?.name)) { - return false; - } - - return true; - }) - .map((model) => { - // Calculate search scores for fuzzy matching - const labelMatch = fuzzyMatch(debouncedModelSearchQuery, model.label); - const nameMatch = fuzzyMatch(debouncedModelSearchQuery, model.name); - const contextMatch = fuzzyMatch(debouncedModelSearchQuery, formatContextSize(model.maxTokenAllowed)); - - const bestScore = Math.max(labelMatch.score, nameMatch.score, contextMatch.score); - const matches = labelMatch.matches || nameMatch.matches || contextMatch.matches || !debouncedModelSearchQuery; // Show all if no query - - return { - ...model, - searchScore: bestScore, - searchMatches: matches, - highlightedLabel: highlightText(model.label, debouncedModelSearchQuery), - highlightedName: highlightText(model.name, debouncedModelSearchQuery), - }; - }) - .filter((model) => model.searchMatches) - .sort((a, b) => { - // Sort by search score (highest first), then by label - if (debouncedModelSearchQuery) { - return b.searchScore - a.searchScore; - } - - return a.label.localeCompare(b.label); - }); - }, [modelList, provider?.name, showFreeModelsOnly, debouncedModelSearchQuery]); - - const filteredProviders = useMemo(() => { - if (!debouncedProviderSearchQuery) { - return providerList; - } - - return providerList - .map((provider) => { - const match = fuzzyMatch(debouncedProviderSearchQuery, provider.name); - return { - ...provider, - searchScore: match.score, - searchMatches: match.matches, - highlightedName: highlightText(provider.name, debouncedProviderSearchQuery), - }; - }) - .filter((provider) => provider.searchMatches) - .sort((a, b) => b.searchScore - a.searchScore); - }, [providerList, debouncedProviderSearchQuery]); - - // Reset free models filter when provider changes - useEffect(() => { - setShowFreeModelsOnly(false); - }, [provider?.name]); - - useEffect(() => { - setFocusedModelIndex(-1); - }, [debouncedModelSearchQuery, isModelDropdownOpen, showFreeModelsOnly]); - - useEffect(() => { - setFocusedProviderIndex(-1); - }, [debouncedProviderSearchQuery, isProviderDropdownOpen]); - - // Clear search functions - const clearModelSearch = useCallback(() => { - setModelSearchQuery(''); - setDebouncedModelSearchQuery(''); - - if (modelSearchInputRef.current) { - modelSearchInputRef.current.focus(); - } - }, []); - - const clearProviderSearch = useCallback(() => { - setProviderSearchQuery(''); - setDebouncedProviderSearchQuery(''); - - if (providerSearchInputRef.current) { - providerSearchInputRef.current.focus(); - } - }, []); - - useEffect(() => { - if (isModelDropdownOpen && modelSearchInputRef.current) { - modelSearchInputRef.current.focus(); - } - }, [isModelDropdownOpen]); - - useEffect(() => { - if (isProviderDropdownOpen && providerSearchInputRef.current) { - providerSearchInputRef.current.focus(); - } - }, [isProviderDropdownOpen]); - - const handleModelKeyDown = (e: KeyboardEvent) => { - if (!isModelDropdownOpen) { - return; - } - - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setFocusedModelIndex((prev) => (prev + 1 >= filteredModels.length ? 0 : prev + 1)); - break; - case 'ArrowUp': - e.preventDefault(); - setFocusedModelIndex((prev) => (prev - 1 < 0 ? filteredModels.length - 1 : prev - 1)); - break; - case 'Enter': - e.preventDefault(); - - if (focusedModelIndex >= 0 && focusedModelIndex < filteredModels.length) { - const selectedModel = filteredModels[focusedModelIndex]; - setModel?.(selectedModel.name); - setIsModelDropdownOpen(false); - setModelSearchQuery(''); - setDebouncedModelSearchQuery(''); - } - - break; - case 'Escape': - e.preventDefault(); - setIsModelDropdownOpen(false); - setModelSearchQuery(''); - setDebouncedModelSearchQuery(''); - break; - case 'Tab': - if (!e.shiftKey && focusedModelIndex === filteredModels.length - 1) { - setIsModelDropdownOpen(false); - } - - break; - case 'k': - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - clearModelSearch(); - } - - break; - } - }; - - const handleProviderKeyDown = (e: KeyboardEvent) => { - if (!isProviderDropdownOpen) { - return; - } - - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setFocusedProviderIndex((prev) => (prev + 1 >= filteredProviders.length ? 0 : prev + 1)); - break; - case 'ArrowUp': - e.preventDefault(); - setFocusedProviderIndex((prev) => (prev - 1 < 0 ? filteredProviders.length - 1 : prev - 1)); - break; - case 'Enter': - e.preventDefault(); - - if (focusedProviderIndex >= 0 && focusedProviderIndex < filteredProviders.length) { - const selectedProvider = filteredProviders[focusedProviderIndex]; - - if (setProvider) { - setProvider(selectedProvider); - - const firstModel = modelList.find((m) => m.provider === selectedProvider.name); - - if (firstModel && setModel) { - setModel(firstModel.name); - } - } - - setIsProviderDropdownOpen(false); - setProviderSearchQuery(''); - setDebouncedProviderSearchQuery(''); - } - - break; - case 'Escape': - e.preventDefault(); - setIsProviderDropdownOpen(false); - setProviderSearchQuery(''); - setDebouncedProviderSearchQuery(''); - break; - case 'Tab': - if (!e.shiftKey && focusedProviderIndex === filteredProviders.length - 1) { - setIsProviderDropdownOpen(false); - } - - break; - case 'k': - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - clearProviderSearch(); - } - - break; - } - }; - - useEffect(() => { - if (focusedModelIndex >= 0 && modelOptionsRef.current[focusedModelIndex]) { - modelOptionsRef.current[focusedModelIndex]?.scrollIntoView({ block: 'nearest' }); - } - }, [focusedModelIndex]); - - useEffect(() => { - if (focusedProviderIndex >= 0 && providerOptionsRef.current[focusedProviderIndex]) { - providerOptionsRef.current[focusedProviderIndex]?.scrollIntoView({ block: 'nearest' }); - } - }, [focusedProviderIndex]); - - useEffect(() => { - if (providerList.length === 0) { - return; - } - - if (provider && !providerList.some((p) => p.name === provider.name)) { - const firstEnabledProvider = providerList[0]; - setProvider?.(firstEnabledProvider); - - const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name); - - if (firstModel) { - setModel?.(firstModel.name); - } - } - }, [providerList, provider, setProvider, modelList, setModel]); - - if (providerList.length === 0) { - return ( -
-

- No providers are currently enabled. Please enable at least one provider in the settings to start using the - chat. -

-
- ); - } - +export const ModelSelector = (_props: ModelSelectorProps) => { return ( -
- {/* Provider Combobox */} -
-
setIsProviderDropdownOpen(!isProviderDropdownOpen)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setIsProviderDropdownOpen(!isProviderDropdownOpen); - } - }} - role="combobox" - aria-expanded={isProviderDropdownOpen} - aria-controls="provider-listbox" - aria-haspopup="listbox" - tabIndex={0} - > -
-
{provider?.name || 'Select provider'}
-
-
-
- - {isProviderDropdownOpen && ( -
-
-
- setProviderSearchQuery(e.target.value)} - placeholder="Search providers... (⌘K to clear)" - className={classNames( - 'w-full pl-8 pr-8 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 providers" - /> -
- -
- {providerSearchQuery && ( - - )} -
-
- -
- {filteredProviders.length === 0 ? ( -
-
- {debouncedProviderSearchQuery - ? `No providers match "${debouncedProviderSearchQuery}"` - : 'No providers found'} -
- {debouncedProviderSearchQuery && ( -
- Try searching for provider names like "OpenAI", "Anthropic", or "Google" -
- )} -
- ) : ( - filteredProviders.map((providerOption, index) => ( -
(providerOptionsRef.current[index] = el)} - key={providerOption.name} - role="option" - aria-selected={provider?.name === providerOption.name} - className={classNames( - 'px-3 py-2 text-sm cursor-pointer', - 'hover:bg-bolt-elements-background-depth-3', - 'text-bolt-elements-textPrimary', - 'outline-none', - provider?.name === providerOption.name || focusedProviderIndex === index - ? 'bg-bolt-elements-background-depth-2' - : undefined, - focusedProviderIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined, - )} - onClick={(e) => { - e.stopPropagation(); - - if (setProvider) { - setProvider(providerOption); - - const firstModel = modelList.find((m) => m.provider === providerOption.name); - - if (firstModel && setModel) { - setModel(firstModel.name); - } - } - - setIsProviderDropdownOpen(false); - setProviderSearchQuery(''); - setDebouncedProviderSearchQuery(''); - }} - tabIndex={focusedProviderIndex === index ? 0 : -1} - > -
-
- )) - )} -
-
- )} -
- - {/* Model Combobox */} -
-
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} - > -
-
{modelList.find((m) => m.name === model)?.label || 'Select model'}
-
-
-
- - {isModelDropdownOpen && ( -
-
- {/* Free Models Filter Toggle - Only show for OpenRouter */} - {provider?.name === 'OpenRouter' && ( -
- - {showFreeModelsOnly && ( - - {filteredModels.length} free model{filteredModels.length !== 1 ? 's' : ''} - - )} -
- )} - - {/* Search Result Count */} - {debouncedModelSearchQuery && filteredModels.length > 0 && ( -
- {filteredModels.length} model{filteredModels.length !== 1 ? 's' : ''} found - {filteredModels.length > 5 && ' (showing best matches)'} -
- )} - - {/* Search Input */} -
- setModelSearchQuery(e.target.value)} - placeholder="Search models... (⌘K to clear)" - className={classNames( - 'w-full pl-8 pr-8 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" - /> -
- -
- {modelSearchQuery && ( - - )} -
-
- -
- {modelLoading === 'all' || modelLoading === provider?.name ? ( -
-
- - Loading models... -
-
- ) : filteredModels.length === 0 ? ( -
-
- {debouncedModelSearchQuery - ? `No models match "${debouncedModelSearchQuery}"${showFreeModelsOnly ? ' (free only)' : ''}` - : showFreeModelsOnly - ? 'No free models available' - : 'No models available'} -
- {debouncedModelSearchQuery && ( -
- Try searching for model names, context sizes (e.g., "128k", "1M"), or capabilities -
- )} - {showFreeModelsOnly && !debouncedModelSearchQuery && ( -
- Try disabling the "Free models only" filter to see all available models -
- )} -
- ) : ( - filteredModels.map((modelOption, index) => ( -
(modelOptionsRef.current[index] = el)} - key={modelOption.name} - 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 || focusedModelIndex === index - ? 'bg-bolt-elements-background-depth-2' - : undefined, - focusedModelIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined, - )} - onClick={(e) => { - e.stopPropagation(); - setModel?.(modelOption.name); - setIsModelDropdownOpen(false); - setModelSearchQuery(''); - setDebouncedModelSearchQuery(''); - }} - tabIndex={focusedModelIndex === index ? 0 : -1} - > -
-
-
- -
-
- - {formatContextSize(modelOption.maxTokenAllowed)} tokens - - {debouncedModelSearchQuery && (modelOption as any).searchScore > 70 && ( - - {(modelOption as any).searchScore.toFixed(0)}% match - - )} -
-
-
- {isModelLikelyFree(modelOption, provider?.name) && ( - - )} - {model === modelOption.name && ( - - )} -
-
-
- )) - )} -
-
- )} +
+
+ Anthropic + / + Claude Sonnet 4.5
); diff --git a/app/lib/modules/llm/providers/amazon-bedrock.ts b/app/lib/modules/llm/providers/amazon-bedrock.ts index 6a4cbc961b..2f18473cc0 100644 --- a/app/lib/modules/llm/providers/amazon-bedrock.ts +++ b/app/lib/modules/llm/providers/amazon-bedrock.ts @@ -20,6 +20,12 @@ export default class AmazonBedrockProvider extends BaseProvider { }; staticModels: ModelInfo[] = [ + { + name: 'us.anthropic.claude-opus-4-5-20251101-v1:0', + label: 'Claude Opus 4.5 (Bedrock)', + provider: 'AmazonBedrock', + maxTokenAllowed: 200000, + }, { name: 'anthropic.claude-3-5-sonnet-20241022-v2:0', label: 'Claude 3.5 Sonnet v2 (Bedrock)', diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 3b5a687ae2..bf48df7cfc 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -6,7 +6,7 @@ export const WORK_DIR = `/home/${WORK_DIR_NAME}`; export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications'; export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/; export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/; -export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest'; +export const DEFAULT_MODEL = 'claude-sonnet-4-5-20241022'; export const PROMPT_COOKIE_KEY = 'cachedPrompt'; export const TOOL_EXECUTION_APPROVAL = { APPROVE: 'Yes, approved.', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4061c9079..eee2a849ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5089,7 +5089,7 @@ packages: glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}