diff --git a/app/components/settings/chat-history/ChatHistoryTab.tsx b/app/components/settings/chat-history/ChatHistoryTab.tsx new file mode 100644 index 0000000..e96f0d8 --- /dev/null +++ b/app/components/settings/chat-history/ChatHistoryTab.tsx @@ -0,0 +1,105 @@ +import { useNavigate } from '@remix-run/react'; +import React, { useState } from 'react'; +import { toast } from 'react-toastify'; +import { db, deleteById, getAll } from '~/lib/persistence'; +import { classNames } from '~/utils/classNames'; +import styles from '~/components/settings/Settings.module.scss'; + +export default function ChatHistoryTab() { + const navigate = useNavigate(); + const [isDeleting, setIsDeleting] = useState(false); + 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); + } + }; + + return ( + <> +
+

Chat History

+ + +
+

Danger Area

+

This action cannot be undone!

+ +
+
+ + ); +} diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/settings/connections/ConnectionsTab.tsx new file mode 100644 index 0000000..32d0fa0 --- /dev/null +++ b/app/components/settings/connections/ConnectionsTab.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { toast } from 'react-toastify'; +import Cookies from 'js-cookie'; + +export default function ConnectionsTab() { + const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || ''); + const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || ''); + + const handleSaveConnection = () => { + Cookies.set('githubUsername', githubUsername); + Cookies.set('githubToken', githubToken); + toast.success('GitHub credentials saved successfully!'); + }; + + return ( +
+

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" + /> +
+
+
+ +
+
+ ); +} diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx new file mode 100644 index 0000000..7a84ec1 --- /dev/null +++ b/app/components/settings/debug/DebugTab.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useSettings } from '~/lib/hooks/useSettings'; +import commit from '~/commit.json'; + +const versionHash = commit.commit; // Get the version hash from commit.json + +export default function DebugTab() { + const { providers } = useSettings(); + const [activeProviders, setActiveProviders] = useState([]); + useEffect(() => { + setActiveProviders( + Object.entries(providers) + .filter(([_key, provider]) => provider.settings.enabled) + .map(([_key, provider]) => provider.name), + ); + }, [providers]); + + const handleCopyToClipboard = useCallback(() => { + const debugInfo = { + OS: navigator.platform, + Browser: navigator.userAgent, + ActiveFeatures: activeProviders, + 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!'); + }); + }, [providers]); + + return ( +
+

Debug Tab

+ + +

System Information

+

OS: {navigator.platform}

+

Browser: {navigator.userAgent}

+ +

Active Features

+ + +

Base URLs

+ + +

Version Information

+

Version Hash: {versionHash}

+
+ ); +} diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx new file mode 100644 index 0000000..0b4fa75 --- /dev/null +++ b/app/components/settings/features/FeaturesTab.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Switch } from '~/components/ui/Switch'; +import { useSettings } from '~/lib/hooks/useSettings'; + +export default function FeaturesTab() { + const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings(); + return ( +
+
+

Optional Features

+
+ Debug Info + +
+
+ +
+

Experimental Features

+

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

+
+ Enable Local Models + +
+
+
+ ); +} diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx new file mode 100644 index 0000000..0b87959 --- /dev/null +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import { Switch } from '~/components/ui/Switch'; +import { useSettings } from '~/lib/hooks/useSettings'; +import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS, type IProviderConfig } from '~/lib/stores/settings'; + +export default function ProvidersTab() { + const { providers, updateProviderSettings, isLocalModel } = useSettings(); + const [filteredProviders, setFilteredProviders] = useState([]); + + // Load base URLs from cookies + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({ + ...value, + name: key, + })); + + if (searchTerm && searchTerm.length > 0) { + newFilteredProviders = newFilteredProviders.filter((provider) => + provider.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + if (!isLocalModel) { + newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name)); + } + + newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name)); + + setFilteredProviders(newFilteredProviders); + }, [providers, searchTerm, isLocalModel]); + + return ( +
+
+ 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} + updateProviderSettings(provider.name, { ...provider.settings, enabled })} + /> +
+ {/* Base URL input for configurable providers */} + {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && ( +
+ + + updateProviderSettings(provider.name, { ...provider.settings, baseUrl: 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" + /> +
+ )} +
+ ))} +
+ ); +} diff --git a/app/lib/hooks/useSettings.tsx b/app/lib/hooks/useSettings.tsx new file mode 100644 index 0000000..9b63430 --- /dev/null +++ b/app/lib/hooks/useSettings.tsx @@ -0,0 +1,103 @@ +import { useStore } from '@nanostores/react'; +import { + isDebugMode, + isLocalModelsEnabled, + LOCAL_PROVIDERS, + providersStore, + type IProviderSetting, +} from '~/lib/stores/settings'; +import { useCallback, useEffect, useState } from 'react'; +import Cookies from 'js-cookie'; +import type { ProviderInfo } from '~/utils/types'; + +export function useSettings() { + const providers = useStore(providersStore); + const debug = useStore(isDebugMode); + const isLocalModel = useStore(isLocalModelsEnabled); + const [activeProviders, setActiveProviders] = useState([]); + + // reading values from cookies on mount + useEffect(() => { + const savedProviders = Cookies.get('providers'); + + if (savedProviders) { + try { + const parsedProviders: Record = JSON.parse(savedProviders); + Object.keys(parsedProviders).forEach((provider) => { + const currentProvider = providers[provider]; + providersStore.setKey(provider, { + ...currentProvider, + settings: { + ...parsedProviders[provider], + enabled: parsedProviders[provider].enabled || true, + }, + }); + }); + } catch (error) { + console.error('Failed to parse providers from cookies:', error); + } + } + + // load debug mode from cookies + const savedDebugMode = Cookies.get('isDebugEnabled'); + + if (savedDebugMode) { + isDebugMode.set(savedDebugMode === 'true'); + } + + // load local models from cookies + const savedLocalModels = Cookies.get('isLocalModelsEnabled'); + + if (savedLocalModels) { + isLocalModelsEnabled.set(savedLocalModels === 'true'); + } + }, []); + + // writing values to cookies on change + useEffect(() => { + const providers = providersStore.get(); + const providerSetting: Record = {}; + Object.keys(providers).forEach((provider) => { + providerSetting[provider] = providers[provider].settings; + }); + Cookies.set('providers', JSON.stringify(providerSetting)); + }, [providers]); + + useEffect(() => { + let 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]); + + // helper function to update settings + const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => { + const settings = providers[provider].settings; + providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } }); + }, []); + + const enableDebugMode = useCallback((enabled: boolean) => { + isDebugMode.set(enabled); + Cookies.set('isDebugEnabled', String(enabled)); + }, []); + + const enableLocalModels = useCallback((enabled: boolean) => { + isLocalModelsEnabled.set(enabled); + Cookies.set('isLocalModelsEnabled', String(enabled)); + }, []); + + return { + providers, + activeProviders, + updateProviderSettings, + debug, + enableDebugMode, + isLocalModel, + enableLocalModels, + }; +}