Stijnus commited on
Commit
78d4e1b
·
1 Parent(s): 436a8e5
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
  import { logStore } from '~/lib/stores/logs';
 
5
 
6
  interface ProviderStatus {
7
  id: string;
@@ -472,51 +473,147 @@ export default function DebugTab() {
472
  }
473
  };
474
 
475
- const handleCheckErrors = () => {
476
  try {
477
  setLoading((prev) => ({ ...prev, errors: true }));
478
 
479
- // Get any errors from the performance entries
480
- const resourceErrors = performance
481
- .getEntriesByType('resource')
482
- .filter((entry) => {
483
- const failedEntry = entry as PerformanceResourceTiming;
484
- return failedEntry.responseEnd - failedEntry.startTime === 0;
485
- })
486
- .map((entry) => ({
487
- type: 'networkError',
488
- resource: entry.name,
489
- timestamp: new Date().toISOString(),
490
- }));
491
-
492
- // Combine collected errors with resource errors
493
- const allErrors = [...errorLog.errors, ...resourceErrors];
494
-
495
- if (allErrors.length > 0) {
496
- logStore.logError('JavaScript Errors Found', {
497
- errors: allErrors,
498
- timestamp: new Date().toISOString(),
499
- });
500
- toast.error(`Found ${allErrors.length} error(s)`);
501
- } else {
502
- toast.success('No errors found');
503
- }
504
 
505
- // Update error log
506
  setErrorLog({
507
  errors: allErrors,
508
  lastCheck: new Date().toISOString(),
509
  });
 
 
 
 
 
 
510
  } catch (error) {
511
- toast.error('Failed to check for errors');
512
- console.error('Failed to check for errors:', error);
513
  } finally {
514
  setLoading((prev) => ({ ...prev, errors: false }));
515
  }
516
  };
517
 
