This commit is contained in:
Stijnus 2025-01-21 11:55:26 +01:00
parent 436a8e54bf
commit 78d4e1bb54
13 changed files with 2011 additions and 548 deletions

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { logStore } from '~/lib/stores/logs';
import type { LogEntry } from '~/lib/stores/logs';
interface ProviderStatus {
id: string;
@ -472,51 +473,147 @@ export default function DebugTab() {
}
};
const handleCheckErrors = () => {
const checkErrors = async () => {
try {
setLoading((prev) => ({ ...prev, errors: true }));
// Get any errors from the performance entries
const resourceErrors = performance
.getEntriesByType('resource')
.filter((entry) => {
const failedEntry = entry as PerformanceResourceTiming;
return failedEntry.responseEnd - failedEntry.startTime === 0;
})
.map((entry) => ({
type: 'networkError',
resource: entry.name,
timestamp: new Date().toISOString(),
}));
// Get errors from log store
const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error');
// Combine collected errors with resource errors
const allErrors = [...errorLog.errors, ...resourceErrors];
// Combine with runtime errors
const allErrors = [
...errorLog.errors,
...storedErrors.map((error) => ({
type: 'stored',
message: error.message,
timestamp: error.timestamp,
details: error.details || {},
})),
];
if (allErrors.length > 0) {
logStore.logError('JavaScript Errors Found', {
errors: allErrors,
timestamp: new Date().toISOString(),
});
toast.error(`Found ${allErrors.length} error(s)`);
} else {
toast.success('No errors found');
}
// Update error log
setErrorLog({
errors: allErrors,
lastCheck: new Date().toISOString(),
});
if (allErrors.length === 0) {
toast.success('No errors found');
} else {
toast.warning(`Found ${allErrors.length} error(s)`);
}
} catch (error) {
toast.error('Failed to check for errors');
console.error('Failed to check for errors:', error);
toast.error('Failed to check errors');
console.error('Failed to check errors:', error);
} finally {
setLoading((prev) => ({ ...prev, errors: false }));
}
};
return (
<div className="space-y-6">
<div className="flex flex-col gap-6">
{/* Action Buttons */}
<div className="flex flex-wrap gap-4">
<button
onClick={checkProviderStatus}
disabled={loading.providers}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
{ 'opacity-50 cursor-not-allowed': loading.providers },
)}
>
{loading.providers ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:plug w-4 h-4" />
)}
Check Providers
</button>
<button
onClick={getSystemInfo}
disabled={loading.systemInfo}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2',
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
)}
>
{loading.systemInfo ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:gear w-4 h-4" />
)}
Update System Info
</button>
<button
onClick={handleLogPerformance}
disabled={loading.performance}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2',
{ 'opacity-50 cursor-not-allowed': loading.performance },
)}
>
{loading.performance ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:chart-bar w-4 h-4" />
)}
Log Performance
</button>
<button
onClick={checkErrors}
disabled={loading.errors}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2',
{ 'opacity-50 cursor-not-allowed': loading.errors },
)}
>
{loading.errors ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:warning w-4 h-4" />
)}
Check Errors
</button>
</div>
{/* Error Log Display */}
{errorLog.errors.length > 0 && (
<div className="mt-4">
<h3 className="text-lg font-semibold mb-2">Error Log</h3>
<div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
{errorLog.errors.map((error, index) => (
<div key={index} className="mb-4 last:mb-0 p-3 bg-white rounded border border-red-200">
<div className="flex items-center gap-2 text-sm text-gray-600">
<span className="font-medium">Type:</span> {error.type}
<span className="font-medium ml-4">Time:</span>
{new Date(error.timestamp).toLocaleString()}
</div>
<div className="mt-2 text-red-600">{error.message}</div>
{error.filename && (
<div className="mt-1 text-sm text-gray-500">
File: {error.filename} (Line: {error.lineNumber}, Column: {error.columnNumber})
</div>
)}
</div>
))}
</div>
</div>
)}
{/* System Information */}
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center justify-between mb-4">
@ -529,9 +626,9 @@ export default function DebugTab() {
onClick={handleLogSystemInfo}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
'transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'transition-colors duration-200',
)}
>
<div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
@ -541,10 +638,12 @@ export default function DebugTab() {
onClick={getSystemInfo}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
'transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'transition-colors duration-200',
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
)}
disabled={loading.systemInfo}
>
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
Refresh
@ -684,10 +783,12 @@ export default function DebugTab() {
onClick={checkProviderStatus}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
'transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'transition-colors duration-200',
{ 'opacity-50 cursor-not-allowed': loading.providers },
)}
disabled={loading.providers}
>
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
Refresh
@ -729,10 +830,12 @@ export default function DebugTab() {
onClick={handleLogPerformance}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
'transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'transition-colors duration-200',
{ 'opacity-50 cursor-not-allowed': loading.performance },
)}
disabled={loading.performance}
>
<div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
Log Performance
@ -811,13 +914,15 @@ export default function DebugTab() {
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
</div>
<button
onClick={handleCheckErrors}
onClick={checkErrors}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
'transition-colors',
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
'transition-colors duration-200',
{ 'opacity-50 cursor-not-allowed': loading.errors },
)}
disabled={loading.errors}
>
<div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
Check for Errors

View File

