import React, { useState, useEffect } from 'react'; 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'; import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog'; interface GitHubCommitResponse { sha: string; commit: { message: string; }; } interface GitHubReleaseResponse { tag_name: string; body: string; assets: Array<{ size: number; browser_download_url: string; }>; } interface UpdateInfo { currentVersion: string; latestVersion: string; branch: string; hasUpdate: boolean; releaseNotes?: string; downloadSize?: string; changelog?: string[]; currentCommit?: string; latestCommit?: string; downloadProgress?: number; installProgress?: number; estimatedTimeRemaining?: number; } interface UpdateSettings { autoUpdate: boolean; notifyInApp: boolean; checkInterval: number; } interface UpdateResponse { success: boolean; error?: string; progress?: { downloaded: number; total: number; stage: 'download' | 'install' | 'complete'; }; } const categorizeChangelog = (messages: string[]) => { const categories = new Map(); 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 => { 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: process.env.APP_VERSION || 'unknown', latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7), branch, 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('Error fetching update info:', error); throw error; } }, }; const UpdateTab = () => { const { isLatestBranch } = useSettings(); const [updateInfo, setUpdateInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [error, setError] = useState(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(() => { const stored = localStorage.getItem('update_settings'); return stored ? JSON.parse(stored) : { autoUpdate: false, notifyInApp: true, checkInterval: 24, }; }); const [lastChecked, setLastChecked] = useState(null); const [showUpdateDialog, setShowUpdateDialog] = useState(false); const [updateChangelog, setUpdateChangelog] = useState([]); useEffect(() => { localStorage.setItem('update_settings', JSON.stringify(updateSettings)); }, [updateSettings]); const handleUpdateProgress = async (response: Response): Promise => { 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 () => { console.log('Starting update check...'); setIsChecking(true); setError(null); setLastChecked(new Date()); // Add a minimum delay of 2 seconds to show the spinning animation const startTime = Date.now(); try { console.log('Fetching update info...'); 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, headers); // Ensure we show the spinning animation for at least 2 seconds const elapsedTime = Date.now() - startTime; if (elapsedTime < 2000) { await new Promise((resolve) => setTimeout(resolve, 2000 - elapsedTime)); } setUpdateInfo(info); if (info.hasUpdate) { const existingLogs = Object.values(logStore.logs.get()); const hasUpdateNotification = existingLogs.some( (log) => log.level === 'warning' && log.details?.type === 'update' && log.details.latestVersion === info.latestVersion, ); if (!hasUpdateNotification && updateSettings.notifyInApp) { logStore.logWarning('Update Available', { currentVersion: info.currentVersion, latestVersion: info.latestVersion, branch: branchToCheck, type: 'update', 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) { setUpdateChangelog(info.changelog || ['No changelog available']); setShowUpdateDialog(true); } } } } catch (err) { console.error('Detailed update check error:', err); setError('Failed to check for updates. Please try again later.'); console.error('Update check failed:', err); setUpdateFailed(true); } finally { console.log('Update check completed'); setIsChecking(false); } }; const initiateUpdate = async () => { setIsUpdating(true); setError(null); let currentRetry = 0; const maxRetries = 3; const attemptUpdate = async (): Promise => { 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]); return (

Updates

Check for and manage application updates

{/* Update Settings Card */}

Update Settings

Automatic Updates

Automatically check and apply updates when available

In-App Notifications

Show notifications when updates are available

Check Interval

How often to check for updates

{/* Update Status Card */}
Currently on {isLatestBranch ? 'main' : 'stable'} branch {updateInfo && ( Version: {updateInfo.currentVersion} ({updateInfo.currentCommit}) )}
{error && (
{error}
)} {updateInfo && (

{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}

{updateInfo.hasUpdate ? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available` : 'You are running the latest version'}

)} {lastChecked && (
Last checked: {lastChecked.toLocaleString()} {error && {error}}
)} {/* Update Details Card */} {updateInfo && updateInfo.hasUpdate && (
Version {updateInfo.latestVersion}
{updateInfo.downloadSize}
{/* Update Options */}
{/* Manual Update Instructions */} {showManualInstructions && (

Update available from {isLatestBranch ? 'main' : 'stable'} branch!

Current: {updateInfo.currentVersion} ({updateInfo.currentCommit})

Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit})

To update:

  1. 1

    Pull the latest changes:

    git pull upstream {isLatestBranch ? 'main' : 'stable'}
  2. 2

    Install dependencies:

    pnpm install
  3. 3

    Build the application:

    pnpm build
  4. 4

    Restart the application

)}
{/* Changelog */} {updateInfo.changelog && updateInfo.changelog.length > 0 && (
{showChangelog && (
{categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
{category} ({messages.length})
{messages.map((message, index) => { const { title, description, prNumber } = parseCommitMessage(message); return (

{title} {prNumber && ( #{prNumber} )}

{description && (

{description}

)}
); })}
))}
)}
)}
)} {/* Update Progress */} {isUpdating && updateInfo?.downloadProgress !== undefined && (
Downloading Update {Math.round(updateInfo.downloadProgress)}%
{retryCount > 0 &&

Retry attempt {retryCount}/3...

}
)} {/* Update Confirmation Dialog */} { setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); logStore.logSystem('Update cancelled by user'); }} >
Update Available A new version is available. Would you like to update now?

Changelog:

{updateChangelog.map((log, index) => (
{log}
))}
{ setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); logStore.logSystem('Update cancelled by user'); }} > Cancel { setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); await initiateUpdate(); }} > Update Now
); }; export default UpdateTab;