Cole Medin commited on
Commit
31e7b48
·
unverified ·
2 Parent(s): 1774bf6 a2acc77

Merge pull request #580 from oTToDev-CE/feat/add-tabbed-setting-modal

Browse files
.github/workflows/commit.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Update Commit Hash File
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ update-commit:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Checkout the code
17
+ uses: actions/checkout@v3
18
+
19
+ - name: Get the latest commit hash
20
+ run: echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
21
+
22
+ - name: Update commit file
23
+ run: |
24
+ echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json
25
+
26
+ - name: Commit and push the update
27
+ run: |
28
+ git config --global user.name "github-actions[bot]"
29
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
30
+ git add app/commit.json
31
+ git commit -m "chore: update commit hash to $COMMIT_HASH"
32
+ git push
app/commit.json ADDED
@@ -0,0 +1 @@
 
 
1
+ { "commit": "228cf1f34fd64b6960460f84c9db47bd7ef03150" }
app/components/chat/BaseChat.tsx CHANGED
@@ -88,13 +88,65 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
88
  ref,
89
  ) => {
90
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
91
- const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  const [modelList, setModelList] = useState(MODEL_LIST);
93
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
94
  const [isListening, setIsListening] = useState(false);
95
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
96
  const [transcript, setTranscript] = useState('');
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  console.log(transcript);
99
  useEffect(() => {
100
  // Load API keys from cookies on component mount
@@ -184,23 +236,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
184
  }
185
  };
186
 
187
- const updateApiKey = (provider: string, key: string) => {
188
- try {
189
- const updatedApiKeys = { ...apiKeys, [provider]: key };
190
- setApiKeys(updatedApiKeys);
191
-
192
- // Save updated API keys to cookies with 30 day expiry and secure settings
193
- Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
194
- expires: 30, // 30 days
195
- secure: true, // Only send over HTTPS
196
- sameSite: 'strict', // Protect against CSRF
197
- path: '/', // Accessible across the site
198
- });
199
- } catch (error) {
200
- console.error('Error saving API keys to cookies:', error);
201
- }
202
- };
203
-
204
  const handleFileUpload = () => {
205
  const input = document.createElement('input');
206
  input.type = 'file';
@@ -360,11 +395,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
360
  providerList={PROVIDER_LIST}
361
  apiKeys={apiKeys}
362
  />
363
- {provider && (
364
  <APIKeyManager
365
  provider={provider}
366
  apiKey={apiKeys[provider.name] || ''}
367
- setApiKey={(key) => updateApiKey(provider.name, key)}
 
 
 
 
368
  />
369
  )}
370
  </div>
 
88
  ref,
89
  ) => {
90
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
91
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>(() => {
92
+ const savedKeys = Cookies.get('apiKeys');
93
+
94
+ if (savedKeys) {
95
+ try {
96
+ return JSON.parse(savedKeys);
97
+ } catch (error) {
98
+ console.error('Failed to parse API keys from cookies:', error);
99
+ return {};
100
+ }
101
+ }
102
+
103
+ return {};
104
+ });
105
  const [modelList, setModelList] = useState(MODEL_LIST);
106
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
107
  const [isListening, setIsListening] = useState(false);
108
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109
  const [transcript, setTranscript] = useState('');
110
 
111
+ // Load enabled providers from cookies
112
+ const [enabledProviders, setEnabledProviders] = useState(() => {
113
+ const savedProviders = Cookies.get('providers');
114
+
115
+ if (savedProviders) {
116
+ try {
117
+ const parsedProviders = JSON.parse(savedProviders);
118
+ return PROVIDER_LIST.filter((p) => parsedProviders[p.name]);
119
+ } catch (error) {
120
+ console.error('Failed to parse providers from cookies:', error);
121
+ return PROVIDER_LIST;
122
+ }
123
+ }
124
+
125
+ return PROVIDER_LIST;
126
+ });
127
+
128
+ // Update enabled providers when cookies change
129
+ useEffect(() => {
130
+ const updateProvidersFromCookies = () => {
131
+ const savedProviders = Cookies.get('providers');
132
+
133
+ if (savedProviders) {
134
+ try {
135
+ const parsedProviders = JSON.parse(savedProviders);
136
+ setEnabledProviders(PROVIDER_LIST.filter((p) => parsedProviders[p.name]));
137
+ } catch (error) {
138
+ console.error('Failed to parse providers from cookies:', error);
139
+ }
140
+ }
141
+ };
142
+
143
+ updateProvidersFromCookies();
144
+
145
+ const interval = setInterval(updateProvidersFromCookies, 1000);
146
+
147
+ return () => clearInterval(interval);
148
+ }, [PROVIDER_LIST]);
149
+
150
  console.log(transcript);
151
  useEffect(() => {
152
  // Load API keys from cookies on component mount
 
236
  }
237
  };
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  const handleFileUpload = () => {
240
  const input = document.createElement('input');
241
  input.type = 'file';
 
395
  providerList={PROVIDER_LIST}
396
  apiKeys={apiKeys}
397
  />
398
+ {enabledProviders.length > 0 && provider && (
399
  <APIKeyManager
400
  provider={provider}
401
  apiKey={apiKeys[provider.name] || ''}
402
+ setApiKey={(key) => {
403
+ const newApiKeys = { ...apiKeys, [provider.name]: key };
404
+ setApiKeys(newApiKeys);
405
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
406
+ }}
407
  />
408
  )}
409
  </div>
app/components/chat/ModelSelector.tsx CHANGED
@@ -1,5 +1,7 @@
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
 
 
3
 
4
  interface ModelSelectorProps {
5
  model?: string;
@@ -19,12 +21,79 @@ export const ModelSelector = ({
19
  modelList,
20
  providerList,
21
  }: ModelSelectorProps) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return (
23
  <div className="mb-2 flex gap-2 flex-col sm:flex-row">
24
  <select
25
  value={provider?.name ?? ''}
26
  onChange={(e) => {
27
- const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
28
 
29
  if (newProvider && setProvider) {
30
  setProvider(newProvider);
@@ -38,7 +107,7 @@ export const ModelSelector = ({
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
40
  >
41
- {providerList.map((provider: ProviderInfo) => (
42
  <option key={provider.name} value={provider.name}>
43
  {provider.name}
44
  </option>
 
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
3
+ import { useEffect, useState } from 'react';
4
+ import Cookies from 'js-cookie';
5
 
6
  interface ModelSelectorProps {
7
  model?: string;
 
21
  modelList,
22
  providerList,
23
  }: ModelSelectorProps) => {
24
+ // Load enabled providers from cookies
25
+ const [enabledProviders, setEnabledProviders] = useState(() => {
26
+ const savedProviders = Cookies.get('providers');
27
+
28
+ if (savedProviders) {
29
+ try {
30
+ const parsedProviders = JSON.parse(savedProviders);
31
+ return providerList.filter((p) => parsedProviders[p.name]);
32
+ } catch (error) {
33
+ console.error('Failed to parse providers from cookies:', error);
34
+ return providerList;
35
+ }
36
+ }
37
+
38
+ return providerList;
39
+ });
40
+
41
+ // Update enabled providers when cookies change
42
+ useEffect(() => {
43
+ // Function to update providers from cookies
44
+ const updateProvidersFromCookies = () => {
45
+ const savedProviders = Cookies.get('providers');
46
+
47
+ if (savedProviders) {
48
+ try {
49
+ const parsedProviders = JSON.parse(savedProviders);
50
+ const newEnabledProviders = providerList.filter((p) => parsedProviders[p.name]);
51
+ setEnabledProviders(newEnabledProviders);
52
+
53
+ // If current provider is disabled, switch to first enabled provider
54
+ if (provider && !parsedProviders[provider.name] && newEnabledProviders.length > 0) {
55
+ const firstEnabledProvider = newEnabledProviders[0];
56
+ setProvider?.(firstEnabledProvider);
57
+
58
+ // Also update the model to the first available one for the new provider
59
+ const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
60
+
61
+ if (firstModel) {
62
+ setModel?.(firstModel.name);
63
+ }
64
+ }
65
+ } catch (error) {
66
+ console.error('Failed to parse providers from cookies:', error);
67
+ }
68
+ }
69
+ };
70
+
71
+ // Initial update
72
+ updateProvidersFromCookies();
73
+
74
+ // Set up an interval to check for cookie changes
75
+ const interval = setInterval(updateProvidersFromCookies, 1000);
76
+
77
+ return () => clearInterval(interval);
78
+ }, [providerList, provider, setProvider, modelList, setModel]);
79
+
80
+ if (enabledProviders.length === 0) {
81
+ return (
82
+ <div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
83
+ <p className="text-center">
84
+ No providers are currently enabled. Please enable at least one provider in the settings to start using the
85
+ chat.
86
+ </p>
87
+ </div>
88
+ );
89
+ }
90
+
91
  return (
92
  <div className="mb-2 flex gap-2 flex-col sm:flex-row">
93
  <select
94
  value={provider?.name ?? ''}
95
  onChange={(e) => {
96
+ const newProvider = enabledProviders.find((p: ProviderInfo) => p.name === e.target.value);
97
 
98
  if (newProvider && setProvider) {
99
  setProvider(newProvider);
 
107
  }}
108
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
109
  >
110
+ {enabledProviders.map((provider: ProviderInfo) => (
111
  <option key={provider.name} value={provider.name}>
112
  {provider.name}
113
  </option>
app/components/sidebar/Menu.client.tsx CHANGED
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
 
 
6
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
7
  import { cubicEasingFn } from '~/utils/easings';
8
  import { logger } from '~/utils/logger';
@@ -39,6 +41,7 @@ export const Menu = () => {
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
41
  const [dialogContent, setDialogContent] = useState<DialogContent>(null);
 
42
 
43
  const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
44
  items: list,
@@ -200,10 +203,12 @@ export const Menu = () => {
200
  </Dialog>
201
  </DialogRoot>
202
  </div>
203
- <div className="flex items-center border-t border-bolt-elements-borderColor p-4">
204
- <ThemeSwitch className="ml-auto" />
 
205
  </div>
206
  </div>
 
207
  </motion.div>
208
  );
209
  };
 
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
+ import { Settings } from '~/components/ui/Settings';
7
+ import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
10
  import { logger } from '~/utils/logger';
 
41
  const [list, setList] = useState<ChatHistoryItem[]>([]);
42
  const [open, setOpen] = useState(false);
43
  const [dialogContent, setDialogContent] = useState<DialogContent>(null);
44
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
45
 
46
  const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
47
  items: list,
 
203
  </Dialog>
204
  </DialogRoot>
205
  </div>
206
+ <div className="flex items-center justify-between border-t border-bolt-elements-borderColor p-4">
207
+ <SettingsButton onClick={() => setIsSettingsOpen(true)} />
208
+ <ThemeSwitch />
209
  </div>
210
  </div>
211
+ <Settings open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
212
  </motion.div>
213
  );
214
  };
app/components/ui/Settings.tsx ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion } from 'framer-motion';
3
+ import { useState } from 'react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { DialogTitle, dialogVariants, dialogBackdropVariants } from './Dialog';
6
+ import { IconButton } from './IconButton';
7
+ import { providersList } from '~/lib/stores/settings';
8
+ import { db, getAll, deleteById } from '~/lib/persistence';
9
+ import { toast } from 'react-toastify';
10
+ import { useNavigate } from '@remix-run/react';
11
+ import commit from '~/commit.json';
12
+ import Cookies from 'js-cookie';
13
+
14
+ interface SettingsProps {
15
+ open: boolean;
16
+ onClose: () => void;
17
+ }
18
+
19
+ type TabType = 'chat-history' | 'providers' | 'features' | 'debug';
20
+
21
+ // Providers that support base URL configuration
22
+ const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
23
+
24
+ export const Settings = ({ open, onClose }: SettingsProps) => {
25
+ const navigate = useNavigate();
26
+ const [activeTab, setActiveTab] = useState<TabType>('chat-history');
27
+ const [isDebugEnabled, setIsDebugEnabled] = useState(false);
28
+ const [searchTerm, setSearchTerm] = useState('');
29
+ const [isDeleting, setIsDeleting] = useState(false);
30
+
31
+ // Load base URLs from cookies
32
+ const [baseUrls, setBaseUrls] = useState(() => {
33
+ const savedUrls = Cookies.get('providerBaseUrls');
34
+
35
+ if (savedUrls) {
36
+ try {
37
+ return JSON.parse(savedUrls);
38
+ } catch (error) {
39
+ console.error('Failed to parse base URLs from cookies:', error);
40
+ return {
41
+ Ollama: 'http://localhost:11434',
42
+ LMStudio: 'http://localhost:1234',
43
+ OpenAILike: '',
44
+ };
45
+ }
46
+ }
47
+
48
+ return {
49
+ Ollama: 'http://localhost:11434',
50
+ LMStudio: 'http://localhost:1234',
51
+ OpenAILike: '',
52
+ };
53
+ });
54
+
55
+ const handleBaseUrlChange = (provider: string, url: string) => {
56
+ setBaseUrls((prev: Record<string, string>) => {
57
+ const newUrls = { ...prev, [provider]: url };
58
+ Cookies.set('providerBaseUrls', JSON.stringify(newUrls));
59
+
60
+ return newUrls;
61
+ });
62
+ };
63
+
64
+ const tabs: { id: TabType; label: string; icon: string }[] = [
65
+ { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book' },
66
+ { id: 'providers', label: 'Providers', icon: 'i-ph:key' },
67
+ { id: 'features', label: 'Features', icon: 'i-ph:star' },
68
+ ...(isDebugEnabled ? [{ id: 'debug' as TabType, label: 'Debug Tab', icon: 'i-ph:bug' }] : []),
69
+ ];
70
+
71
+ // Load providers from cookies on mount
72
+ const [providers, setProviders] = useState(() => {
73
+ const savedProviders = Cookies.get('providers');
74
+
75
+ if (savedProviders) {
76
+ try {
77
+ const parsedProviders = JSON.parse(savedProviders);
78
+
79
+ // Merge saved enabled states with the base provider list
80
+ return providersList.map((provider) => ({
81
+ ...provider,
82
+ isEnabled: parsedProviders[provider.name] || false,
83
+ }));
84
+ } catch (error) {
85
+ console.error('Failed to parse providers from cookies:', error);
86
+ }
87
+ }
88
+
89
+ return providersList;
90
+ });
91
+
92
+ const handleToggleProvider = (providerName: string) => {
93
+ setProviders((prevProviders) => {
94
+ const newProviders = prevProviders.map((provider) =>
95
+ provider.name === providerName ? { ...provider, isEnabled: !provider.isEnabled } : provider,
96
+ );
97
+
98
+ // Save to cookies
99
+ const enabledStates = newProviders.reduce(
100
+ (acc, provider) => ({
101
+ ...acc,
102
+ [provider.name]: provider.isEnabled,
103
+ }),
104
+ {},
105
+ );
106
+ Cookies.set('providers', JSON.stringify(enabledStates));
107
+
108
+ return newProviders;
109
+ });
110
+ };
111
+
112
+ const filteredProviders = providers
113
+ .filter((provider) => provider.name.toLowerCase().includes(searchTerm.toLowerCase()))
114
+ .sort((a, b) => a.name.localeCompare(b.name));
115
+
116
+ const handleCopyToClipboard = () => {
117
+ const debugInfo = {
118
+ OS: navigator.platform,
119
+ Browser: navigator.userAgent,
120
+ ActiveFeatures: providers.filter((provider) => provider.isEnabled).map((provider) => provider.name),
121
+ BaseURLs: {
122
+ Ollama: process.env.REACT_APP_OLLAMA_URL,
123
+ OpenAI: process.env.REACT_APP_OPENAI_URL,
124
+ LMStudio: process.env.REACT_APP_LM_STUDIO_URL,
125
+ },
126
+ Version: versionHash,
127
+ };
128
+ navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
129
+ alert('Debug information copied to clipboard!');
130
+ });
131
+ };
132
+
133
+ const downloadAsJson = (data: any, filename: string) => {
134
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
135
+ const url = URL.createObjectURL(blob);
136
+ const link = document.createElement('a');
137
+ link.href = url;
138
+ link.download = filename;
139
+ document.body.appendChild(link);
140
+ link.click();
141
+ document.body.removeChild(link);
142
+ URL.revokeObjectURL(url);
143
+ };
144
+
145
+ const handleDeleteAllChats = async () => {
146
+ if (!db) {
147
+ toast.error('Database is not available');
148
+ return;
149
+ }
150
+
151
+ try {
152
+ setIsDeleting(true);
153
+
154
+ const allChats = await getAll(db);
155
+
156
+ // Delete all chats one by one
157
+ await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
158
+
159
+ toast.success('All chats deleted successfully');
160
+ navigate('/', { replace: true });
161
+ } catch (error) {
162
+ toast.error('Failed to delete chats');
163
+ console.error(error);
164
+ } finally {
165
+ setIsDeleting(false);
166
+ }
167
+ };
168
+
169
+ const handleExportAllChats = async () => {
170
+ if (!db) {
171
+ toast.error('Database is not available');
172
+ return;
173
+ }
174
+
175
+ try {
176
+ const allChats = await getAll(db);
177
+ const exportData = {
178
+ chats: allChats,
179
+ exportDate: new Date().toISOString(),
180
+ };
181
+
182
+ downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
183
+ toast.success('Chats exported successfully');
184
+ } catch (error) {
185
+ toast.error('Failed to export chats');
186
+ console.error(error);
187
+ }
188
+ };
189
+
190
+ const versionHash = commit.commit; // Get the version hash from commit.json
191
+
192
+ return (
193
+ <RadixDialog.Root open={open}>
194
+ <RadixDialog.Portal>
195
+ <RadixDialog.Overlay asChild>
196
+ <motion.div
197
+ className="bg-black/50 fixed inset-0 z-max"
198
+ initial="closed"
199
+ animate="open"
200
+ exit="closed"
201
+ variants={dialogBackdropVariants}
202
+ />
203
+ </RadixDialog.Overlay>
204
+ <RadixDialog.Content asChild>
205
+ <motion.div
206
+ className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg bg-gray-800 shadow-lg focus:outline-none overflow-hidden"
207
+ initial="closed"
208
+ animate="open"
209
+ exit="closed"
210
+ variants={dialogVariants}
211
+ >
212
+ <div className="flex h-full">
213
+ <div className="w-48 border-r border-bolt-elements-borderColor bg-gray-700 p-4 flex flex-col justify-between">
214
+ {tabs.map((tab) => (
215
+ <button
216
+ key={tab.id}
217
+ onClick={() => setActiveTab(tab.id)}
218
+ className={classNames(
219
+ 'w-full flex items-center gap-2 px-4 py-3 rounded-lg text-left text-sm transition-all mb-2',
220
+ activeTab === tab.id ? 'bg-blue-600 text-white' : 'bg-gray-600 text-gray-200 hover:bg-blue-500',
221
+ )}
222
+ >
223
+ <div className={tab.icon} />
224
+ {tab.label}
225
+ </button>
226
+ ))}
227
+ <div className="mt-auto flex flex-col gap-2">
228
+ <a
229
+ href="https://github.com/coleam00/bolt.new-any-llm"
230
+ target="_blank"
231
+ rel="noopener noreferrer"
232
+ className="flex items-center justify-center bg-blue-600 text-white rounded-lg py-2 hover:bg-blue-500 transition-colors duration-200"
233
+ >
234
+ GitHub
235
+ </a>
236
+ <a
237
+ href="https://coleam00.github.io/bolt.new-any-llm"
238
+ target="_blank"
239
+ rel="noopener noreferrer"
240
+ className="flex items-center justify-center bg-blue-600 text-white rounded-lg py-2 hover:bg-blue-500 transition-colors duration-200"
241
+ >
242
+ Docs
243
+ </a>
244
+ </div>
245
+ </div>
246
+
247
+ <div className="flex-1 flex flex-col p-8">
248
+ <DialogTitle className="flex-shrink-0 text-lg font-semibold text-white">Settings</DialogTitle>
249
+ <div className="flex-1 overflow-y-auto">
250
+ {activeTab === 'chat-history' && (
251
+ <div className="p-4">
252
+ <h3 className="text-lg font-medium text-white mb-4">Chat History</h3>
253
+ <button
254
+ onClick={handleExportAllChats}
255
+ className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
256
+ >
257
+ Export All Chats
258
+ </button>
259
+
260
+ <div className="bg-red-500 text-white rounded-lg p-4 mb-4">
261
+ <h4 className="font-semibold">Danger Area</h4>
262
+ <p className="mb-2">This action cannot be undone!</p>
263
+ <button
264
+ onClick={handleDeleteAllChats}
265
+ disabled={isDeleting}
266
+ className={classNames(
267
+ 'bg-red-700 text-white rounded-lg px-4 py-2 transition-colors duration-200',
268
+ isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-red-800',
269
+ )}
270
+ >
271
+ {isDeleting ? 'Deleting...' : 'Delete All Chats'}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ )}
276
+ {activeTab === 'providers' && (
277
+ <div className="p-4">
278
+ <h3 className="text-lg font-medium text-white mb-4">Providers</h3>
279
+ <input
280
+ type="text"
281
+ placeholder="Search providers..."
282
+ value={searchTerm}
283
+ onChange={(e) => setSearchTerm(e.target.value)}
284
+ className="mb-4 p-2 rounded border border-gray-300 w-full"
285
+ />
286
+ {filteredProviders.map((provider) => (
287
+ <div
288
+ key={provider.name}
289
+ className="flex flex-col mb-6 provider-item hover:bg-gray-600 p-4 rounded-lg"
290
+ >
291
+ <div className="flex items-center justify-between mb-2">
292
+ <span className="text-white">{provider.name}</span>
293
+ <label className="relative inline-flex items-center cursor-pointer">
294
+ <input
295
+ type="checkbox"
296
+ className="sr-only"
297
+ checked={provider.isEnabled}
298
+ onChange={() => handleToggleProvider(provider.name)}
299
+ />
300
+ <div className="w-11 h-6 bg-gray-300 rounded-full shadow-inner"></div>
301
+ <div
302
+ className={`absolute left-0 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
303
+ provider.isEnabled ? 'transform translate-x-full bg-green-500' : ''
304
+ }`}
305
+ ></div>
306
+ </label>
307
+ </div>
308
+
309
+ {/* Base URL input for configurable providers */}
310
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.isEnabled && (
311
+ <div className="mt-2">
312
+ <label className="block text-sm text-gray-300 mb-1">Base URL:</label>
313
+ <input
314
+ type="text"
315
+ value={baseUrls[provider.name]}
316
+ onChange={(e) => handleBaseUrlChange(provider.name, e.target.value)}
317
+ placeholder={`Enter ${provider.name} base URL`}
318
+ className="w-full p-2 rounded border border-gray-600 bg-gray-700 text-white text-sm"
319
+ />
320
+ </div>
321
+ )}
322
+ </div>
323
+ ))}
324
+ </div>
325
+ )}
326
+ {activeTab === 'features' && (
327
+ <div className="p-4">
328
+ <div className="flex items-center justify-between mb-2">
329
+ <span className="text-white">Debug Info</span>
330
+ <label className="relative inline-flex items-center cursor-pointer">
331
+ <input
332
+ type="checkbox"
333
+ className="sr-only"
334
+ checked={isDebugEnabled}
335
+ onChange={() => setIsDebugEnabled(!isDebugEnabled)}
336
+ />
337
+ <div className="w-11 h-6 bg-gray-300 rounded-full shadow-inner"></div>
338
+ <div
339
+ className={`absolute left-0 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
340
+ isDebugEnabled ? 'transform translate-x-full bg-green-500' : ''
341
+ }`}
342
+ ></div>
343
+ </label>
344
+ </div>
345
+ <div className="feature-row">{/* Your feature content here */}</div>
346
+ </div>
347
+ )}
348
+ {activeTab === 'debug' && isDebugEnabled && (
349
+ <div className="p-4">
350
+ <h3 className="text-lg font-medium text-white mb-4">Debug Tab</h3>
351
+ <button
352
+ onClick={handleCopyToClipboard}
353
+ className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
354
+ >
355
+ Copy to Clipboard
356
+ </button>
357
+
358
+ <h4 className="text-md font-medium text-white">System Information</h4>
359
+ <p className="text-white">OS: {navigator.platform}</p>
360
+ <p className="text-white">Browser: {navigator.userAgent}</p>
361
+
362
+ <h4 className="text-md font-medium text-white mt-4">Active Features</h4>
363
+ <ul>
364
+ {providers
365
+ .filter((provider) => provider.isEnabled)
366
+ .map((provider) => (
367
+ <li key={provider.name} className="text-white">
368
+ {provider.name}
369
+ </li>
370
+ ))}
371
+ </ul>
372
+
373
+ <h4 className="text-md font-medium text-white mt-4">Base URLs</h4>
374
+ <ul>
375
+ <li className="text-white">Ollama: {process.env.REACT_APP_OLLAMA_URL}</li>
376
+ <li className="text-white">OpenAI: {process.env.REACT_APP_OPENAI_URL}</li>
377
+ <li className="text-white">LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}</li>
378
+ </ul>
379
+
380
+ <h4 className="text-md font-medium text-white mt-4">Version Information</h4>
381
+ <p className="text-white">Version Hash: {versionHash}</p>
382
+ </div>
383
+ )}
384
+ </div>
385
+ </div>
386
+ </div>
387
+ <RadixDialog.Close asChild onClick={onClose}>
388
+ <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
389
+ </RadixDialog.Close>
390
+ </motion.div>
391
+ </RadixDialog.Content>
392
+ </RadixDialog.Portal>
393
+ </RadixDialog.Root>
394
+ );
395
+ };
app/components/ui/SettingsButton.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { IconButton } from './IconButton';
3
+
4
+ interface SettingsButtonProps {
5
+ onClick: () => void;
6
+ }
7
+
8
+ export const SettingsButton = memo(({ onClick }: SettingsButtonProps) => {
9
+ return (
10
+ <IconButton
11
+ onClick={onClick}
12
+ icon="i-ph:gear"
13
+ size="xl"
14
+ title="Settings"
15
+ className="text-[#666] hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive/10 transition-colors"
16
+ />
17
+ );
18
+ });
app/components/ui/SettingsSlider.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion';
2
+ import { memo } from 'react';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface SliderOption<T> {
6
+ value: T;
7
+ text: string;
8
+ }
9
+
10
+ export interface SliderOptions<T> {
11
+ left: SliderOption<T>;
12
+ right: SliderOption<T>;
13
+ }
14
+
15
+ interface SettingsSliderProps<T> {
16
+ selected: T;
17
+ options: SliderOptions<T>;
18
+ setSelected?: (selected: T) => void;
19
+ }
20
+
21
+ export const SettingsSlider = memo(<T,>({ selected, options, setSelected }: SettingsSliderProps<T>) => {
22
+ const isLeftSelected = selected === options.left.value;
23
+
24
+ return (
25
+ <div className="relative flex items-center bg-bolt-elements-prompt-background rounded-lg">
26
+ <motion.div
27
+ className={classNames(
28
+ 'absolute h-full bg-green-500 transition-all duration-300 rounded-lg',
29
+ isLeftSelected ? 'left-0 w-1/2' : 'right-0 w-1/2',
30
+ )}
31
+ initial={false}
32
+ animate={{
33
+ x: isLeftSelected ? 0 : '100%',
34
+ opacity: 0.2,
35
+ }}
36
+ transition={{
37
+ type: 'spring',
38
+ stiffness: 300,
39
+ damping: 30,
40
+ }}
41
+ />
42
+ <button
43
+ onClick={() => setSelected?.(options.left.value)}
44
+ className={classNames(
45
+ 'relative z-10 flex-1 p-2 rounded-lg text-sm transition-colors duration-200',
46
+ isLeftSelected ? 'text-white' : 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary',
47
+ )}
48
+ >
49
+ {options.left.text}
50
+ </button>
51
+ <button
52
+ onClick={() => setSelected?.(options.right.value)}
53
+ className={classNames(
54
+ 'relative z-10 flex-1 p-2 rounded-lg text-sm transition-colors duration-200',
55
+ !isLeftSelected ? 'text-white' : 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary',
56
+ )}
57
+ >
58
+ {options.right.text}
59
+ </button>
60
+ </div>
61
+ );
62
+ });
app/lib/stores/settings.ts CHANGED
@@ -15,10 +15,33 @@ export interface Shortcuts {
15
  toggleTerminal: Shortcut;
16
  }
17
 
 
 
 
 
 
18
  export interface Settings {
19
  shortcuts: Shortcuts;
 
20
  }
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  export const shortcutsStore = map<Shortcuts>({
23
  toggleTerminal: {
24
  key: 'j',
@@ -29,6 +52,7 @@ export const shortcutsStore = map<Shortcuts>({
29
 
30
  export const settingsStore = map<Settings>({
31
  shortcuts: shortcutsStore.get(),
 
32
  });
33
 
34
  shortcutsStore.subscribe((shortcuts) => {
 
15
  toggleTerminal: Shortcut;
16
  }
17
 
18
+ export interface Provider {
19
+ name: string;
20
+ isEnabled: boolean;
21
+ }
22
+
23
  export interface Settings {
24
  shortcuts: Shortcuts;
25
+ providers: Provider[];
26
  }
27
 
28
+ export const providersList: Provider[] = [
29
+ { name: 'Groq', isEnabled: false },
30
+ { name: 'HuggingFace', isEnabled: false },
31
+ { name: 'OpenAI', isEnabled: false },
32
+ { name: 'Anthropic', isEnabled: false },
33
+ { name: 'OpenRouter', isEnabled: false },
34
+ { name: 'Google', isEnabled: false },
35
+ { name: 'Ollama', isEnabled: false },
36
+ { name: 'OpenAILike', isEnabled: false },
37
+ { name: 'Together', isEnabled: false },
38
+ { name: 'Deepseek', isEnabled: false },
39
+ { name: 'Mistral', isEnabled: false },
40
+ { name: 'Cohere', isEnabled: false },
41
+ { name: 'LMStudio', isEnabled: false },
42
+ { name: 'xAI', isEnabled: false },
43
+ ];
44
+
45
  export const shortcutsStore = map<Shortcuts>({
46
  toggleTerminal: {
47
  key: 'j',
 
52
 
53
  export const settingsStore = map<Settings>({
54
  shortcuts: shortcutsStore.get(),
55
+ providers: providersList,
56
  });
57
 
58
  shortcutsStore.subscribe((shortcuts) => {
app/types/model.ts CHANGED
@@ -7,4 +7,5 @@ export type ProviderInfo = {
7
  getApiKeyLink?: string;
8
  labelForGetApiKey?: string;
9
  icon?: string;
 
10
  };
 
7
  getApiKeyLink?: string;
8
  labelForGetApiKey?: string;
9
  icon?: string;
10
+ isEnabled?: boolean;
11
  };