@ -1,9 +1,10 @@
import * as RadixDialog from '@radix-ui/react-dialog';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { classNames } from '~/utils/classNames';
import { TabManagement } from './TabManagement';
import { TabTile } from '~/components/settings/shared/TabTile';
import { DialogTitle } from '~/components/ui/Dialog';
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
import { useStore } from '@nanostores/react';
@ -20,6 +21,7 @@ import SettingsTab from '~/components/settings/settings/SettingsTab';
import ProfileTab from '~/components/settings/profile/ProfileTab';
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
interface DraggableTabTileProps {
tab: TabVisibilityConfig;
@ -102,6 +104,24 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
const [activeTab, setActiveTab] = useState<TabType | null>(null);
const [showTabManagement, setShowTabManagement] = useState(false);
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
const [profile, setProfile] = useState(() => {
const saved = localStorage.getItem('bolt_user_profile');
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
});
// Listen for profile changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'bolt_user_profile') {
const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
setProfile(newProfile);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
// Status hooks
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
@ -120,7 +140,14 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
// Only show tabs that are assigned to the developer window AND are visible
const visibleDeveloperTabs = tabConfiguration.developerTabs
.filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible)
.filter((tab) => {
// Hide notifications tab if notifications are disabled
if (tab.id === 'notifications' && !profile.notifications) {
return false;
}
return tab.visible;
})
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
const moveTab = (dragIndex: number, hoverIndex: number) => {
@ -136,32 +163,38 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
updateTabConfiguration(updatedTargetTab);
};
const handleTabClick = async (tabId: TabType) => {
const handleTabClick = (tabId: TabType) => {
// Don't allow clicking notifications tab if disabled
if (tabId === 'notifications' && !profile.notifications) {
return;
}
setLoadingTab(tabId);
setActiveTab(tabId);
// Acknowledge the status based on tab type
switch (tabId) {
case 'update':
await acknowledgeUpdate();
acknowledgeUpdate();
break;
case 'features':
await acknowledgeAllFeatures();
acknowledgeAllFeatures();
break;
case 'notifications':
await markAllAsRead();
markAllAsRead();
break;
case 'connection':
acknowledgeIssue();
break;
case 'debug':
await acknowledgeAllIssues();
acknowledgeAllIssues();
break;
}
// Simulate loading time (remove this in production)
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoadingTab(null);
// Clear loading state after a short delay
setTimeout(() => {
setLoadingTab(null);
}, 500);
};
const getTabComponent = () => {
@ -238,7 +271,7 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
<RadixDialog.Root open={open}>
<RadixDialog.Portal>
<div className="fixed inset-0 flex items-center justify-center z-[60]">
<RadixDialog.Overlay asChild>
<RadixDialog.Overlay className="fixed inset-0">
<motion.div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
initial={{ opacity: 0 }}
@ -247,16 +280,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
transition={{ duration: 0.2 }}
/>
</RadixDialog.Overlay>
<RadixDialog.Content aria-describedby={undefined} asChild>
<RadixDialog.Content aria-describedby={undefined} className="relative z-[61]">
<motion.div
className={classNames(
'relative',
'w-[1200px] h-[90vh]',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'rounded-2xl shadow-2xl',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'flex flex-col overflow-hidden',
'z-[61]',
)}
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
@ -264,68 +296,142 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
transition={{ duration: 0.2 }}
>
{/* Header */}
<div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-4">
{(activeTab || showTabManagement) && (
<motion.button
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-4">
{activeTab || showTabManagement ? (
<button
onClick={handleBack}
className={classNames(
'flex items-center justify-center w-8 h-8 rounded-lg',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
'group transition-all duration-200',
)}
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"
>
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
) : (
<motion.div
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
initial={{ rotate: -10 }}
animate={{ rotate: 10 }}
transition={{
repeat: Infinity,
repeatType: 'reverse',
duration: 2,
ease: 'easeInOut',
}}
/>
)}
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
{showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'}
</DialogTitle>
</div>
<div className="flex items-center space-x-4">
{!activeTab && !showTabManagement && (
<motion.button
onClick={() => setShowTabManagement(true)}
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"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
<div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
<span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
Manage Tabs
</span>
</motion.button>
)}
<div className="flex items-center gap-3">
<motion.div
className="i-ph:code-fill w-5 h-5 text-purple-500"
initial={{ rotate: 0 }}
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
duration: 8,
ease: 'linear',
}}
/>
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">
{showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'}
</h2>
<div className="relative">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<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">
{profile.avatar ? (
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<svg
className="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
)}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200]"
sideOffset={5}
align="end"
>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('profile')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Profile</span>
</DropdownMenu.Item>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('settings')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Settings</span>
</DropdownMenu.Item>
{profile.notifications && (
<>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('notifications')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">
Notifications
{hasUnreadNotifications && (
<span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
{unreadNotifications.length}
</span>
)}
</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
</>
)}
<DropdownMenu.Item
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"
onSelect={onClose}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Close</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
</div>
<div className="flex items-center gap-3">
{!showTabManagement && !activeTab && (
<motion.button
onClick={() => setShowTabManagement(true)}
className={classNames(
'px-3 py-1.5 rounded-lg text-sm',
'bg-purple-500/10 text-purple-500',
'hover:bg-purple-500/20',
'transition-colors duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
Manage Tabs
</motion.button>
)}
<motion.button
<button
onClick={onClose}
className={classNames(
'flex items-center justify-center w-8 h-8 rounded-lg',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
'group transition-all duration-200',
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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"
>
<div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
</motion.button>
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
</div>
</div>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { memo } from 'react';
import { motion } from 'framer-motion';
import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';
@ -19,6 +19,93 @@ interface FeatureToggle {
tooltip?: string;
}
const FeatureCard = memo(
({
feature,
index,
onToggle,
}: {
feature: FeatureToggle;
index: number;
onToggle: (id: string, enabled: boolean) => void;
}) => (
<motion.div
key={feature.id}
layoutId={feature.id}
className={classNames(
'relative group cursor-pointer',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-colors duration-200',
'rounded-lg overflow-hidden',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={classNames(feature.icon, 'w-5 h-5 text-bolt-elements-textSecondary')} />
<div className="flex items-center gap-2">
<h4 className="font-medium text-bolt-elements-textPrimary">{feature.title}</h4>
{feature.beta && (
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">Beta</span>
)}
{feature.experimental && (
<span className="px-2 py-0.5 text-xs rounded-full bg-orange-500/10 text-orange-500 font-medium">
Experimental
</span>
)}
</div>
</div>
<Switch checked={feature.enabled} onCheckedChange={(checked) => onToggle(feature.id, checked)} />
</div>
<p className="mt-2 text-sm text-bolt-elements-textSecondary">{feature.description}</p>
{feature.tooltip && <p className="mt-1 text-xs text-bolt-elements-textTertiary">{feature.tooltip}</p>}
</div>
</motion.div>
),
);
const FeatureSection = memo(
({
title,
features,
icon,
description,
onToggleFeature,
}: {
title: string;
features: FeatureToggle[];
icon: string;
description: string;
onToggleFeature: (id: string, enabled: boolean) => void;
}) => (
<motion.div
layout
className="flex flex-col gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="flex items-center gap-3">
<div className={classNames(icon, 'text-xl text-purple-500')} />
<div>
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">{title}</h3>
<p className="text-sm text-bolt-elements-textSecondary">{description}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{features.map((feature, index) => (
<FeatureCard key={feature.id} feature={feature} index={index} onToggle={onToggleFeature} />
))}
</div>
</motion.div>
),
);
export default function FeaturesTab() {
const {
setEventLogs,
@ -36,50 +123,56 @@ export default function FeaturesTab() {
const eventLogs = useStore(isEventLogsEnabled);
const features: FeatureToggle[] = [
{
id: 'latestBranch',
title: 'Use Main Branch',
description: 'Check for updates against the main branch instead of stable',
icon: 'i-ph:git-branch',
enabled: isLatestBranch,
beta: true,
tooltip: 'Get the latest features and improvements before they are officially released',
},
{
id: 'autoTemplate',
title: 'Auto Select Code Template',
description: 'Let Bolt select the best starter template for your project',
icon: 'i-ph:magic-wand',
enabled: autoSelectTemplate,
tooltip: 'Automatically choose the most suitable template based on your project type',
},
{
id: 'contextOptimization',
title: 'Context Optimization',
description: 'Optimize chat context by redacting file contents and using system prompts',
icon: 'i-ph:arrows-in',
enabled: contextOptimizationEnabled,
tooltip: 'Improve AI responses by optimizing the context window and system prompts',
},
{
id: 'experimentalProviders',
title: 'Experimental Providers',
description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
icon: 'i-ph:robot',
enabled: isLocalModel,
experimental: true,
tooltip: 'Try out new AI providers and models in development',
},
{
id: 'eventLogs',
title: 'Event Logging',
description: 'Enable detailed event logging and history',
icon: 'i-ph:list-bullets',
enabled: eventLogs,
tooltip: 'Record detailed logs of system events and user actions',
},
];
const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
stable: [
{
id: 'autoTemplate',
title: 'Auto Select Code Template',
description: 'Let Bolt select the best starter template for your project',
icon: 'i-ph:magic-wand',
enabled: autoSelectTemplate,
tooltip: 'Automatically choose the most suitable template based on your project type',
},
{
id: 'contextOptimization',
title: 'Context Optimization',
description: 'Optimize chat context by redacting file contents and using system prompts',
icon: 'i-ph:arrows-in',
enabled: contextOptimizationEnabled,
tooltip: 'Improve AI responses by optimizing the context window and system prompts',
},
{
id: 'eventLogs',
title: 'Event Logging',
description: 'Enable detailed event logging and history',
icon: 'i-ph:list-bullets',
enabled: eventLogs,
tooltip: 'Record detailed logs of system events and user actions',
},
],
beta: [
{
id: 'latestBranch',
title: 'Use Main Branch',
description: 'Check for updates against the main branch instead of stable',
icon: 'i-ph:git-branch',
enabled: isLatestBranch,
beta: true,
tooltip: 'Get the latest features and improvements before they are officially released',
},
],
experimental: [
{
id: 'experimentalProviders',
title: 'Experimental Providers',
description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
icon: 'i-ph:robot',
enabled: isLocalModel,
experimental: true,
tooltip: 'Try out new AI providers and models in development',
},
],
};
const handleToggleFeature = (featureId: string, enabled: boolean) => {
switch (featureId) {
@ -107,163 +200,88 @@ export default function FeaturesTab() {
};
return (
<div className="flex flex-col gap-6">
<motion.div
className="flex items-center gap-3"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="i-ph:puzzle-piece text-xl text-purple-500" />
<div>
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
<p className="text-sm text-bolt-elements-textSecondary">
Customize your Bolt experience with experimental features
</p>
</div>
</motion.div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{features.map((feature, index) => (
<motion.div
key={feature.id}
className={classNames(
'relative group cursor-pointer',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-colors duration-200',
'rounded-lg overflow-hidden',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<div className="absolute top-0 right-0 p-2 flex gap-1">
{feature.beta && (
<motion.span
className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Beta
</motion.span>
)}
{feature.experimental && (
<motion.span
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Experimental
</motion.span>
)}
</div>
<div className="flex items-start gap-4 p-4">
<motion.div
className={classNames(
'p-2 rounded-lg text-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-colors duration-200',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div className={classNames(feature.icon, 'text-purple-500')} />
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{feature.title}
</h4>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">{feature.description}</p>
</div>
<Switch
checked={feature.enabled}
onCheckedChange={(checked) => handleToggleFeature(feature.id, checked)}
/>
</div>
</div>
</div>
<motion.div
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
animate={{
borderColor: feature.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
scale: feature.enabled ? 1 : 0.98,
}}
transition={{ duration: 0.2 }}
/>
</motion.div>
))}
</motion.div>
<div className="flex flex-col gap-8">
<FeatureSection
title="Stable Features"
features={features.stable}
icon="i-ph:check-circle"
description="Production-ready features that have been thoroughly tested"
onToggleFeature={handleToggleFeature}
/>
{features.beta.length > 0 && (
<FeatureSection
title="Beta Features"
features={features.beta}
icon="i-ph:test-tube"
description="New features that are ready for testing but may have some rough edges"
onToggleFeature={handleToggleFeature}
/>
)}
{features.experimental.length > 0 && (
<FeatureSection
title="Experimental Features"
features={features.experimental}
icon="i-ph:flask"
description="Features in early development that may be unstable or require additional setup"
onToggleFeature={handleToggleFeature}
/>
)}
<motion.div
layout
className={classNames(
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
'rounded-lg',
'rounded-lg p-4',
'group',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
whileHover={{ scale: 1.01 }}
transition={{ delay: 0.3 }}
>
<div className="flex items-start gap-4 p-4">
<motion.div
<div className="flex items-center gap-4">
<div
className={classNames(
'p-2 rounded-lg text-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-colors duration-200',
'text-purple-500',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div className="i-ph:book text-purple-500" />
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
Prompt Library
</h4>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
Choose a prompt from the library to use as the system prompt
</p>
</div>
<select
value={promptId}
onChange={(e) => {
setPromptId(e.target.value);
toast.success('Prompt template updated');
}}
className={classNames(
'p-2 rounded-lg text-sm min-w-[200px]',
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'group-hover:border-purple-500/30',
'transition-all duration-200',
)}
>
{PromptLibrary.getList().map((x) => (
<option key={x.id} value={x.id}>
{x.label}
</option>
))}
</select>
</div>
<div className="i-ph:book" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
Prompt Library
</h4>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
Choose a prompt from the library to use as the system prompt
</p>
</div>
<select
value={promptId}
onChange={(e) => {
setPromptId(e.target.value);
toast.success('Prompt template updated');
}}
className={classNames(
'p-2 rounded-lg text-sm min-w-[200px]',
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'group-hover:border-purple-500/30',
'transition-all duration-200',
)}
>
{PromptLibrary.getList().map((x) => (
<option key={x.id} value={x.id}>
{x.label}
</option>
))}
</select>
</div>
</motion.div>
</div>

View File

@ -15,7 +15,7 @@ interface NotificationDetails {
}
const NotificationsTab = () => {
const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all');
const [filter, setFilter] = useState<'all' | 'error' | 'warning' | 'update'>('all');
const logs = useStore(logStore.logs);
const handleClearNotifications = () => {
@ -29,13 +29,44 @@ const NotificationsTab = () => {
const filteredLogs = Object.values(logs)
.filter((log) => {
if (filter === 'all') {
return log.level === 'error' || log.level === 'warning';
return log.level === 'error' || log.level === 'warning' || log.details?.type === 'update';
}
if (filter === 'update') {
return log.details?.type === 'update';
}
return log.level === filter;
})
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
const getNotificationStyle = (log: (typeof filteredLogs)[0]) => {
if (log.details?.type === 'update') {
return {
border: 'border-purple-200 dark:border-purple-900/50',
bg: 'bg-purple-50 dark:bg-purple-900/20',
icon: 'i-ph:arrow-circle-up text-purple-600 dark:text-purple-400',
text: 'text-purple-900 dark:text-purple-300',
};
}
if (log.level === 'error') {
return {
border: 'border-red-200 dark:border-red-900/50',
bg: 'bg-red-50 dark:bg-red-900/20',
icon: 'i-ph:warning-circle text-red-600 dark:text-red-400',
text: 'text-red-900 dark:text-red-300',
};
}
return {
border: 'border-yellow-200 dark:border-yellow-900/50',
bg: 'bg-yellow-50 dark:bg-yellow-900/20',
icon: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
text: 'text-yellow-900 dark:text-yellow-300',
};
};
const renderNotificationDetails = (details: NotificationDetails) => {
if (details.type === 'update') {
return (
@ -48,7 +79,7 @@ const NotificationsTab = () => {
</div>
<button
onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
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"
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"
>
<span className="i-ph:git-branch text-lg" />
View Changes
@ -66,10 +97,11 @@ const NotificationsTab = () => {
<div className="flex items-center gap-2">
<select
value={filter}
onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning')}
onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning' | 'update')}
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"
>
<option value="all">All Notifications</option>
<option value="update">Updates</option>
<option value="error">Errors</option>
<option value="warning">Warnings</option>
</select>
@ -92,48 +124,30 @@ const NotificationsTab = () => {
</div>
</div>
) : (
filteredLogs.map((log) => (
<motion.div
key={log.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={classNames(
'flex flex-col gap-2 rounded-lg border p-4',
log.level === 'error'
? 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-900/20'
: 'border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20',
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<span
className={classNames(
'text-lg',
log.level === 'error'
? 'i-ph:warning-circle text-red-600 dark:text-red-400'
: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
)}
/>
<div>
<h3
className={classNames(
'text-sm font-medium',
log.level === 'error'
? 'text-red-900 dark:text-red-300'
: 'text-yellow-900 dark:text-yellow-300',
)}
>
{log.message}
</h3>
{log.details && renderNotificationDetails(log.details as NotificationDetails)}
filteredLogs.map((log) => {
const style = getNotificationStyle(log);
return (
<motion.div
key={log.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={classNames('flex flex-col gap-2 rounded-lg border p-4', style.border, style.bg)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<span className={classNames('text-lg', style.icon)} />
<div>
<h3 className={classNames('text-sm font-medium', style.text)}>{log.message}</h3>
{log.details && renderNotificationDetails(log.details as NotificationDetails)}
</div>
</div>
<time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
</time>
</div>
<time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
</time>
</div>
</motion.div>
))
</motion.div>
);
})
)}
</div>
</div>

View File

@ -17,6 +17,7 @@ export type TabType =
export type WindowType = 'user' | 'developer';
export interface UserProfile {
nickname: any;
name: string;
email: string;
avatar?: string;

View File

@ -1,11 +1,24 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { useSettings } from '~/lib/hooks/useSettings';
import { logStore } from '~/lib/stores/logs';
import { classNames } from '~/utils/classNames';
import { toast } from 'react-toastify';
interface GitHubCommitResponse {
sha: string;
commit: {
message: string;
};
}
interface GitHubReleaseResponse {
tag_name: string;
body: string;
assets: Array<{
size: number;
browser_download_url: string;
}>;
}
interface UpdateInfo {
@ -13,26 +26,136 @@ interface UpdateInfo {
latestVersion: string;
branch: string;
hasUpdate: boolean;
releaseNotes?: string;
downloadSize?: string;
changelog?: string[];
currentCommit?: string;
latestCommit?: string;
downloadProgress?: number;
installProgress?: number;
estimatedTimeRemaining?: number;
}
const GITHUB_URLS = {
commitJson: async (branch: string): Promise<UpdateInfo> => {
try {
const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
const data = (await response.json()) as GitHubCommitResponse;
interface UpdateSettings {
autoUpdate: boolean;
notifyInApp: boolean;
checkInterval: number;
}
const currentCommitHash = __COMMIT_HASH;
const remoteCommitHash = data.sha.slice(0, 7);
interface UpdateResponse {
success: boolean;
error?: string;
progress?: {
downloaded: number;
total: number;
stage: 'download' | 'install' | 'complete';
};
}
const categorizeChangelog = (messages: string[]) => {
const categories = new Map<string, string[]>();
messages.forEach((message) => {
let category = 'Other';
if (message.startsWith('feat:')) {
category = 'Features';
} else if (message.startsWith('fix:')) {
category = 'Bug Fixes';
} else if (message.startsWith('docs:')) {
category = 'Documentation';
} else if (message.startsWith('ci:')) {
category = 'CI Improvements';
} else if (message.startsWith('refactor:')) {
category = 'Refactoring';
} else if (message.startsWith('test:')) {
category = 'Testing';
} else if (message.startsWith('style:')) {
category = 'Styling';
} else if (message.startsWith('perf:')) {
category = 'Performance';
}
if (!categories.has(category)) {
categories.set(category, []);
}
categories.get(category)!.push(message);
});
const order = [
'Features',
'Bug Fixes',
'Documentation',
'CI Improvements',
'Refactoring',
'Performance',
'Testing',
'Styling',
'Other',
];
return Array.from(categories.entries())
.sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))
.filter(([_, messages]) => messages.length > 0);
};
const parseCommitMessage = (message: string) => {
const prMatch = message.match(/#(\d+)/);
const prNumber = prMatch ? prMatch[1] : null;
let cleanMessage = message.replace(/^[a-z]+:\s*/i, '');
cleanMessage = cleanMessage.replace(/#\d+/g, '').trim();
const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/);
const title = parts[0].trim();
const description = parts
.slice(1)
.map((p) => p.trim())
.filter((p) => p && !p.includes('Co-authored-by:'))
.join('\n');
return { title, description, prNumber };
};
const GITHUB_URLS = {
commitJson: async (branch: string, headers: HeadersInit = {}): Promise<UpdateInfo> => {
try {
const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([
fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }),
fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }),
fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }),
]);
if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) {
throw new Error(
`GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`,
);
}
const commitData = (await commitResponse.json()) as GitHubCommitResponse;
const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse;
const commits = (await changelogResponse.json()) as GitHubCommitResponse[];
const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0;
const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB';
const changelog = commits.map((commit) => commit.commit.message);
return {
currentVersion: currentCommitHash,
latestVersion: remoteCommitHash,
currentVersion: process.env.APP_VERSION || 'unknown',
latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7),
branch,
hasUpdate: remoteCommitHash !== currentCommitHash,
hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT,
releaseNotes: releaseData.body || '',
downloadSize,
changelog,
currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7),
latestCommit: commitData.sha.substring(0, 7),
};
} catch (error) {
console.error('Failed to fetch commit info:', error);
throw new Error('Failed to fetch commit info');
console.error('Error fetching update info:', error);
throw error;
}
},
};
@ -41,19 +164,71 @@ const UpdateTab = () => {
const { isLatestBranch } = useSettings();
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const [showChangelog, setShowChangelog] = useState(false);
const [showManualInstructions, setShowManualInstructions] = useState(false);
const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
const [updateFailed, setUpdateFailed] = useState(false);
const [updateSettings, setUpdateSettings] = useState<UpdateSettings>(() => {
const stored = localStorage.getItem('update_settings');
return stored
? JSON.parse(stored)
: {
autoUpdate: false,
notifyInApp: true,
checkInterval: 24,
};
});
useEffect(() => {
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
}, [updateSettings]);
const handleUpdateProgress = async (response: Response): Promise<void> => {
const reader = response.body?.getReader();
if (!reader) {
return;
}
const contentLength = +(response.headers.get('Content-Length') ?? 0);
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
receivedLength += value.length;
const progress = (receivedLength / contentLength) * 100;
setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev));
}
};
const checkForUpdates = async () => {
setIsChecking(true);
setError(null);
try {
const githubToken = localStorage.getItem('github_connection');
const headers: HeadersInit = {};
if (githubToken) {
const { token } = JSON.parse(githubToken);
headers.Authorization = `Bearer ${token}`;
}
const branchToCheck = isLatestBranch ? 'main' : 'stable';
const info = await GITHUB_URLS.commitJson(branchToCheck);
const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
setUpdateInfo(info);
if (info.hasUpdate) {
// Add update notification only if it doesn't already exist
const existingLogs = Object.values(logStore.logs.get());
const hasUpdateNotification = existingLogs.some(
(log) =>
@ -62,7 +237,7 @@ const UpdateTab = () => {
log.details.latestVersion === info.latestVersion,
);
if (!hasUpdateNotification) {
if (!hasUpdateNotification && updateSettings.notifyInApp) {
logStore.logWarning('Update Available', {
currentVersion: info.currentVersion,
latestVersion: info.latestVersion,
@ -71,29 +246,123 @@ const UpdateTab = () => {
message: `A new version is available on the ${branchToCheck} branch`,
updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
});
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
const changelogText = info.changelog?.join('\n') || 'No changelog available';
const userWantsUpdate = confirm(
`An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`,
);
setHasUserRespondedToUpdate(true);
if (userWantsUpdate) {
await initiateUpdate();
} else {
logStore.logSystem('Update cancelled by user');
}
}
}
}
} catch (err) {
setError('Failed to check for updates. Please try again later.');
console.error('Update check failed:', err);
setUpdateFailed(true);
} finally {
setIsChecking(false);
}
};
const initiateUpdate = async () => {
setIsUpdating(true);
setError(null);
let currentRetry = 0;
const maxRetries = 3;
const attemptUpdate = async (): Promise<void> => {
try {
const platform = process.platform;
if (platform === 'darwin' || platform === 'linux') {
const response = await fetch('/api/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
branch: isLatestBranch ? 'main' : 'stable',
settings: updateSettings,
}),
});
if (!response.ok) {
throw new Error('Failed to initiate update');
}
await handleUpdateProgress(response);
const result = (await response.json()) as UpdateResponse;
if (result.success) {
logStore.logSuccess('Update downloaded successfully', {
type: 'update',
message: 'Update completed successfully.',
});
toast.success('Update completed successfully!');
setUpdateFailed(false);
return;
}
throw new Error(result.error || 'Update failed');
}
window.open('https://github.com/stackblitz-labs/bolt.diy/releases/latest', '_blank');
logStore.logInfo('Manual update required', {
type: 'update',
message: 'Please download and install the latest version from the GitHub releases page.',
});
return;
} catch (err) {
currentRetry++;
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
if (currentRetry < maxRetries) {
toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 });
setRetryCount(currentRetry);
await new Promise((resolve) => setTimeout(resolve, 2000));
await attemptUpdate();
return;
}
setError('Failed to initiate update. Please try again or update manually.');
console.error('Update failed:', err);
logStore.logSystem('Update failed: ' + errorMessage);
toast.error('Update failed: ' + errorMessage);
setUpdateFailed(true);
return;
}
};
await attemptUpdate();
setIsUpdating(false);
setRetryCount(0);
};
useEffect(() => {
const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
const intervalId = setInterval(checkForUpdates, checkInterval);
return () => clearInterval(intervalId);
}, [updateSettings.checkInterval, isLatestBranch]);
useEffect(() => {
checkForUpdates();
}, [isLatestBranch]);
const handleViewChanges = () => {
if (updateInfo) {
window.open(
`https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`,
'_blank',
);
}
};
return (
<div className="flex flex-col gap-6">
<motion.div
@ -109,43 +378,130 @@ const UpdateTab = () => {
</div>
</motion.div>
{/* Update Settings Card */}
<motion.div
className="flex flex-col gap-4"
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 mb-6">
<div className="i-ph:gear text-purple-500 w-5 h-5" />
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Settings</h3>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-bolt-elements-textPrimary">Automatic Updates</span>
<p className="text-xs text-bolt-elements-textSecondary">
Automatically check and apply updates when available
</p>
</div>
<button
onClick={() => setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
className={classNames(
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
updateSettings.autoUpdate ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
)}
>
<span
className={classNames(
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
updateSettings.autoUpdate ? 'translate-x-6' : 'translate-x-1',
)}
/>
</button>
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-bolt-elements-textPrimary">In-App Notifications</span>
<p className="text-xs text-bolt-elements-textSecondary">Show notifications when updates are available</p>
</div>
<button
onClick={() => setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
className={classNames(
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
updateSettings.notifyInApp ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
)}
>
<span
className={classNames(
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
updateSettings.notifyInApp ? 'translate-x-6' : 'translate-x-1',
)}
/>
</button>
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-bolt-elements-textPrimary">Check Interval</span>
<p className="text-xs text-bolt-elements-textSecondary">How often to check for updates</p>
</div>
<select
value={updateSettings.checkInterval}
onChange={(e) => setUpdateSettings((prev) => ({ ...prev, checkInterval: Number(e.target.value) }))}
className={classNames(
'px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'text-bolt-elements-textPrimary',
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'transition-colors duration-200',
)}
>
<option value="6">6 hours</option>
<option value="12">12 hours</option>
<option value="24">24 hours</option>
<option value="48">48 hours</option>
</select>
</div>
</div>
</motion.div>
{/* Update Status Card */}
<motion.div
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<span className="text-sm text-bolt-elements-textSecondary">
Currently on {isLatestBranch ? 'main' : 'stable'} branch
</span>
{updateInfo && (
<span className="text-xs text-bolt-elements-textTertiary">Version: {updateInfo.currentVersion}</span>
<span className="text-xs text-bolt-elements-textTertiary">
Version: {updateInfo.currentVersion} ({updateInfo.currentCommit})
</span>
)}
</div>
<button
onClick={checkForUpdates}
disabled={isChecking}
onClick={() => {
setHasUserRespondedToUpdate(false);
setUpdateFailed(false);
checkForUpdates();
}}
disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)}
className={classNames(
'px-3 py-2 rounded-lg text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary',
'hover:bg-bolt-elements-background-depth-3',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
'transition-colors duration-200',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
<div className="flex items-center gap-2">
<div className={classNames('i-ph:arrows-clockwise', isChecking ? 'animate-spin' : '')} />
{isChecking ? 'Checking...' : 'Check for Updates'}
</div>
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', isChecking ? 'animate-spin' : '')} />
{isChecking ? 'Checking...' : 'Check for Updates'}
</button>
</div>
{error && (
<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">
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
<div className="flex items-center gap-2">
<div className="i-ph:warning-circle" />
{error}
@ -156,60 +512,250 @@ const UpdateTab = () => {
{updateInfo && (
<div
className={classNames(
'p-4 rounded-lg border',
'p-4 rounded-lg',
updateInfo.hasUpdate
? 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-900/50'
: 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-900/50',
? 'bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20'
: 'bg-green-500/5 dark:bg-green-500/10 border border-green-500/20',
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<span
className={classNames(
'text-lg',
updateInfo.hasUpdate
? 'i-ph:warning text-yellow-600 dark:text-yellow-400'
: 'i-ph:check-circle text-green-600 dark:text-green-400',
)}
/>
<div>
<h3
className={classNames(
'text-sm font-medium',
updateInfo.hasUpdate
? 'text-yellow-900 dark:text-yellow-300'
: 'text-green-900 dark:text-green-300',
)}
>
{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
</h3>
<p className="text-sm text-bolt-elements-textSecondary mt-1">
{updateInfo.hasUpdate
? `A new version is available on the ${updateInfo.branch} branch`
: 'You are running the latest version'}
</p>
{updateInfo.hasUpdate && (
<div className="mt-2 flex flex-col gap-1 text-xs text-bolt-elements-textTertiary">
<p>Current Version: {updateInfo.currentVersion}</p>
<p>Latest Version: {updateInfo.latestVersion}</p>
<p>Branch: {updateInfo.branch}</p>
</div>
)}
</div>
<div className="flex items-center gap-3">
<span
className={classNames(
'text-lg',
updateInfo.hasUpdate ? 'i-ph:warning text-purple-500' : 'i-ph:check-circle text-green-500',
)}
/>
<div>
<h4 className="font-medium text-bolt-elements-textPrimary">
{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
</h4>
<p className="text-sm text-bolt-elements-textSecondary">
{updateInfo.hasUpdate
? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available`
: 'You are running the latest version'}
</p>
</div>
{updateInfo.hasUpdate && (
<button
onClick={handleViewChanges}
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"
>
<span className="i-ph:git-branch text-lg" />
View Changes
</button>
)}
</div>
</div>
)}
</motion.div>
{/* Update Details Card */}
{updateInfo && updateInfo.hasUpdate && (
<motion.div
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="i-ph:arrow-circle-up text-purple-500 w-5 h-5" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">
Version {updateInfo.latestVersion}
</span>
</div>
<span className="text-xs px-3 py-1 rounded-full bg-purple-500/10 text-purple-500">
{updateInfo.downloadSize}
</span>
</div>
{/* Update Options */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<button
onClick={initiateUpdate}
disabled={isUpdating || updateFailed}
className={classNames(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
'bg-purple-500 hover:bg-purple-600',
'text-white',
'transition-all duration-200',
'hover:shadow-lg hover:shadow-purple-500/20',
'disabled:opacity-50 disabled:cursor-not-allowed',
)}
>
<div className={classNames('i-ph:arrow-circle-up w-4 h-4', isUpdating ? 'animate-spin' : '')} />
{isUpdating ? 'Updating...' : 'Auto Update'}
</button>
<button
onClick={() => setShowManualInstructions(!showManualInstructions)}
className={classNames(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
'text-bolt-elements-textPrimary',
'transition-all duration-200',
)}
>
<div className="i-ph:book-open w-4 h-4" />
{showManualInstructions ? 'Hide Instructions' : 'Manual Update'}
</button>
</div>
{/* Manual Update Instructions */}
<AnimatePresence>
{showManualInstructions && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="space-y-6 text-bolt-elements-textSecondary"
>
<div className="p-4 rounded-lg bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20">
<p className="font-medium text-purple-500">
Update available from {isLatestBranch ? 'main' : 'stable'} branch!
</p>
<div className="mt-2 space-y-1">
<p>
Current: {updateInfo.currentVersion} ({updateInfo.currentCommit})
</p>
<p>
Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit})
</p>
</div>
</div>
<div>
<h4 className="text-base font-medium text-bolt-elements-textPrimary mb-3">To update:</h4>
<ol className="space-y-4">
<li className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
1
</div>
<div>
<p className="font-medium text-bolt-elements-textPrimary">Pull the latest changes:</p>
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
git pull upstream {isLatestBranch ? 'main' : 'stable'}
</code>
</div>
</li>
<li className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
2
</div>
<div>
<p className="font-medium text-bolt-elements-textPrimary">Install dependencies:</p>
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
pnpm install
</code>
</div>
</li>
<li className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
3
</div>
<div>
<p className="font-medium text-bolt-elements-textPrimary">Build the application:</p>
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
pnpm build
</code>
</div>
</li>
<li className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
4
</div>
<p className="font-medium text-bolt-elements-textPrimary">Restart the application</p>
</li>
</ol>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Changelog */}
{updateInfo.changelog && updateInfo.changelog.length > 0 && (
<div className="mt-4">
<button
onClick={() => setShowChangelog(!showChangelog)}
className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary hover:text-purple-500 transition-colors"
>
<div className={`i-ph:${showChangelog ? 'caret-up' : 'caret-down'} w-4 h-4`} />
{showChangelog ? 'Hide Changelog' : 'View Changelog'}
</button>
<AnimatePresence>
{showChangelog && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
>
<div className="max-h-[400px] overflow-y-auto">
{categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
<div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
<div className="p-3 bg-bolt-elements-bg-depth-4">
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">
{category}
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
({messages.length})
</span>
</h5>
</div>
<div className="divide-y divide-bolt-elements-borderColor">
{messages.map((message, index) => {
const { title, description, prNumber } = parseCommitMessage(message);
return (
<div key={index} className="p-3 hover:bg-bolt-elements-bg-depth-4 transition-colors">
<div className="flex items-start gap-3">
<div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-bolt-elements-textSecondary" />
<div className="space-y-1 flex-1">
<p className="text-sm font-medium text-bolt-elements-textPrimary">
{title}
{prNumber && (
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
#{prNumber}
</span>
)}
</p>
{description && (
<p className="text-xs text-bolt-elements-textSecondary">{description}</p>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
</motion.div>
)}
{/* Update Progress */}
{isUpdating && updateInfo?.downloadProgress !== undefined && (
<motion.div
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textPrimary">Downloading Update</span>
<span className="text-sm text-bolt-elements-textSecondary">
{Math.round(updateInfo.downloadProgress)}%
</span>
</div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 transition-all duration-300"
style={{ width: `${updateInfo.downloadProgress}%` }}
/>
</div>
{retryCount > 0 && <p className="text-sm text-yellow-500">Retry attempt {retryCount}/3...</p>}
</div>
</motion.div>
)}
</div>
);
};

View File

@ -1,6 +1,7 @@
import * as RadixDialog from '@radix-ui/react-dialog';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { classNames } from '~/utils/classNames';
import { DialogTitle } from '~/components/ui/Dialog';
import { Switch } from '~/components/ui/Switch';
@ -117,6 +118,24 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
const [profile, setProfile] = useState(() => {
const saved = localStorage.getItem('bolt_user_profile');
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
});
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'bolt_user_profile') {
const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
setProfile(newProfile);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
const handleDeveloperModeChange = (checked: boolean) => {
setDeveloperMode(checked);
};
@ -127,7 +146,14 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
// Only show tabs that are assigned to the user window AND are visible
const visibleUserTabs = tabConfiguration.userTabs
.filter((tab: TabVisibilityConfig) => tab.window === 'user' && tab.visible)
.filter((tab) => {
// Hide notifications tab if notifications are disabled
if (tab.id === 'notifications' && !profile.notifications) {
return false;
}
return tab.visible;
})
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
const moveTab = (dragIndex: number, hoverIndex: number) => {
@ -240,6 +266,142 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
}
};
const renderHeader = () => (
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-4">
{activeTab ? (
<button
onClick={handleBack}
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"
>
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
) : (
<motion.div
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
initial={{ rotate: -10 }}
animate={{ rotate: 10 }}
transition={{
repeat: Infinity,
repeatType: 'reverse',
duration: 2,
ease: 'easeInOut',
}}
/>
)}
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
{activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
</DialogTitle>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center gap-2">
<Switch
checked={developerMode}
onCheckedChange={handleDeveloperModeChange}
className="data-[state=checked]:bg-purple-500"
aria-label="Toggle developer mode"
/>
<label className="text-sm text-gray-500 dark:text-gray-400">Developer Mode</label>
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<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">
{profile.avatar ? (
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<svg
className="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
)}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
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"
sideOffset={5}
align="end"
>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('profile')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Profile</span>
</DropdownMenu.Item>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('settings')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Settings</span>
</DropdownMenu.Item>
{profile.notifications && (
<>
<DropdownMenu.Item
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"
onSelect={() => handleTabClick('notifications')}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">
Notifications
{hasUnreadNotifications && (
<span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
{unreadNotifications.length}
</span>
)}
</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
</>
)}
<DropdownMenu.Item
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"
onSelect={onClose}
>
<div className="mr-3 flex h-5 w-5 items-center justify-center">
<div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</div>
<span className="group-hover:text-purple-500 transition-colors">Close</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<button
onClick={onClose}
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"
>
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
</div>
</div>
);
return (
<>
<DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
@ -273,64 +435,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
transition={{ duration: 0.2 }}
>
{/* Header */}
<div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-3">
{activeTab ? (
<motion.button
onClick={handleBack}
className={classNames(
'flex items-center justify-center w-8 h-8 rounded-lg',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
'group transition-all duration-200',
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
</motion.button>
) : (
<motion.div
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
initial={{ rotate: -10 }}
animate={{ rotate: 10 }}
transition={{
repeat: Infinity,
repeatType: 'reverse',
duration: 2,
ease: 'easeInOut',
}}
/>
)}
<DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
{activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
</DialogTitle>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Switch
checked={developerMode}
onCheckedChange={handleDeveloperModeChange}
className="data-[state=checked]:bg-purple-500"
aria-label="Toggle developer mode"
/>
<label className="text-sm text-bolt-elements-textSecondary">Developer Mode</label>
</div>
<motion.button
onClick={onClose}
className={classNames(
'flex items-center justify-center w-8 h-8 rounded-lg',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
'group transition-all duration-200',
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
</motion.button>
</div>
</div>
{renderHeader()}
{/* Content */}
<div

View File

@ -1,40 +1,94 @@
import { logStore, type LogEntry } from '~/lib/stores/logs';
export type NotificationType = 'info' | 'warning' | 'error' | 'success' | 'update';
export interface NotificationDetails {
type?: string;
message?: string;
currentVersion?: string;
latestVersion?: string;
branch?: string;
updateUrl?: string;
}
export interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'warning' | 'error' | 'success';
type: NotificationType;
read: boolean;
timestamp: string;
details?: NotificationDetails;
}
interface LogEntryWithRead extends LogEntry {
read?: boolean;
}
const mapLogToNotification = (log: LogEntryWithRead): Notification => {
const type: NotificationType =
log.details?.type === 'update'
? 'update'
: log.level === 'error'
? 'error'
: log.level === 'warning'
? 'warning'
: 'info';
const baseNotification: Notification = {
id: log.id,
title: log.category.charAt(0).toUpperCase() + log.category.slice(1),
message: log.message,
type,
read: log.read || false,
timestamp: log.timestamp,
};
if (log.details) {
return {
...baseNotification,
details: log.details as NotificationDetails,
};
}
return baseNotification;
};
export const getNotifications = async (): Promise<Notification[]> => {
/*
* TODO: Implement actual notifications logic
* This is a mock implementation
*/
return [
{
id: 'notif-1',
title: 'Welcome to Bolt',
message: 'Get started by exploring the features',
type: 'info',
read: true,
timestamp: new Date().toISOString(),
},
{
id: 'notif-2',
title: 'New Update Available',
message: 'Version 1.0.1 is now available',
type: 'info',
read: false,
timestamp: new Date().toISOString(),
},
];
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
return logs
.filter((log) => {
if (log.details?.type === 'update') {
return true;
}
return log.level === 'error' || log.level === 'warning';
})
.map(mapLogToNotification)
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
};
export const markNotificationRead = async (notificationId: string): Promise<void> => {
/*
* TODO: Implement actual notification read logic
*/
console.log(`Marking notification ${notificationId} as read`);
logStore.markAsRead(notificationId);
};
export const clearNotifications = async (): Promise<void> => {
logStore.clearLogs();
};
export const getUnreadCount = (): number => {
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
return logs.filter((log) => {
if (!log.read) {
if (log.details?.type === 'update') {
return true;
}
return log.level === 'error' || log.level === 'warning';
}
return false;
}).length;
};

View File

@ -1,34 +1,17 @@
import { useState, useEffect } from 'react';
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
const getReadNotifications = (): string[] => {
try {
const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setReadNotifications = (notificationIds: string[]) => {
try {
localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
} catch (error) {
console.error('Failed to persist read notifications:', error);
}
};
import { logStore } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
export const useNotifications = () => {
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
const logs = useStore(logStore.logs);
const checkNotifications = async () => {
try {
const notifications = await getNotifications();
const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
const unread = notifications.filter((n) => !logStore.isRead(n.id));
setUnreadNotifications(unread);
setHasUnreadNotifications(unread.length > 0);
} catch (error) {
@ -43,17 +26,12 @@ export const useNotifications = () => {
const interval = setInterval(checkNotifications, 60 * 1000);
return () => clearInterval(interval);
}, [readNotificationIds]);
}, [logs]); // Re-run when logs change
const markAsRead = async (notificationId: string) => {
try {
await markNotificationRead(notificationId);
const newReadIds = [...readNotificationIds, notificationId];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
setHasUnreadNotifications(unreadNotifications.length > 1);
await checkNotifications();
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
@ -61,13 +39,9 @@ export const useNotifications = () => {
const markAllAsRead = async () => {
try {
await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications([]);
setHasUnreadNotifications(false);
const notifications = await getNotifications();
await Promise.all(notifications.map((n) => markNotificationRead(n.id)));
await checkNotifications();
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
}

View File

@ -19,12 +19,25 @@ export interface LogEntry {
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
class LogStore {
logInfo(message: string, details: { type: string; message: string }) {
return this.addLog(message, 'info', 'system', details);
}
logSuccess(message: string, details: { type: string; message: string }) {
return this.addLog(message, 'info', 'system', { ...details, success: true });
}
private _logs = map<Record<string, LogEntry>>({});
showLogs = atom(true);
private _readLogs = new Set<string>();
constructor() {
// Load saved logs from cookies on initialization
this._loadLogs();
// Only load read logs in browser environment
if (typeof window !== 'undefined') {
this._loadReadLogs();
}
}
// Expose the logs store for subscription
@ -45,11 +58,36 @@ class LogStore {
}
}
private _loadReadLogs() {
if (typeof window === 'undefined') {
return;
}
const savedReadLogs = localStorage.getItem('bolt_read_logs');
if (savedReadLogs) {
try {
const parsedReadLogs = JSON.parse(savedReadLogs);
this._readLogs = new Set(parsedReadLogs);
} catch (error) {
logger.error('Failed to parse read logs:', error);
}
}
}
private _saveLogs() {
const currentLogs = this._logs.get();
Cookies.set('eventLogs', JSON.stringify(currentLogs));
}
private _saveReadLogs() {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs)));
}
private _generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
@ -210,6 +248,20 @@ class LogStore {
return matchesLevel && matchesCategory && matchesSearch;
});
}
markAsRead(logId: string) {
this._readLogs.add(logId);
this._saveReadLogs();
}
isRead(logId: string): boolean {
return this._readLogs.has(logId);
}
clearReadLogs() {
this._readLogs.clear();
this._saveReadLogs();
}
}
export const logStore = new LogStore();

View File

@ -90,6 +90,7 @@
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"nanostores": "^0.10.3",
"next": "^15.1.5",
"ollama-ai-provider": "^0.15.2",
"react": "^18.3.1",
"react-dnd": "^16.0.1",

435
pnpm-lock.yaml generated
View File

@ -191,6 +191,9 @@ importers:
nanostores:
specifier: ^0.10.3
version: 0.10.3
next:
specifier: ^15.1.5
version: 15.1.5(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ollama-ai-provider:
specifier: ^0.15.2
version: 0.15.2(zod@3.23.8)
@ -849,6 +852,9 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
@ -1506,6 +1512,111 @@ packages:
'@iconify/utils@2.1.33':
resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -1577,6 +1688,57 @@ packages:
nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0
react: '>=18.0.0'
'@next/env@15.1.5':
resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==}
'@next/swc-darwin-arm64@15.1.5':
resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.1.5':
resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.1.5':
resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.1.5':
resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.1.5':
resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.1.5':
resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.1.5':
resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.1.5':
resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -2475,6 +2637,9 @@ packages:
peerDependencies:
eslint: '>=8.40.0'
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@ -2991,6 +3156,10 @@ packages:
peerDependencies:
esbuild: '>=0.18'
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -3104,6 +3273,13 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@ -3305,6 +3481,10 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
@ -3961,6 +4141,9 @@ packages:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@ -4650,6 +4833,27 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
next@15.1.5:
resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
babel-plugin-react-compiler:
optional: true
sass:
optional: true
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@ -4942,6 +5146,10 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
@ -5496,6 +5704,10 @@ packages:
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
hasBin: true
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -5527,6 +5739,9 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
@ -5598,6 +5813,10 @@ packages:
stream-slice@0.1.2:
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-hash@1.1.3:
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
@ -5650,6 +5869,19 @@ packages:
style-to-object@1.0.8:
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -7161,6 +7393,11 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.3.1':
dependencies:
tslib: 2.8.1
optional: true
'@emotion/hash@0.9.2': {}
'@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)':
@ -7556,6 +7793,81 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
'@img/sharp-darwin-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
optional: true
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
'@img/sharp-linux-s390x@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
optional: true
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.3.1
optional: true
'@img/sharp-win32-ia32@0.33.5':
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -7673,6 +7985,32 @@ snapshots:
nanostores: 0.10.3
react: 18.3.1
'@next/env@15.1.5': {}
'@next/swc-darwin-arm64@15.1.5':
optional: true
'@next/swc-darwin-x64@15.1.5':
optional: true
'@next/swc-linux-arm64-gnu@15.1.5':
optional: true
'@next/swc-linux-arm64-musl@15.1.5':
optional: true
'@next/swc-linux-x64-gnu@15.1.5':
optional: true
'@next/swc-linux-x64-musl@15.1.5':
optional: true
'@next/swc-win32-arm64-msvc@15.1.5':
optional: true
'@next/swc-win32-x64-msvc@15.1.5':
optional: true
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -8739,6 +9077,8 @@ snapshots:
- supports-color
- typescript
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@ -9432,6 +9772,10 @@ snapshots:
esbuild: 0.23.1
load-tsconfig: 0.2.5
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {}
cac@6.7.14: {}
@ -9546,6 +9890,18 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
optional: true
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
optional: true
colorette@2.0.20: {}
colorjs.io@0.5.2: {}
@ -9711,6 +10067,9 @@ snapshots:
destroy@1.2.0: {}
detect-libc@2.0.3:
optional: true
detect-node-es@1.1.0: {}
devlop@1.1.0:
@ -10569,6 +10928,9 @@ snapshots:
call-bind: 1.0.7
has-tostringtag: 1.0.2
is-arrayish@0.3.2:
optional: true
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
@ -11605,6 +11967,32 @@ snapshots:
negotiator@0.6.3: {}
next@15.1.5(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.1.5
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001685
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.6(@babel/core@7.26.0)(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.1.5
'@next/swc-darwin-x64': 15.1.5
'@next/swc-linux-arm64-gnu': 15.1.5
'@next/swc-linux-arm64-musl': 15.1.5
'@next/swc-linux-x64-gnu': 15.1.5
'@next/swc-linux-x64-musl': 15.1.5
'@next/swc-win32-arm64-msvc': 15.1.5
'@next/swc-win32-x64-msvc': 15.1.5
'@opentelemetry/api': 1.9.0
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
node-domexception@1.0.0: {}
node-fetch-native@1.6.4: {}
@ -11929,6 +12317,12 @@ snapshots:
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.4.49:
dependencies:
nanoid: 3.3.8
@ -12512,6 +12906,33 @@ snapshots:
inherits: 2.0.4
safe-buffer: 5.2.1
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.0.3
semver: 7.6.3
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
optional: true
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -12548,6 +12969,11 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
optional: true
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.28
@ -12616,6 +13042,8 @@ snapshots:
stream-slice@0.1.2: {}
streamsearch@1.1.0: {}
string-hash@1.1.3: {}
string-width@4.2.3:
@ -12669,6 +13097,13 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
styled-jsx@5.1.6(@babel/core@7.26.0)(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
optionalDependencies:
'@babel/core': 7.26.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0

52
scripts/update.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
# Exit on any error
set -e
echo "Starting Bolt.DIY update process..."
# Get the current directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Store current version
CURRENT_VERSION=$(cat "$PROJECT_ROOT/package.json" | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
echo "Current version: $CURRENT_VERSION"
echo "Fetching latest version..."
# Create temp directory
TMP_DIR=$(mktemp -d)
cd "$TMP_DIR"
# Download latest release
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 \")
if [ -z "$LATEST_RELEASE_URL" ]; then
echo "Error: Could not find latest release download URL"
exit 1
fi
echo "Downloading latest release..."
curl -L -o latest.zip "$LATEST_RELEASE_URL"
echo "Extracting update..."
unzip -q latest.zip
# Backup current installation
echo "Creating backup..."
BACKUP_DIR="$PROJECT_ROOT/backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -r "$PROJECT_ROOT"/* "$BACKUP_DIR/"
# Install update
echo "Installing update..."
cp -r ./* "$PROJECT_ROOT/"
# Clean up
cd "$PROJECT_ROOT"
rm -rf "$TMP_DIR"
echo "Update completed successfully!"
echo "Please restart the application to apply the changes."
exit 0