diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 411da94b..57a153d3 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -45,6 +45,7 @@ interface BaseChatProps { setModel?: (model: string) => void; provider?: ProviderInfo; setProvider?: (provider: ProviderInfo) => void; + providerList?: ProviderInfo[]; handleStop?: () => void; sendMessage?: (event: React.UIEvent, messageInput?: string) => void; handleInputChange?: (event: React.ChangeEvent) => void; @@ -70,6 +71,7 @@ export const BaseChat = React.forwardRef( setModel, provider, setProvider, + providerList, input = '', enhancingPrompt, handleInputChange, @@ -108,45 +110,7 @@ export const BaseChat = React.forwardRef( const [recognition, setRecognition] = useState(null); const [transcript, setTranscript] = useState(''); - // Load enabled providers from cookies - const [enabledProviders, setEnabledProviders] = useState(() => { - const savedProviders = Cookies.get('providers'); - - if (savedProviders) { - try { - const parsedProviders = JSON.parse(savedProviders); - return PROVIDER_LIST.filter((p) => parsedProviders[p.name]); - } catch (error) { - console.error('Failed to parse providers from cookies:', error); - return PROVIDER_LIST; - } - } - - return PROVIDER_LIST; - }); - // Update enabled providers when cookies change - useEffect(() => { - const updateProvidersFromCookies = () => { - const savedProviders = Cookies.get('providers'); - - if (savedProviders) { - try { - const parsedProviders = JSON.parse(savedProviders); - setEnabledProviders(PROVIDER_LIST.filter((p) => parsedProviders[p.name])); - } catch (error) { - console.error('Failed to parse providers from cookies:', error); - } - } - }; - - updateProvidersFromCookies(); - - const interval = setInterval(updateProvidersFromCookies, 1000); - - return () => clearInterval(interval); - }, [PROVIDER_LIST]); - console.log(transcript); useEffect(() => { // Load API keys from cookies on component mount @@ -377,10 +341,10 @@ export const BaseChat = React.forwardRef( modelList={modelList} provider={provider} setProvider={setProvider} - providerList={PROVIDER_LIST} + providerList={providerList || PROVIDER_LIST} apiKeys={apiKeys} /> - {enabledProviders.length > 0 && provider && ( + {(providerList || []).length > 0 && provider && ( ( 0 || isStreaming || uploadedFiles.length > 0} isStreaming={isStreaming} - disabled={enabledProviders.length === 0} + disabled={!providerList || providerList.length === 0} onClick={(event) => { if (isStreaming) { handleStop?.(); @@ -536,7 +500,7 @@ export const BaseChat = React.forwardRef( !isModelSettingsCollapsed, })} onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)} - disabled={enabledProviders.length === 0} + disabled={!providerList || providerList.length === 0} >
{isModelSettingsCollapsed ? {model} : } diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c6a01eef..7c67a750 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -19,6 +19,7 @@ import { BaseChat } from './BaseChat'; import Cookies from 'js-cookie'; import type { ProviderInfo } from '~/utils/types'; import { debounce } from '~/utils/debounce'; +import { useSettings } from '~/lib/hooks/useSettings'; const toastAnimation = cssTransition({ enter: 'animated fadeInRight', @@ -91,6 +92,7 @@ export const ChatImpl = memo( const [chatStarted, setChatStarted] = useState(initialMessages.length > 0); const [uploadedFiles, setUploadedFiles] = useState([]); // Move here const [imageDataList, setImageDataList] = useState([]); // Move here + const { activeProviders } = useSettings(); const [model, setModel] = useState(() => { const savedModel = Cookies.get('selectedModel'); @@ -316,6 +318,7 @@ export const ChatImpl = memo( setModel={handleModelChange} provider={provider} setProvider={handleProviderChange} + providerList={activeProviders} messageRef={messageRef} scrollRef={scrollRef} handleInputChange={(e) => { diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx index 0910859d..8ae4a484 100644 --- a/app/components/settings/SettingsWindow.tsx +++ b/app/components/settings/SettingsWindow.tsx @@ -1,17 +1,16 @@ import * as RadixDialog from '@radix-ui/react-dialog'; import { motion } from 'framer-motion'; -import { useState } from 'react'; +import { useState, type ReactElement } from 'react'; import { classNames } from '~/utils/classNames'; import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog'; import { IconButton } from '~/components/ui/IconButton'; -import { providersList } from '~/lib/stores/settings'; -import { db, getAll, deleteById } from '~/lib/persistence'; -import { toast } from 'react-toastify'; -import { useNavigate } from '@remix-run/react'; -import commit from '~/commit.json'; -import Cookies from 'js-cookie'; import styles from './Settings.module.scss'; -import { Switch } from '~/components/ui/Switch'; +import ChatHistoryTab from './chat-history/ChatHistoryTab'; +import ProvidersTab from './providers/ProvidersTab'; +import { useSettings } from '~/lib/hooks/useSettings'; +import FeaturesTab from './features/FeaturesTab'; +import DebugTab from './debug/DebugTab'; +import ConnectionsTab from './connections/ConnectionsTab'; interface SettingsProps { open: boolean; @@ -21,206 +20,27 @@ interface SettingsProps { type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'connection'; // Providers that support base URL configuration -const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; - export const SettingsWindow = ({ open, onClose }: SettingsProps) => { - const navigate = useNavigate(); + const { debug } = useSettings(); const [activeTab, setActiveTab] = useState('chat-history'); - const [isDebugEnabled, setIsDebugEnabled] = useState(() => { - const savedDebugState = Cookies.get('isDebugEnabled'); - return savedDebugState === 'true'; - }); - const [searchTerm, setSearchTerm] = useState(''); - const [isDeleting, setIsDeleting] = useState(false); - const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || ''); - const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || ''); - const [isLocalModelsEnabled, setIsLocalModelsEnabled] = useState(() => { - const savedLocalModelsState = Cookies.get('isLocalModelsEnabled'); - return savedLocalModelsState === 'true'; - }); - // Load base URLs from cookies - const [baseUrls, setBaseUrls] = useState(() => { - const savedUrls = Cookies.get('providerBaseUrls'); - - if (savedUrls) { - try { - return JSON.parse(savedUrls); - } catch (error) { - console.error('Failed to parse base URLs from cookies:', error); - return { - Ollama: 'http://localhost:11434', - LMStudio: 'http://localhost:1234', - OpenAILike: '', - }; - } - } - - return { - Ollama: 'http://localhost:11434', - LMStudio: 'http://localhost:1234', - OpenAILike: '', - }; - }); - - const handleBaseUrlChange = (provider: string, url: string) => { - setBaseUrls((prev: Record) => { - const newUrls = { ...prev, [provider]: url }; - Cookies.set('providerBaseUrls', JSON.stringify(newUrls)); - - return newUrls; - }); - }; - - const tabs: { id: TabType; label: string; icon: string }[] = [ - { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book' }, - { id: 'providers', label: 'Providers', icon: 'i-ph:key' }, - { id: 'features', label: 'Features', icon: 'i-ph:star' }, - { id: 'connection', label: 'Connection', icon: 'i-ph:link' }, - ...(isDebugEnabled ? [{ id: 'debug' as TabType, label: 'Debug Tab', icon: 'i-ph:bug' }] : []), + const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [ + { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: }, + { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: }, + { id: 'features', label: 'Features', icon: 'i-ph:star', component: }, + { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: }, + ...(debug + ? [ + { + id: 'debug' as TabType, + label: 'Debug Tab', + icon: 'i-ph:bug', + component: , + }, + ] + : []), ]; - // Load providers from cookies on mount - const [providers, setProviders] = useState(() => { - const savedProviders = Cookies.get('providers'); - - if (savedProviders) { - try { - const parsedProviders = JSON.parse(savedProviders); - - // Merge saved enabled states with the base provider list - return providersList.map((provider) => ({ - ...provider, - isEnabled: parsedProviders[provider.name] || false, - })); - } catch (error) { - console.error('Failed to parse providers from cookies:', error); - } - } - - return providersList; - }); - - const handleToggleProvider = (providerName: string, enabled: boolean) => { - setProviders((prevProviders) => { - const newProviders = prevProviders.map((provider) => - provider.name === providerName ? { ...provider, isEnabled: enabled } : provider, - ); - - // Save to cookies - const enabledStates = newProviders.reduce( - (acc, provider) => ({ - ...acc, - [provider.name]: provider.isEnabled, - }), - {}, - ); - Cookies.set('providers', JSON.stringify(enabledStates)); - - return newProviders; - }); - }; - - const filteredProviders = providers - .filter((provider) => { - const isLocalModelProvider = ['OpenAILike', 'LMStudio', 'Ollama'].includes(provider.name); - return isLocalModelsEnabled || !isLocalModelProvider; - }) - .filter((provider) => provider.name.toLowerCase().includes(searchTerm.toLowerCase())) - .sort((a, b) => a.name.localeCompare(b.name)); - - const handleCopyToClipboard = () => { - const debugInfo = { - OS: navigator.platform, - Browser: navigator.userAgent, - ActiveFeatures: providers.filter((provider) => provider.isEnabled).map((provider) => provider.name), - BaseURLs: { - Ollama: process.env.REACT_APP_OLLAMA_URL, - OpenAI: process.env.REACT_APP_OPENAI_URL, - LMStudio: process.env.REACT_APP_LM_STUDIO_URL, - }, - Version: versionHash, - }; - navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => { - alert('Debug information copied to clipboard!'); - }); - }; - - const downloadAsJson = (data: any, filename: string) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - const handleDeleteAllChats = async () => { - if (!db) { - toast.error('Database is not available'); - return; - } - - try { - setIsDeleting(true); - - const allChats = await getAll(db); - - // Delete all chats one by one - await Promise.all(allChats.map((chat) => deleteById(db!, chat.id))); - - toast.success('All chats deleted successfully'); - navigate('/', { replace: true }); - } catch (error) { - toast.error('Failed to delete chats'); - console.error(error); - } finally { - setIsDeleting(false); - } - }; - - const handleExportAllChats = async () => { - if (!db) { - toast.error('Database is not available'); - return; - } - - try { - const allChats = await getAll(db); - const exportData = { - chats: allChats, - exportDate: new Date().toISOString(), - }; - - downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`); - toast.success('Chats exported successfully'); - } catch (error) { - toast.error('Failed to export chats'); - console.error(error); - } - }; - - const versionHash = commit.commit; // Get the version hash from commit.json - - const handleSaveConnection = () => { - Cookies.set('githubUsername', githubUsername); - Cookies.set('githubToken', githubToken); - toast.success('GitHub credentials saved successfully!'); - }; - - const handleToggleDebug = (enabled: boolean) => { - setIsDebugEnabled(enabled); - Cookies.set('isDebugEnabled', String(enabled)); - }; - - const handleToggleLocalModels = (enabled: boolean) => { - setIsLocalModelsEnabled(enabled); - Cookies.set('isLocalModelsEnabled', String(enabled)); - }; - return ( @@ -284,190 +104,7 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
-
- {activeTab === 'chat-history' && ( -
-

Chat History

- - -
-

Danger Area

-

This action cannot be undone!

- -
-
- )} - {activeTab === 'providers' && ( -
-
- setSearchTerm(e.target.value)} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
- {filteredProviders.map((provider) => ( -
-
- {provider.name} - handleToggleProvider(provider.name, enabled)} - /> -
- {/* Base URL input for configurable providers */} - {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.isEnabled && ( -
- - handleBaseUrlChange(provider.name, e.target.value)} - placeholder={`Enter ${provider.name} base URL`} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
- )} -
- ))} -
- )} - {activeTab === 'features' && ( -
-
-

Optional Features

-
- Debug Info - -
-
- -
-

Experimental Features

-

- Disclaimer: Experimental features may be unstable and are subject to change. -

-
- Enable Local Models - -
-
-
- )} - {activeTab === 'debug' && isDebugEnabled && ( -
-

Debug Tab

- - -

System Information

-

OS: {navigator.platform}

-

Browser: {navigator.userAgent}

- -

Active Features

-
    - {providers - .filter((provider) => provider.isEnabled) - .map((provider) => ( -
  • - {provider.name} -
  • - ))} -
- -

Base URLs

-
    -
  • Ollama: {process.env.REACT_APP_OLLAMA_URL}
  • -
  • OpenAI: {process.env.REACT_APP_OPENAI_URL}
  • -
  • - LM Studio: {process.env.REACT_APP_LM_STUDIO_URL} -
  • -
- -

Version Information

-

Version Hash: {versionHash}

-
- )} - {activeTab === 'connection' && ( -
-

GitHub Connection

-
-
- - setGithubUsername(e.target.value)} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
-
- - setGithubToken(e.target.value)} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
-
-
- -
-
- )} -
+
{tabs.find((tab) => tab.id === activeTab)?.component}
diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index 7106cfb6..b6dbc06a 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -1,5 +1,7 @@ -import { map } from 'nanostores'; +import { atom, map } from 'nanostores'; import { workbenchStore } from './workbench'; +import type { ProviderInfo } from '~/utils/types'; +import { PROVIDER_LIST } from '~/utils/constants'; export interface Shortcut { key: string; @@ -15,32 +17,18 @@ export interface Shortcuts { toggleTerminal: Shortcut; } -export interface Provider { - name: string; - isEnabled: boolean; +export interface IProviderSetting { + enabled?: boolean; + baseUrl?: string; } +export type IProviderConfig = ProviderInfo & { + settings: IProviderSetting; +}; -export interface Settings { - shortcuts: Shortcuts; - providers: Provider[]; -} +export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; +export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama']; -export const providersList: Provider[] = [ - { name: 'Groq', isEnabled: false }, - { name: 'HuggingFace', isEnabled: false }, - { name: 'OpenAI', isEnabled: false }, - { name: 'Anthropic', isEnabled: false }, - { name: 'OpenRouter', isEnabled: false }, - { name: 'Google', isEnabled: false }, - { name: 'Ollama', isEnabled: false }, - { name: 'OpenAILike', isEnabled: false }, - { name: 'Together', isEnabled: false }, - { name: 'Deepseek', isEnabled: false }, - { name: 'Mistral', isEnabled: false }, - { name: 'Cohere', isEnabled: false }, - { name: 'LMStudio', isEnabled: false }, - { name: 'xAI', isEnabled: false }, -]; +export type ProviderSetting = Record; export const shortcutsStore = map({ toggleTerminal: { @@ -50,14 +38,17 @@ export const shortcutsStore = map({ }, }); -export const settingsStore = map({ - shortcuts: shortcutsStore.get(), - providers: providersList, +const initialProviderSettings: ProviderSetting = {}; +PROVIDER_LIST.forEach((provider) => { + initialProviderSettings[provider.name] = { + ...provider, + settings: { + enabled: false, + }, + }; }); +export const providersStore = map(initialProviderSettings); -shortcutsStore.subscribe((shortcuts) => { - settingsStore.set({ - ...settingsStore.get(), - shortcuts, - }); -}); +export const isDebugMode = atom(false); + +export const isLocalModelsEnabled = atom(true);