codacus commited on
Commit
25f725f
·
1 Parent(s): aae38ea

refactor: settinge menu refactored with useSettings hook

Browse files
app/components/settings/chat-history/ChatHistoryTab.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useNavigate } from '@remix-run/react';
2
+ import React, { useState } from 'react';
3
+ import { toast } from 'react-toastify';
4
+ import { db, deleteById, getAll } from '~/lib/persistence';
5
+ import { classNames } from '~/utils/classNames';
6
+ import styles from '~/components/settings/Settings.module.scss';
7
+
8
+ export default function ChatHistoryTab() {
9
+ const navigate = useNavigate();
10
+ const [isDeleting, setIsDeleting] = useState(false);
11
+ const downloadAsJson = (data: any, filename: string) => {
12
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
13
+ const url = URL.createObjectURL(blob);
14
+ const link = document.createElement('a');
15
+ link.href = url;
16
+ link.download = filename;
17
+ document.body.appendChild(link);
18
+ link.click();
19
+ document.body.removeChild(link);
20
+ URL.revokeObjectURL(url);
21
+ };
22
+
23
+ const handleDeleteAllChats = async () => {
24
+ if (!db) {
25
+ toast.error('Database is not available');
26
+ return;
27
+ }
28
+
29
+ try {
30
+ setIsDeleting(true);
31
+
32
+ const allChats = await getAll(db);
33
+
34
+ // Delete all chats one by one
35
+ await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
36
+
37
+ toast.success('All chats deleted successfully');
38
+ navigate('/', { replace: true });
39
+ } catch (error) {
40
+ toast.error('Failed to delete chats');
41
+ console.error(error);
42
+ } finally {
43
+ setIsDeleting(false);
44
+ }
45
+ };
46
+
47
+ const handleExportAllChats = async () => {
48
+ if (!db) {
49
+ toast.error('Database is not available');
50
+ return;
51
+ }
52
+
53
+ try {
54
+ const allChats = await getAll(db);
55
+ const exportData = {
56
+ chats: allChats,
57
+ exportDate: new Date().toISOString(),
58
+ };
59
+
60
+ downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
61
+ toast.success('Chats exported successfully');
62
+ } catch (error) {
63
+ toast.error('Failed to export chats');
64
+ console.error(error);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <>
70
+ <div className="p-4">
71
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Chat History</h3>
72
+ <button
73
+ onClick={handleExportAllChats}
74
+ className={classNames(
75
+ 'bg-bolt-elements-button-primary-background',
76
+ 'rounded-lg px-4 py-2 mb-4 transition-colors duration-200',
77
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
78
+ 'text-bolt-elements-button-primary-text',
79
+ )}
80
+ >
81
+ Export All Chats
82
+ </button>
83
+
84
+ <div
85
+ className={classNames('text-bolt-elements-textPrimary rounded-lg py-4 mb-4', styles['settings-danger-area'])}
86
+ >
87
+ <h4 className="font-semibold">Danger Area</h4>
88
+ <p className="mb-2">This action cannot be undone!</p>
89
+ <button
90
+ onClick={handleDeleteAllChats}
91
+ disabled={isDeleting}
92
+ className={classNames(
93
+ 'bg-bolt-elements-button-danger-background',
94
+ 'rounded-lg px-4 py-2 transition-colors duration-200',
95
+ isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-bolt-elements-button-danger-backgroundHover',
96
+ 'text-bolt-elements-button-danger-text',
97
+ )}
98
+ >
99
+ {isDeleting ? 'Deleting...' : 'Delete All Chats'}
100
+ </button>
101
+ </div>
102
+ </div>
103
+ </>
104
+ );
105
+ }
app/components/settings/connections/ConnectionsTab.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import Cookies from 'js-cookie';
4
+
5
+ export default function ConnectionsTab() {
6
+ const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
7
+ const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || '');
8
+
9
+ const handleSaveConnection = () => {
10
+ Cookies.set('githubUsername', githubUsername);
11
+ Cookies.set('githubToken', githubToken);
12
+ toast.success('GitHub credentials saved successfully!');
13
+ };
14
+
15
+ return (
16
+ <div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
17
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">GitHub Connection</h3>
18
+ <div className="flex mb-4">
19
+ <div className="flex-1 mr-2">
20
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">GitHub Username:</label>
21
+ <input
22
+ type="text"
23
+ value={githubUsername}
24
+ onChange={(e) => setGithubUsername(e.target.value)}
25
+ 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"
26
+ />
27
+ </div>
28
+ <div className="flex-1">
29
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
30
+ <input
31
+ type="password"
32
+ value={githubToken}
33
+ onChange={(e) => setGithubToken(e.target.value)}
34
+ 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"
35
+ />
36
+ </div>
37
+ </div>
38
+ <div className="flex mb-4">
39
+ <button
40
+ onClick={handleSaveConnection}
41
+ className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
42
+ >
43
+ Save Connection
44
+ </button>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
app/components/settings/debug/DebugTab.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { useSettings } from '~/lib/hooks/useSettings';
3
+ import commit from '~/commit.json';
4
+
5
+ const versionHash = commit.commit; // Get the version hash from commit.json
6
+
7
+ export default function DebugTab() {
8
+ const { providers } = useSettings();
9
+ const [activeProviders, setActiveProviders] = useState<string[]>([]);
10
+ useEffect(() => {
11
+ setActiveProviders(
12
+ Object.entries(providers)
13
+ .filter(([_key, provider]) => provider.settings.enabled)
14
+ .map(([_key, provider]) => provider.name),
15
+ );
16
+ }, [providers]);
17
+
18
+ const handleCopyToClipboard = useCallback(() => {
19
+ const debugInfo = {
20
+ OS: navigator.platform,
21
+ Browser: navigator.userAgent,
22
+ ActiveFeatures: activeProviders,
23
+ BaseURLs: {
24
+ Ollama: process.env.REACT_APP_OLLAMA_URL,
25
+ OpenAI: process.env.REACT_APP_OPENAI_URL,
26
+ LMStudio: process.env.REACT_APP_LM_STUDIO_URL,
27
+ },
28
+ Version: versionHash,
29
+ };
30
+ navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
31
+ alert('Debug information copied to clipboard!');
32
+ });
33
+ }, [providers]);
34
+
35
+ return (
36
+ <div className="p-4">
37
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Debug Tab</h3>
38
+ <button
39
+ onClick={handleCopyToClipboard}
40
+ className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
41
+ >
42
+ Copy to Clipboard
43
+ </button>
44
+
45
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">System Information</h4>
46
+ <p className="text-bolt-elements-textSecondary">OS: {navigator.platform}</p>
47
+ <p className="text-bolt-elements-textSecondary">Browser: {navigator.userAgent}</p>
48
+
49
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Active Features</h4>
50
+ <ul>
51
+ {activeProviders.map((name) => (
52
+ <li key={name} className="text-bolt-elements-textSecondary">
53
+ {name}
54
+ </li>
55
+ ))}
56
+ </ul>
57
+
58
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Base URLs</h4>
59
+ <ul>
60
+ <li className="text-bolt-elements-textSecondary">Ollama: {process.env.REACT_APP_OLLAMA_URL}</li>
61
+ <li className="text-bolt-elements-textSecondary">OpenAI: {process.env.REACT_APP_OPENAI_URL}</li>
62
+ <li className="text-bolt-elements-textSecondary">LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}</li>
63
+ </ul>
64
+
65
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Version Information</h4>
66
+ <p className="text-bolt-elements-textSecondary">Version Hash: {versionHash}</p>
67
+ </div>
68
+ );
69
+ }
app/components/settings/features/FeaturesTab.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+
5
+ export default function FeaturesTab() {
6
+ const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings();
7
+ return (
8
+ <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
9
+ <div className="mb-6">
10
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
11
+ <div className="flex items-center justify-between mb-2">
12
+ <span className="text-bolt-elements-textPrimary">Debug Info</span>
13
+ <Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
14
+ </div>
15
+ </div>
16
+
17
+ <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
18
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
19
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">
20
+ Disclaimer: Experimental features may be unstable and are subject to change.
21
+ </p>
22
+ <div className="flex items-center justify-between mb-2">
23
+ <span className="text-bolt-elements-textPrimary">Enable Local Models</span>
24
+ <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
app/components/settings/providers/ProvidersTab.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS, type IProviderConfig } from '~/lib/stores/settings';
5
+
6
+ export default function ProvidersTab() {
7
+ const { providers, updateProviderSettings, isLocalModel } = useSettings();
8
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
9
+
10
+ // Load base URLs from cookies
11
+ const [searchTerm, setSearchTerm] = useState('');
12
+
13
+ useEffect(() => {
14
+ let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
15
+ ...value,
16
+ name: key,
17
+ }));
18
+
19
+ if (searchTerm && searchTerm.length > 0) {
20
+ newFilteredProviders = newFilteredProviders.filter((provider) =>
21
+ provider.name.toLowerCase().includes(searchTerm.toLowerCase()),
22
+ );
23
+ }
24
+
25
+ if (!isLocalModel) {
26
+ newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
27
+ }
28
+
29
+ newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
30
+
31
+ setFilteredProviders(newFilteredProviders);
32
+ }, [providers, searchTerm, isLocalModel]);
33
+
34
+ return (
35
+ <div className="p-4">
36
+ <div className="flex mb-4">
37
+ <input
38
+ type="text"
39
+ placeholder="Search providers..."
40
+ value={searchTerm}
41
+ onChange={(e) => setSearchTerm(e.target.value)}
42
+ 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"
43
+ />
44
+ </div>
45
+ {filteredProviders.map((provider) => (
46
+ <div
47
+ key={provider.name}
48
+ className="flex flex-col mb-2 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor "
49
+ >
50
+ <div className="flex items-center justify-between mb-2">
51
+ <span className="text-bolt-elements-textPrimary">{provider.name}</span>
52
+ <Switch
53
+ className="ml-auto"
54
+ checked={provider.settings.enabled}
55
+ onCheckedChange={(enabled) => updateProviderSettings(provider.name, { ...provider.settings, enabled })}
56
+ />
57
+ </div>
58
+ {/* Base URL input for configurable providers */}
59
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && (
60
+ <div className="mt-2">
61
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">Base URL:</label>
62
+ <input
63
+ type="text"
64
+ value={provider.settings.baseUrl || ''}
65
+ onChange={(e) =>
66
+ updateProviderSettings(provider.name, { ...provider.settings, baseUrl: e.target.value })
67
+ }
68
+ placeholder={`Enter ${provider.name} base URL`}
69
+ 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"
70
+ />
71
+ </div>
72
+ )}
73
+ </div>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
app/lib/hooks/useSettings.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from '@nanostores/react';
2
+ import {
3
+ isDebugMode,
4
+ isLocalModelsEnabled,
5
+ LOCAL_PROVIDERS,
6
+ providersStore,
7
+ type IProviderSetting,
8
+ } from '~/lib/stores/settings';
9
+ import { useCallback, useEffect, useState } from 'react';
10
+ import Cookies from 'js-cookie';
11
+ import type { ProviderInfo } from '~/utils/types';
12
+
13
+ export function useSettings() {
14
+ const providers = useStore(providersStore);
15
+ const debug = useStore(isDebugMode);
16
+ const isLocalModel = useStore(isLocalModelsEnabled);
17
+ const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
18
+
19
+ // reading values from cookies on mount
20
+ useEffect(() => {
21
+ const savedProviders = Cookies.get('providers');
22
+
23
+ if (savedProviders) {
24
+ try {
25
+ const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
26
+ Object.keys(parsedProviders).forEach((provider) => {
27
+ const currentProvider = providers[provider];
28
+ providersStore.setKey(provider, {
29
+ ...currentProvider,
30
+ settings: {
31
+ ...parsedProviders[provider],
32
+ enabled: parsedProviders[provider].enabled || true,
33
+ },
34
+ });
35
+ });
36
+ } catch (error) {
37
+ console.error('Failed to parse providers from cookies:', error);
38
+ }
39
+ }
40
+
41
+ // load debug mode from cookies
42
+ const savedDebugMode = Cookies.get('isDebugEnabled');
43
+
44
+ if (savedDebugMode) {
45
+ isDebugMode.set(savedDebugMode === 'true');
46
+ }
47
+
48
+ // load local models from cookies
49
+ const savedLocalModels = Cookies.get('isLocalModelsEnabled');
50
+
51
+ if (savedLocalModels) {
52
+ isLocalModelsEnabled.set(savedLocalModels === 'true');
53
+ }
54
+ }, []);
55
+
56
+ // writing values to cookies on change
57
+ useEffect(() => {
58
+ const providers = providersStore.get();
59
+ const providerSetting: Record<string, IProviderSetting> = {};
60
+ Object.keys(providers).forEach((provider) => {
61
+ providerSetting[provider] = providers[provider].settings;
62
+ });
63
+ Cookies.set('providers', JSON.stringify(providerSetting));
64
+ }, [providers]);
65
+
66
+ useEffect(() => {
67
+ let active = Object.entries(providers)
68
+ .filter(([_key, provider]) => provider.settings.enabled)
69
+ .map(([_k, p]) => p);
70
+
71
+ if (!isLocalModel) {
72
+ active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name));
73
+ }
74
+
75
+ setActiveProviders(active);
76
+ }, [providers, isLocalModel]);
77
+
78
+ // helper function to update settings
79
+ const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => {
80
+ const settings = providers[provider].settings;
81
+ providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
82
+ }, []);
83
+
84
+ const enableDebugMode = useCallback((enabled: boolean) => {
85
+ isDebugMode.set(enabled);
86
+ Cookies.set('isDebugEnabled', String(enabled));
87
+ }, []);
88
+
89
+ const enableLocalModels = useCallback((enabled: boolean) => {
90
+ isLocalModelsEnabled.set(enabled);
91
+ Cookies.set('isLocalModelsEnabled', String(enabled));
92
+ }, []);
93
+
94
+ return {
95
+ providers,
96
+ activeProviders,
97
+ updateProviderSettings,
98
+ debug,
99
+ enableDebugMode,
100
+ isLocalModel,
101
+ enableLocalModels,
102
+ };
103
+ }