LlamaFinetuneGGUF commited on
Commit
42ebd3d
·
1 Parent(s): 2af32b0

Added a tabbed setting modal

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