518
  return (
519
- <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  {/* System Information */}
521
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
522
  <div className="flex items-center justify-between mb-4">
@@ -529,9 +626,9 @@ export default function DebugTab() {
529
  onClick={handleLogSystemInfo}
530
  className={classNames(
531
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
532
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
533
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
534
- 'transition-colors',
535
  )}
536
  >
537
  <div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
@@ -541,10 +638,12 @@ export default function DebugTab() {
541
  onClick={getSystemInfo}
542
  className={classNames(
543
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
544
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
545
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
546
- 'transition-colors',
 
547
  )}
 
548
  >
549
  <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
550
  Refresh
@@ -684,10 +783,12 @@ export default function DebugTab() {
684
  onClick={checkProviderStatus}
685
  className={classNames(
686
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
687
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
688
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
689
- 'transition-colors',
 
690
  )}
 
691
  >
692
  <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
693
  Refresh
@@ -729,10 +830,12 @@ export default function DebugTab() {
729
  onClick={handleLogPerformance}
730
  className={classNames(
731
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
732
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
733
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
734
- 'transition-colors',
 
735
  )}
 
736
  >
737
  <div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
738
  Log Performance
@@ -811,13 +914,15 @@ export default function DebugTab() {
811
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
812
  </div>
813
  <button
814
- onClick={handleCheckErrors}
815
  className={classNames(
816
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
817
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
818
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
819
- 'transition-colors',
 
820
  )}
 
821
  >
822
  <div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
823
  Check for Errors
 
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
  import { logStore } from '~/lib/stores/logs';
5
+ import type { LogEntry } from '~/lib/stores/logs';
6
 
7
  interface ProviderStatus {
8
  id: string;
 
473
  }
474
  };
475
 
476
+ const checkErrors = async () => {
477
  try {
478
  setLoading((prev) => ({ ...prev, errors: true }));
479
 
480
+ // Get errors from log store
481
+ const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error');
482
+
483
+ // Combine with runtime errors
484
+ const allErrors = [
485
+ ...errorLog.errors,
486
+ ...storedErrors.map((error) => ({
487
+ type: 'stored',
488
+ message: error.message,
489
+ timestamp: error.timestamp,
490
+ details: error.details || {},
491
+ })),
492
+ ];
 
 
 
 
 
 
 
 
 
 
 
 
493
 
 
494
  setErrorLog({
495
  errors: allErrors,
496
  lastCheck: new Date().toISOString(),
497
  });
498
+
499
+ if (allErrors.length === 0) {
500
+ toast.success('No errors found');
501
+ } else {
502
+ toast.warning(`Found ${allErrors.length} error(s)`);
503
+ }
504
  } catch (error) {
505
+ toast.error('Failed to check errors');
506
+ console.error('Failed to check errors:', error);
507
  } finally {
508
  setLoading((prev) => ({ ...prev, errors: false }));
509
  }
510
  };
511
 
512
  return (
513
+ <div className="flex flex-col gap-6">
514
+ {/* Action Buttons */}
515
+ <div className="flex flex-wrap gap-4">
516
+ <button
517
+ onClick={checkProviderStatus}
518
+ disabled={loading.providers}
519
+ className={classNames(
520
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
521
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
522
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
523
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
524
+ { 'opacity-50 cursor-not-allowed': loading.providers },
525
+ )}
526
+ >
527
+ {loading.providers ? (
528
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
529
+ ) : (
530
+ <div className="i-ph:plug w-4 h-4" />
531
+ )}
532
+ Check Providers
533
+ </button>
534
+
535
+ <button
536
+ onClick={getSystemInfo}
537
+ disabled={loading.systemInfo}
538
+ className={classNames(
539
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
540
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
541
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
542
+ 'focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2',
543
+ { 'opacity-50 cursor-not-allowed': loading.systemInfo },
544
+ )}
545
+ >
546
+ {loading.systemInfo ? (
547
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
548
+ ) : (
549
+ <div className="i-ph:gear w-4 h-4" />
550
+ )}
551
+ Update System Info
552
+ </button>
553
+
554
+ <button
555
+ onClick={handleLogPerformance}
556
+ disabled={loading.performance}
557
+ className={classNames(
558
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
559
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
560
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
561
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2',
562
+ { 'opacity-50 cursor-not-allowed': loading.performance },
563
+ )}
564
+ >
565
+ {loading.performance ? (
566
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
567
+ ) : (
568
+ <div className="i-ph:chart-bar w-4 h-4" />
569
+ )}
570
+ Log Performance
571
+ </button>
572
+
573
+ <button
574
+ onClick={checkErrors}
575
+ disabled={loading.errors}
576
+ className={classNames(
577
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
578
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
579
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
580
+ 'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2',
581
+ { 'opacity-50 cursor-not-allowed': loading.errors },
582
+ )}
583
+ >
584
+ {loading.errors ? (
585
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
586
+ ) : (
587
+ <div className="i-ph:warning w-4 h-4" />
588
+ )}
589
+ Check Errors
590
+ </button>
591
+ </div>
592
+
593
+ {/* Error Log Display */}
594
+ {errorLog.errors.length > 0 && (
595
+ <div className="mt-4">
596
+ <h3 className="text-lg font-semibold mb-2">Error Log</h3>
597
+ <div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
598
+ {errorLog.errors.map((error, index) => (
599
+ <div key={index} className="mb-4 last:mb-0 p-3 bg-white rounded border border-red-200">
600
+ <div className="flex items-center gap-2 text-sm text-gray-600">
601
+ <span className="font-medium">Type:</span> {error.type}
602
+ <span className="font-medium ml-4">Time:</span>
603
+ {new Date(error.timestamp).toLocaleString()}
604
+ </div>
605
+ <div className="mt-2 text-red-600">{error.message}</div>
606
+ {error.filename && (
607
+ <div className="mt-1 text-sm text-gray-500">
608
+ File: {error.filename} (Line: {error.lineNumber}, Column: {error.columnNumber})
609
+ </div>
610
+ )}
611
+ </div>
612
+ ))}
613
+ </div>
614
+ </div>
615
+ )}
616
+
617
  {/* System Information */}
618
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
619
  <div className="flex items-center justify-between mb-4">
 
626
  onClick={handleLogSystemInfo}
627
  className={classNames(
628
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
629
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
630
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
631
+ 'transition-colors duration-200',
632
  )}
633
  >
634
  <div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
 
638
  onClick={getSystemInfo}
639
  className={classNames(
640
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
641
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
642
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
643
+ 'transition-colors duration-200',
644
+ { 'opacity-50 cursor-not-allowed': loading.systemInfo },
645
  )}
646
+ disabled={loading.systemInfo}
647
  >
648
  <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
649
  Refresh
 
783
  onClick={checkProviderStatus}
784
  className={classNames(
785
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
786
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
787
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
788
+ 'transition-colors duration-200',
789
+ { 'opacity-50 cursor-not-allowed': loading.providers },
790
  )}
791
+ disabled={loading.providers}
792
  >
793
  <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
794
  Refresh
 
830
  onClick={handleLogPerformance}
831
  className={classNames(
832
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
833
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
834
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
835
+ 'transition-colors duration-200',
836
+ { 'opacity-50 cursor-not-allowed': loading.performance },
837
  )}
838
+ disabled={loading.performance}
839
  >
840
  <div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
841
  Log Performance
 
914
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
915
  </div>
916
  <button
917
+ onClick={checkErrors}
918
  className={classNames(
919
  'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
920
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
921
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
922
+ 'transition-colors duration-200',
923
+ { 'opacity-50 cursor-not-allowed': loading.errors },
924
  )}
925
+ disabled={loading.errors}
926
  >
927
  <div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
928
  Check for Errors
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -1,9 +1,10 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
  import { motion } from 'framer-motion';
3
- import { useState } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { TabManagement } from './TabManagement';
6
  import { TabTile } from '~/components/settings/shared/TabTile';
 
7
  import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
8
  import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
9
  import { useStore } from '@nanostores/react';
@@ -20,6 +21,7 @@ import SettingsTab from '~/components/settings/settings/SettingsTab';
20
  import ProfileTab from '~/components/settings/profile/ProfileTab';
21
  import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
22
  import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
 
23
 
24
  interface DraggableTabTileProps {
25
  tab: TabVisibilityConfig;
@@ -102,6 +104,24 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
102
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
103
  const [showTabManagement, setShowTabManagement] = useState(false);
104
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  // Status hooks
107
  const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
@@ -120,7 +140,14 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
120
 
121
  // Only show tabs that are assigned to the developer window AND are visible
122
  const visibleDeveloperTabs = tabConfiguration.developerTabs
123
- .filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible)
 
 
 
 
 
 
 
124
  .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
125
 
126
  const moveTab = (dragIndex: number, hoverIndex: number) => {
@@ -136,32 +163,38 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
136
  updateTabConfiguration(updatedTargetTab);
137
  };
138
 
139
- const handleTabClick = async (tabId: TabType) => {
 
 
 
 
 
140
  setLoadingTab(tabId);
141
  setActiveTab(tabId);
142
 
143
  // Acknowledge the status based on tab type
144
  switch (tabId) {
145
  case 'update':
146
- await acknowledgeUpdate();
147
  break;
148
  case 'features':
149
- await acknowledgeAllFeatures();
150
  break;
151
  case 'notifications':
152
- await markAllAsRead();
153
  break;
154
  case 'connection':
155
  acknowledgeIssue();
156
  break;
157
  case 'debug':
158
- await acknowledgeAllIssues();
159
  break;
160
  }
161
 
162
- // Simulate loading time (remove this in production)
163
- await new Promise((resolve) => setTimeout(resolve, 1000));
164
- setLoadingTab(null);
 
165
  };
166
 
167
  const getTabComponent = () => {
@@ -238,7 +271,7 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
238
  <RadixDialog.Root open={open}>
239
  <RadixDialog.Portal>
240
  <div className="fixed inset-0 flex items-center justify-center z-[60]">
241
- <RadixDialog.Overlay asChild>
242
  <motion.div
243
  className="absolute inset-0 bg-black/50 backdrop-blur-sm"
244
  initial={{ opacity: 0 }}
@@ -247,16 +280,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
247
  transition={{ duration: 0.2 }}
248
  />
249
  </RadixDialog.Overlay>
250
- <RadixDialog.Content aria-describedby={undefined} asChild>
 
251
  <motion.div
252
  className={classNames(
253
- 'relative',
254
  'w-[1200px] h-[90vh]',
255
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
256
  'rounded-2xl shadow-2xl',
257
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
258
  'flex flex-col overflow-hidden',
259
- 'z-[61]',
260
  )}
261
  initial={{ opacity: 0, scale: 0.95, y: 20 }}
262
  animate={{ opacity: 1, scale: 1, y: 0 }}
@@ -264,68 +296,142 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
264
  transition={{ duration: 0.2 }}
265
  >
266
  {/* Header */}
267
- <div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
268
- <div className="flex items-center gap-4">
269
- {(activeTab || showTabManagement) && (
270
- <motion.button
271
  onClick={handleBack}
272
- className={classNames(
273
- 'flex items-center justify-center w-8 h-8 rounded-lg',
274
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
275
- 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
276
- 'group transition-all duration-200',
277
- )}
278
- whileHover={{ scale: 1.05 }}
279
- whileTap={{ scale: 0.95 }}
280
  >
281
- <div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
282
- </motion.button>
283
- )}
284
- <div className="flex items-center gap-3">
285
  <motion.div
286
- className="i-ph:code-fill w-5 h-5 text-purple-500"
287
- initial={{ rotate: 0 }}
288
- animate={{ rotate: 360 }}
289
  transition={{
290
  repeat: Infinity,
291
- duration: 8,
292
- ease: 'linear',
 
293
  }}
294
  />
295
- <h2 className="text-lg font-medium text-bolt-elements-textPrimary">
296
- {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'}
297
- </h2>
298
- </div>
299
  </div>
300
- <div className="flex items-center gap-3">
301
- {!showTabManagement && !activeTab && (
 
302
  <motion.button
303
  onClick={() => setShowTabManagement(true)}
304
- className={classNames(
305
- 'px-3 py-1.5 rounded-lg text-sm',
306
- 'bg-purple-500/10 text-purple-500',
307
- 'hover:bg-purple-500/20',
308
- 'transition-colors duration-200',
309
- )}
310
- whileHover={{ scale: 1.02 }}
311
- whileTap={{ scale: 0.98 }}
312
  >
313
- Manage Tabs
 
 
 
314
  </motion.button>
315
  )}
316
- <motion.button
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  onClick={onClose}
318
- className={classNames(
319
- 'flex items-center justify-center w-8 h-8 rounded-lg',
320
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
321
- 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
322
- 'group transition-all duration-200',
323
- )}
324
- whileHover={{ scale: 1.05 }}
325
- whileTap={{ scale: 0.95 }}
326
  >
327
- <div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
328
- </motion.button>
329
  </div>
330
  </div>
331
 
 
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 { TabManagement } from './TabManagement';
6
  import { TabTile } from '~/components/settings/shared/TabTile';
7
+ import { DialogTitle } from '~/components/ui/Dialog';
8
  import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
9
  import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
10
  import { useStore } from '@nanostores/react';
 
21
  import ProfileTab from '~/components/settings/profile/ProfileTab';
22
  import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
23
  import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
24
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
25
 
26
  interface DraggableTabTileProps {
27
  tab: TabVisibilityConfig;
 
104
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
105
  const [showTabManagement, setShowTabManagement] = useState(false);
106
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
107
+ const [profile, setProfile] = useState(() => {
108
+ const saved = localStorage.getItem('bolt_user_profile');
109
+ return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
110
+ });
111
+
112
+ // Listen for profile changes
113
+ useEffect(() => {
114
+ const handleStorageChange = (e: StorageEvent) => {
115
+ if (e.key === 'bolt_user_profile') {
116
+ const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
117
+ setProfile(newProfile);
118
+ }
119
+ };
120
+
121
+ window.addEventListener('storage', handleStorageChange);
122
+
123
+ return () => window.removeEventListener('storage', handleStorageChange);
124
+ }, []);
125
 
126
  // Status hooks
127
  const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
 
140
 
141
  // Only show tabs that are assigned to the developer window AND are visible
142
  const visibleDeveloperTabs = tabConfiguration.developerTabs
143
+ .filter((tab) => {
144
+ // Hide notifications tab if notifications are disabled
145
+ if (tab.id === 'notifications' && !profile.notifications) {
146
+ return false;
147
+ }
148
+
149
+ return tab.visible;
150
+ })
151
  .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
152
 
153
  const moveTab = (dragIndex: number, hoverIndex: number) => {
 
163
  updateTabConfiguration(updatedTargetTab);
164
  };
165
 
166
+ const handleTabClick = (tabId: TabType) => {
167
+ // Don't allow clicking notifications tab if disabled
168
+ if (tabId === 'notifications' && !profile.notifications) {
169
+ return;
170
+ }
171
+
172
  setLoadingTab(tabId);
173
  setActiveTab(tabId);
174
 
175
  // Acknowledge the status based on tab type
176
  switch (tabId) {
177
  case 'update':
178
+ acknowledgeUpdate();
179
  break;
180
  case 'features':
181
+ acknowledgeAllFeatures();
182
  break;
183
  case 'notifications':
184
+ markAllAsRead();
185
  break;
186
  case 'connection':
187
  acknowledgeIssue();
188
  break;
189
  case 'debug':
190
+ acknowledgeAllIssues();
191
  break;
192
  }
193
 
194
+ // Clear loading state after a short delay
195
+ setTimeout(() => {
196
+ setLoadingTab(null);
197
+ }, 500);
198
  };
199
 
200
  const getTabComponent = () => {
 
271
  <RadixDialog.Root open={open}>
272
  <RadixDialog.Portal>
273
  <div className="fixed inset-0 flex items-center justify-center z-[60]">
274
+ <RadixDialog.Overlay className="fixed inset-0">
275
  <motion.div
276
  className="absolute inset-0 bg-black/50 backdrop-blur-sm"
277
  initial={{ opacity: 0 }}
 
280
  transition={{ duration: 0.2 }}
281
  />
282
  </RadixDialog.Overlay>
283
+
284
+ <RadixDialog.Content aria-describedby={undefined} className="relative z-[61]">
285
  <motion.div
286
  className={classNames(
 
287
  'w-[1200px] h-[90vh]',
288
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
289
  'rounded-2xl shadow-2xl',
290
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
291
  'flex flex-col overflow-hidden',
 
292
  )}
293
  initial={{ opacity: 0, scale: 0.95, y: 20 }}
294
  animate={{ opacity: 1, scale: 1, y: 0 }}
 
296
  transition={{ duration: 0.2 }}
297
  >
298
  {/* Header */}
299
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
300
+ <div className="flex items-center space-x-4">
301
+ {activeTab || showTabManagement ? (
302
+ <button
303
  onClick={handleBack}
304
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
 
 
 
 
 
 
 
305
  >
306
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
307
+ </button>
308
+ ) : (
 
309
  <motion.div
310
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
311
+ initial={{ rotate: -10 }}
312
+ animate={{ rotate: 10 }}
313
  transition={{
314
  repeat: Infinity,
315
+ repeatType: 'reverse',
316
+ duration: 2,
317
+ ease: 'easeInOut',
318
  }}
319
  />
320
+ )}
321
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
322
+ {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'}
323
+ </DialogTitle>
324
  </div>
325
+
326
+ <div className="flex items-center space-x-4">
327
+ {!activeTab && !showTabManagement && (
328
  <motion.button
329
  onClick={() => setShowTabManagement(true)}
330
+ className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
331
+ whileHover={{ scale: 1.05 }}
332
+ whileTap={{ scale: 0.95 }}
 
 
 
 
 
333
  >
334
+ <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
335
+ <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
336
+ Manage Tabs
337
+ </span>
338
  </motion.button>
339
  )}
340
+
341
+ <div className="relative">
342
+ <DropdownMenu.Root>
343
+ <DropdownMenu.Trigger asChild>
344
+ <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
345
+ {profile.avatar ? (
346
+ <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
347
+ ) : (
348
+ <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
349
+ <svg
350
+ className="w-5 h-5 text-gray-500 dark:text-gray-400"
351
+ fill="none"
352
+ stroke="currentColor"
353
+ viewBox="0 0 24 24"
354
+ >
355
+ <path
356
+ strokeLinecap="round"
357
+ strokeLinejoin="round"
358
+ strokeWidth={2}
359
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
360
+ />
361
+ </svg>
362
+ </div>
363
+ )}
364
+ </button>
365
+ </DropdownMenu.Trigger>
366
+
367
+ <DropdownMenu.Portal>
368
+ <DropdownMenu.Content
369
+ className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200]"
370
+ sideOffset={5}
371
+ align="end"
372
+ >
373
+ <DropdownMenu.Item
374
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
375
+ onSelect={() => handleTabClick('profile')}
376
+ >
377
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
378
+ <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
379
+ </div>
380
+ <span className="group-hover:text-purple-500 transition-colors">Profile</span>
381
+ </DropdownMenu.Item>
382
+
383
+ <DropdownMenu.Item
384
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
385
+ onSelect={() => handleTabClick('settings')}
386
+ >
387
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
388
+ <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
389
+ </div>
390
+ <span className="group-hover:text-purple-500 transition-colors">Settings</span>
391
+ </DropdownMenu.Item>
392
+
393
+ {profile.notifications && (
394
+ <>
395
+ <DropdownMenu.Item
396
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
397
+ onSelect={() => handleTabClick('notifications')}
398
+ >
399
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
400
+ <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
401
+ </div>
402
+ <span className="group-hover:text-purple-500 transition-colors">
403
+ Notifications
404
+ {hasUnreadNotifications && (
405
+ <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
406
+ {unreadNotifications.length}
407
+ </span>
408
+ )}
409
+ </span>
410
+ </DropdownMenu.Item>
411
+
412
+ <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
413
+ </>
414
+ )}
415
+ <DropdownMenu.Item
416
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
417
+ onSelect={onClose}
418
+ >
419
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
420
+ <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
421
+ </div>
422
+ <span className="group-hover:text-purple-500 transition-colors">Close</span>
423
+ </DropdownMenu.Item>
424
+ </DropdownMenu.Content>
425
+ </DropdownMenu.Portal>
426
+ </DropdownMenu.Root>
427
+ </div>
428
+
429
+ <button
430
  onClick={onClose}
431
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
 
 
 
 
 
 
 
432
  >
433
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
434
+ </button>
435
  </div>
436
  </div>
437
 
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
4
  import { useSettings } from '~/lib/hooks/useSettings';
@@ -19,6 +19,93 @@ interface FeatureToggle {
19
  tooltip?: string;
20
  }
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  export default function FeaturesTab() {
23
  const {
24
  setEventLogs,
@@ -36,50 +123,56 @@ export default function FeaturesTab() {
36
 
37
  const eventLogs = useStore(isEventLogsEnabled);
38
 
39
- const features: FeatureToggle[] = [
40
- {
41
- id: 'latestBranch',
42
- title: 'Use Main Branch',
43
- description: 'Check for updates against the main branch instead of stable',
44
- icon: 'i-ph:git-branch',
45
- enabled: isLatestBranch,
46
- beta: true,
47
- tooltip: 'Get the latest features and improvements before they are officially released',
48
- },
49
- {
50
- id: 'autoTemplate',
51
- title: 'Auto Select Code Template',
52
- description: 'Let Bolt select the best starter template for your project',
53
- icon: 'i-ph:magic-wand',
54
- enabled: autoSelectTemplate,
55
- tooltip: 'Automatically choose the most suitable template based on your project type',
56
- },
57
- {
58
- id: 'contextOptimization',
59
- title: 'Context Optimization',
60
- description: 'Optimize chat context by redacting file contents and using system prompts',
61
- icon: 'i-ph:arrows-in',
62
- enabled: contextOptimizationEnabled,
63
- tooltip: 'Improve AI responses by optimizing the context window and system prompts',
64
- },
65
- {
66
- id: 'experimentalProviders',
67
- title: 'Experimental Providers',
68
- description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
69
- icon: 'i-ph:robot',
70
- enabled: isLocalModel,
71
- experimental: true,
72
- tooltip: 'Try out new AI providers and models in development',
73
- },
74
- {
75
- id: 'eventLogs',
76
- title: 'Event Logging',
77
- description: 'Enable detailed event logging and history',
78
- icon: 'i-ph:list-bullets',
79
- enabled: eventLogs,
80
- tooltip: 'Record detailed logs of system events and user actions',
81
- },
82
- ];
 
 
 
 
 
 
83
 
84
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
85
  switch (featureId) {
@@ -107,163 +200,88 @@ export default function FeaturesTab() {
107
  };
108
 
109
  return (
110
- <div className="flex flex-col gap-6">
111
- <motion.div
112
- className="flex items-center gap-3"
113
- initial={{ opacity: 0, y: -20 }}
114
- animate={{ opacity: 1, y: 0 }}
115
- transition={{ duration: 0.3 }}
116
- >
117
- <div className="i-ph:puzzle-piece text-xl text-purple-500" />
118
- <div>
119
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
120
- <p className="text-sm text-bolt-elements-textSecondary">
121
- Customize your Bolt experience with experimental features
122
- </p>
123
- </div>
124
- </motion.div>
125
-
126
- <motion.div
127
- className="grid grid-cols-1 md:grid-cols-2 gap-4"
128
- initial={{ opacity: 0, y: 20 }}
129
- animate={{ opacity: 1, y: 0 }}
130
- transition={{ duration: 0.3 }}
131
- >
132
- {features.map((feature, index) => (
133
- <motion.div
134
- key={feature.id}
135
- className={classNames(
136
- 'relative group cursor-pointer',
137
- 'bg-bolt-elements-background-depth-2',
138
- 'hover:bg-bolt-elements-background-depth-3',
139
- 'transition-colors duration-200',
140
- 'rounded-lg overflow-hidden',
141
- )}
142
- initial={{ opacity: 0, y: 20 }}
143
- animate={{ opacity: 1, y: 0 }}
144
- transition={{ delay: index * 0.1 }}
145
- >
146
- <div className="absolute top-0 right-0 p-2 flex gap-1">
147
- {feature.beta && (
148
- <motion.span
149
- className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium"
150
- whileHover={{ scale: 1.05 }}
151
- whileTap={{ scale: 0.95 }}
152
- >
153
- Beta
154
- </motion.span>
155
- )}
156
- {feature.experimental && (
157
- <motion.span
158
- className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
159
- whileHover={{ scale: 1.05 }}
160
- whileTap={{ scale: 0.95 }}
161
- >
162
- Experimental
163
- </motion.span>
164
- )}
165
- </div>
166
-
167
- <div className="flex items-start gap-4 p-4">
168
- <motion.div
169
- className={classNames(
170
- 'p-2 rounded-lg text-xl',
171
- 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
172
- 'transition-colors duration-200',
173
- )}
174
- whileHover={{ scale: 1.1 }}
175
- whileTap={{ scale: 0.9 }}
176
- >
177
- <div className={classNames(feature.icon, 'text-purple-500')} />
178
- </motion.div>
179
 
180
- <div className="flex-1 min-w-0">
181
- <div className="flex items-center justify-between gap-4">
182
- <div>
183
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
184
- {feature.title}
185
- </h4>
186
- <p className="text-xs text-bolt-elements-textSecondary mt-0.5">{feature.description}</p>
187
- </div>
188
- <Switch
189
- checked={feature.enabled}
190
- onCheckedChange={(checked) => handleToggleFeature(feature.id, checked)}
191
- />
192
- </div>
193
- </div>
194
- </div>
195
 
196
- <motion.div
197
- className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
198
- animate={{
199
- borderColor: feature.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
200
- scale: feature.enabled ? 1 : 0.98,
201
- }}
202
- transition={{ duration: 0.2 }}
203
- />
204
- </motion.div>
205
- ))}
206
- </motion.div>
207
 
208
  <motion.div
 
209
  className={classNames(
210
  'bg-bolt-elements-background-depth-2',
211
  'hover:bg-bolt-elements-background-depth-3',
212
  'transition-all duration-200',
213
- 'rounded-lg',
214
  'group',
215
  )}
216
  initial={{ opacity: 0, y: 20 }}
217
  animate={{ opacity: 1, y: 0 }}
218
- transition={{ delay: 0.6 }}
219
- whileHover={{ scale: 1.01 }}
220
  >
221
- <div className="flex items-start gap-4 p-4">
222
- <motion.div
223
  className={classNames(
224
  'p-2 rounded-lg text-xl',
225
  'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
226
  'transition-colors duration-200',
 
227
  )}
228
- whileHover={{ scale: 1.1 }}
229
- whileTap={{ scale: 0.9 }}
230
  >
231
- <div className="i-ph:book text-purple-500" />
232
- </motion.div>
233
-
234
- <div className="flex-1 min-w-0">
235
- <div className="flex items-center justify-between gap-4">
236
- <div>
237
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
238
- Prompt Library
239
- </h4>
240
- <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
241
- Choose a prompt from the library to use as the system prompt
242
- </p>
243
- </div>
244
- <select
245
- value={promptId}
246
- onChange={(e) => {
247
- setPromptId(e.target.value);
248
- toast.success('Prompt template updated');
249
- }}
250
- className={classNames(
251
- 'p-2 rounded-lg text-sm min-w-[200px]',
252
- 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
253
- 'text-bolt-elements-textPrimary',
254
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
255
- 'group-hover:border-purple-500/30',
256
- 'transition-all duration-200',
257
- )}
258
- >
259
- {PromptLibrary.getList().map((x) => (
260
- <option key={x.id} value={x.id}>
261
- {x.label}
262
- </option>
263
- ))}
264
- </select>
265
- </div>
266
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
  </motion.div>
269
  </div>
 
1
+ import React, { memo } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
4
  import { useSettings } from '~/lib/hooks/useSettings';
 
19
  tooltip?: string;
20
  }
21
 
22
+ const FeatureCard = memo(
23
+ ({
24
+ feature,
25
+ index,
26
+ onToggle,
27
+ }: {
28
+ feature: FeatureToggle;
29
+ index: number;
30
+ onToggle: (id: string, enabled: boolean) => void;
31
+ }) => (
32
+ <motion.div
33
+ key={feature.id}
34
+ layoutId={feature.id}
35
+ className={classNames(
36
+ 'relative group cursor-pointer',
37
+ 'bg-bolt-elements-background-depth-2',
38
+ 'hover:bg-bolt-elements-background-depth-3',
39
+ 'transition-colors duration-200',
40
+ 'rounded-lg overflow-hidden',
41
+ )}
42
+ initial={{ opacity: 0, y: 20 }}
43
+ animate={{ opacity: 1, y: 0 }}
44
+ transition={{ delay: index * 0.1 }}
45
+ >
46
+ <div className="p-4">
47
+ <div className="flex items-center justify-between">
48
+ <div className="flex items-center gap-3">
49
+ <div className={classNames(feature.icon, 'w-5 h-5 text-bolt-elements-textSecondary')} />
50
+ <div className="flex items-center gap-2">
51
+ <h4 className="font-medium text-bolt-elements-textPrimary">{feature.title}</h4>
52
+ {feature.beta && (
53
+ <span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">Beta</span>
54
+ )}
55
+ {feature.experimental && (
56
+ <span className="px-2 py-0.5 text-xs rounded-full bg-orange-500/10 text-orange-500 font-medium">
57
+ Experimental
58
+ </span>
59
+ )}
60
+ </div>
61
+ </div>
62
+ <Switch checked={feature.enabled} onCheckedChange={(checked) => onToggle(feature.id, checked)} />
63
+ </div>
64
+ <p className="mt-2 text-sm text-bolt-elements-textSecondary">{feature.description}</p>
65
+ {feature.tooltip && <p className="mt-1 text-xs text-bolt-elements-textTertiary">{feature.tooltip}</p>}
66
+ </div>
67
+ </motion.div>
68
+ ),
69
+ );
70
+
71
+ const FeatureSection = memo(
72
+ ({
73
+ title,
74
+ features,
75
+ icon,
76
+ description,
77
+ onToggleFeature,
78
+ }: {
79
+ title: string;
80
+ features: FeatureToggle[];
81
+ icon: string;
82
+ description: string;
83
+ onToggleFeature: (id: string, enabled: boolean) => void;
84
+ }) => (
85
+ <motion.div
86
+ layout
87
+ className="flex flex-col gap-4"
88
+ initial={{ opacity: 0, y: 20 }}
89
+ animate={{ opacity: 1, y: 0 }}
90
+ transition={{ duration: 0.3 }}
91
+ >
92
+ <div className="flex items-center gap-3">
93
+ <div className={classNames(icon, 'text-xl text-purple-500')} />
94
+ <div>
95
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">{title}</h3>
96
+ <p className="text-sm text-bolt-elements-textSecondary">{description}</p>
97
+ </div>
98
+ </div>
99
+
100
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
101
+ {features.map((feature, index) => (
102
+ <FeatureCard key={feature.id} feature={feature} index={index} onToggle={onToggleFeature} />
103
+ ))}
104
+ </div>
105
+ </motion.div>
106
+ ),
107
+ );
108
+
109
  export default function FeaturesTab() {
110
  const {
111
  setEventLogs,
 
123
 
124
  const eventLogs = useStore(isEventLogsEnabled);
125
 
126
+ const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
127
+ stable: [
128
+ {
129
+ id: 'autoTemplate',
130
+ title: 'Auto Select Code Template',
131
+ description: 'Let Bolt select the best starter template for your project',
132
+ icon: 'i-ph:magic-wand',
133
+ enabled: autoSelectTemplate,
134
+ tooltip: 'Automatically choose the most suitable template based on your project type',
135
+ },
136
+ {
137
+ id: 'contextOptimization',
138
+ title: 'Context Optimization',
139
+ description: 'Optimize chat context by redacting file contents and using system prompts',
140
+ icon: 'i-ph:arrows-in',
141
+ enabled: contextOptimizationEnabled,
142
+ tooltip: 'Improve AI responses by optimizing the context window and system prompts',
143
+ },
144
+ {
145
+ id: 'eventLogs',
146
+ title: 'Event Logging',
147
+ description: 'Enable detailed event logging and history',
148
+ icon: 'i-ph:list-bullets',
149
+ enabled: eventLogs,
150
+ tooltip: 'Record detailed logs of system events and user actions',
151
+ },
152
+ ],
153
+ beta: [
154
+ {
155
+ id: 'latestBranch',
156
+ title: 'Use Main Branch',
157
+ description: 'Check for updates against the main branch instead of stable',
158
+ icon: 'i-ph:git-branch',
159
+ enabled: isLatestBranch,
160
+ beta: true,
161
+ tooltip: 'Get the latest features and improvements before they are officially released',
162
+ },
163
+ ],
164
+ experimental: [
165
+ {
166
+ id: 'experimentalProviders',
167
+ title: 'Experimental Providers',
168
+ description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
169
+ icon: 'i-ph:robot',
170
+ enabled: isLocalModel,
171
+ experimental: true,
172
+ tooltip: 'Try out new AI providers and models in development',
173
+ },
174
+ ],
175
+ };
176
 
177
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
178
  switch (featureId) {
 
200
  };
201
 
202
  return (
203
+ <div className="flex flex-col gap-8">
204
+ <FeatureSection
205
+ title="Stable Features"
206
+ features={features.stable}
207
+ icon="i-ph:check-circle"
208
+ description="Production-ready features that have been thoroughly tested"
209
+ onToggleFeature={handleToggleFeature}
210
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ {features.beta.length > 0 && (
213
+ <FeatureSection
214
+ title="Beta Features"
215
+ features={features.beta}
216
+ icon="i-ph:test-tube"
217
+ description="New features that are ready for testing but may have some rough edges"
218
+ onToggleFeature={handleToggleFeature}
219
+ />
220
+ )}
 
 
 
 
 
 
221
 
222
+ {features.experimental.length > 0 && (
223
+ <FeatureSection
224
+ title="Experimental Features"
225
+ features={features.experimental}
226
+ icon="i-ph:flask"
227
+ description="Features in early development that may be unstable or require additional setup"
228
+ onToggleFeature={handleToggleFeature}
229
+ />
230
+ )}
 
 
231
 
232
  <motion.div
233
+ layout
234
  className={classNames(
235
  'bg-bolt-elements-background-depth-2',
236
  'hover:bg-bolt-elements-background-depth-3',
237
  'transition-all duration-200',
238
+ 'rounded-lg p-4',
239
  'group',
240
  )}
241
  initial={{ opacity: 0, y: 20 }}
242
  animate={{ opacity: 1, y: 0 }}
243
+ transition={{ delay: 0.3 }}
 
244
  >
245
+ <div className="flex items-center gap-4">
246
+ <div
247
  className={classNames(
248
  'p-2 rounded-lg text-xl',
249
  'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
250
  'transition-colors duration-200',
251
+ 'text-purple-500',
252
  )}
 
 
253
  >
254
+ <div className="i-ph:book" />
255
+ </div>
256
+ <div className="flex-1">
257
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
258
+ Prompt Library
259
+ </h4>
260
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
261
+ Choose a prompt from the library to use as the system prompt
262
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  </div>
264
+ <select
265
+ value={promptId}
266
+ onChange={(e) => {
267
+ setPromptId(e.target.value);
268
+ toast.success('Prompt template updated');
269
+ }}
270
+ className={classNames(
271
+ 'p-2 rounded-lg text-sm min-w-[200px]',
272
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
273
+ 'text-bolt-elements-textPrimary',
274
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
275
+ 'group-hover:border-purple-500/30',
276
+ 'transition-all duration-200',
277
+ )}
278
+ >
279
+ {PromptLibrary.getList().map((x) => (
280
+ <option key={x.id} value={x.id}>
281
+ {x.label}
282
+ </option>
283
+ ))}
284
+ </select>
285
  </div>
286
  </motion.div>
287
  </div>
app/components/settings/notifications/NotificationsTab.tsx CHANGED
@@ -15,7 +15,7 @@ interface NotificationDetails {
15
  }
16
 
17
  const NotificationsTab = () => {
18
- const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all');
19
  const logs = useStore(logStore.logs);
20
 
21
  const handleClearNotifications = () => {
@@ -29,13 +29,44 @@ const NotificationsTab = () => {
29
  const filteredLogs = Object.values(logs)
30
  .filter((log) => {
31
  if (filter === 'all') {
32
- return log.level === 'error' || log.level === 'warning';
 
 
 
 
33
  }
34
 
35
  return log.level === filter;
36
  })
37
  .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  const renderNotificationDetails = (details: NotificationDetails) => {
40
  if (details.type === 'update') {
41
  return (
@@ -48,7 +79,7 @@ const NotificationsTab = () => {
48
  </div>
49
  <button
50
  onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
51
- className="mt-2 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
52
  >
53
  <span className="i-ph:git-branch text-lg" />
54
  View Changes
@@ -66,10 +97,11 @@ const NotificationsTab = () => {
66
  <div className="flex items-center gap-2">
67
  <select
68
  value={filter}
69
- onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning')}
70
  className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
71
  >
72
  <option value="all">All Notifications</option>
 
73
  <option value="error">Errors</option>
74
  <option value="warning">Warnings</option>
75
  </select>
@@ -92,48 +124,30 @@ const NotificationsTab = () => {
92
  </div>
93
  </div>
94
  ) : (
95
- filteredLogs.map((log) => (
96
- <motion.div
97
- key={log.id}
98
- initial={{ opacity: 0, y: 20 }}
99
- animate={{ opacity: 1, y: 0 }}
100
- className={classNames(
101
- 'flex flex-col gap-2 rounded-lg border p-4',
102
- log.level === 'error'
103
- ? 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-900/20'
104
- : 'border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20',
105
- )}
106
- >
107
- <div className="flex items-start justify-between gap-4">
108
- <div className="flex items-center gap-3">
109
- <span
110
- className={classNames(
111
- 'text-lg',
112
- log.level === 'error'
113
- ? 'i-ph:warning-circle text-red-600 dark:text-red-400'
114
- : 'i-ph:warning text-yellow-600 dark:text-yellow-400',
115
- )}
116
- />
117
- <div>
118
- <h3
119
- className={classNames(
120
- 'text-sm font-medium',
121
- log.level === 'error'
122
- ? 'text-red-900 dark:text-red-300'
123
- : 'text-yellow-900 dark:text-yellow-300',
124
- )}
125
- >
126
- {log.message}
127
- </h3>
128
- {log.details && renderNotificationDetails(log.details as NotificationDetails)}
129
  </div>
 
 
 
130
  </div>
131
- <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
132
- {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
133
- </time>
134
- </div>
135
- </motion.div>
136
- ))
137
  )}
138
  </div>
139
  </div>
 
15
  }
16
 
17
  const NotificationsTab = () => {
18
+ const [filter, setFilter] = useState<'all' | 'error' | 'warning' | 'update'>('all');
19
  const logs = useStore(logStore.logs);
20
 
21
  const handleClearNotifications = () => {
 
29
  const filteredLogs = Object.values(logs)
30
  .filter((log) => {
31
  if (filter === 'all') {
32
+ return log.level === 'error' || log.level === 'warning' || log.details?.type === 'update';
33
+ }
34
+
35
+ if (filter === 'update') {
36
+ return log.details?.type === 'update';
37
  }
38
 
39
  return log.level === filter;
40
  })
41
  .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
42
 
43
+ const getNotificationStyle = (log: (typeof filteredLogs)[0]) => {
44
+ if (log.details?.type === 'update') {
45
+ return {
46
+ border: 'border-purple-200 dark:border-purple-900/50',
47
+ bg: 'bg-purple-50 dark:bg-purple-900/20',
48
+ icon: 'i-ph:arrow-circle-up text-purple-600 dark:text-purple-400',
49
+ text: 'text-purple-900 dark:text-purple-300',
50
+ };
51
+ }
52
+
53
+ if (log.level === 'error') {
54
+ return {
55
+ border: 'border-red-200 dark:border-red-900/50',
56
+ bg: 'bg-red-50 dark:bg-red-900/20',
57
+ icon: 'i-ph:warning-circle text-red-600 dark:text-red-400',
58
+ text: 'text-red-900 dark:text-red-300',
59
+ };
60
+ }
61
+
62
+ return {
63
+ border: 'border-yellow-200 dark:border-yellow-900/50',
64
+ bg: 'bg-yellow-50 dark:bg-yellow-900/20',
65
+ icon: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
66
+ text: 'text-yellow-900 dark:text-yellow-300',
67
+ };
68
+ };
69
+
70
  const renderNotificationDetails = (details: NotificationDetails) => {
71
  if (details.type === 'update') {
72
  return (
 
79
  </div>
80
  <button
81
  onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
82
+ className="mt-2 inline-flex items-center gap-2 rounded-md bg-purple-50 px-3 py-1.5 text-sm font-medium text-purple-600 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/30"
83
  >
84
  <span className="i-ph:git-branch text-lg" />
85
  View Changes
 
97
  <div className="flex items-center gap-2">
98
  <select
99
  value={filter}
100
+ onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning' | 'update')}
101
  className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
102
  >
103
  <option value="all">All Notifications</option>
104
+ <option value="update">Updates</option>
105
  <option value="error">Errors</option>
106
  <option value="warning">Warnings</option>
107
  </select>
 
124
  </div>
125
  </div>
126
  ) : (
127
+ filteredLogs.map((log) => {
128
+ const style = getNotificationStyle(log);
129
+ return (
130
+ <motion.div
131
+ key={log.id}
132
+ initial={{ opacity: 0, y: 20 }}
133
+ animate={{ opacity: 1, y: 0 }}
134
+ className={classNames('flex flex-col gap-2 rounded-lg border p-4', style.border, style.bg)}
135
+ >
136
+ <div className="flex items-start justify-between gap-4">
137
+ <div className="flex items-center gap-3">
138
+ <span className={classNames('text-lg', style.icon)} />
139
+ <div>
140
+ <h3 className={classNames('text-sm font-medium', style.text)}>{log.message}</h3>
141
+ {log.details && renderNotificationDetails(log.details as NotificationDetails)}
142
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  </div>
144
+ <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
145
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
146
+ </time>
147
  </div>
148
+ </motion.div>
149
+ );
150
+ })
 
 
 
151
  )}
152
  </div>
153
  </div>
app/components/settings/settings.types.ts CHANGED
@@ -17,6 +17,7 @@ export type TabType =
17
  export type WindowType = 'user' | 'developer';
18
 
19
  export interface UserProfile {
 
20
  name: string;
21
  email: string;
22
  avatar?: string;
 
17
  export type WindowType = 'user' | 'developer';
18
 
19
  export interface UserProfile {
20
+ nickname: any;
21
  name: string;
22
  email: string;
23
  avatar?: string;
app/components/settings/update/UpdateTab.tsx CHANGED
@@ -1,11 +1,24 @@
1
  import React, { useState, useEffect } from 'react';
2
- import { motion } from 'framer-motion';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { logStore } from '~/lib/stores/logs';
5
  import { classNames } from '~/utils/classNames';
 
6
 
7
  interface GitHubCommitResponse {
8
  sha: string;
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
  interface UpdateInfo {
@@ -13,26 +26,136 @@ interface UpdateInfo {
13
  latestVersion: string;
14
  branch: string;
15
  hasUpdate: boolean;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const GITHUB_URLS = {
19
- commitJson: async (branch: string): Promise<UpdateInfo> => {
20
  try {
21
- const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
22
- const data = (await response.json()) as GitHubCommitResponse;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- const currentCommitHash = __COMMIT_HASH;
25
- const remoteCommitHash = data.sha.slice(0, 7);
26
 
27
  return {
28
- currentVersion: currentCommitHash,
29
- latestVersion: remoteCommitHash,
30
  branch,
31
- hasUpdate: remoteCommitHash !== currentCommitHash,
 
 
 
 
 
32
  };
33
  } catch (error) {
34
- console.error('Failed to fetch commit info:', error);
35
- throw new Error('Failed to fetch commit info');
36
  }
37
  },
38
  };
@@ -41,19 +164,71 @@ const UpdateTab = () => {
41
  const { isLatestBranch } = useSettings();
42
  const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
43
  const [isChecking, setIsChecking] = useState(false);
 
44
  const [error, setError] = useState<string | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  const checkForUpdates = async () => {
47
  setIsChecking(true);
48
  setError(null);
49
 
50
  try {
 
 
 
 
 
 
 
 
51
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
52
- const info = await GITHUB_URLS.commitJson(branchToCheck);
53
  setUpdateInfo(info);
54
 
55
  if (info.hasUpdate) {
56
- // Add update notification only if it doesn't already exist
57
  const existingLogs = Object.values(logStore.logs.get());
58
  const hasUpdateNotification = existingLogs.some(
59
  (log) =>
@@ -62,7 +237,7 @@ const UpdateTab = () => {
62
  log.details.latestVersion === info.latestVersion,
63
  );
64
 
65
- if (!hasUpdateNotification) {
66
  logStore.logWarning('Update Available', {
67
  currentVersion: info.currentVersion,
68
  latestVersion: info.latestVersion,
@@ -71,29 +246,123 @@ const UpdateTab = () => {
71
  message: `A new version is available on the ${branchToCheck} branch`,
72
  updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
73
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
  }
76
  } catch (err) {
77
  setError('Failed to check for updates. Please try again later.');
78
  console.error('Update check failed:', err);
 
79
  } finally {
80
  setIsChecking(false);
81
  }
82
  };
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  useEffect(() => {
85
  checkForUpdates();
86
  }, [isLatestBranch]);
87
 
88
- const handleViewChanges = () => {
89
- if (updateInfo) {
90
- window.open(
91
- `https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`,
92
- '_blank',
93
- );
94
- }
95
- };
96
-
97
  return (
98
  <div className="flex flex-col gap-6">
99
  <motion.div
@@ -109,43 +378,130 @@ const UpdateTab = () => {
109
  </div>
110
  </motion.div>
111
 
 
112
  <motion.div
113
- className="flex flex-col gap-4"
114
  initial={{ opacity: 0, y: 20 }}
115
  animate={{ opacity: 1, y: 0 }}
116
  transition={{ duration: 0.3, delay: 0.1 }}
117
  >
118
- <div className="flex items-center justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  <div className="flex items-center gap-4">
120
  <span className="text-sm text-bolt-elements-textSecondary">
121
  Currently on {isLatestBranch ? 'main' : 'stable'} branch
122
  </span>
123
  {updateInfo && (
124
- <span className="text-xs text-bolt-elements-textTertiary">Version: {updateInfo.currentVersion}</span>
 
 
125
  )}
126
  </div>
127
  <button
128
- onClick={checkForUpdates}
129
- disabled={isChecking}
 
 
 
 
130
  className={classNames(
131
- 'px-3 py-2 rounded-lg text-sm',
132
- 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
 
133
  'text-bolt-elements-textPrimary',
134
- 'hover:bg-bolt-elements-background-depth-3',
135
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
136
- 'transition-all duration-200',
137
  'disabled:opacity-50 disabled:cursor-not-allowed',
138
  )}
139
  >
140
- <div className="flex items-center gap-2">
141
- <div className={classNames('i-ph:arrows-clockwise', isChecking ? 'animate-spin' : '')} />
142
- {isChecking ? 'Checking...' : 'Check for Updates'}
143
- </div>
144
  </button>
145
  </div>
146
 
147
  {error && (
148
- <div className="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 dark:bg-red-900/20 dark:border-red-900/50 dark:text-red-400">
149
  <div className="flex items-center gap-2">
150
  <div className="i-ph:warning-circle" />
151
  {error}
@@ -156,60 +512,250 @@ const UpdateTab = () => {
156
  {updateInfo && (
157
  <div
158
  className={classNames(
159
- 'p-4 rounded-lg border',
160
  updateInfo.hasUpdate
161
- ? 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-900/50'
162
- : 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-900/50',
163
  )}
164
  >
165
- <div className="flex items-start justify-between gap-4">
166
- <div className="flex items-center gap-3">
167
- <span
168
- className={classNames(
169
- 'text-lg',
170
- updateInfo.hasUpdate
171
- ? 'i-ph:warning text-yellow-600 dark:text-yellow-400'
172
- : 'i-ph:check-circle text-green-600 dark:text-green-400',
173
- )}
174
- />
175
- <div>
176
- <h3
177
- className={classNames(
178
- 'text-sm font-medium',
179
- updateInfo.hasUpdate
180
- ? 'text-yellow-900 dark:text-yellow-300'
181
- : 'text-green-900 dark:text-green-300',
182
- )}
183
- >
184
- {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
185
- </h3>
186
- <p className="text-sm text-bolt-elements-textSecondary mt-1">
187
- {updateInfo.hasUpdate
188
- ? `A new version is available on the ${updateInfo.branch} branch`
189
- : 'You are running the latest version'}
190
- </p>
191
- {updateInfo.hasUpdate && (
192
- <div className="mt-2 flex flex-col gap-1 text-xs text-bolt-elements-textTertiary">
193
- <p>Current Version: {updateInfo.currentVersion}</p>
194
- <p>Latest Version: {updateInfo.latestVersion}</p>
195
- <p>Branch: {updateInfo.branch}</p>
196
- </div>
197
- )}
198
- </div>
199
  </div>
200
- {updateInfo.hasUpdate && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  <button
202
- onClick={handleViewChanges}
203
- className="shrink-0 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
204
  >
205
- <span className="i-ph:git-branch text-lg" />
206
- View Changes
207
  </button>
208
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  </div>
 
210
  </div>
211
- )}
212
- </motion.div>
213
  </div>
214
  );
215
  };
 
1
  import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { logStore } from '~/lib/stores/logs';
5
  import { classNames } from '~/utils/classNames';
6
+ import { toast } from 'react-toastify';
7
 
8
  interface GitHubCommitResponse {
9
  sha: string;
10
+ commit: {
11
+ message: string;
12
+ };
13
+ }
14
+
15
+ interface GitHubReleaseResponse {
16
+ tag_name: string;
17
+ body: string;
18
+ assets: Array<{
19
+ size: number;
20
+ browser_download_url: string;
21
+ }>;
22
  }
23
 
24
  interface UpdateInfo {
 
26
  latestVersion: string;
27
  branch: string;
28
  hasUpdate: boolean;
29
+ releaseNotes?: string;
30
+ downloadSize?: string;
31
+ changelog?: string[];
32
+ currentCommit?: string;
33
+ latestCommit?: string;
34
+ downloadProgress?: number;
35
+ installProgress?: number;
36
+ estimatedTimeRemaining?: number;
37
+ }
38
+
39
+ interface UpdateSettings {
40
+ autoUpdate: boolean;
41
+ notifyInApp: boolean;
42
+ checkInterval: number;
43
+ }
44
+
45
+ interface UpdateResponse {
46
+ success: boolean;
47
+ error?: string;
48
+ progress?: {
49
+ downloaded: number;
50
+ total: number;
51
+ stage: 'download' | 'install' | 'complete';
52
+ };
53
  }
54
 
55
+ const categorizeChangelog = (messages: string[]) => {
56
+ const categories = new Map<string, string[]>();
57
+
58
+ messages.forEach((message) => {
59
+ let category = 'Other';
60
+
61
+ if (message.startsWith('feat:')) {
62
+ category = 'Features';
63
+ } else if (message.startsWith('fix:')) {
64
+ category = 'Bug Fixes';
65
+ } else if (message.startsWith('docs:')) {
66
+ category = 'Documentation';
67
+ } else if (message.startsWith('ci:')) {
68
+ category = 'CI Improvements';
69
+ } else if (message.startsWith('refactor:')) {
70
+ category = 'Refactoring';
71
+ } else if (message.startsWith('test:')) {
72
+ category = 'Testing';
73
+ } else if (message.startsWith('style:')) {
74
+ category = 'Styling';
75
+ } else if (message.startsWith('perf:')) {
76
+ category = 'Performance';
77
+ }
78
+
79
+ if (!categories.has(category)) {
80
+ categories.set(category, []);
81
+ }
82
+
83
+ categories.get(category)!.push(message);
84
+ });
85
+
86
+ const order = [
87
+ 'Features',
88
+ 'Bug Fixes',
89
+ 'Documentation',
90
+ 'CI Improvements',
91
+ 'Refactoring',
92
+ 'Performance',
93
+ 'Testing',
94
+ 'Styling',
95
+ 'Other',
96
+ ];
97
+
98
+ return Array.from(categories.entries())
99
+ .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))
100
+ .filter(([_, messages]) => messages.length > 0);
101
+ };
102
+
103
+ const parseCommitMessage = (message: string) => {
104
+ const prMatch = message.match(/#(\d+)/);
105
+ const prNumber = prMatch ? prMatch[1] : null;
106
+
107
+ let cleanMessage = message.replace(/^[a-z]+:\s*/i, '');
108
+ cleanMessage = cleanMessage.replace(/#\d+/g, '').trim();
109
+
110
+ const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/);
111
+ const title = parts[0].trim();
112
+ const description = parts
113
+ .slice(1)
114
+ .map((p) => p.trim())
115
+ .filter((p) => p && !p.includes('Co-authored-by:'))
116
+ .join('\n');
117
+
118
+ return { title, description, prNumber };
119
+ };
120
+
121
  const GITHUB_URLS = {
122
+ commitJson: async (branch: string, headers: HeadersInit = {}): Promise<UpdateInfo> => {
123
  try {
124
+ const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([
125
+ fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }),
126
+ fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }),
127
+ fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }),
128
+ ]);
129
+
130
+ if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) {
131
+ throw new Error(
132
+ `GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`,
133
+ );
134
+ }
135
+
136
+ const commitData = (await commitResponse.json()) as GitHubCommitResponse;
137
+ const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse;
138
+ const commits = (await changelogResponse.json()) as GitHubCommitResponse[];
139
+
140
+ const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0;
141
+ const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB';
142
 
143
+ const changelog = commits.map((commit) => commit.commit.message);
 
144
 
145
  return {
146
+ currentVersion: process.env.APP_VERSION || 'unknown',
147
+ latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7),
148
  branch,
149
+ hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT,
150
+ releaseNotes: releaseData.body || '',
151
+ downloadSize,
152
+ changelog,
153
+ currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7),
154
+ latestCommit: commitData.sha.substring(0, 7),
155
  };
156
  } catch (error) {
157
+ console.error('Error fetching update info:', error);
158
+ throw error;
159
  }
160
  },
161
  };
 
164
  const { isLatestBranch } = useSettings();
165
  const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
166
  const [isChecking, setIsChecking] = useState(false);
167
+ const [isUpdating, setIsUpdating] = useState(false);
168
  const [error, setError] = useState<string | null>(null);
169
+ const [retryCount, setRetryCount] = useState(0);
170
+ const [showChangelog, setShowChangelog] = useState(false);
171
+ const [showManualInstructions, setShowManualInstructions] = useState(false);
172
+ const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
173
+ const [updateFailed, setUpdateFailed] = useState(false);
174
+ const [updateSettings, setUpdateSettings] = useState<UpdateSettings>(() => {
175
+ const stored = localStorage.getItem('update_settings');
176
+ return stored
177
+ ? JSON.parse(stored)
178
+ : {
179
+ autoUpdate: false,
180
+ notifyInApp: true,
181
+ checkInterval: 24,
182
+ };
183
+ });
184
+
185
+ useEffect(() => {
186
+ localStorage.setItem('update_settings', JSON.stringify(updateSettings));
187
+ }, [updateSettings]);
188
+
189
+ const handleUpdateProgress = async (response: Response): Promise<void> => {
190
+ const reader = response.body?.getReader();
191
+
192
+ if (!reader) {
193
+ return;
194
+ }
195
+
196
+ const contentLength = +(response.headers.get('Content-Length') ?? 0);
197
+ let receivedLength = 0;
198
+
199
+ while (true) {
200
+ const { done, value } = await reader.read();
201
+
202
+ if (done) {
203
+ break;
204
+ }
205
+
206
+ receivedLength += value.length;
207
+
208
+ const progress = (receivedLength / contentLength) * 100;
209
+
210
+ setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev));
211
+ }
212
+ };
213
 
214
  const checkForUpdates = async () => {
215
  setIsChecking(true);
216
  setError(null);
217
 
218
  try {
219
+ const githubToken = localStorage.getItem('github_connection');
220
+ const headers: HeadersInit = {};
221
+
222
+ if (githubToken) {
223
+ const { token } = JSON.parse(githubToken);
224
+ headers.Authorization = `Bearer ${token}`;
225
+ }
226
+
227
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
228
+ const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
229
  setUpdateInfo(info);
230
 
231
  if (info.hasUpdate) {
 
232
  const existingLogs = Object.values(logStore.logs.get());
233
  const hasUpdateNotification = existingLogs.some(
234
  (log) =>
 
237
  log.details.latestVersion === info.latestVersion,
238
  );
239
 
240
+ if (!hasUpdateNotification && updateSettings.notifyInApp) {
241
  logStore.logWarning('Update Available', {
242
  currentVersion: info.currentVersion,
243
  latestVersion: info.latestVersion,
 
246
  message: `A new version is available on the ${branchToCheck} branch`,
247
  updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
248
  });
249
+
250
+ if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
251
+ const changelogText = info.changelog?.join('\n') || 'No changelog available';
252
+ const userWantsUpdate = confirm(
253
+ `An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`,
254
+ );
255
+ setHasUserRespondedToUpdate(true);
256
+
257
+ if (userWantsUpdate) {
258
+ await initiateUpdate();
259
+ } else {
260
+ logStore.logSystem('Update cancelled by user');
261
+ }
262
+ }
263
  }
264
  }
265
  } catch (err) {
266
  setError('Failed to check for updates. Please try again later.');
267
  console.error('Update check failed:', err);
268
+ setUpdateFailed(true);
269
  } finally {
270
  setIsChecking(false);
271
  }
272
  };
273
 
274
+ const initiateUpdate = async () => {
275
+ setIsUpdating(true);
276
+ setError(null);
277
+
278
+ let currentRetry = 0;
279
+ const maxRetries = 3;
280
+
281
+ const attemptUpdate = async (): Promise<void> => {
282
+ try {
283
+ const platform = process.platform;
284
+
285
+ if (platform === 'darwin' || platform === 'linux') {
286
+ const response = await fetch('/api/update', {
287
+ method: 'POST',
288
+ headers: {
289
+ 'Content-Type': 'application/json',
290
+ },
291
+ body: JSON.stringify({
292
+ branch: isLatestBranch ? 'main' : 'stable',
293
+ settings: updateSettings,
294
+ }),
295
+ });
296
+
297
+ if (!response.ok) {
298
+ throw new Error('Failed to initiate update');
299
+ }
300
+
301
+ await handleUpdateProgress(response);
302
+
303
+ const result = (await response.json()) as UpdateResponse;
304
+
305
+ if (result.success) {
306
+ logStore.logSuccess('Update downloaded successfully', {
307
+ type: 'update',
308
+ message: 'Update completed successfully.',
309
+ });
310
+ toast.success('Update completed successfully!');
311
+ setUpdateFailed(false);
312
+
313
+ return;
314
+ }
315
+
316
+ throw new Error(result.error || 'Update failed');
317
+ }
318
+
319
+ window.open('https://github.com/stackblitz-labs/bolt.diy/releases/latest', '_blank');
320
+ logStore.logInfo('Manual update required', {
321
+ type: 'update',
322
+ message: 'Please download and install the latest version from the GitHub releases page.',
323
+ });
324
+
325
+ return;
326
+ } catch (err) {
327
+ currentRetry++;
328
+
329
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
330
+
331
+ if (currentRetry < maxRetries) {
332
+ toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 });
333
+ setRetryCount(currentRetry);
334
+ await new Promise((resolve) => setTimeout(resolve, 2000));
335
+ await attemptUpdate();
336
+
337
+ return;
338
+ }
339
+
340
+ setError('Failed to initiate update. Please try again or update manually.');
341
+ console.error('Update failed:', err);
342
+ logStore.logSystem('Update failed: ' + errorMessage);
343
+ toast.error('Update failed: ' + errorMessage);
344
+ setUpdateFailed(true);
345
+
346
+ return;
347
+ }
348
+ };
349
+
350
+ await attemptUpdate();
351
+ setIsUpdating(false);
352
+ setRetryCount(0);
353
+ };
354
+
355
+ useEffect(() => {
356
+ const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
357
+ const intervalId = setInterval(checkForUpdates, checkInterval);
358
+
359
+ return () => clearInterval(intervalId);
360
+ }, [updateSettings.checkInterval, isLatestBranch]);
361
+
362
  useEffect(() => {
363
  checkForUpdates();
364
  }, [isLatestBranch]);
365
 
 
 
 
 
 
 
 
 
 
366
  return (
367
  <div className="flex flex-col gap-6">
368
  <motion.div
 
378
  </div>
379
  </motion.div>
380
 
381
+ {/* Update Settings Card */}
382
  <motion.div
383
+ className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
384
  initial={{ opacity: 0, y: 20 }}
385
  animate={{ opacity: 1, y: 0 }}
386
  transition={{ duration: 0.3, delay: 0.1 }}
387
  >
388
+ <div className="flex items-center gap-3 mb-6">
389
+ <div className="i-ph:gear text-purple-500 w-5 h-5" />
390
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Settings</h3>
391
+ </div>
392
+
393
+ <div className="space-y-4">
394
+ <div className="flex items-center justify-between">
395
+ <div>
396
+ <span className="text-sm text-bolt-elements-textPrimary">Automatic Updates</span>
397
+ <p className="text-xs text-bolt-elements-textSecondary">
398
+ Automatically check and apply updates when available
399
+ </p>
400
+ </div>
401
+ <button
402
+ onClick={() => setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
403
+ className={classNames(
404
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
405
+ updateSettings.autoUpdate ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
406
+ )}
407
+ >
408
+ <span
409
+ className={classNames(
410
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
411
+ updateSettings.autoUpdate ? 'translate-x-6' : 'translate-x-1',
412
+ )}
413
+ />
414
+ </button>
415
+ </div>
416
+
417
+ <div className="flex items-center justify-between">
418
+ <div>
419
+ <span className="text-sm text-bolt-elements-textPrimary">In-App Notifications</span>
420
+ <p className="text-xs text-bolt-elements-textSecondary">Show notifications when updates are available</p>
421
+ </div>
422
+ <button
423
+ onClick={() => setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
424
+ className={classNames(
425
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
426
+ updateSettings.notifyInApp ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
427
+ )}
428
+ >
429
+ <span
430
+ className={classNames(
431
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
432
+ updateSettings.notifyInApp ? 'translate-x-6' : 'translate-x-1',
433
+ )}
434
+ />
435
+ </button>
436
+ </div>
437
+
438
+ <div className="flex items-center justify-between">
439
+ <div>
440
+ <span className="text-sm text-bolt-elements-textPrimary">Check Interval</span>
441
+ <p className="text-xs text-bolt-elements-textSecondary">How often to check for updates</p>
442
+ </div>
443
+ <select
444
+ value={updateSettings.checkInterval}
445
+ onChange={(e) => setUpdateSettings((prev) => ({ ...prev, checkInterval: Number(e.target.value) }))}
446
+ className={classNames(
447
+ 'px-3 py-2 rounded-lg text-sm',
448
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
449
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
450
+ 'text-bolt-elements-textPrimary',
451
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
452
+ 'transition-colors duration-200',
453
+ )}
454
+ >
455
+ <option value="6">6 hours</option>
456
+ <option value="12">12 hours</option>
457
+ <option value="24">24 hours</option>
458
+ <option value="48">48 hours</option>
459
+ </select>
460
+ </div>
461
+ </div>
462
+ </motion.div>
463
+
464
+ {/* Update Status Card */}
465
+ <motion.div
466
+ className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
467
+ initial={{ opacity: 0, y: 20 }}
468
+ animate={{ opacity: 1, y: 0 }}
469
+ transition={{ duration: 0.3, delay: 0.2 }}
470
+ >
471
+ <div className="flex items-center justify-between mb-4">
472
  <div className="flex items-center gap-4">
473
  <span className="text-sm text-bolt-elements-textSecondary">
474
  Currently on {isLatestBranch ? 'main' : 'stable'} branch
475
  </span>
476
  {updateInfo && (
477
+ <span className="text-xs text-bolt-elements-textTertiary">
478
+ Version: {updateInfo.currentVersion} ({updateInfo.currentCommit})
479
+ </span>
480
  )}
481
  </div>
482
  <button
483
+ onClick={() => {
484
+ setHasUserRespondedToUpdate(false);
485
+ setUpdateFailed(false);
486
+ checkForUpdates();
487
+ }}
488
+ disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)}
489
  className={classNames(
490
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
491
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
492
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
493
  'text-bolt-elements-textPrimary',
494
+ 'transition-colors duration-200',
 
 
495
  'disabled:opacity-50 disabled:cursor-not-allowed',
496
  )}
497
  >
498
+ <div className={classNames('i-ph:arrows-clockwise w-4 h-4', isChecking ? 'animate-spin' : '')} />
499
+ {isChecking ? 'Checking...' : 'Check for Updates'}
 
 
500
  </button>
501
  </div>
502
 
503
  {error && (
504
+ <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
505
  <div className="flex items-center gap-2">
506
  <div className="i-ph:warning-circle" />
507
  {error}
 
512
  {updateInfo && (
513
  <div
514
  className={classNames(
515
+ 'p-4 rounded-lg',
516
  updateInfo.hasUpdate
517
+ ? 'bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20'
518
+ : 'bg-green-500/5 dark:bg-green-500/10 border border-green-500/20',
519
  )}
520
  >
521
+ <div className="flex items-center gap-3">
522
+ <span
523
+ className={classNames(
524
+ 'text-lg',
525
+ updateInfo.hasUpdate ? 'i-ph:warning text-purple-500' : 'i-ph:check-circle text-green-500',
526
+ )}
527
+ />
528
+ <div>
529
+ <h4 className="font-medium text-bolt-elements-textPrimary">
530
+ {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
531
+ </h4>
532
+ <p className="text-sm text-bolt-elements-textSecondary">
533
+ {updateInfo.hasUpdate
534
+ ? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available`
535
+ : 'You are running the latest version'}
536
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  </div>
538
+ </div>
539
+ </div>
540
+ )}
541
+ </motion.div>
542
+
543
+ {/* Update Details Card */}
544
+ {updateInfo && updateInfo.hasUpdate && (
545
+ <motion.div
546
+ className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
547
+ initial={{ opacity: 0, y: 20 }}
548
+ animate={{ opacity: 1, y: 0 }}
549
+ transition={{ duration: 0.3, delay: 0.2 }}
550
+ >
551
+ <div className="flex items-center justify-between mb-6">
552
+ <div className="flex items-center gap-3">
553
+ <div className="i-ph:arrow-circle-up text-purple-500 w-5 h-5" />
554
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">
555
+ Version {updateInfo.latestVersion}
556
+ </span>
557
+ </div>
558
+ <span className="text-xs px-3 py-1 rounded-full bg-purple-500/10 text-purple-500">
559
+ {updateInfo.downloadSize}
560
+ </span>
561
+ </div>
562
+
563
+ {/* Update Options */}
564
+ <div className="flex flex-col gap-4">
565
+ <div className="flex items-center gap-3">
566
+ <button
567
+ onClick={initiateUpdate}
568
+ disabled={isUpdating || updateFailed}
569
+ className={classNames(
570
+ 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
571
+ 'bg-purple-500 hover:bg-purple-600',
572
+ 'text-white',
573
+ 'transition-all duration-200',
574
+ 'hover:shadow-lg hover:shadow-purple-500/20',
575
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
576
+ )}
577
+ >
578
+ <div className={classNames('i-ph:arrow-circle-up w-4 h-4', isUpdating ? 'animate-spin' : '')} />
579
+ {isUpdating ? 'Updating...' : 'Auto Update'}
580
+ </button>
581
+ <button
582
+ onClick={() => setShowManualInstructions(!showManualInstructions)}
583
+ className={classNames(
584
+ 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
585
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
586
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
587
+ 'text-bolt-elements-textPrimary',
588
+ 'transition-all duration-200',
589
+ )}
590
+ >
591
+ <div className="i-ph:book-open w-4 h-4" />
592
+ {showManualInstructions ? 'Hide Instructions' : 'Manual Update'}
593
+ </button>
594
+ </div>
595
+
596
+ {/* Manual Update Instructions */}
597
+ <AnimatePresence>
598
+ {showManualInstructions && (
599
+ <motion.div
600
+ initial={{ opacity: 0, height: 0 }}
601
+ animate={{ opacity: 1, height: 'auto' }}
602
+ exit={{ opacity: 0, height: 0 }}
603
+ className="space-y-6 text-bolt-elements-textSecondary"
604
+ >
605
+ <div className="p-4 rounded-lg bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20">
606
+ <p className="font-medium text-purple-500">
607
+ Update available from {isLatestBranch ? 'main' : 'stable'} branch!
608
+ </p>
609
+ <div className="mt-2 space-y-1">
610
+ <p>
611
+ Current: {updateInfo.currentVersion} ({updateInfo.currentCommit})
612
+ </p>
613
+ <p>
614
+ Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit})
615
+ </p>
616
+ </div>
617
+ </div>
618
+
619
+ <div>
620
+ <h4 className="text-base font-medium text-bolt-elements-textPrimary mb-3">To update:</h4>
621
+ <ol className="space-y-4">
622
+ <li className="flex items-start gap-3">
623
+ <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
624
+ 1
625
+ </div>
626
+ <div>
627
+ <p className="font-medium text-bolt-elements-textPrimary">Pull the latest changes:</p>
628
+ <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
629
+ git pull upstream {isLatestBranch ? 'main' : 'stable'}
630
+ </code>
631
+ </div>
632
+ </li>
633
+ <li className="flex items-start gap-3">
634
+ <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
635
+ 2
636
+ </div>
637
+ <div>
638
+ <p className="font-medium text-bolt-elements-textPrimary">Install dependencies:</p>
639
+ <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
640
+ pnpm install
641
+ </code>
642
+ </div>
643
+ </li>
644
+ <li className="flex items-start gap-3">
645
+ <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
646
+ 3
647
+ </div>
648
+ <div>
649
+ <p className="font-medium text-bolt-elements-textPrimary">Build the application:</p>
650
+ <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
651
+ pnpm build
652
+ </code>
653
+ </div>
654
+ </li>
655
+ <li className="flex items-start gap-3">
656
+ <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
657
+ 4
658
+ </div>
659
+ <p className="font-medium text-bolt-elements-textPrimary">Restart the application</p>
660
+ </li>
661
+ </ol>
662
+ </div>
663
+ </motion.div>
664
+ )}
665
+ </AnimatePresence>
666
+
667
+ {/* Changelog */}
668
+ {updateInfo.changelog && updateInfo.changelog.length > 0 && (
669
+ <div className="mt-4">
670
  <button
671
+ onClick={() => setShowChangelog(!showChangelog)}
672
+ className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary hover:text-purple-500 transition-colors"
673
  >
674
+ <div className={`i-ph:${showChangelog ? 'caret-up' : 'caret-down'} w-4 h-4`} />
675
+ {showChangelog ? 'Hide Changelog' : 'View Changelog'}
676
  </button>
677
+
678
+ <AnimatePresence>
679
+ {showChangelog && (
680
+ <motion.div
681
+ initial={{ opacity: 0, height: 0 }}
682
+ animate={{ opacity: 1, height: 'auto' }}
683
+ exit={{ opacity: 0, height: 0 }}
684
+ className="mt-4 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
685
+ >
686
+ <div className="max-h-[400px] overflow-y-auto">
687
+ {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
688
+ <div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
689
+ <div className="p-3 bg-bolt-elements-bg-depth-4">
690
+ <h5 className="text-sm font-medium text-bolt-elements-textPrimary">
691
+ {category}
692
+ <span className="ml-2 text-xs text-bolt-elements-textSecondary">
693
+ ({messages.length})
694
+ </span>
695
+ </h5>
696
+ </div>
697
+ <div className="divide-y divide-bolt-elements-borderColor">
698
+ {messages.map((message, index) => {
699
+ const { title, description, prNumber } = parseCommitMessage(message);
700
+ return (
701
+ <div key={index} className="p-3 hover:bg-bolt-elements-bg-depth-4 transition-colors">
702
+ <div className="flex items-start gap-3">
703
+ <div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-bolt-elements-textSecondary" />
704
+ <div className="space-y-1 flex-1">
705
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">
706
+ {title}
707
+ {prNumber && (
708
+ <span className="ml-2 text-xs text-bolt-elements-textSecondary">
709
+ #{prNumber}
710
+ </span>
711
+ )}
712
+ </p>
713
+ {description && (
714
+ <p className="text-xs text-bolt-elements-textSecondary">{description}</p>
715
+ )}
716
+ </div>
717
+ </div>
718
+ </div>
719
+ );
720
+ })}
721
+ </div>
722
+ </div>
723
+ ))}
724
+ </div>
725
+ </motion.div>
726
+ )}
727
+ </AnimatePresence>
728
+ </div>
729
+ )}
730
+ </div>
731
+ </motion.div>
732
+ )}
733
+
734
+ {/* Update Progress */}
735
+ {isUpdating && updateInfo?.downloadProgress !== undefined && (
736
+ <motion.div
737
+ className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
738
+ initial={{ opacity: 0, y: 20 }}
739
+ animate={{ opacity: 1, y: 0 }}
740
+ transition={{ duration: 0.3 }}
741
+ >
742
+ <div className="space-y-4">
743
+ <div className="flex items-center justify-between">
744
+ <span className="text-sm text-bolt-elements-textPrimary">Downloading Update</span>
745
+ <span className="text-sm text-bolt-elements-textSecondary">
746
+ {Math.round(updateInfo.downloadProgress)}%
747
+ </span>
748
+ </div>
749
+ <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
750
+ <div
751
+ className="h-full bg-purple-500 transition-all duration-300"
752
+ style={{ width: `${updateInfo.downloadProgress}%` }}
753
+ />
754
  </div>
755
+ {retryCount > 0 && <p className="text-sm text-yellow-500">Retry attempt {retryCount}/3...</p>}
756
  </div>
757
+ </motion.div>
758
+ )}
759
  </div>
760
  );
761
  };
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
 
2
  import { motion } from 'framer-motion';
3
- import { useState } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { DialogTitle } from '~/components/ui/Dialog';
6
  import { Switch } from '~/components/ui/Switch';
@@ -117,6 +118,24 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
117
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
118
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  const handleDeveloperModeChange = (checked: boolean) => {
121
  setDeveloperMode(checked);
122
  };
@@ -127,7 +146,14 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
127
 
128
  // Only show tabs that are assigned to the user window AND are visible
129
  const visibleUserTabs = tabConfiguration.userTabs
130
- .filter((tab: TabVisibilityConfig) => tab.window === 'user' && tab.visible)
 
 
 
 
 
 
 
131
  .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
132
 
133
  const moveTab = (dragIndex: number, hoverIndex: number) => {
@@ -240,6 +266,142 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
240
  }
241
  };
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  return (
244
  <>
245
  <DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
@@ -273,64 +435,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
273
  transition={{ duration: 0.2 }}
274
  >
275
  {/* Header */}
276
- <div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
277
- <div className="flex items-center gap-3">
278
- {activeTab ? (
279
- <motion.button
280
- onClick={handleBack}
281
- className={classNames(
282
- 'flex items-center justify-center w-8 h-8 rounded-lg',
283
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
284
- 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
285
- 'group transition-all duration-200',
286
- )}
287
- whileHover={{ scale: 1.05 }}
288
- whileTap={{ scale: 0.95 }}
289
- >
290
- <div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
291
- </motion.button>
292
- ) : (
293
- <motion.div
294
- className="i-ph:lightning-fill w-5 h-5 text-purple-500"
295
- initial={{ rotate: -10 }}
296
- animate={{ rotate: 10 }}
297
- transition={{
298
- repeat: Infinity,
299
- repeatType: 'reverse',
300
- duration: 2,
301
- ease: 'easeInOut',
302
- }}
303
- />
304
- )}
305
- <DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
306
- {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
307
- </DialogTitle>
308
- </div>
309
- <div className="flex items-center gap-3">
310
- <div className="flex items-center gap-2">
311
- <Switch
312
- checked={developerMode}
313
- onCheckedChange={handleDeveloperModeChange}
314
- className="data-[state=checked]:bg-purple-500"
315
- aria-label="Toggle developer mode"
316
- />
317
- <label className="text-sm text-bolt-elements-textSecondary">Developer Mode</label>
318
- </div>
319
- <motion.button
320
- onClick={onClose}
321
- className={classNames(
322
- 'flex items-center justify-center w-8 h-8 rounded-lg',
323
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
324
- 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
325
- 'group transition-all duration-200',
326
- )}
327
- whileHover={{ scale: 1.05 }}
328
- whileTap={{ scale: 0.95 }}
329
- >
330
- <div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
331
- </motion.button>
332
- </div>
333
- </div>
334
 
335
  {/* Content */}
336
  <div
 
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
  import { motion } from 'framer-motion';
4
+ import { useState, useEffect } from 'react';
5
  import { classNames } from '~/utils/classNames';
6
  import { DialogTitle } from '~/components/ui/Dialog';
7
  import { Switch } from '~/components/ui/Switch';
 
118
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
119
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
120
 
121
+ const [profile, setProfile] = useState(() => {
122
+ const saved = localStorage.getItem('bolt_user_profile');
123
+ return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
124
+ });
125
+
126
+ useEffect(() => {
127
+ const handleStorageChange = (e: StorageEvent) => {
128
+ if (e.key === 'bolt_user_profile') {
129
+ const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
130
+ setProfile(newProfile);
131
+ }
132
+ };
133
+
134
+ window.addEventListener('storage', handleStorageChange);
135
+
136
+ return () => window.removeEventListener('storage', handleStorageChange);
137
+ }, []);
138
+
139
  const handleDeveloperModeChange = (checked: boolean) => {
140
  setDeveloperMode(checked);
141
  };
 
146
 
147
  // Only show tabs that are assigned to the user window AND are visible
148
  const visibleUserTabs = tabConfiguration.userTabs
149
+ .filter((tab) => {
150
+ // Hide notifications tab if notifications are disabled
151
+ if (tab.id === 'notifications' && !profile.notifications) {
152
+ return false;
153
+ }
154
+
155
+ return tab.visible;
156
+ })
157
  .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
158
 
159
  const moveTab = (dragIndex: number, hoverIndex: number) => {
 
266
  }
267
  };
268
 
269
+ const renderHeader = () => (
270
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
271
+ <div className="flex items-center space-x-4">
272
+ {activeTab ? (
273
+ <button
274
+ onClick={handleBack}
275
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
276
+ >
277
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
278
+ </button>
279
+ ) : (
280
+ <motion.div
281
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
282
+ initial={{ rotate: -10 }}
283
+ animate={{ rotate: 10 }}
284
+ transition={{
285
+ repeat: Infinity,
286
+ repeatType: 'reverse',
287
+ duration: 2,
288
+ ease: 'easeInOut',
289
+ }}
290
+ />
291
+ )}
292
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
293
+ {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
294
+ </DialogTitle>
295
+ </div>
296
+
297
+ <div className="flex items-center space-x-4">
298
+ <div className="flex items-center gap-2">
299
+ <Switch
300
+ checked={developerMode}
301
+ onCheckedChange={handleDeveloperModeChange}
302
+ className="data-[state=checked]:bg-purple-500"
303
+ aria-label="Toggle developer mode"
304
+ />
305
+ <label className="text-sm text-gray-500 dark:text-gray-400">Developer Mode</label>
306
+ </div>
307
+
308
+ <DropdownMenu.Root>
309
+ <DropdownMenu.Trigger asChild>
310
+ <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
311
+ {profile.avatar ? (
312
+ <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
313
+ ) : (
314
+ <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
315
+ <svg
316
+ className="w-5 h-5 text-gray-500 dark:text-gray-400"
317
+ fill="none"
318
+ stroke="currentColor"
319
+ viewBox="0 0 24 24"
320
+ >
321
+ <path
322
+ strokeLinecap="round"
323
+ strokeLinejoin="round"
324
+ strokeWidth={2}
325
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
326
+ />
327
+ </svg>
328
+ </div>
329
+ )}
330
+ </button>
331
+ </DropdownMenu.Trigger>
332
+
333
+ <DropdownMenu.Portal>
334
+ <DropdownMenu.Content
335
+ className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-50 animate-in fade-in-0 zoom-in-95"
336
+ sideOffset={5}
337
+ align="end"
338
+ >
339
+ <DropdownMenu.Item
340
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
341
+ onSelect={() => handleTabClick('profile')}
342
+ >
343
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
344
+ <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
345
+ </div>
346
+ <span className="group-hover:text-purple-500 transition-colors">Profile</span>
347
+ </DropdownMenu.Item>
348
+
349
+ <DropdownMenu.Item
350
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
351
+ onSelect={() => handleTabClick('settings')}
352
+ >
353
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
354
+ <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
355
+ </div>
356
+ <span className="group-hover:text-purple-500 transition-colors">Settings</span>
357
+ </DropdownMenu.Item>
358
+
359
+ {profile.notifications && (
360
+ <>
361
+ <DropdownMenu.Item
362
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
363
+ onSelect={() => handleTabClick('notifications')}
364
+ >
365
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
366
+ <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
367
+ </div>
368
+ <span className="group-hover:text-purple-500 transition-colors">
369
+ Notifications
370
+ {hasUnreadNotifications && (
371
+ <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
372
+ {unreadNotifications.length}
373
+ </span>
374
+ )}
375
+ </span>
376
+ </DropdownMenu.Item>
377
+
378
+ <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
379
+ </>
380
+ )}
381
+
382
+ <DropdownMenu.Item
383
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
384
+ onSelect={onClose}
385
+ >
386
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
387
+ <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
388
+ </div>
389
+ <span className="group-hover:text-purple-500 transition-colors">Close</span>
390
+ </DropdownMenu.Item>
391
+ </DropdownMenu.Content>
392
+ </DropdownMenu.Portal>
393
+ </DropdownMenu.Root>
394
+
395
+ <button
396
+ onClick={onClose}
397
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
398
+ >
399
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
400
+ </button>
401
+ </div>
402
+ </div>
403
+ );
404
+
405
  return (
406
  <>
407
  <DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
 
435
  transition={{ duration: 0.2 }}
436
  >
437
  {/* Header */}
438
+ {renderHeader()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  {/* Content */}
441
  <div
app/lib/api/notifications.ts CHANGED
@@ -1,40 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  export interface Notification {
2
  id: string;
3
  title: string;
4
  message: string;
5
- type: 'info' | 'warning' | 'error' | 'success';
6
  read: boolean;
7
  timestamp: string;
 
 
 
 
 
8
  }
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  export const getNotifications = async (): Promise<Notification[]> => {
11
- /*
12
- * TODO: Implement actual notifications logic
13
- * This is a mock implementation
14
- */
15
- return [
16
- {
17
- id: 'notif-1',
18
- title: 'Welcome to Bolt',
19
- message: 'Get started by exploring the features',
20
- type: 'info',
21
- read: true,
22
- timestamp: new Date().toISOString(),
23
- },
24
- {
25
- id: 'notif-2',
26
- title: 'New Update Available',
27
- message: 'Version 1.0.1 is now available',
28
- type: 'info',
29
- read: false,
30
- timestamp: new Date().toISOString(),
31
- },
32
- ];
33
  };
34
 
35
  export const markNotificationRead = async (notificationId: string): Promise<void> => {
36
- /*
37
- * TODO: Implement actual notification read logic
38
- */
39
- console.log(`Marking notification ${notificationId} as read`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  };
 
1
+ import { logStore, type LogEntry } from '~/lib/stores/logs';
2
+
3
+ export type NotificationType = 'info' | 'warning' | 'error' | 'success' | 'update';
4
+
5
+ export interface NotificationDetails {
6
+ type?: string;
7
+ message?: string;
8
+ currentVersion?: string;
9
+ latestVersion?: string;
10
+ branch?: string;
11
+ updateUrl?: string;
12
+ }
13
+
14
  export interface Notification {
15
  id: string;
16
  title: string;
17
  message: string;
18
+ type: NotificationType;
19
  read: boolean;
20
  timestamp: string;
21
+ details?: NotificationDetails;
22
+ }
23
+
24
+ interface LogEntryWithRead extends LogEntry {
25
+ read?: boolean;
26
  }
27
 
28
+ const mapLogToNotification = (log: LogEntryWithRead): Notification => {
29
+ const type: NotificationType =
30
+ log.details?.type === 'update'
31
+ ? 'update'
32
+ : log.level === 'error'
33
+ ? 'error'
34
+ : log.level === 'warning'
35
+ ? 'warning'
36
+ : 'info';
37
+
38
+ const baseNotification: Notification = {
39
+ id: log.id,
40
+ title: log.category.charAt(0).toUpperCase() + log.category.slice(1),
41
+ message: log.message,
42
+ type,
43
+ read: log.read || false,
44
+ timestamp: log.timestamp,
45
+ };
46
+
47
+ if (log.details) {
48
+ return {
49
+ ...baseNotification,
50
+ details: log.details as NotificationDetails,
51
+ };
52
+ }
53
+
54
+ return baseNotification;
55
+ };
56
+
57
  export const getNotifications = async (): Promise<Notification[]> => {
58
+ const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
59
+
60
+ return logs
61
+ .filter((log) => {
62
+ if (log.details?.type === 'update') {
63
+ return true;
64
+ }
65
+
66
+ return log.level === 'error' || log.level === 'warning';
67
+ })
68
+ .map(mapLogToNotification)
69
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
 
 
 
 
 
 
 
 
 
 
70
  };
71
 
72
  export const markNotificationRead = async (notificationId: string): Promise<void> => {
73
+ logStore.markAsRead(notificationId);
74
+ };
75
+
76
+ export const clearNotifications = async (): Promise<void> => {
77
+ logStore.clearLogs();
78
+ };
79
+
80
+ export const getUnreadCount = (): number => {
81
+ const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
82
+
83
+ return logs.filter((log) => {
84
+ if (!log.read) {
85
+ if (log.details?.type === 'update') {
86
+ return true;
87
+ }
88
+
89
+ return log.level === 'error' || log.level === 'warning';
90
+ }
91
+
92
+ return false;
93
+ }).length;
94
  };
app/lib/hooks/useNotifications.ts CHANGED
@@ -1,34 +1,17 @@
1
  import { useState, useEffect } from 'react';
2
  import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
3
-
4
- const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
5
-
6
- const getReadNotifications = (): string[] => {
7
- try {
8
- const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
9
- return stored ? JSON.parse(stored) : [];
10
- } catch {
11
- return [];
12
- }
13
- };
14
-
15
- const setReadNotifications = (notificationIds: string[]) => {
16
- try {
17
- localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
18
- } catch (error) {
19
- console.error('Failed to persist read notifications:', error);
20
- }
21
- };
22
 
23
  export const useNotifications = () => {
24
  const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
25
  const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
26
- const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
27
 
28
  const checkNotifications = async () => {
29
  try {
30
  const notifications = await getNotifications();
31
- const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
32
  setUnreadNotifications(unread);
33
  setHasUnreadNotifications(unread.length > 0);
34
  } catch (error) {
@@ -43,17 +26,12 @@ export const useNotifications = () => {
43
  const interval = setInterval(checkNotifications, 60 * 1000);
44
 
45
  return () => clearInterval(interval);
46
- }, [readNotificationIds]);
47
 
48
  const markAsRead = async (notificationId: string) => {
49
  try {
50
  await markNotificationRead(notificationId);
51
-
52
- const newReadIds = [...readNotificationIds, notificationId];
53
- setReadNotificationIds(newReadIds);
54
- setReadNotifications(newReadIds);
55
- setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
56
- setHasUnreadNotifications(unreadNotifications.length > 1);
57
  } catch (error) {
58
  console.error('Failed to mark notification as read:', error);
59
  }
@@ -61,13 +39,9 @@ export const useNotifications = () => {
61
 
62
  const markAllAsRead = async () => {
63
  try {
64
- await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
65
-
66
- const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
67
- setReadNotificationIds(newReadIds);
68
- setReadNotifications(newReadIds);
69
- setUnreadNotifications([]);
70
- setHasUnreadNotifications(false);
71
  } catch (error) {
72
  console.error('Failed to mark all notifications as read:', error);
73
  }
 
1
  import { useState, useEffect } from 'react';
2
  import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
3
+ import { logStore } from '~/lib/stores/logs';
4
+ import { useStore } from '@nanostores/react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  export const useNotifications = () => {
7
  const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
8
  const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
9
+ const logs = useStore(logStore.logs);
10
 
11
  const checkNotifications = async () => {
12
  try {
13
  const notifications = await getNotifications();
14
+ const unread = notifications.filter((n) => !logStore.isRead(n.id));
15
  setUnreadNotifications(unread);
16
  setHasUnreadNotifications(unread.length > 0);
17
  } catch (error) {
 
26
  const interval = setInterval(checkNotifications, 60 * 1000);
27
 
28
  return () => clearInterval(interval);
29
+ }, [logs]); // Re-run when logs change
30
 
31
  const markAsRead = async (notificationId: string) => {
32
  try {
33
  await markNotificationRead(notificationId);
34
+ await checkNotifications();
 
 
 
 
 
35
  } catch (error) {
36
  console.error('Failed to mark notification as read:', error);
37
  }
 
39
 
40
  const markAllAsRead = async () => {
41
  try {
42
+ const notifications = await getNotifications();
43
+ await Promise.all(notifications.map((n) => markNotificationRead(n.id)));
44
+ await checkNotifications();
 
 
 
 
45
  } catch (error) {
46
  console.error('Failed to mark all notifications as read:', error);
47
  }
app/lib/stores/logs.ts CHANGED
@@ -19,12 +19,25 @@ export interface LogEntry {
19
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
20
 
21
  class LogStore {
 
 
 
 
 
 
 
22
  private _logs = map<Record<string, LogEntry>>({});
23
  showLogs = atom(true);
 
24
 
25
  constructor() {
26
  // Load saved logs from cookies on initialization
27
  this._loadLogs();
 
 
 
 
 
28
  }
29
 
30
  // Expose the logs store for subscription
@@ -45,11 +58,36 @@ class LogStore {
45
  }
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  private _saveLogs() {
49
  const currentLogs = this._logs.get();
50
  Cookies.set('eventLogs', JSON.stringify(currentLogs));
51
  }
52
 
 
 
 
 
 
 
 
 
53
  private _generateId(): string {
54
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
55
  }
@@ -210,6 +248,20 @@ class LogStore {
210
  return matchesLevel && matchesCategory && matchesSearch;
211
  });
212
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
 
215
  export const logStore = new LogStore();
 
19
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
20
 
21
  class LogStore {
22
+ logInfo(message: string, details: { type: string; message: string }) {
23
+ return this.addLog(message, 'info', 'system', details);
24
+ }
25
+
26
+ logSuccess(message: string, details: { type: string; message: string }) {
27
+ return this.addLog(message, 'info', 'system', { ...details, success: true });
28
+ }
29
  private _logs = map<Record<string, LogEntry>>({});
30
  showLogs = atom(true);
31
+ private _readLogs = new Set<string>();
32
 
33
  constructor() {
34
  // Load saved logs from cookies on initialization
35
  this._loadLogs();
36
+
37
+ // Only load read logs in browser environment
38
+ if (typeof window !== 'undefined') {
39
+ this._loadReadLogs();
40
+ }
41
  }
42
 
43
  // Expose the logs store for subscription
 
58
  }
59
  }
60
 
61
+ private _loadReadLogs() {
62
+ if (typeof window === 'undefined') {
63
+ return;
64
+ }
65
+
66
+ const savedReadLogs = localStorage.getItem('bolt_read_logs');
67
+
68
+ if (savedReadLogs) {
69
+ try {
70
+ const parsedReadLogs = JSON.parse(savedReadLogs);
71
+ this._readLogs = new Set(parsedReadLogs);
72
+ } catch (error) {
73
+ logger.error('Failed to parse read logs:', error);
74
+ }
75
+ }
76
+ }
77
+
78
  private _saveLogs() {
79
  const currentLogs = this._logs.get();
80
  Cookies.set('eventLogs', JSON.stringify(currentLogs));
81
  }
82
 
83
+ private _saveReadLogs() {
84
+ if (typeof window === 'undefined') {
85
+ return;
86
+ }
87
+
88
+ localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs)));
89
+ }
90
+
91
  private _generateId(): string {
92
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
93
  }
 
248
  return matchesLevel && matchesCategory && matchesSearch;
249
  });
250
  }
251
+
252
+ markAsRead(logId: string) {
253
+ this._readLogs.add(logId);
254
+ this._saveReadLogs();
255
+ }
256
+
257
+ isRead(logId: string): boolean {
258
+ return this._readLogs.has(logId);
259
+ }
260
+
261
+ clearReadLogs() {
262
+ this._readLogs.clear();
263
+ this._saveReadLogs();
264
+ }
265
  }
266
 
267
  export const logStore = new LogStore();
package.json CHANGED
@@ -90,6 +90,7 @@
90
  "js-cookie": "^3.0.5",
91
  "jszip": "^3.10.1",
92
  "nanostores": "^0.10.3",
 
93
  "ollama-ai-provider": "^0.15.2",
94
  "react": "^18.3.1",
95
  "react-dnd": "^16.0.1",
 
90
  "js-cookie": "^3.0.5",
91
  "jszip": "^3.10.1",
92
  "nanostores": "^0.10.3",
93
+ "next": "^15.1.5",
94
  "ollama-ai-provider": "^0.15.2",
95
  "react": "^18.3.1",
96
  "react-dnd": "^16.0.1",
pnpm-lock.yaml CHANGED
@@ -191,6 +191,9 @@ importers:
191
  nanostores:
192
  specifier: ^0.10.3
193
  version: 0.10.3
 
 
 
194
  ollama-ai-provider:
195
  specifier: ^0.15.2
196
  version: 0.15.2([email protected])
@@ -849,6 +852,9 @@ packages:
849
  resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
850
  engines: {node: '>=12'}
851
 
 
 
 
852
  '@emotion/[email protected]':
853
  resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
854
 
@@ -1506,6 +1512,111 @@ packages:
1506
  '@iconify/[email protected]':
1507
  resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
1508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1509
  '@isaacs/[email protected]':
1510
  resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
1511
  engines: {node: '>=12'}
@@ -1577,6 +1688,57 @@ packages:
1577
  nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0
1578
  react: '>=18.0.0'
1579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1580
  '@nodelib/[email protected]':
1581
  resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
1582
  engines: {node: '>= 8'}
@@ -2475,6 +2637,9 @@ packages:
2475
  peerDependencies:
2476
  eslint: '>=8.40.0'
2477
 
 
 
 
2478
  '@swc/[email protected]':
2479
  resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
2480
 
@@ -2991,6 +3156,10 @@ packages:
2991
  peerDependencies:
2992
  esbuild: '>=0.18'
2993
 
 
 
 
 
2994
2995
  resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
2996
  engines: {node: '>= 0.8'}
@@ -3104,6 +3273,13 @@ packages:
3104
3105
  resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
3106
 
 
 
 
 
 
 
 
3107
3108
  resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
3109
 
@@ -3305,6 +3481,10 @@ packages:
3305
  resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
3306
  engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
3307
 
 
 
 
 
3308
3309
  resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
3310
 
@@ -3961,6 +4141,9 @@ packages:
3961
  resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
3962
  engines: {node: '>= 0.4'}
3963
 
 
 
 
3964
3965
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
3966
  engines: {node: '>=8'}
@@ -4650,6 +4833,27 @@ packages:
4650
  resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
4651
  engines: {node: '>= 0.6'}
4652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4653
4654
  resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
4655
  engines: {node: '>=10.5.0'}
@@ -4942,6 +5146,10 @@ packages:
4942
4943
  resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
4944
 
 
 
 
 
4945
4946
  resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
4947
  engines: {node: ^10 || ^12 || >=14}
@@ -5496,6 +5704,10 @@ packages:
5496
  resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
5497
  hasBin: true
5498
 
 
 
 
 
5499
5500
  resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
5501
  engines: {node: '>=8'}
@@ -5527,6 +5739,9 @@ packages:
5527
5528
  resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
5529
 
 
 
 
5530
5531
  resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
5532
  engines: {node: '>= 10'}
@@ -5598,6 +5813,10 @@ packages:
5598
5599
  resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
5600
 
 
 
 
 
5601
5602
  resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
5603
 
@@ -5650,6 +5869,19 @@ packages:
5650
5651
  resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
5652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5653
5654
  resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
5655
  engines: {node: '>=8'}
@@ -7161,6 +7393,11 @@ snapshots:
7161
  dependencies:
7162
  '@jridgewell/trace-mapping': 0.3.9
7163
 
 
 
 
 
 
7164
  '@emotion/[email protected]': {}
7165
 
7166
  '@esbuild-plugins/[email protected]([email protected])':
@@ -7556,6 +7793,81 @@ snapshots:
7556
  transitivePeerDependencies:
7557
  - supports-color
7558
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7559
  '@isaacs/[email protected]':
7560
  dependencies:
7561
  string-width: 5.1.2
@@ -7673,6 +7985,32 @@ snapshots:
7673
  nanostores: 0.10.3
7674
  react: 18.3.1
7675
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7676
  '@nodelib/[email protected]':
7677
  dependencies:
7678
  '@nodelib/fs.stat': 2.0.5
@@ -8739,6 +9077,8 @@ snapshots:
8739
  - supports-color
8740
  - typescript
8741
 
 
 
8742
  '@swc/[email protected]':
8743
  dependencies:
8744
  tslib: 2.8.1
@@ -9432,6 +9772,10 @@ snapshots:
9432
  esbuild: 0.23.1
9433
  load-tsconfig: 0.2.5
9434
 
 
 
 
 
9435
9436
 
9437
@@ -9546,6 +9890,18 @@ snapshots:
9546
 
9547
9548
 
 
 
 
 
 
 
 
 
 
 
 
 
9549
9550
 
9551
@@ -9711,6 +10067,9 @@ snapshots:
9711
 
9712
9713
 
 
 
 
9714
9715
 
9716
@@ -10569,6 +10928,9 @@ snapshots:
10569
  call-bind: 1.0.7
10570
  has-tostringtag: 1.0.2
10571
 
 
 
 
10572
10573
  dependencies:
10574
  binary-extensions: 2.3.0
@@ -11605,6 +11967,32 @@ snapshots:
11605
 
11606
11607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11608
11609
 
11610
@@ -11929,6 +12317,12 @@ snapshots:
11929
 
11930
11931
 
 
 
 
 
 
 
11932
11933
  dependencies:
11934
  nanoid: 3.3.8
@@ -12512,6 +12906,33 @@ snapshots:
12512
  inherits: 2.0.4
12513
  safe-buffer: 5.2.1
12514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12515
12516
  dependencies:
12517
  shebang-regex: 3.0.0
@@ -12548,6 +12969,11 @@ snapshots:
12548
  once: 1.4.0
12549
  simple-concat: 1.0.1
12550
 
 
 
 
 
 
12551
12552
  dependencies:
12553
  '@polka/url': 1.0.0-next.28
@@ -12616,6 +13042,8 @@ snapshots:
12616
 
12617
12618
 
 
 
12619
12620
 
12621
@@ -12669,6 +13097,13 @@ snapshots:
12669
  dependencies:
12670
  inline-style-parser: 0.2.4
12671
 
 
 
 
 
 
 
 
12672
12673
  dependencies:
12674
  has-flag: 4.0.0
 
191
  nanostores:
192
  specifier: ^0.10.3
193
  version: 0.10.3
194
+ next:
195
+ specifier: ^15.1.5
196
197
  ollama-ai-provider:
198
  specifier: ^0.15.2
199
  version: 0.15.2([email protected])
 
852
  resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
853
  engines: {node: '>=12'}
854
 
855
+ '@emnapi/[email protected]':
856
+ resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
857
+
858
  '@emotion/[email protected]':
859
  resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
860
 
 
1512
  '@iconify/[email protected]':
1513
  resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
1514
 
1515
+ '@img/[email protected]':
1516
+ resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
1517
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1518
+ cpu: [arm64]
1519
+ os: [darwin]
1520
+
1521
+ '@img/[email protected]':
1522
+ resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
1523
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1524
+ cpu: [x64]
1525
+ os: [darwin]
1526
+
1527
+ '@img/[email protected]':
1528
+ resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
1529
+ cpu: [arm64]
1530
+ os: [darwin]
1531
+
1532
+ '@img/[email protected]':
1533
+ resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
1534
+ cpu: [x64]
1535
+ os: [darwin]
1536
+
1537
+ '@img/[email protected]':
1538
+ resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
1539
+ cpu: [arm64]
1540
+ os: [linux]
1541
+
1542
+ '@img/[email protected]':
1543
+ resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
1544
+ cpu: [arm]
1545
+ os: [linux]
1546
+
1547
+ '@img/[email protected]':
1548
+ resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
1549
+ cpu: [s390x]
1550
+ os: [linux]
1551
+
1552
+ '@img/[email protected]':
1553
+ resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
1554
+ cpu: [x64]
1555
+ os: [linux]
1556
+
1557
+ '@img/[email protected]':
1558
+ resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
1559
+ cpu: [arm64]
1560
+ os: [linux]
1561
+
1562
+ '@img/[email protected]':
1563
+ resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
1564
+ cpu: [x64]
1565
+ os: [linux]
1566
+
1567
+ '@img/[email protected]':
1568
+ resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
1569
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1570
+ cpu: [arm64]
1571
+ os: [linux]
1572
+
1573
+ '@img/[email protected]':
1574
+ resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
1575
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1576
+ cpu: [arm]
1577
+ os: [linux]
1578
+
1579
+ '@img/[email protected]':
1580
+ resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
1581
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1582
+ cpu: [s390x]
1583
+ os: [linux]
1584
+
1585
+ '@img/[email protected]':
1586
+ resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
1587
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1588
+ cpu: [x64]
1589
+ os: [linux]
1590
+
1591
+ '@img/[email protected]':
1592
+ resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
1593
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1594
+ cpu: [arm64]
1595
+ os: [linux]
1596
+
1597
+ '@img/[email protected]':
1598
+ resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
1599
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1600
+ cpu: [x64]
1601
+ os: [linux]
1602
+
1603
+ '@img/[email protected]':
1604
+ resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
1605
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1606
+ cpu: [wasm32]
1607
+
1608
+ '@img/[email protected]':
1609
+ resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
1610
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1611
+ cpu: [ia32]
1612
+ os: [win32]
1613
+
1614
+ '@img/[email protected]':
1615
+ resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
1616
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
1617
+ cpu: [x64]
1618
+ os: [win32]
1619
+
1620
  '@isaacs/[email protected]':
1621
  resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
1622
  engines: {node: '>=12'}
 
1688
  nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0
1689
  react: '>=18.0.0'
1690
 
1691
+ '@next/[email protected]':
1692
+ resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==}
1693
+
1694
+ '@next/[email protected]':
1695
+ resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==}
1696
+ engines: {node: '>= 10'}
1697
+ cpu: [arm64]
1698
+ os: [darwin]
1699
+
1700
+ '@next/[email protected]':
1701
+ resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==}
1702
+ engines: {node: '>= 10'}
1703
+ cpu: [x64]
1704
+ os: [darwin]
1705
+
1706
+ '@next/[email protected]':
1707
+ resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
1708
+ engines: {node: '>= 10'}
1709
+ cpu: [arm64]
1710
+ os: [linux]
1711
+
1712
+ '@next/[email protected]':
1713
+ resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
1714
+ engines: {node: '>= 10'}
1715
+ cpu: [arm64]
1716
+ os: [linux]
1717
+
1718
+ '@next/[email protected]':
1719
+ resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
1720
+ engines: {node: '>= 10'}
1721
+ cpu: [x64]
1722
+ os: [linux]
1723
+
1724
+ '@next/[email protected]':
1725
+ resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
1726
+ engines: {node: '>= 10'}
1727
+ cpu: [x64]
1728
+ os: [linux]
1729
+
1730
+ '@next/[email protected]':
1731
+ resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==}
1732
+ engines: {node: '>= 10'}
1733
+ cpu: [arm64]
1734
+ os: [win32]
1735
+
1736
+ '@next/[email protected]':
1737
+ resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==}
1738
+ engines: {node: '>= 10'}
1739
+ cpu: [x64]
1740
+ os: [win32]
1741
+
1742
  '@nodelib/[email protected]':
1743
  resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
1744
  engines: {node: '>= 8'}
 
2637
  peerDependencies:
2638
  eslint: '>=8.40.0'
2639
 
2640
+ '@swc/[email protected]':
2641
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
2642
+
2643
  '@swc/[email protected]':
2644
  resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
2645
 
 
3156
  peerDependencies:
3157
  esbuild: '>=0.18'
3158
 
3159
3160
+ resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
3161
+ engines: {node: '>=10.16.0'}
3162
+
3163
3164
  resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
3165
  engines: {node: '>= 0.8'}
 
3273
3274
  resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
3275
 
3276
3277
+ resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
3278
+
3279
3280
+ resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
3281
+ engines: {node: '>=12.5.0'}
3282
+
3283
3284
  resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
3285
 
 
3481
  resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
3482
  engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
3483
 
3484
3485
+ resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
3486
+ engines: {node: '>=8'}
3487
+
3488
3489
  resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
3490
 
 
4141
  resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
4142
  engines: {node: '>= 0.4'}
4143
 
4144
4145
+ resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
4146
+
4147
4148
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
4149
  engines: {node: '>=8'}
 
4833
  resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
4834
  engines: {node: '>= 0.6'}
4835
 
4836
4837
+ resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==}
4838
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
4839
+ hasBin: true
4840
+ peerDependencies:
4841
+ '@opentelemetry/api': ^1.1.0
4842
+ '@playwright/test': ^1.41.2
4843
+ babel-plugin-react-compiler: '*'
4844
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
4845
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
4846
+ sass: ^1.3.0
4847
+ peerDependenciesMeta:
4848
+ '@opentelemetry/api':
4849
+ optional: true
4850
+ '@playwright/test':
4851
+ optional: true
4852
+ babel-plugin-react-compiler:
4853
+ optional: true
4854
+ sass:
4855
+ optional: true
4856
+
4857
4858
  resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
4859
  engines: {node: '>=10.5.0'}
 
5146
5147
  resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
5148
 
5149
5150
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
5151
+ engines: {node: ^10 || ^12 || >=14}
5152
+
5153
5154
  resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
5155
  engines: {node: ^10 || ^12 || >=14}
 
5704
  resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
5705
  hasBin: true
5706
 
5707
5708
+ resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
5709
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
5710
+
5711
5712
  resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
5713
  engines: {node: '>=8'}
 
5739
5740
  resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
5741
 
5742
5743
+ resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
5744
+
5745
5746
  resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
5747
  engines: {node: '>= 10'}
 
5813
5814
  resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
5815
 
5816
5817
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
5818
+ engines: {node: '>=10.0.0'}
5819
+
5820
5821
  resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
5822
 
 
5869
5870
  resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
5871
 
5872
5873
+ resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
5874
+ engines: {node: '>= 12.0.0'}
5875
+ peerDependencies:
5876
+ '@babel/core': '*'
5877
+ babel-plugin-macros: '*'
5878
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
5879
+ peerDependenciesMeta:
5880
+ '@babel/core':
5881
+ optional: true
5882
+ babel-plugin-macros:
5883
+ optional: true
5884
+
5885
5886
  resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
5887
  engines: {node: '>=8'}
 
7393
  dependencies:
7394
  '@jridgewell/trace-mapping': 0.3.9
7395
 
7396
+ '@emnapi/[email protected]':
7397
+ dependencies:
7398
+ tslib: 2.8.1
7399
+ optional: true
7400
+
7401
  '@emotion/[email protected]': {}
7402
 
7403
  '@esbuild-plugins/[email protected]([email protected])':
 
7793
  transitivePeerDependencies:
7794
  - supports-color
7795
 
7796
+ '@img/[email protected]':
7797
+ optionalDependencies:
7798
+ '@img/sharp-libvips-darwin-arm64': 1.0.4
7799
+ optional: true
7800
+
7801
+ '@img/[email protected]':
7802
+ optionalDependencies:
7803
+ '@img/sharp-libvips-darwin-x64': 1.0.4
7804
+ optional: true
7805
+
7806
+ '@img/[email protected]':
7807
+ optional: true
7808
+
7809
+ '@img/[email protected]':
7810
+ optional: true
7811
+
7812
+ '@img/[email protected]':
7813
+ optional: true
7814
+
7815
+ '@img/[email protected]':
7816
+ optional: true
7817
+
7818
+ '@img/[email protected]':
7819
+ optional: true
7820
+
7821
+ '@img/[email protected]':
7822
+ optional: true
7823
+
7824
+ '@img/[email protected]':
7825
+ optional: true
7826
+
7827
+ '@img/[email protected]':
7828
+ optional: true
7829
+
7830
+ '@img/[email protected]':
7831
+ optionalDependencies:
7832
+ '@img/sharp-libvips-linux-arm64': 1.0.4
7833
+ optional: true
7834
+
7835
+ '@img/[email protected]':
7836
+ optionalDependencies:
7837
+ '@img/sharp-libvips-linux-arm': 1.0.5
7838
+ optional: true
7839
+
7840
+ '@img/[email protected]':
7841
+ optionalDependencies:
7842
+ '@img/sharp-libvips-linux-s390x': 1.0.4
7843
+ optional: true
7844
+
7845
+ '@img/[email protected]':
7846
+ optionalDependencies:
7847
+ '@img/sharp-libvips-linux-x64': 1.0.4
7848
+ optional: true
7849
+
7850
+ '@img/[email protected]':
7851
+ optionalDependencies:
7852
+ '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
7853
+ optional: true
7854
+
7855
+ '@img/[email protected]':
7856
+ optionalDependencies:
7857
+ '@img/sharp-libvips-linuxmusl-x64': 1.0.4
7858
+ optional: true
7859
+
7860
+ '@img/[email protected]':
7861
+ dependencies:
7862
+ '@emnapi/runtime': 1.3.1
7863
+ optional: true
7864
+
7865
+ '@img/[email protected]':
7866
+ optional: true
7867
+
7868
+ '@img/[email protected]':
7869
+ optional: true
7870
+
7871
  '@isaacs/[email protected]':
7872
  dependencies:
7873
  string-width: 5.1.2
 
7985
  nanostores: 0.10.3
7986
  react: 18.3.1
7987
 
7988
+ '@next/[email protected]': {}
7989
+
7990
+ '@next/[email protected]':
7991
+ optional: true
7992
+
7993
+ '@next/[email protected]':
7994
+ optional: true
7995
+
7996
+ '@next/[email protected]':
7997
+ optional: true
7998
+
7999
+ '@next/[email protected]':
8000
+ optional: true
8001
+
8002
+ '@next/[email protected]':
8003
+ optional: true
8004
+
8005
+ '@next/[email protected]':
8006
+ optional: true
8007
+
8008
+ '@next/[email protected]':
8009
+ optional: true
8010
+
8011
+ '@next/[email protected]':
8012
+ optional: true
8013
+
8014
  '@nodelib/[email protected]':
8015
  dependencies:
8016
  '@nodelib/fs.stat': 2.0.5
 
9077
  - supports-color
9078
  - typescript
9079
 
9080
+ '@swc/[email protected]': {}
9081
+
9082
  '@swc/[email protected]':
9083
  dependencies:
9084
  tslib: 2.8.1
 
9772
  esbuild: 0.23.1
9773
  load-tsconfig: 0.2.5
9774
 
9775
9776
+ dependencies:
9777
+ streamsearch: 1.1.0
9778
+
9779
9780
 
9781
 
9890
 
9891
9892
 
9893
9894
+ dependencies:
9895
+ color-name: 1.1.4
9896
+ simple-swizzle: 0.2.2
9897
+ optional: true
9898
+
9899
9900
+ dependencies:
9901
+ color-convert: 2.0.1
9902
+ color-string: 1.9.1
9903
+ optional: true
9904
+
9905
9906
 
9907
 
10067
 
10068
10069
 
10070
10071
+ optional: true
10072
+
10073
10074
 
10075
 
10928
  call-bind: 1.0.7
10929
  has-tostringtag: 1.0.2
10930
 
10931
10932
+ optional: true
10933
+
10934
10935
  dependencies:
10936
  binary-extensions: 2.3.0
 
11967
 
11968
11969
 
11970
11971
+ dependencies:
11972
+ '@next/env': 15.1.5
11973
+ '@swc/counter': 0.1.3
11974
+ '@swc/helpers': 0.5.15
11975
+ busboy: 1.6.0
11976
+ caniuse-lite: 1.0.30001685
11977
+ postcss: 8.4.31
11978
+ react: 18.3.1
11979
+ react-dom: 18.3.1([email protected])
11980
+ styled-jsx: 5.1.6(@babel/[email protected])([email protected])
11981
+ optionalDependencies:
11982
+ '@next/swc-darwin-arm64': 15.1.5
11983
+ '@next/swc-darwin-x64': 15.1.5
11984
+ '@next/swc-linux-arm64-gnu': 15.1.5
11985
+ '@next/swc-linux-arm64-musl': 15.1.5
11986
+ '@next/swc-linux-x64-gnu': 15.1.5
11987
+ '@next/swc-linux-x64-musl': 15.1.5
11988
+ '@next/swc-win32-arm64-msvc': 15.1.5
11989
+ '@next/swc-win32-x64-msvc': 15.1.5
11990
+ '@opentelemetry/api': 1.9.0
11991
+ sharp: 0.33.5
11992
+ transitivePeerDependencies:
11993
+ - '@babel/core'
11994
+ - babel-plugin-macros
11995
+
11996
11997
 
11998
 
12317
 
12318
12319
 
12320
12321
+ dependencies:
12322
+ nanoid: 3.3.8
12323
+ picocolors: 1.1.1
12324
+ source-map-js: 1.2.1
12325
+
12326
12327
  dependencies:
12328
  nanoid: 3.3.8
 
12906
  inherits: 2.0.4
12907
  safe-buffer: 5.2.1
12908
 
12909
12910
+ dependencies:
12911
+ color: 4.2.3
12912
+ detect-libc: 2.0.3
12913
+ semver: 7.6.3
12914
+ optionalDependencies:
12915
+ '@img/sharp-darwin-arm64': 0.33.5
12916
+ '@img/sharp-darwin-x64': 0.33.5
12917
+ '@img/sharp-libvips-darwin-arm64': 1.0.4
12918
+ '@img/sharp-libvips-darwin-x64': 1.0.4
12919
+ '@img/sharp-libvips-linux-arm': 1.0.5
12920
+ '@img/sharp-libvips-linux-arm64': 1.0.4
12921
+ '@img/sharp-libvips-linux-s390x': 1.0.4
12922
+ '@img/sharp-libvips-linux-x64': 1.0.4
12923
+ '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
12924
+ '@img/sharp-libvips-linuxmusl-x64': 1.0.4
12925
+ '@img/sharp-linux-arm': 0.33.5
12926
+ '@img/sharp-linux-arm64': 0.33.5
12927
+ '@img/sharp-linux-s390x': 0.33.5
12928
+ '@img/sharp-linux-x64': 0.33.5
12929
+ '@img/sharp-linuxmusl-arm64': 0.33.5
12930
+ '@img/sharp-linuxmusl-x64': 0.33.5
12931
+ '@img/sharp-wasm32': 0.33.5
12932
+ '@img/sharp-win32-ia32': 0.33.5
12933
+ '@img/sharp-win32-x64': 0.33.5
12934
+ optional: true
12935
+
12936
12937
  dependencies:
12938
  shebang-regex: 3.0.0
 
12969
  once: 1.4.0
12970
  simple-concat: 1.0.1
12971
 
12972
12973
+ dependencies:
12974
+ is-arrayish: 0.3.2
12975
+ optional: true
12976
+
12977
12978
  dependencies:
12979
  '@polka/url': 1.0.0-next.28
 
13042
 
13043
13044
 
13045
13046
+
13047
13048
 
13049
 
13097
  dependencies:
13098
  inline-style-parser: 0.2.4
13099
 
13100
13101
+ dependencies:
13102
+ client-only: 0.0.1
13103
+ react: 18.3.1
13104
+ optionalDependencies:
13105
+ '@babel/core': 7.26.0
13106
+
13107
13108
  dependencies:
13109
  has-flag: 4.0.0
scripts/update.sh ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ echo "Starting Bolt.DIY update process..."
7
+
8
+ # Get the current directory
9
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
10
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
11
+
12
+ # Store current version
13
+ CURRENT_VERSION=$(cat "$PROJECT_ROOT/package.json" | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
14
+
15
+ echo "Current version: $CURRENT_VERSION"
16
+ echo "Fetching latest version..."
17
+
18
+ # Create temp directory
19
+ TMP_DIR=$(mktemp -d)
20
+ cd "$TMP_DIR"
21
+
22
+ # Download latest release
23
+ LATEST_RELEASE_URL=$(curl -s https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d \")
24
+ if [ -z "$LATEST_RELEASE_URL" ]; then
25
+ echo "Error: Could not find latest release download URL"
26
+ exit 1
27
+ fi
28
+
29
+ echo "Downloading latest release..."
30
+ curl -L -o latest.zip "$LATEST_RELEASE_URL"
31
+
32
+ echo "Extracting update..."
33
+ unzip -q latest.zip
34
+
35
+ # Backup current installation
36
+ echo "Creating backup..."
37
+ BACKUP_DIR="$PROJECT_ROOT/backup_$(date +%Y%m%d_%H%M%S)"
38
+ mkdir -p "$BACKUP_DIR"
39
+ cp -r "$PROJECT_ROOT"/* "$BACKUP_DIR/"
40
+
41
+ # Install update
42
+ echo "Installing update..."
43
+ cp -r ./* "$PROJECT_ROOT/"
44
+
45
+ # Clean up
46
+ cd "$PROJECT_ROOT"
47
+ rm -rf "$TMP_DIR"
48
+
49
+ echo "Update completed successfully!"
50
+ echo "Please restart the application to apply the changes."
51
+
52
+ exit 0