diff --git a/app/components/@settings/core/ControlPanel.tsx b/app/components/@settings/core/ControlPanel.tsx index c0e19035..0d90975c 100644 --- a/app/components/@settings/core/ControlPanel.tsx +++ b/app/components/@settings/core/ControlPanel.tsx @@ -263,6 +263,27 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { }, }; + // Reset to default view when modal opens/closes + useEffect(() => { + if (!open) { + // Reset when closing + setActiveTab(null); + setLoadingTab(null); + setShowTabManagement(false); + } else { + // When opening, set to null to show the main view + setActiveTab(null); + } + }, [open]); + + // Handle closing + const handleClose = () => { + setActiveTab(null); + setLoadingTab(null); + setShowTabManagement(false); + onClose(); + }; + // Handlers const handleBack = () => { if (showTabManagement) { @@ -405,8 +426,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { { {/* Close Button */} - ))} - + Preferences
@@ -245,7 +175,7 @@ export default function SettingsTab() {
- {/* Keyboard Shortcuts */} + {/* Simplified Keyboard Shortcuts */}
- {Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => ( -
-
- - {name.replace(/([A-Z])/g, ' $1').toLowerCase()} - - {shortcut.description && ( - {shortcut.description} - )} -
-
- {shortcut.ctrlOrMetaKey && ( - - {getModifierSymbol(isMac ? 'meta' : 'ctrl')} - - )} - {shortcut.ctrlKey && ( - - {getModifierSymbol('ctrl')} - - )} - {shortcut.metaKey && ( - - {getModifierSymbol('meta')} - - )} - {shortcut.altKey && ( - - {getModifierSymbol('alt')} - - )} - {shortcut.shiftKey && ( - - {getModifierSymbol('shift')} - - )} - - {formatShortcutKey(shortcut.key)} - -
+
+
+ Toggle Theme + Switch between light and dark mode
- ))} +
+ + {getModifierSymbol('meta')} + + + {getModifierSymbol('alt')} + + + {getModifierSymbol('shift')} + + + D + +
+
diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index c3956995..f38a5ccf 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -35,6 +35,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types'; import ProgressCompilation from './ProgressCompilation'; import type { ProgressAnnotation } from '~/types/context'; import type { ActionRunner } from '~/lib/runtime/action-runner'; +import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; const TEXTAREA_MIN_HEIGHT = 76; @@ -406,7 +407,7 @@ export const BaseChat = React.forwardRef( apiKeys={apiKeys} modelLoading={isModelLoading} /> - {(providerList || []).length > 0 && provider && ( + {(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && ( { - // Load enabled providers from cookies + const [modelSearchQuery, setModelSearchQuery] = useState(''); + const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); + const searchInputRef = useRef(null); + const optionsRef = useRef<(HTMLDivElement | null)[]>([]); + const dropdownRef = useRef(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) => { + 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 = ({ ))} - 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" + /> +
+ +
+ + + +
+ {modelLoading === 'all' || modelLoading === provider?.name ? ( +
Loading...
+ ) : filteredModels.length === 0 ? ( +
No models found
+ ) : ( + filteredModels.map((modelOption, index) => ( +
(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} +
+ )) + )} +
+ )} - + ); }; diff --git a/app/lib/hooks/useSettings.ts b/app/lib/hooks/useSettings.ts index 12338248..69e06bdf 100644 --- a/app/lib/hooks/useSettings.ts +++ b/app/lib/hooks/useSettings.ts @@ -2,8 +2,6 @@ import { useStore } from '@nanostores/react'; import { isDebugMode, isEventLogsEnabled, - isLocalModelsEnabled, - LOCAL_PROVIDERS, promptStore, providersStore, latestBranchStore, @@ -17,7 +15,6 @@ import { updateAutoSelectTemplate, updateContextOptimization, updateEventLogs, - updateLocalModels, updatePromptId, } from '~/lib/stores/settings'; import { useCallback, useEffect, useState } from 'react'; @@ -49,8 +46,6 @@ export interface UseSettingsReturn { providers: Record; activeProviders: ProviderInfo[]; updateProviderSettings: (provider: string, config: IProviderSetting) => void; - isLocalModel: boolean; - enableLocalModels: (enabled: boolean) => void; // Debug and development settings debug: boolean; @@ -81,7 +76,6 @@ export function useSettings(): UseSettingsReturn { const debug = useStore(isDebugMode); const eventLogs = useStore(isEventLogsEnabled); const promptId = useStore(promptStore); - const isLocalModel = useStore(isLocalModelsEnabled); const isLatestBranch = useStore(latestBranchStore); const autoSelectTemplate = useStore(autoSelectStarterTemplate); const [activeProviders, setActiveProviders] = useState([]); @@ -100,16 +94,12 @@ export function useSettings(): UseSettingsReturn { }); useEffect(() => { - let active = Object.entries(providers) + const active = Object.entries(providers) .filter(([_key, provider]) => provider.settings.enabled) .map(([_k, p]) => p); - if (!isLocalModel) { - active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name)); - } - setActiveProviders(active); - }, [providers, isLocalModel]); + }, [providers]); const saveSettings = useCallback((newSettings: Partial) => { setSettings((prev) => { @@ -135,11 +125,6 @@ export function useSettings(): UseSettingsReturn { logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`); }, []); - const enableLocalModels = useCallback((enabled: boolean) => { - updateLocalModels(enabled); - logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`); - }, []); - const setPromptId = useCallback((id: string) => { updatePromptId(id); logStore.logSystem(`Prompt template updated to ${id}`); @@ -188,14 +173,11 @@ export function useSettings(): UseSettingsReturn { [saveSettings], ); - // Fix the providers cookie sync useEffect(() => { const providers = providersStore.get(); - const providerSetting: Record = {}; + const providerSetting: Record = {}; // preserve the entire settings object for each provider Object.keys(providers).forEach((provider) => { - providerSetting[provider] = { - enabled: providers[provider].settings.enabled || false, // Add fallback for undefined - }; + providerSetting[provider] = providers[provider].settings; }); Cookies.set('providers', JSON.stringify(providerSetting)); }, [providers]); @@ -205,8 +187,6 @@ export function useSettings(): UseSettingsReturn { providers, activeProviders, updateProviderSettings, - isLocalModel, - enableLocalModels, debug, enableDebugMode, eventLogs, diff --git a/app/lib/modules/llm/providers/amazon-bedrock.ts b/app/lib/modules/llm/providers/amazon-bedrock.ts index f01b13ac..6a4cbc96 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: '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)', diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index d8b1ca18..ccd3e2b1 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -1,5 +1,4 @@ import { atom, map } from 'nanostores'; -import { workbenchStore } from './workbench'; import { PROVIDER_LIST } from '~/utils/constants'; import type { IProviderConfig } from '~/types/model'; import type { @@ -11,7 +10,7 @@ import type { import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants'; import Cookies from 'js-cookie'; import { toggleTheme } from './theme'; -import { chatStore } from './chat'; +import { create } from 'zustand'; export interface Shortcut { key: string; @@ -26,10 +25,8 @@ export interface Shortcut { } export interface Shortcuts { - toggleTerminal: Shortcut; toggleTheme: Shortcut; - toggleChat: Shortcut; - toggleSettings: Shortcut; + toggleTerminal: Shortcut; } export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; @@ -37,15 +34,8 @@ export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama']; export type ProviderSetting = Record; -// Define safer shortcuts that don't conflict with browser defaults +// Simplified shortcuts store with only theme toggle export const shortcutsStore = map({ - toggleTerminal: { - key: '`', - ctrlOrMetaKey: true, - action: () => workbenchStore.toggleTerminal(), - description: 'Toggle terminal', - isPreventDefault: true, - }, toggleTheme: { key: 'd', metaKey: true, @@ -55,22 +45,13 @@ export const shortcutsStore = map({ description: 'Toggle theme', isPreventDefault: true, }, - toggleChat: { - key: 'j', // Changed from 'k' to 'j' to avoid conflicts + toggleTerminal: { + key: '`', ctrlOrMetaKey: true, - altKey: true, // Added alt key to make it more unique - action: () => chatStore.setKey('showChat', !chatStore.get().showChat), - description: 'Toggle chat', - isPreventDefault: true, - }, - toggleSettings: { - key: 's', - ctrlOrMetaKey: true, - altKey: true, action: () => { - document.dispatchEvent(new CustomEvent('toggle-settings')); + // This will be handled by the terminal component }, - description: 'Toggle settings', + description: 'Toggle terminal', isPreventDefault: true, }, }); @@ -148,7 +129,6 @@ const SETTINGS_KEYS = { AUTO_SELECT_TEMPLATE: 'autoSelectTemplate', CONTEXT_OPTIMIZATION: 'contextOptimizationEnabled', EVENT_LOGS: 'isEventLogsEnabled', - LOCAL_MODELS: 'isLocalModelsEnabled', PROMPT_ID: 'promptId', DEVELOPER_MODE: 'isDeveloperMode', } as const; @@ -175,10 +155,9 @@ const getInitialSettings = () => { return { latestBranch: getStoredBoolean(SETTINGS_KEYS.LATEST_BRANCH, false), - autoSelectTemplate: getStoredBoolean(SETTINGS_KEYS.AUTO_SELECT_TEMPLATE, false), - contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false), + autoSelectTemplate: getStoredBoolean(SETTINGS_KEYS.AUTO_SELECT_TEMPLATE, true), + contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, true), eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true), - localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true), promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default', developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false), }; @@ -191,7 +170,6 @@ export const latestBranchStore = atom(initialSettings.latestBranch); export const autoSelectStarterTemplate = atom(initialSettings.autoSelectTemplate); export const enableContextOptimizationStore = atom(initialSettings.contextOptimization); export const isEventLogsEnabled = atom(initialSettings.eventLogs); -export const isLocalModelsEnabled = atom(initialSettings.localModels); export const promptStore = atom(initialSettings.promptId); // Helper functions to update settings with persistence @@ -215,11 +193,6 @@ export const updateEventLogs = (enabled: boolean) => { localStorage.setItem(SETTINGS_KEYS.EVENT_LOGS, JSON.stringify(enabled)); }; -export const updateLocalModels = (enabled: boolean) => { - isLocalModelsEnabled.set(enabled); - localStorage.setItem(SETTINGS_KEYS.LOCAL_MODELS, JSON.stringify(enabled)); -}; - export const updatePromptId = (id: string) => { promptStore.set(id); localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id); @@ -319,3 +292,35 @@ export const setDeveloperMode = (value: boolean) => { localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value)); } }; + +// First, let's define the SettingsStore interface +interface SettingsStore { + isOpen: boolean; + selectedTab: string; + openSettings: () => void; + closeSettings: () => void; + setSelectedTab: (tab: string) => void; +} + +export const useSettingsStore = create((set) => ({ + isOpen: false, + selectedTab: 'user', // Default tab + + openSettings: () => { + set({ + isOpen: true, + selectedTab: 'user', // Always open to user tab + }); + }, + + closeSettings: () => { + set({ + isOpen: false, + selectedTab: 'user', // Reset to user tab when closing + }); + }, + + setSelectedTab: (tab: string) => { + set({ selectedTab: tab }); + }, +})); diff --git a/app/utils/debounce.ts b/app/utils/debounce.ts index 3b91d7a4..56813aa9 100644 --- a/app/utils/debounce.ts +++ b/app/utils/debounce.ts @@ -1,17 +1,13 @@ -export function debounce(fn: (...args: Args) => void, delay = 100) { - if (delay === 0) { - return fn; - } +export function debounce any>(func: T, wait: number): (...args: Parameters) => void { + let timeout: NodeJS.Timeout; - let timer: number | undefined; + return function executedFunction(...args: Parameters) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; - return function (this: U, ...args: Args) { - const context = this; - - clearTimeout(timer); - - timer = window.setTimeout(() => { - fn.apply(context, args); - }, delay); + clearTimeout(timeout); + timeout = setTimeout(later, wait); }; }