pauloj commited on
Commit
c24e697
·
unverified ·
2 Parent(s): 382bf2c 220e2da

Merge branch 'main' into diff-view-v2

Browse files
app/components/@settings/core/ControlPanel.tsx CHANGED
@@ -263,6 +263,27 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
263
  },
264
  };
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  // Handlers
267
  const handleBack = () => {
268
  if (showTabManagement) {
@@ -405,8 +426,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
405
 
406
  <RadixDialog.Content
407
  aria-describedby={undefined}
408
- onEscapeKeyDown={onClose}
409
- onPointerDownOutside={onClose}
410
  className="relative z-[101]"
411
  >
412
  <motion.div
@@ -461,7 +482,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
461
 
462
  {/* Close Button */}
463
  <button
464
- onClick={onClose}
465
  className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
466
  >
467
  <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
 
263
  },
264
  };
265
 
266
+ // Reset to default view when modal opens/closes
267
+ useEffect(() => {
268
+ if (!open) {
269
+ // Reset when closing
270
+ setActiveTab(null);
271
+ setLoadingTab(null);
272
+ setShowTabManagement(false);
273
+ } else {
274
+ // When opening, set to null to show the main view
275
+ setActiveTab(null);
276
+ }
277
+ }, [open]);
278
+
279
+ // Handle closing
280
+ const handleClose = () => {
281
+ setActiveTab(null);
282
+ setLoadingTab(null);
283
+ setShowTabManagement(false);
284
+ onClose();
285
+ };
286
+
287
  // Handlers
288
  const handleBack = () => {
289
  if (showTabManagement) {
 
426
 
427
  <RadixDialog.Content
428
  aria-describedby={undefined}
429
+ onEscapeKeyDown={handleClose}
430
+ onPointerDownOutside={handleClose}
431
  className="relative z-[101]"
432
  >
433
  <motion.div
 
482
 
483
  {/* Close Button */}
484
  <button
485
+ onClick={handleClose}
486
  className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
487
  >
488
  <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
app/components/@settings/shared/components/TabManagement.tsx CHANGED
@@ -1,13 +1,14 @@
1
- import { useState } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { useStore } from '@nanostores/react';
4
- import { Switch } from '@radix-ui/react-switch';
5
  import { classNames } from '~/utils/classNames';
6
  import { tabConfigurationStore } from '~/lib/stores/settings';
7
  import { TAB_LABELS } from '~/components/@settings/core/constants';
8
  import type { TabType } from '~/components/@settings/core/types';
9
  import { toast } from 'react-toastify';
10
  import { TbLayoutGrid } from 'react-icons/tb';
 
11
 
12
  // Define tab icons mapping
13
  const TAB_ICONS: Record<TabType, string> = {
@@ -55,6 +56,7 @@ const BetaLabel = () => (
55
  export const TabManagement = () => {
56
  const [searchQuery, setSearchQuery] = useState('');
57
  const tabConfiguration = useStore(tabConfigurationStore);
 
58
 
59
  const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
60
  // Get current tab configuration
@@ -126,6 +128,13 @@ export const TabManagement = () => {
126
  // Filter tabs based on search query
127
  const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
128
 
 
 
 
 
 
 
 
129
  return (
130
  <div className="space-y-6">
131
  <motion.div
@@ -177,92 +186,193 @@ export const TabManagement = () => {
177
 
178
  {/* Tab Grid */}
179
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
180
- {filteredTabs.map((tab, index) => (
181
- <motion.div
182
- key={tab.id}
183
- className={classNames(
184
- 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
185
- 'bg-bolt-elements-background-depth-2',
186
- 'hover:bg-bolt-elements-background-depth-3',
187
- 'transition-all duration-200',
188
- 'relative overflow-hidden group',
189
- )}
190
- initial={{ opacity: 0, y: 20 }}
191
- animate={{ opacity: 1, y: 0 }}
192
- transition={{ delay: index * 0.1 }}
193
- whileHover={{ scale: 1.02 }}
194
- >
195
- {/* Status Badges */}
196
- <div className="absolute top-2 right-2 flex gap-1">
197
- {DEFAULT_USER_TABS.includes(tab.id) && (
198
- <span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium">
 
 
 
 
 
 
 
 
 
 
199
  Default
200
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  )}
202
- {OPTIONAL_USER_TABS.includes(tab.id) && (
203
- <span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">
 
 
 
 
 
 
204
  Optional
205
  </span>
206
- )}
207
- </div>
208
 
209
- <div className="flex items-start gap-4 p-4">
210
- <motion.div
211
- className={classNames(
212
- 'w-10 h-10 flex items-center justify-center rounded-xl',
213
- 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
214
- 'transition-all duration-200',
215
- tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
216
- )}
217
- whileHover={{ scale: 1.1 }}
218
- whileTap={{ scale: 0.9 }}
219
- >
220
- <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
221
- <div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
222
- </div>
223
- </motion.div>
224
-
225
- <div className="flex-1 min-w-0">
226
- <div className="flex items-center justify-between gap-4">
227
- <div>
228
- <div className="flex items-center gap-2">
229
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
230
- {TAB_LABELS[tab.id]}
231
- </h4>
232
- {BETA_TABS.has(tab.id) && <BetaLabel />}
233
- </div>
234
- <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
235
- {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
236
- </p>
237
  </div>
238
- <Switch
239
- checked={tab.visible}
240
- onCheckedChange={(checked) => handleTabVisibilityChange(tab.id, checked)}
241
- disabled={!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id)}
242
- className={classNames(
243
- 'relative inline-flex h-5 w-9 items-center rounded-full',
244
- 'transition-colors duration-200',
245
- tab.visible ? 'bg-purple-500' : 'bg-bolt-elements-background-depth-4',
246
- {
247
- 'opacity-50 cursor-not-allowed':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
249
- },
250
- )}
251
- />
252
  </div>
253
  </div>
254
- </div>
255
 
256
- <motion.div
257
- className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
258
- animate={{
259
- borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
260
- scale: tab.visible ? 1 : 0.98,
261
- }}
262
- transition={{ duration: 0.2 }}
263
- />
264
- </motion.div>
265
- ))}
266
  </div>
267
  </motion.div>
268
  </div>
 
1
+ import { useState, useEffect } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { useStore } from '@nanostores/react';
4
+ import { Switch } from '~/components/ui/Switch';
5
  import { classNames } from '~/utils/classNames';
6
  import { tabConfigurationStore } from '~/lib/stores/settings';
7
  import { TAB_LABELS } from '~/components/@settings/core/constants';
8
  import type { TabType } from '~/components/@settings/core/types';
9
  import { toast } from 'react-toastify';
10
  import { TbLayoutGrid } from 'react-icons/tb';
11
+ import { useSettingsStore } from '~/lib/stores/settings';
12
 
13
  // Define tab icons mapping
14
  const TAB_ICONS: Record<TabType, string> = {
 
56
  export const TabManagement = () => {
57
  const [searchQuery, setSearchQuery] = useState('');
58
  const tabConfiguration = useStore(tabConfigurationStore);
59
+ const { setSelectedTab } = useSettingsStore();
60
 
61
  const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
62
  // Get current tab configuration
 
128
  // Filter tabs based on search query
129
  const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
130
 
131
+ useEffect(() => {
132
+ // Reset to first tab when component unmounts
133
+ return () => {
134
+ setSelectedTab('user'); // Reset to user tab when unmounting
135
+ };
136
+ }, [setSelectedTab]);
137
+
138
  return (
139
  <div className="space-y-6">
140
  <motion.div
 
186
 
187
  {/* Tab Grid */}
188
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
189
+ {/* Default Section Header */}
190
+ {filteredTabs.some((tab) => DEFAULT_USER_TABS.includes(tab.id)) && (
191
+ <div className="col-span-full flex items-center gap-2 mt-4 mb-2">
192
+ <div className="i-ph:star-fill w-4 h-4 text-purple-500" />
193
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Default Tabs</span>
194
+ </div>
195
+ )}
196
+
197
+ {/* Default Tabs */}
198
+ {filteredTabs
199
+ .filter((tab) => DEFAULT_USER_TABS.includes(tab.id))
200
+ .map((tab, index) => (
201
+ <motion.div
202
+ key={tab.id}
203
+ className={classNames(
204
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
205
+ 'bg-bolt-elements-background-depth-2',
206
+ 'hover:bg-bolt-elements-background-depth-3',
207
+ 'transition-all duration-200',
208
+ 'relative overflow-hidden group',
209
+ )}
210
+ initial={{ opacity: 0, y: 20 }}
211
+ animate={{ opacity: 1, y: 0 }}
212
+ transition={{ delay: index * 0.1 }}
213
+ whileHover={{ scale: 1.02 }}
214
+ >
215
+ {/* Status Badges */}
216
+ <div className="absolute top-1 right-1.5 flex gap-1">
217
+ <span className="px-1.5 py-0.25 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium mr-2">
218
  Default
219
  </span>
220
+ </div>
221
+
222
+ <div className="flex items-start gap-4 p-4">
223
+ <motion.div
224
+ className={classNames(
225
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
226
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
227
+ 'transition-all duration-200',
228
+ tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
229
+ )}
230
+ whileHover={{ scale: 1.1 }}
231
+ whileTap={{ scale: 0.9 }}
232
+ >
233
+ <div
234
+ className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
235
+ >
236
+ <div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
237
+ </div>
238
+ </motion.div>
239
+
240
+ <div className="flex-1 min-w-0">
241
+ <div className="flex items-center justify-between gap-4">
242
+ <div>
243
+ <div className="flex items-center gap-2">
244
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
245
+ {TAB_LABELS[tab.id]}
246
+ </h4>
247
+ {BETA_TABS.has(tab.id) && <BetaLabel />}
248
+ </div>
249
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
250
+ {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
251
+ </p>
252
+ </div>
253
+ <Switch
254
+ checked={tab.visible}
255
+ onCheckedChange={(checked) => {
256
+ const isDisabled =
257
+ !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
258
+
259
+ if (!isDisabled) {
260
+ handleTabVisibilityChange(tab.id, checked);
261
+ }
262
+ }}
263
+ className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
264
+ 'opacity-50 pointer-events-none':
265
+ !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
266
+ })}
267
+ />
268
+ </div>
269
+ </div>
270
+ </div>
271
+
272
+ <motion.div
273
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
274
+ animate={{
275
+ borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
276
+ scale: tab.visible ? 1 : 0.98,
277
+ }}
278
+ transition={{ duration: 0.2 }}
279
+ />
280
+ </motion.div>
281
+ ))}
282
+
283
+ {/* Optional Section Header */}
284
+ {filteredTabs.some((tab) => OPTIONAL_USER_TABS.includes(tab.id)) && (
285
+ <div className="col-span-full flex items-center gap-2 mt-8 mb-2">
286
+ <div className="i-ph:plus-circle-fill w-4 h-4 text-blue-500" />
287
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Optional Tabs</span>
288
+ </div>
289
+ )}
290
+
291
+ {/* Optional Tabs */}
292
+ {filteredTabs
293
+ .filter((tab) => OPTIONAL_USER_TABS.includes(tab.id))
294
+ .map((tab, index) => (
295
+ <motion.div
296
+ key={tab.id}
297
+ className={classNames(
298
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
299
+ 'bg-bolt-elements-background-depth-2',
300
+ 'hover:bg-bolt-elements-background-depth-3',
301
+ 'transition-all duration-200',
302
+ 'relative overflow-hidden group',
303
  )}
304
+ initial={{ opacity: 0, y: 20 }}
305
+ animate={{ opacity: 1, y: 0 }}
306
+ transition={{ delay: index * 0.1 }}
307
+ whileHover={{ scale: 1.02 }}
308
+ >
309
+ {/* Status Badges */}
310
+ <div className="absolute top-1 right-1.5 flex gap-1">
311
+ <span className="px-1.5 py-0.25 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium mr-2">
312
  Optional
313
  </span>
314
+ </div>
 
315
 
316
+ <div className="flex items-start gap-4 p-4">
317
+ <motion.div
318
+ className={classNames(
319
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
320
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
321
+ 'transition-all duration-200',
322
+ tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
323
+ )}
324
+ whileHover={{ scale: 1.1 }}
325
+ whileTap={{ scale: 0.9 }}
326
+ >
327
+ <div
328
+ className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
329
+ >
330
+ <div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  </div>
332
+ </motion.div>
333
+
334
+ <div className="flex-1 min-w-0">
335
+ <div className="flex items-center justify-between gap-4">
336
+ <div>
337
+ <div className="flex items-center gap-2">
338
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
339
+ {TAB_LABELS[tab.id]}
340
+ </h4>
341
+ {BETA_TABS.has(tab.id) && <BetaLabel />}
342
+ </div>
343
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
344
+ {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
345
+ </p>
346
+ </div>
347
+ <Switch
348
+ checked={tab.visible}
349
+ onCheckedChange={(checked) => {
350
+ const isDisabled =
351
+ !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
352
+
353
+ if (!isDisabled) {
354
+ handleTabVisibilityChange(tab.id, checked);
355
+ }
356
+ }}
357
+ className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
358
+ 'opacity-50 pointer-events-none':
359
  !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
360
+ })}
361
+ />
362
+ </div>
363
  </div>
364
  </div>
 
365
 
366
+ <motion.div
367
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
368
+ animate={{
369
+ borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
370
+ scale: tab.visible ? 1 : 0.98,
371
+ }}
372
+ transition={{ duration: 0.2 }}
373
+ />
374
+ </motion.div>
375
+ ))}
376
  </div>
377
  </motion.div>
378
  </div>
app/components/@settings/tabs/debug/DebugTab.tsx CHANGED
@@ -238,7 +238,7 @@ export default function DebugTab() {
238
  performance: false,
239
  });
240
 
241
- const { isLocalModel, providers } = useSettings();
242
 
243
  // Subscribe to logStore updates
244
  const logs = useStore(logStore.logs);
@@ -1103,15 +1103,18 @@ export default function DebugTab() {
1103
  // Add Ollama health check function
1104
  const checkOllamaStatus = useCallback(async () => {
1105
  try {
 
 
 
1106
  // First check if service is running
1107
- const versionResponse = await fetch('http://127.0.0.1:11434/api/version');
1108
 
1109
  if (!versionResponse.ok) {
1110
  throw new Error('Service not running');
1111
  }
1112
 
1113
  // Then fetch installed models
1114
- const modelsResponse = await fetch('http://127.0.0.1:11434/api/tags');
1115
 
1116
  const modelsData = (await modelsResponse.json()) as {
1117
  models: Array<{ name: string; size: string; quantization: string }>;
@@ -1130,18 +1133,24 @@ export default function DebugTab() {
1130
  models: undefined,
1131
  });
1132
  }
1133
- }, []);
1134
 
1135
- // Monitor isLocalModel changes and check status periodically
1136
  useEffect(() => {
1137
- // Check immediately when isLocalModel changes
1138
- checkOllamaStatus();
1139
 
1140
- // Set up periodic checks every 10 seconds
1141
- const intervalId = setInterval(checkOllamaStatus, 10000);
 
1142
 
1143
- return () => clearInterval(intervalId);
1144
- }, [isLocalModel, checkOllamaStatus]);
 
 
 
 
 
 
1145
 
1146
  // Replace the existing export button with this new component
1147
  const ExportButton = () => {
@@ -1219,15 +1228,6 @@ export default function DebugTab() {
1219
  const ollamaProvider = providers?.Ollama;
1220
  const isOllamaEnabled = ollamaProvider?.settings?.enabled;
1221
 
1222
- if (!isLocalModel) {
1223
- return {
1224
- status: 'Disabled',
1225
- color: 'text-red-500',
1226
- bgColor: 'bg-red-500',
1227
- message: 'Local models are disabled in settings',
1228
- };
1229
- }
1230
-
1231
  if (!isOllamaEnabled) {
1232
  return {
1233
  status: 'Disabled',
@@ -1270,60 +1270,32 @@ export default function DebugTab() {
1270
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
1271
  {/* Quick Stats Banner */}
1272
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1273
- {/* Ollama Service Status Card */}
1274
- <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
1275
  <div className="flex items-center gap-2">
1276
- <div className="i-ph:robot text-purple-500 w-4 h-4" />
1277
- <div className="text-sm text-bolt-elements-textSecondary">Ollama Service</div>
1278
  </div>
1279
  <div className="flex items-center gap-2 mt-2">
1280
- <div
1281
- className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
1282
- 'shadow-lg shadow-green-500/20': status.status === 'Running',
1283
- 'shadow-lg shadow-red-500/20': status.status === 'Not Running',
1284
- })}
1285
- />
1286
- <span className={classNames('text-sm font-medium flex items-center gap-1.5', status.color)}>
1287
- {status.status === 'Running' && <div className="i-ph:check-circle-fill w-3.5 h-3.5" />}
1288
- {status.status === 'Not Running' && <div className="i-ph:x-circle-fill w-3.5 h-3.5" />}
1289
- {status.status === 'Disabled' && <div className="i-ph:prohibit-fill w-3.5 h-3.5" />}
1290
- {status.status}
1291
  </span>
1292
  </div>
1293
  <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1294
  <div
1295
- className={classNames('w-3.5 h-3.5', {
1296
- 'i-ph:info text-green-500': status.status === 'Running',
1297
- 'i-ph:warning text-red-500': status.status === 'Not Running' || status.status === 'Disabled',
1298
- })}
1299
  />
1300
- {status.message}
1301
- </div>
1302
- {ollamaStatus.models && ollamaStatus.models.length > 0 && (
1303
- <div className="mt-3 space-y-1 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-2">
1304
- <div className="text-xs font-medium text-bolt-elements-textSecondary flex items-center gap-1.5">
1305
- <div className="i-ph:cube-duotone w-3.5 h-3.5 text-purple-500" />
1306
- Installed Models
1307
- </div>
1308
- {ollamaStatus.models.map((model) => (
1309
- <div key={model.name} className="text-xs text-bolt-elements-textSecondary flex items-center gap-2 pl-5">
1310
- <div className="i-ph:cube w-3 h-3 text-purple-500/70" />
1311
- <span className="font-mono">{model.name}</span>
1312
- <span className="text-bolt-elements-textTertiary">
1313
- ({Math.round(parseInt(model.size) / 1024 / 1024)}MB, {model.quantization})
1314
- </span>
1315
- </div>
1316
- ))}
1317
- </div>
1318
- )}
1319
- <div className="text-xs text-bolt-elements-textTertiary mt-3 flex items-center gap-1.5">
1320
- <div className="i-ph:clock w-3 h-3" />
1321
- Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()}
1322
  </div>
1323
  </div>
1324
 
1325
  {/* Memory Usage Card */}
1326
- <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
1327
  <div className="flex items-center gap-2">
1328
  <div className="i-ph:cpu text-purple-500 w-4 h-4" />
1329
  <div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
@@ -1360,7 +1332,7 @@ export default function DebugTab() {
1360
  </div>
1361
 
1362
  {/* Page Load Time Card */}
1363
- <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
1364
  <div className="flex items-center gap-2">
1365
  <div className="i-ph:timer text-purple-500 w-4 h-4" />
1366
  <div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
@@ -1386,7 +1358,7 @@ export default function DebugTab() {
1386
  </div>
1387
 
1388
  {/* Network Speed Card */}
1389
- <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
1390
  <div className="flex items-center gap-2">
1391
  <div className="i-ph:wifi-high text-purple-500 w-4 h-4" />
1392
  <div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
@@ -1411,27 +1383,80 @@ export default function DebugTab() {
1411
  </div>
1412
  </div>
1413
 
1414
- {/* Errors Card */}
1415
- <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
1416
- <div className="flex items-center gap-2">
1417
- <div className="i-ph:warning-octagon text-purple-500 w-4 h-4" />
1418
- <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
1419
- </div>
1420
- <div className="flex items-center gap-2 mt-2">
1421
- <span
1422
- className={classNames('text-2xl font-semibold', errorLogs.length > 0 ? 'text-red-500' : 'text-green-500')}
1423
- >
1424
- {errorLogs.length}
1425
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1426
  </div>
1427
- <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1428
- <div
1429
- className={classNames(
1430
- 'w-3.5 h-3.5',
1431
- errorLogs.length > 0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500',
1432
- )}
1433
- />
1434
- {errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1435
  </div>
1436
  </div>
1437
  </div>
 
238
  performance: false,
239
  });
240
 
241
+ const { providers } = useSettings();
242
 
243
  // Subscribe to logStore updates
244
  const logs = useStore(logStore.logs);
 
1103
  // Add Ollama health check function
1104
  const checkOllamaStatus = useCallback(async () => {
1105
  try {
1106
+ const ollamaProvider = providers?.Ollama;
1107
+ const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434';
1108
+
1109
  // First check if service is running
1110
+ const versionResponse = await fetch(`${baseUrl}/api/version`);
1111
 
1112
  if (!versionResponse.ok) {
1113
  throw new Error('Service not running');
1114
  }
1115
 
1116
  // Then fetch installed models
1117
+ const modelsResponse = await fetch(`${baseUrl}/api/tags`);
1118
 
1119
  const modelsData = (await modelsResponse.json()) as {
1120
  models: Array<{ name: string; size: string; quantization: string }>;
 
1133
  models: undefined,
1134
  });
1135
  }
1136
+ }, [providers]);
1137
 
1138
+ // Monitor Ollama provider status and check periodically
1139
  useEffect(() => {
1140
+ const ollamaProvider = providers?.Ollama;
 
1141
 
1142
+ if (ollamaProvider?.settings?.enabled) {
1143
+ // Check immediately when provider is enabled
1144
+ checkOllamaStatus();
1145
 
1146
+ // Set up periodic checks every 10 seconds
1147
+ const intervalId = setInterval(checkOllamaStatus, 10000);
1148
+
1149
+ return () => clearInterval(intervalId);
1150
+ }
1151
+
1152
+ return undefined;
1153
+ }, [providers, checkOllamaStatus]);
1154
 
1155
  // Replace the existing export button with this new component
1156
  const ExportButton = () => {
 
1228
  const ollamaProvider = providers?.Ollama;
1229
  const isOllamaEnabled = ollamaProvider?.settings?.enabled;
1230
 
 
 
 
 
 
 
 
 
 
1231
  if (!isOllamaEnabled) {
1232
  return {
1233
  status: 'Disabled',
 
1270
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
1271
  {/* Quick Stats Banner */}
1272
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1273
+ {/* Errors Card */}
1274
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1275
  <div className="flex items-center gap-2">
1276
+ <div className="i-ph:warning-octagon text-purple-500 w-4 h-4" />
1277
+ <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
1278
  </div>
1279
  <div className="flex items-center gap-2 mt-2">
1280
+ <span
1281
+ className={classNames('text-2xl font-semibold', errorLogs.length > 0 ? 'text-red-500' : 'text-green-500')}
1282
+ >
1283
+ {errorLogs.length}
 
 
 
 
 
 
 
1284
  </span>
1285
  </div>
1286
  <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1287
  <div
1288
+ className={classNames(
1289
+ 'w-3.5 h-3.5',
1290
+ errorLogs.length > 0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500',
1291
+ )}
1292
  />
1293
+ {errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1294
  </div>
1295
  </div>
1296
 
1297
  {/* Memory Usage Card */}
1298
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1299
  <div className="flex items-center gap-2">
1300
  <div className="i-ph:cpu text-purple-500 w-4 h-4" />
1301
  <div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
 
1332
  </div>
1333
 
1334
  {/* Page Load Time Card */}
1335
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1336
  <div className="flex items-center gap-2">
1337
  <div className="i-ph:timer text-purple-500 w-4 h-4" />
1338
  <div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
 
1358
  </div>
1359
 
1360
  {/* Network Speed Card */}
1361
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1362
  <div className="flex items-center gap-2">
1363
  <div className="i-ph:wifi-high text-purple-500 w-4 h-4" />
1364
  <div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
 
1383
  </div>
1384
  </div>
1385
 
1386
+ {/* Ollama Service Card - Now spans all 4 columns */}
1387
+ <div className="md:col-span-4 p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[260px] flex flex-col">
1388
+ <div className="flex items-center justify-between">
1389
+ <div className="flex items-center gap-3">
1390
+ <div className="i-ph:robot text-purple-500 w-5 h-5" />
1391
+ <div>
1392
+ <div className="text-base font-medium text-bolt-elements-textPrimary">Ollama Service</div>
1393
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">{status.message}</div>
1394
+ </div>
1395
+ </div>
1396
+ <div className="flex items-center gap-3">
1397
+ <div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-bolt-elements-background-depth-3">
1398
+ <div
1399
+ className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
1400
+ 'shadow-lg shadow-green-500/20': status.status === 'Running',
1401
+ 'shadow-lg shadow-red-500/20': status.status === 'Not Running',
1402
+ })}
1403
+ />
1404
+ <span className={classNames('text-xs font-medium flex items-center gap-1', status.color)}>
1405
+ {status.status}
1406
+ </span>
1407
+ </div>
1408
+ <div className="text-[10px] text-bolt-elements-textTertiary flex items-center gap-1.5">
1409
+ <div className="i-ph:clock w-3 h-3" />
1410
+ {ollamaStatus.lastChecked.toLocaleTimeString()}
1411
+ </div>
1412
+ </div>
1413
  </div>
1414
+
1415
+ <div className="mt-6 flex-1 min-h-0 flex flex-col">
1416
+ {status.status === 'Running' && ollamaStatus.models && ollamaStatus.models.length > 0 ? (
1417
+ <>
1418
+ <div className="text-xs font-medium text-bolt-elements-textSecondary flex items-center justify-between mb-3">
1419
+ <div className="flex items-center gap-2">
1420
+ <div className="i-ph:cube-duotone w-4 h-4 text-purple-500" />
1421
+ <span>Installed Models</span>
1422
+ <Badge variant="secondary" className="ml-1">
1423
+ {ollamaStatus.models.length}
1424
+ </Badge>
1425
+ </div>
1426
+ </div>
1427
+ <div className="overflow-y-auto flex-1 scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-400 dark:hover:scrollbar-thumb-gray-600">
1428
+ <div className="grid grid-cols-2 gap-3 pr-2">
1429
+ {ollamaStatus.models.map((model) => (
1430
+ <div
1431
+ key={model.name}
1432
+ className="text-sm bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4 rounded-lg px-4 py-3 flex items-center justify-between transition-colors group"
1433
+ >
1434
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
1435
+ <div className="i-ph:cube w-4 h-4 text-purple-500/70 group-hover:text-purple-500 transition-colors" />
1436
+ <span className="font-mono truncate">{model.name}</span>
1437
+ </div>
1438
+ <Badge variant="outline" className="ml-2 text-xs font-mono">
1439
+ {Math.round(parseInt(model.size) / 1024 / 1024)}MB
1440
+ </Badge>
1441
+ </div>
1442
+ ))}
1443
+ </div>
1444
+ </div>
1445
+ </>
1446
+ ) : (
1447
+ <div className="flex-1 flex items-center justify-center">
1448
+ <div className="flex flex-col items-center gap-3 max-w-[280px] text-center">
1449
+ <div
1450
+ className={classNames('w-12 h-12', {
1451
+ 'i-ph:warning-circle text-red-500/80':
1452
+ status.status === 'Not Running' || status.status === 'Disabled',
1453
+ 'i-ph:cube-duotone text-purple-500/80': status.status === 'Running',
1454
+ })}
1455
+ />
1456
+ <span className="text-sm text-bolt-elements-textSecondary">{status.message}</span>
1457
+ </div>
1458
+ </div>
1459
+ )}
1460
  </div>
1461
  </div>
1462
  </div>
app/components/@settings/tabs/features/FeaturesTab.tsx CHANGED
@@ -123,23 +123,23 @@ export default function FeaturesTab() {
123
  React.useEffect(() => {
124
  // Only set defaults if values are undefined
125
  if (isLatestBranch === undefined) {
126
- enableLatestBranch(true);
127
  }
128
 
129
  if (contextOptimizationEnabled === undefined) {
130
- enableContextOptimization(true);
131
  }
132
 
133
  if (autoSelectTemplate === undefined) {
134
- setAutoSelectTemplate(true);
135
  }
136
 
137
  if (promptId === undefined) {
138
- setPromptId('optimized');
139
  }
140
 
141
  if (eventLogs === undefined) {
142
- setEventLogs(true);
143
  }
144
  }, []); // Only run once on component mount
145
 
 
123
  React.useEffect(() => {
124
  // Only set defaults if values are undefined
125
  if (isLatestBranch === undefined) {
126
+ enableLatestBranch(false); // Default: OFF - Don't auto-update from main branch
127
  }
128
 
129
  if (contextOptimizationEnabled === undefined) {
130
+ enableContextOptimization(true); // Default: ON - Enable context optimization
131
  }
132
 
133
  if (autoSelectTemplate === undefined) {
134
+ setAutoSelectTemplate(true); // Default: ON - Enable auto-select templates
135
  }
136
 
137
  if (promptId === undefined) {
138
+ setPromptId('default'); // Default: 'default'
139
  }
140
 
141
  if (eventLogs === undefined) {
142
+ setEventLogs(true); // Default: ON - Enable event logging
143
  }
144
  }, []); // Only run once on component mount
145
 
app/components/@settings/tabs/profile/ProfileTab.tsx CHANGED
@@ -1,13 +1,23 @@
1
- import { useState } from 'react';
2
  import { useStore } from '@nanostores/react';
3
  import { classNames } from '~/utils/classNames';
4
  import { profileStore, updateProfile } from '~/lib/stores/profile';
5
  import { toast } from 'react-toastify';
 
6
 
7
  export default function ProfileTab() {
8
  const profile = useStore(profileStore);
9
  const [isUploading, setIsUploading] = useState(false);
10
 
 
 
 
 
 
 
 
 
 
11
  const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
12
  const file = e.target.files?.[0];
13
 
@@ -42,14 +52,11 @@ export default function ProfileTab() {
42
  };
43
 
44
  const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
 
45
  updateProfile({ [field]: value });
46
 
47
- // Only show toast for completed typing (after 1 second of no typing)
48
- const debounceToast = setTimeout(() => {
49
- toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
50
- }, 1000);
51
-
52
- return () => clearTimeout(debounceToast);
53
  };
54
 
55
  return (
 
1
+ import { useState, useCallback } from 'react';
2
  import { useStore } from '@nanostores/react';
3
  import { classNames } from '~/utils/classNames';
4
  import { profileStore, updateProfile } from '~/lib/stores/profile';
5
  import { toast } from 'react-toastify';
6
+ import { debounce } from '~/utils/debounce';
7
 
8
  export default function ProfileTab() {
9
  const profile = useStore(profileStore);
10
  const [isUploading, setIsUploading] = useState(false);
11
 
12
+ // Create debounced update functions
13
+ const debouncedUpdate = useCallback(
14
+ debounce((field: 'username' | 'bio', value: string) => {
15
+ updateProfile({ [field]: value });
16
+ toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
17
+ }, 1000),
18
+ [],
19
+ );
20
+
21
  const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
22
  const file = e.target.files?.[0];
23
 
 
52
  };
53
 
54
  const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
55
+ // Update the store immediately for UI responsiveness
56
  updateProfile({ [field]: value });
57
 
58
+ // Debounce the toast notification
59
+ debouncedUpdate(field, value);
 
 
 
 
60
  };
61
 
62
  return (
app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx CHANGED
@@ -471,6 +471,60 @@ export default function LocalProvidersTab() {
471
  />
472
  </div>
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  {/* Ollama Models Section */}
475
  {provider.settings.enabled && (
476
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-6 space-y-4">
@@ -505,7 +559,19 @@ export default function LocalProvidersTab() {
505
  <div className="text-center py-8 text-bolt-elements-textSecondary">
506
  <div className="i-ph:cube-transparent text-4xl mx-auto mb-2" />
507
  <p>No models installed yet</p>
508
- <p className="text-sm">Install your first model below</p>
 
 
 
 
 
 
 
 
 
 
 
 
509
  </div>
510
  ) : (
511
  ollamaModels.map((model) => (
 
471
  />
472
  </div>
473
 
474
+ {/* URL Configuration Section */}
475
+ <AnimatePresence>
476
+ {provider.settings.enabled && (
477
+ <motion.div
478
+ initial={{ opacity: 0, height: 0 }}
479
+ animate={{ opacity: 1, height: 'auto' }}
480
+ exit={{ opacity: 0, height: 0 }}
481
+ className="mt-4"
482
+ >
483
+ <div className="flex flex-col gap-2">
484
+ <label className="text-sm text-bolt-elements-textSecondary">API Endpoint</label>
485
+ {editingProvider === provider.name ? (
486
+ <input
487
+ type="text"
488
+ defaultValue={provider.settings.baseUrl || OLLAMA_API_URL}
489
+ placeholder="Enter Ollama base URL"
490
+ className={classNames(
491
+ 'w-full px-3 py-2 rounded-lg text-sm',
492
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
493
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
494
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
495
+ 'transition-all duration-200',
496
+ )}
497
+ onKeyDown={(e) => {
498
+ if (e.key === 'Enter') {
499
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
500
+ } else if (e.key === 'Escape') {
501
+ setEditingProvider(null);
502
+ }
503
+ }}
504
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
505
+ autoFocus
506
+ />
507
+ ) : (
508
+ <div
509
+ onClick={() => setEditingProvider(provider.name)}
510
+ className={classNames(
511
+ 'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
512
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
513
+ 'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
514
+ 'transition-all duration-200',
515
+ )}
516
+ >
517
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
518
+ <div className="i-ph:link text-sm" />
519
+ <span>{provider.settings.baseUrl || OLLAMA_API_URL}</span>
520
+ </div>
521
+ </div>
522
+ )}
523
+ </div>
524
+ </motion.div>
525
+ )}
526
+ </AnimatePresence>
527
+
528
  {/* Ollama Models Section */}
529
  {provider.settings.enabled && (
530
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-6 space-y-4">
 
559
  <div className="text-center py-8 text-bolt-elements-textSecondary">
560
  <div className="i-ph:cube-transparent text-4xl mx-auto mb-2" />
561
  <p>No models installed yet</p>
562
+ <p className="text-sm text-bolt-elements-textTertiary px-1">
563
+ Browse models at{' '}
564
+ <a
565
+ href="https://ollama.com/library"
566
+ target="_blank"
567
+ rel="noopener noreferrer"
568
+ className="text-purple-500 hover:underline inline-flex items-center gap-0.5 text-base font-medium"
569
+ >
570
+ ollama.com/library
571
+ <div className="i-ph:arrow-square-out text-xs" />
572
+ </a>{' '}
573
+ and copy model names to install
574
+ </p>
575
  </div>
576
  ) : (
577
  ollamaModels.map((model) => (
app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx CHANGED
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
3
  import { classNames } from '~/utils/classNames';
4
  import { Progress } from '~/components/ui/Progress';
5
  import { useToast } from '~/components/ui/use-toast';
 
6
 
7
  interface OllamaModelInstallerProps {
8
  onModelInstalled: () => void;
@@ -141,11 +142,15 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
141
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
142
  const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
143
  const { toast } = useToast();
 
 
 
 
144
 
145
  // Function to check installed models and their versions
146
  const checkInstalledModels = async () => {
147
  try {
148
- const response = await fetch('http://127.0.0.1:11434/api/tags', {
149
  method: 'GET',
150
  });
151
 
@@ -181,7 +186,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
181
  // Check installed models on mount and after installation
182
  useEffect(() => {
183
  checkInstalledModels();
184
- }, []);
185
 
186
  const handleCheckUpdates = async () => {
187
  setIsChecking(true);
@@ -224,7 +229,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
224
  setModelString('');
225
  setSearchQuery('');
226
 
227
- const response = await fetch('http://127.0.0.1:11434/api/pull', {
228
  method: 'POST',
229
  headers: {
230
  'Content-Type': 'application/json',
@@ -302,7 +307,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
302
  try {
303
  setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
304
 
305
- const response = await fetch('http://127.0.0.1:11434/api/pull', {
306
  method: 'POST',
307
  headers: {
308
  'Content-Type': 'application/json',
@@ -429,16 +434,16 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
429
  }}
430
  disabled={isInstalling}
431
  />
432
- <p className="text-xs text-bolt-elements-textTertiary px-1">
433
  Browse models at{' '}
434
  <a
435
  href="https://ollama.com/library"
436
  target="_blank"
437
  rel="noopener noreferrer"
438
- className="text-purple-500 hover:underline inline-flex items-center gap-0.5"
439
  >
440
  ollama.com/library
441
- <div className="i-ph:arrow-square-out text-[10px]" />
442
  </a>{' '}
443
  and copy model names to install
444
  </p>
@@ -448,10 +453,11 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
448
  onClick={() => handleInstallModel(modelString)}
449
  disabled={!modelString || isInstalling}
450
  className={classNames(
451
- 'rounded-xl px-6 py-3',
452
- 'bg-purple-500 text-white',
453
  'hover:bg-purple-600',
454
  'transition-all duration-200',
 
455
  { 'opacity-50 cursor-not-allowed': !modelString || isInstalling },
456
  )}
457
  whileHover={{ scale: 1.02 }}
@@ -459,7 +465,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
459
  >
460
  {isInstalling ? (
461
  <div className="flex items-center gap-2">
462
- <div className="i-ph:spinner-gap-bold animate-spin" />
463
  <span>Installing...</span>
464
  </div>
465
  ) : (
 
3
  import { classNames } from '~/utils/classNames';
4
  import { Progress } from '~/components/ui/Progress';
5
  import { useToast } from '~/components/ui/use-toast';
6
+ import { useSettings } from '~/lib/hooks/useSettings';
7
 
8
  interface OllamaModelInstallerProps {
9
  onModelInstalled: () => void;
 
142
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
143
  const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
144
  const { toast } = useToast();
145
+ const { providers } = useSettings();
146
+
147
+ // Get base URL from provider settings
148
+ const baseUrl = providers?.Ollama?.settings?.baseUrl || 'http://127.0.0.1:11434';
149
 
150
  // Function to check installed models and their versions
151
  const checkInstalledModels = async () => {
152
  try {
153
+ const response = await fetch(`${baseUrl}/api/tags`, {
154
  method: 'GET',
155
  });
156
 
 
186
  // Check installed models on mount and after installation
187
  useEffect(() => {
188
  checkInstalledModels();
189
+ }, [baseUrl]);
190
 
191
  const handleCheckUpdates = async () => {
192
  setIsChecking(true);
 
229
  setModelString('');
230
  setSearchQuery('');
231
 
232
+ const response = await fetch(`${baseUrl}/api/pull`, {
233
  method: 'POST',
234
  headers: {
235
  'Content-Type': 'application/json',
 
307
  try {
308
  setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
309
 
310
+ const response = await fetch(`${baseUrl}/api/pull`, {
311
  method: 'POST',
312
  headers: {
313
  'Content-Type': 'application/json',
 
434
  }}
435
  disabled={isInstalling}
436
  />
437
+ <p className="text-sm text-bolt-elements-textSecondary px-1">
438
  Browse models at{' '}
439
  <a
440
  href="https://ollama.com/library"
441
  target="_blank"
442
  rel="noopener noreferrer"
443
+ className="text-purple-500 hover:underline inline-flex items-center gap-1 text-base font-medium"
444
  >
445
  ollama.com/library
446
+ <div className="i-ph:arrow-square-out text-sm" />
447
  </a>{' '}
448
  and copy model names to install
449
  </p>
 
453
  onClick={() => handleInstallModel(modelString)}
454
  disabled={!modelString || isInstalling}
455
  className={classNames(
456
+ 'rounded-lg px-4 py-2',
457
+ 'bg-purple-500 text-white text-sm',
458
  'hover:bg-purple-600',
459
  'transition-all duration-200',
460
+ 'flex items-center gap-2',
461
  { 'opacity-50 cursor-not-allowed': !modelString || isInstalling },
462
  )}
463
  whileHover={{ scale: 1.02 }}
 
465
  >
466
  {isInstalling ? (
467
  <div className="flex items-center gap-2">
468
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
469
  <span>Installing...</span>
470
  </div>
471
  ) : (
app/components/@settings/tabs/settings/SettingsTab.tsx CHANGED
@@ -3,21 +3,9 @@ import { motion } from 'framer-motion';
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
  import { Switch } from '~/components/ui/Switch';
6
- import { themeStore, kTheme } from '~/lib/stores/theme';
7
  import type { UserProfile } from '~/components/@settings/core/types';
8
- import { useStore } from '@nanostores/react';
9
- import { shortcutsStore } from '~/lib/stores/settings';
10
  import { isMac } from '~/utils/os';
11
 
12
- // Helper to format shortcut key display
13
- const formatShortcutKey = (key: string) => {
14
- if (key === '`') {
15
- return '`';
16
- }
17
-
18
- return key.toUpperCase();
19
- };
20
-
21
  // Helper to get modifier key symbols/text
22
  const getModifierSymbol = (modifier: string): string => {
23
  switch (modifier) {
@@ -25,8 +13,6 @@ const getModifierSymbol = (modifier: string): string => {
25
  return isMac ? '⌘' : 'Win';
26
  case 'alt':
27
  return isMac ? '⌥' : 'Alt';
28
- case 'ctrl':
29
- return isMac ? '⌃' : 'Ctrl';
30
  case 'shift':
31
  return '⇧';
32
  default:
@@ -41,7 +27,6 @@ export default function SettingsTab() {
41
  return saved
42
  ? JSON.parse(saved)
43
  : {
44
- theme: 'system',
45
  notifications: true,
46
  language: 'en',
47
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -52,22 +37,6 @@ export default function SettingsTab() {
52
  setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
53
  }, []);
54
 
55
- // Apply theme when settings changes
56
- useEffect(() => {
57
- if (settings.theme === 'system') {
58
- // Remove theme override
59
- localStorage.removeItem(kTheme);
60
-
61
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
62
- document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
63
- themeStore.set(prefersDark ? 'dark' : 'light');
64
- } else {
65
- themeStore.set(settings.theme);
66
- localStorage.setItem(kTheme, settings.theme);
67
- document.querySelector('html')?.setAttribute('data-theme', settings.theme);
68
- }
69
- }, [settings.theme]);
70
-
71
  // Save settings automatically when they change
72
  useEffect(() => {
73
  try {
@@ -77,7 +46,6 @@ export default function SettingsTab() {
77
  // Merge with new settings
78
  const updatedProfile = {
79
  ...existingProfile,
80
- theme: settings.theme,
81
  notifications: settings.notifications,
82
  language: settings.language,
83
  timezone: settings.timezone,
@@ -93,7 +61,7 @@ export default function SettingsTab() {
93
 
94
  return (
95
  <div className="space-y-4">
96
- {/* Theme & Language */}
97
  <motion.div
98
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
99
  initial={{ opacity: 0, y: 20 }}
@@ -102,45 +70,7 @@ export default function SettingsTab() {
102
  >
103
  <div className="flex items-center gap-2 mb-4">
104
  <div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
105
- <span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
106
- </div>
107
-
108
- <div>
109
- <div className="flex items-center gap-2 mb-2">
110
- <div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
111
- <label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
112
- </div>
113
- <div className="flex gap-2">
114
- {(['light', 'dark', 'system'] as const).map((theme) => (
115
- <button
116
- key={theme}
117
- onClick={() => {
118
- setSettings((prev) => ({ ...prev, theme }));
119
-
120
- if (theme !== 'system') {
121
- themeStore.set(theme);
122
- }
123
- }}
124
- className={classNames(
125
- 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
126
- settings.theme === theme
127
- ? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600'
128
- : 'bg-bolt-elements-hover dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-purple-500/10 hover:text-purple-500 dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
129
- )}
130
- >
131
- <div
132
- className={`w-4 h-4 ${
133
- theme === 'light'
134
- ? 'i-ph:sun-fill'
135
- : theme === 'dark'
136
- ? 'i-ph:moon-stars-fill'
137
- : 'i-ph:monitor-fill'
138
- }`}
139
- />
140
- <span className="capitalize">{theme}</span>
141
- </button>
142
- ))}
143
- </div>
144
  </div>
145
 
146
  <div>
@@ -245,7 +175,7 @@ export default function SettingsTab() {
245
  </div>
246
  </motion.div>
247
 
248
- {/* Keyboard Shortcuts */}
249
  <motion.div
250
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
251
  initial={{ opacity: 0, y: 20 }}
@@ -258,51 +188,26 @@ export default function SettingsTab() {
258
  </div>
259
 
260
  <div className="space-y-2">
261
- {Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
262
- <div
263
- key={name}
264
- className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
265
- >
266
- <div className="flex flex-col">
267
- <span className="text-sm text-bolt-elements-textPrimary capitalize">
268
- {name.replace(/([A-Z])/g, ' $1').toLowerCase()}
269
- </span>
270
- {shortcut.description && (
271
- <span className="text-xs text-bolt-elements-textSecondary">{shortcut.description}</span>
272
- )}
273
- </div>
274
- <div className="flex items-center gap-1">
275
- {shortcut.ctrlOrMetaKey && (
276
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
277
- {getModifierSymbol(isMac ? 'meta' : 'ctrl')}
278
- </kbd>
279
- )}
280
- {shortcut.ctrlKey && (
281
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
282
- {getModifierSymbol('ctrl')}
283
- </kbd>
284
- )}
285
- {shortcut.metaKey && (
286
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
287
- {getModifierSymbol('meta')}
288
- </kbd>
289
- )}
290
- {shortcut.altKey && (
291
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
292
- {getModifierSymbol('alt')}
293
- </kbd>
294
- )}
295
- {shortcut.shiftKey && (
296
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
297
- {getModifierSymbol('shift')}
298
- </kbd>
299
- )}
300
- <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
301
- {formatShortcutKey(shortcut.key)}
302
- </kbd>
303
- </div>
304
  </div>
305
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  </div>
307
  </motion.div>
308
  </div>
 
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
  import { Switch } from '~/components/ui/Switch';
 
6
  import type { UserProfile } from '~/components/@settings/core/types';
 
 
7
  import { isMac } from '~/utils/os';
8
 
 
 
 
 
 
 
 
 
 
9
  // Helper to get modifier key symbols/text
10
  const getModifierSymbol = (modifier: string): string => {
11
  switch (modifier) {
 
13
  return isMac ? '⌘' : 'Win';
14
  case 'alt':
15
  return isMac ? '⌥' : 'Alt';
 
 
16
  case 'shift':
17
  return '⇧';
18
  default:
 
27
  return saved
28
  ? JSON.parse(saved)
29
  : {
 
30
  notifications: true,
31
  language: 'en',
32
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
 
37
  setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
38
  }, []);
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  // Save settings automatically when they change
41
  useEffect(() => {
42
  try {
 
46
  // Merge with new settings
47
  const updatedProfile = {
48
  ...existingProfile,
 
49
  notifications: settings.notifications,
50
  language: settings.language,
51
  timezone: settings.timezone,
 
61
 
62
  return (
63
  <div className="space-y-4">
64
+ {/* Language & Notifications */}
65
  <motion.div
66
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
67
  initial={{ opacity: 0, y: 20 }}
 
70
  >
71
  <div className="flex items-center gap-2 mb-4">
72
  <div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
73
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Preferences</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
 
76
  <div>
 
175
  </div>
176
  </motion.div>
177
 
178
+ {/* Simplified Keyboard Shortcuts */}
179
  <motion.div
180
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
181
  initial={{ opacity: 0, y: 20 }}
 
188
  </div>
189
 
190
  <div className="space-y-2">
191
+ <div className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A]">
192
+ <div className="flex flex-col">
193
+ <span className="text-sm text-bolt-elements-textPrimary">Toggle Theme</span>
194
+ <span className="text-xs text-bolt-elements-textSecondary">Switch between light and dark mode</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
+ <div className="flex items-center gap-1">
197
+ <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
198
+ {getModifierSymbol('meta')}
199
+ </kbd>
200
+ <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
201
+ {getModifierSymbol('alt')}
202
+ </kbd>
203
+ <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
204
+ {getModifierSymbol('shift')}
205
+ </kbd>
206
+ <kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
207
+ D
208
+ </kbd>
209
+ </div>
210
+ </div>
211
  </div>
212
  </motion.div>
213
  </div>
app/components/chat/BaseChat.tsx CHANGED
@@ -35,6 +35,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
35
  import ProgressCompilation from './ProgressCompilation';
36
  import type { ProgressAnnotation } from '~/types/context';
37
  import type { ActionRunner } from '~/lib/runtime/action-runner';
 
38
 
39
  const TEXTAREA_MIN_HEIGHT = 76;
40
 
@@ -406,7 +407,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
406
  apiKeys={apiKeys}
407
  modelLoading={isModelLoading}
408
  />
409
- {(providerList || []).length > 0 && provider && (
410
  <APIKeyManager
411
  provider={provider}
412
  apiKey={apiKeys[provider.name] || ''}
 
35
  import ProgressCompilation from './ProgressCompilation';
36
  import type { ProgressAnnotation } from '~/types/context';
37
  import type { ActionRunner } from '~/lib/runtime/action-runner';
38
+ import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
39
 
40
  const TEXTAREA_MIN_HEIGHT = 76;
41
 
 
407
  apiKeys={apiKeys}
408
  modelLoading={isModelLoading}
409
  />
410
+ {(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && (
411
  <APIKeyManager
412
  provider={provider}
413
  apiKey={apiKeys[provider.name] || ''}
app/components/chat/ModelSelector.tsx CHANGED
@@ -1,6 +1,9 @@
1
  import type { ProviderInfo } from '~/types/model';
2
- import { useEffect } from 'react';
 
3
  import type { ModelInfo } from '~/lib/modules/llm/types';
 
 
4
 
5
  interface ModelSelectorProps {
6
  model?: string;
@@ -22,12 +25,118 @@ export const ModelSelector = ({
22
  providerList,
23
  modelLoading,
24
  }: ModelSelectorProps) => {
25
- // Load enabled providers from cookies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  // Update enabled providers when cookies change
28
  useEffect(() => {
29
  // If current provider is disabled, switch to first enabled provider
30
- if (providerList.length == 0) {
31
  return;
32
  }
33
 
@@ -80,27 +189,124 @@ export const ModelSelector = ({
80
  </option>
81
  ))}
82
  </select>
83
- <select
84
- key={provider?.name}
85
- value={model}
86
- onChange={(e) => setModel?.(e.target.value)}
87
- 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 lg:max-w-[70%]"
88
- disabled={modelLoading === 'all' || modelLoading === provider?.name}
89
- >
90
- {modelLoading == 'all' || modelLoading == provider?.name ? (
91
- <option key={0} value="">
92
- Loading...
93
- </option>
94
- ) : (
95
- [...modelList]
96
- .filter((e) => e.provider == provider?.name && e.name)
97
- .map((modelOption, index) => (
98
- <option key={index} value={modelOption.name}>
99
- {modelOption.label}
100
- </option>
101
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  )}
103
- </select>
104
  </div>
105
  );
106
  };
 
1
  import type { ProviderInfo } from '~/types/model';
2
+ import { useEffect, useState, useRef } from 'react';
3
+ import type { KeyboardEvent } from 'react';
4
  import type { ModelInfo } from '~/lib/modules/llm/types';
5
+ import { classNames } from '~/utils/classNames';
6
+ import * as React from 'react';
7
 
8
  interface ModelSelectorProps {
9
  model?: string;
 
25
  providerList,
26
  modelLoading,
27
  }: ModelSelectorProps) => {
28
+ const [modelSearchQuery, setModelSearchQuery] = useState('');
29
+ const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
30
+ const [focusedIndex, setFocusedIndex] = useState(-1);
31
+ const searchInputRef = useRef<HTMLInputElement>(null);
32
+ const optionsRef = useRef<(HTMLDivElement | null)[]>([]);
33
+ const dropdownRef = useRef<HTMLDivElement>(null);
34
+
35
+ useEffect(() => {
36
+ const handleClickOutside = (event: MouseEvent) => {
37
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
38
+ setIsModelDropdownOpen(false);
39
+ setModelSearchQuery('');
40
+ }
41
+ };
42
+
43
+ document.addEventListener('mousedown', handleClickOutside);
44
+
45
+ return () => document.removeEventListener('mousedown', handleClickOutside);
46
+ }, []);
47
+
48
+ // Filter models based on search query
49
+ const filteredModels = [...modelList]
50
+ .filter((e) => e.provider === provider?.name && e.name)
51
+ .filter(
52
+ (model) =>
53
+ model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
54
+ model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()),
55
+ );
56
+
57
+ // Reset focused index when search query changes or dropdown opens/closes
58
+ useEffect(() => {
59
+ setFocusedIndex(-1);
60
+ }, [modelSearchQuery, isModelDropdownOpen]);
61
+
62
+ // Focus search input when dropdown opens
63
+ useEffect(() => {
64
+ if (isModelDropdownOpen && searchInputRef.current) {
65
+ searchInputRef.current.focus();
66
+ }
67
+ }, [isModelDropdownOpen]);
68
+
69
+ // Handle keyboard navigation
70
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
71
+ if (!isModelDropdownOpen) {
72
+ return;
73
+ }
74
+
75
+ switch (e.key) {
76
+ case 'ArrowDown':
77
+ e.preventDefault();
78
+ setFocusedIndex((prev) => {
79
+ const next = prev + 1;
80
+
81
+ if (next >= filteredModels.length) {
82
+ return 0;
83
+ }
84
+
85
+ return next;
86
+ });
87
+ break;
88
+
89
+ case 'ArrowUp':
90
+ e.preventDefault();
91
+ setFocusedIndex((prev) => {
92
+ const next = prev - 1;
93
+
94
+ if (next < 0) {
95
+ return filteredModels.length - 1;
96
+ }
97
+
98
+ return next;
99
+ });
100
+ break;
101
+
102
+ case 'Enter':
103
+ e.preventDefault();
104
+
105
+ if (focusedIndex >= 0 && focusedIndex < filteredModels.length) {
106
+ const selectedModel = filteredModels[focusedIndex];
107
+ setModel?.(selectedModel.name);
108
+ setIsModelDropdownOpen(false);
109
+ setModelSearchQuery('');
110
+ }
111
+
112
+ break;
113
+
114
+ case 'Escape':
115
+ e.preventDefault();
116
+ setIsModelDropdownOpen(false);
117
+ setModelSearchQuery('');
118
+ break;
119
+
120
+ case 'Tab':
121
+ if (!e.shiftKey && focusedIndex === filteredModels.length - 1) {
122
+ setIsModelDropdownOpen(false);
123
+ }
124
+
125
+ break;
126
+ }
127
+ };
128
+
129
+ // Focus the selected option
130
+ useEffect(() => {
131
+ if (focusedIndex >= 0 && optionsRef.current[focusedIndex]) {
132
+ optionsRef.current[focusedIndex]?.scrollIntoView({ block: 'nearest' });
133
+ }
134
+ }, [focusedIndex]);
135
 
136
  // Update enabled providers when cookies change
137
  useEffect(() => {
138
  // If current provider is disabled, switch to first enabled provider
139
+ if (providerList.length === 0) {
140
  return;
141
  }
142
 
 
189
  </option>
190
  ))}
191
  </select>
192
+
193
+ <div className="relative flex-1 lg:max-w-[70%]" onKeyDown={handleKeyDown} ref={dropdownRef}>
194
+ <div
195
+ className={classNames(
196
+ 'w-full p-2 rounded-lg border border-bolt-elements-borderColor',
197
+ 'bg-bolt-elements-prompt-background text-bolt-elements-textPrimary',
198
+ 'focus-within:outline-none focus-within:ring-2 focus-within:ring-bolt-elements-focus',
199
+ 'transition-all cursor-pointer',
200
+ isModelDropdownOpen ? 'ring-2 ring-bolt-elements-focus' : undefined,
201
+ )}
202
+ onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}
203
+ onKeyDown={(e) => {
204
+ if (e.key === 'Enter' || e.key === ' ') {
205
+ e.preventDefault();
206
+ setIsModelDropdownOpen(!isModelDropdownOpen);
207
+ }
208
+ }}
209
+ role="combobox"
210
+ aria-expanded={isModelDropdownOpen}
211
+ aria-controls="model-listbox"
212
+ aria-haspopup="listbox"
213
+ tabIndex={0}
214
+ >
215
+ <div className="flex items-center justify-between">
216
+ <div className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</div>
217
+ <div
218
+ className={classNames(
219
+ 'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75',
220
+ isModelDropdownOpen ? 'rotate-180' : undefined,
221
+ )}
222
+ />
223
+ </div>
224
+ </div>
225
+
226
+ {isModelDropdownOpen && (
227
+ <div
228
+ className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 shadow-lg"
229
+ role="listbox"
230
+ id="model-listbox"
231
+ >
232
+ <div className="px-2 pb-2">
233
+ <div className="relative">
234
+ <input
235
+ ref={searchInputRef}
236
+ type="text"
237
+ value={modelSearchQuery}
238
+ onChange={(e) => setModelSearchQuery(e.target.value)}
239
+ placeholder="Search models..."
240
+ className={classNames(
241
+ 'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
242
+ 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
243
+ 'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
244
+ 'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
245
+ 'transition-all',
246
+ )}
247
+ onClick={(e) => e.stopPropagation()}
248
+ role="searchbox"
249
+ aria-label="Search models"
250
+ />
251
+ <div className="absolute left-2.5 top-1/2 -translate-y-1/2">
252
+ <span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <div
258
+ className={classNames(
259
+ 'max-h-60 overflow-y-auto',
260
+ 'sm:scrollbar-none',
261
+ '[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2',
262
+ '[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor',
263
+ '[&::-webkit-scrollbar-thumb]:hover:bg-bolt-elements-borderColorHover',
264
+ '[&::-webkit-scrollbar-thumb]:rounded-full',
265
+ '[&::-webkit-scrollbar-track]:bg-bolt-elements-background-depth-2',
266
+ '[&::-webkit-scrollbar-track]:rounded-full',
267
+ 'sm:[&::-webkit-scrollbar]:w-1.5 sm:[&::-webkit-scrollbar]:h-1.5',
268
+ 'sm:hover:[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor/50',
269
+ 'sm:hover:[&::-webkit-scrollbar-thumb:hover]:bg-bolt-elements-borderColor',
270
+ 'sm:[&::-webkit-scrollbar-track]:bg-transparent',
271
+ )}
272
+ >
273
+ {modelLoading === 'all' || modelLoading === provider?.name ? (
274
+ <div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>
275
+ ) : filteredModels.length === 0 ? (
276
+ <div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No models found</div>
277
+ ) : (
278
+ filteredModels.map((modelOption, index) => (
279
+ <div
280
+ ref={(el) => (optionsRef.current[index] = el)}
281
+ key={index}
282
+ role="option"
283
+ aria-selected={model === modelOption.name}
284
+ className={classNames(
285
+ 'px-3 py-2 text-sm cursor-pointer',
286
+ 'hover:bg-bolt-elements-background-depth-3',
287
+ 'text-bolt-elements-textPrimary',
288
+ 'outline-none',
289
+ model === modelOption.name || focusedIndex === index
290
+ ? 'bg-bolt-elements-background-depth-2'
291
+ : undefined,
292
+ focusedIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined,
293
+ )}
294
+ onClick={(e) => {
295
+ e.stopPropagation();
296
+ setModel?.(modelOption.name);
297
+ setIsModelDropdownOpen(false);
298
+ setModelSearchQuery('');
299
+ }}
300
+ tabIndex={focusedIndex === index ? 0 : -1}
301
+ >
302
+ {modelOption.label}
303
+ </div>
304
+ ))
305
+ )}
306
+ </div>
307
+ </div>
308
  )}
309
+ </div>
310
  </div>
311
  );
312
  };
app/lib/hooks/useSettings.ts CHANGED
@@ -2,8 +2,6 @@ import { useStore } from '@nanostores/react';
2
  import {
3
  isDebugMode,
4
  isEventLogsEnabled,
5
- isLocalModelsEnabled,
6
- LOCAL_PROVIDERS,
7
  promptStore,
8
  providersStore,
9
  latestBranchStore,
@@ -17,7 +15,6 @@ import {
17
  updateAutoSelectTemplate,
18
  updateContextOptimization,
19
  updateEventLogs,
20
- updateLocalModels,
21
  updatePromptId,
22
  } from '~/lib/stores/settings';
23
  import { useCallback, useEffect, useState } from 'react';
@@ -49,8 +46,6 @@ export interface UseSettingsReturn {
49
  providers: Record<string, IProviderConfig>;
50
  activeProviders: ProviderInfo[];
51
  updateProviderSettings: (provider: string, config: IProviderSetting) => void;
52
- isLocalModel: boolean;
53
- enableLocalModels: (enabled: boolean) => void;
54
 
55
  // Debug and development settings
56
  debug: boolean;
@@ -81,7 +76,6 @@ export function useSettings(): UseSettingsReturn {
81
  const debug = useStore(isDebugMode);
82
  const eventLogs = useStore(isEventLogsEnabled);
83
  const promptId = useStore(promptStore);
84
- const isLocalModel = useStore(isLocalModelsEnabled);
85
  const isLatestBranch = useStore(latestBranchStore);
86
  const autoSelectTemplate = useStore(autoSelectStarterTemplate);
87
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
@@ -100,16 +94,12 @@ export function useSettings(): UseSettingsReturn {
100
  });
101
 
102
  useEffect(() => {
103
- let active = Object.entries(providers)
104
  .filter(([_key, provider]) => provider.settings.enabled)
105
  .map(([_k, p]) => p);
106
 
107
- if (!isLocalModel) {
108
- active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name));
109
- }
110
-
111
  setActiveProviders(active);
112
- }, [providers, isLocalModel]);
113
 
114
  const saveSettings = useCallback((newSettings: Partial<Settings>) => {
115
  setSettings((prev) => {
@@ -135,11 +125,6 @@ export function useSettings(): UseSettingsReturn {
135
  logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
136
  }, []);
137
 
138
- const enableLocalModels = useCallback((enabled: boolean) => {
139
- updateLocalModels(enabled);
140
- logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
141
- }, []);
142
-
143
  const setPromptId = useCallback((id: string) => {
144
  updatePromptId(id);
145
  logStore.logSystem(`Prompt template updated to ${id}`);
@@ -188,14 +173,11 @@ export function useSettings(): UseSettingsReturn {
188
  [saveSettings],
189
  );
190
 
191
- // Fix the providers cookie sync
192
  useEffect(() => {
193
  const providers = providersStore.get();
194
- const providerSetting: Record<string, { enabled: boolean }> = {};
195
  Object.keys(providers).forEach((provider) => {
196
- providerSetting[provider] = {
197
- enabled: providers[provider].settings.enabled || false, // Add fallback for undefined
198
- };
199
  });
200
  Cookies.set('providers', JSON.stringify(providerSetting));
201
  }, [providers]);
@@ -205,8 +187,6 @@ export function useSettings(): UseSettingsReturn {
205
  providers,
206
  activeProviders,
207
  updateProviderSettings,
208
- isLocalModel,
209
- enableLocalModels,
210
  debug,
211
  enableDebugMode,
212
  eventLogs,
 
2
  import {
3
  isDebugMode,
4
  isEventLogsEnabled,
 
 
5
  promptStore,
6
  providersStore,
7
  latestBranchStore,
 
15
  updateAutoSelectTemplate,
16
  updateContextOptimization,
17
  updateEventLogs,
 
18
  updatePromptId,
19
  } from '~/lib/stores/settings';
20
  import { useCallback, useEffect, useState } from 'react';
 
46
  providers: Record<string, IProviderConfig>;
47
  activeProviders: ProviderInfo[];
48
  updateProviderSettings: (provider: string, config: IProviderSetting) => void;
 
 
49
 
50
  // Debug and development settings
51
  debug: boolean;
 
76
  const debug = useStore(isDebugMode);
77
  const eventLogs = useStore(isEventLogsEnabled);
78
  const promptId = useStore(promptStore);
 
79
  const isLatestBranch = useStore(latestBranchStore);
80
  const autoSelectTemplate = useStore(autoSelectStarterTemplate);
81
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
 
94
  });
95
 
96
  useEffect(() => {
97
+ const active = Object.entries(providers)
98
  .filter(([_key, provider]) => provider.settings.enabled)
99
  .map(([_k, p]) => p);
100
 
 
 
 
 
101
  setActiveProviders(active);
102
+ }, [providers]);
103
 
104
  const saveSettings = useCallback((newSettings: Partial<Settings>) => {
105
  setSettings((prev) => {
 
125
  logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
126
  }, []);
127
 
 
 
 
 
 
128
  const setPromptId = useCallback((id: string) => {
129
  updatePromptId(id);
130
  logStore.logSystem(`Prompt template updated to ${id}`);
 
173
  [saveSettings],
174
  );
175
 
 
176
  useEffect(() => {
177
  const providers = providersStore.get();
178
+ const providerSetting: Record<string, IProviderSetting> = {}; // preserve the entire settings object for each provider
179
  Object.keys(providers).forEach((provider) => {
180
+ providerSetting[provider] = providers[provider].settings;
 
 
181
  });
182
  Cookies.set('providers', JSON.stringify(providerSetting));
183
  }, [providers]);
 
187
  providers,
188
  activeProviders,
189
  updateProviderSettings,
 
 
190
  debug,
191
  enableDebugMode,
192
  eventLogs,
app/lib/modules/llm/providers/amazon-bedrock.ts CHANGED
@@ -20,6 +20,12 @@ export default class AmazonBedrockProvider extends BaseProvider {
20
  };
21
 
22
  staticModels: ModelInfo[] = [
 
 
 
 
 
 
23
  {
24
  name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
25
  label: 'Claude 3.5 Sonnet (Bedrock)',
 
20
  };
21
 
22
  staticModels: ModelInfo[] = [
23
+ {
24
+ name: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
25
+ label: 'Claude 3.5 Sonnet v2 (Bedrock)',
26
+ provider: 'AmazonBedrock',
27
+ maxTokenAllowed: 200000,
28
+ },
29
  {
30
  name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
31
  label: 'Claude 3.5 Sonnet (Bedrock)',
app/lib/stores/settings.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { atom, map } from 'nanostores';
2
- import { workbenchStore } from './workbench';
3
  import { PROVIDER_LIST } from '~/utils/constants';
4
  import type { IProviderConfig } from '~/types/model';
5
  import type {
@@ -11,7 +10,7 @@ import type {
11
  import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
12
  import Cookies from 'js-cookie';
13
  import { toggleTheme } from './theme';
14
- import { chatStore } from './chat';
15
 
16
  export interface Shortcut {
17
  key: string;
@@ -26,10 +25,8 @@ export interface Shortcut {
26
  }
27
 
28
  export interface Shortcuts {
29
- toggleTerminal: Shortcut;
30
  toggleTheme: Shortcut;
31
- toggleChat: Shortcut;
32
- toggleSettings: Shortcut;
33
  }
34
 
35
  export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
@@ -37,15 +34,8 @@ export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama'];
37
 
38
  export type ProviderSetting = Record<string, IProviderConfig>;
39
 
40
- // Define safer shortcuts that don't conflict with browser defaults
41
  export const shortcutsStore = map<Shortcuts>({
42
- toggleTerminal: {
43
- key: '`',
44
- ctrlOrMetaKey: true,
45
- action: () => workbenchStore.toggleTerminal(),
46
- description: 'Toggle terminal',
47
- isPreventDefault: true,
48
- },
49
  toggleTheme: {
50
  key: 'd',
51
  metaKey: true,
@@ -55,22 +45,13 @@ export const shortcutsStore = map<Shortcuts>({
55
  description: 'Toggle theme',
56
  isPreventDefault: true,
57
  },
58
- toggleChat: {
59
- key: 'j', // Changed from 'k' to 'j' to avoid conflicts
60
- ctrlOrMetaKey: true,
61
- altKey: true, // Added alt key to make it more unique
62
- action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
63
- description: 'Toggle chat',
64
- isPreventDefault: true,
65
- },
66
- toggleSettings: {
67
- key: 's',
68
  ctrlOrMetaKey: true,
69
- altKey: true,
70
  action: () => {
71
- document.dispatchEvent(new CustomEvent('toggle-settings'));
72
  },
73
- description: 'Toggle settings',
74
  isPreventDefault: true,
75
  },
76
  });
@@ -148,7 +129,6 @@ const SETTINGS_KEYS = {
148
  AUTO_SELECT_TEMPLATE: 'autoSelectTemplate',
149
  CONTEXT_OPTIMIZATION: 'contextOptimizationEnabled',
150
  EVENT_LOGS: 'isEventLogsEnabled',
151
- LOCAL_MODELS: 'isLocalModelsEnabled',
152
  PROMPT_ID: 'promptId',
153
  DEVELOPER_MODE: 'isDeveloperMode',
154
  } as const;
@@ -175,10 +155,9 @@ const getInitialSettings = () => {
175
 
176
  return {
177
  latestBranch: getStoredBoolean(SETTINGS_KEYS.LATEST_BRANCH, false),
178
- autoSelectTemplate: getStoredBoolean(SETTINGS_KEYS.AUTO_SELECT_TEMPLATE, false),
179
- contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
180
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
181
- localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
182
  promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
183
  developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
184
  };
@@ -191,7 +170,6 @@ export const latestBranchStore = atom<boolean>(initialSettings.latestBranch);
191
  export const autoSelectStarterTemplate = atom<boolean>(initialSettings.autoSelectTemplate);
192
  export const enableContextOptimizationStore = atom<boolean>(initialSettings.contextOptimization);
193
  export const isEventLogsEnabled = atom<boolean>(initialSettings.eventLogs);
194
- export const isLocalModelsEnabled = atom<boolean>(initialSettings.localModels);
195
  export const promptStore = atom<string>(initialSettings.promptId);
196
 
197
  // Helper functions to update settings with persistence
@@ -215,11 +193,6 @@ export const updateEventLogs = (enabled: boolean) => {
215
  localStorage.setItem(SETTINGS_KEYS.EVENT_LOGS, JSON.stringify(enabled));
216
  };
217
 
218
- export const updateLocalModels = (enabled: boolean) => {
219
- isLocalModelsEnabled.set(enabled);
220
- localStorage.setItem(SETTINGS_KEYS.LOCAL_MODELS, JSON.stringify(enabled));
221
- };
222
-
223
  export const updatePromptId = (id: string) => {
224
  promptStore.set(id);
225
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
@@ -319,3 +292,35 @@ export const setDeveloperMode = (value: boolean) => {
319
  localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
320
  }
321
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { atom, map } from 'nanostores';
 
2
  import { PROVIDER_LIST } from '~/utils/constants';
3
  import type { IProviderConfig } from '~/types/model';
4
  import type {
 
10
  import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
11
  import Cookies from 'js-cookie';
12
  import { toggleTheme } from './theme';
13
+ import { create } from 'zustand';
14
 
15
  export interface Shortcut {
16
  key: string;
 
25
  }
26
 
27
  export interface Shortcuts {
 
28
  toggleTheme: Shortcut;
29
+ toggleTerminal: Shortcut;
 
30
  }
31
 
32
  export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
 
34
 
35
  export type ProviderSetting = Record<string, IProviderConfig>;
36
 
37
+ // Simplified shortcuts store with only theme toggle
38
  export const shortcutsStore = map<Shortcuts>({
 
 
 
 
 
 
 
39
  toggleTheme: {
40
  key: 'd',
41
  metaKey: true,
 
45
  description: 'Toggle theme',
46
  isPreventDefault: true,
47
  },
48
+ toggleTerminal: {
49
+ key: '`',
 
 
 
 
 
 
 
 
50
  ctrlOrMetaKey: true,
 
51
  action: () => {
52
+ // This will be handled by the terminal component
53
  },
54
+ description: 'Toggle terminal',
55
  isPreventDefault: true,
56
  },
57
  });
 
129
  AUTO_SELECT_TEMPLATE: 'autoSelectTemplate',
130
  CONTEXT_OPTIMIZATION: 'contextOptimizationEnabled',
131
  EVENT_LOGS: 'isEventLogsEnabled',
 
132
  PROMPT_ID: 'promptId',
133
  DEVELOPER_MODE: 'isDeveloperMode',
134
  } as const;
 
155
 
156
  return {
157
  latestBranch: getStoredBoolean(SETTINGS_KEYS.LATEST_BRANCH, false),
158
+ autoSelectTemplate: getStoredBoolean(SETTINGS_KEYS.AUTO_SELECT_TEMPLATE, true),
159
+ contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, true),
160
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
 
161
  promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
162
  developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
163
  };
 
170
  export const autoSelectStarterTemplate = atom<boolean>(initialSettings.autoSelectTemplate);
171
  export const enableContextOptimizationStore = atom<boolean>(initialSettings.contextOptimization);
172
  export const isEventLogsEnabled = atom<boolean>(initialSettings.eventLogs);
 
173
  export const promptStore = atom<string>(initialSettings.promptId);
174
 
175
  // Helper functions to update settings with persistence
 
193
  localStorage.setItem(SETTINGS_KEYS.EVENT_LOGS, JSON.stringify(enabled));
194
  };
195
 
 
 
 
 
 
196
  export const updatePromptId = (id: string) => {
197
  promptStore.set(id);
198
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
 
292
  localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
293
  }
294
  };
295
+
296
+ // First, let's define the SettingsStore interface
297
+ interface SettingsStore {
298
+ isOpen: boolean;
299
+ selectedTab: string;
300
+ openSettings: () => void;
301
+ closeSettings: () => void;
302
+ setSelectedTab: (tab: string) => void;
303
+ }
304
+
305
+ export const useSettingsStore = create<SettingsStore>((set) => ({
306
+ isOpen: false,
307
+ selectedTab: 'user', // Default tab
308
+
309
+ openSettings: () => {
310
+ set({
311
+ isOpen: true,
312
+ selectedTab: 'user', // Always open to user tab
313
+ });
314
+ },
315
+
316
+ closeSettings: () => {
317
+ set({
318
+ isOpen: false,
319
+ selectedTab: 'user', // Reset to user tab when closing
320
+ });
321
+ },
322
+
323
+ setSelectedTab: (tab: string) => {
324
+ set({ selectedTab: tab });
325
+ },
326
+ }));
app/utils/debounce.ts CHANGED
@@ -1,17 +1,13 @@
1
- export function debounce<Args extends any[]>(fn: (...args: Args) => void, delay = 100) {
2
- if (delay === 0) {
3
- return fn;
4
- }
5
 
6
- let timer: number | undefined;
 
 
 
 
7
 
8
- return function <U>(this: U, ...args: Args) {
9
- const context = this;
10
-
11
- clearTimeout(timer);
12
-
13
- timer = window.setTimeout(() => {
14
- fn.apply(context, args);
15
- }, delay);
16
  };
17
  }
 
1
+ export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
2
+ let timeout: NodeJS.Timeout;
 
 
3
 
4
+ return function executedFunction(...args: Parameters<T>) {
5
+ const later = () => {
6
+ clearTimeout(timeout);
7
+ func(...args);
8
+ };
9
 
10
+ clearTimeout(timeout);
11
+ timeout = setTimeout(later, wait);
 
 
 
 
 
 
12
  };
13
  }