import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { useSettings } from '~/lib/hooks/useSettings'; import { logStore } from '~/lib/stores/logs'; import { toast } from 'react-toastify'; import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog'; import { classNames } from '~/utils/classNames'; import { Markdown } from '~/components/chat/Markdown'; interface UpdateProgress { stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete'; message: string; progress?: number; error?: string; details?: { changedFiles?: string[]; additions?: number; deletions?: number; commitMessages?: string[]; totalSize?: string; currentCommit?: string; remoteCommit?: string; updateReady?: boolean; changelog?: string; compareUrl?: string; }; } interface UpdateSettings { autoUpdate: boolean; notifyInApp: boolean; checkInterval: number; } const ProgressBar = ({ progress }: { progress: number }) => (
); const UpdateProgressDisplay = ({ progress }: { progress: UpdateProgress }) => (
{progress.message} {progress.progress}%
{progress.details && (
{progress.details.changedFiles && progress.details.changedFiles.length > 0 && (
Changed Files:
{/* Group files by type */} {['Modified', 'Added', 'Deleted'].map((type) => { const filesOfType = progress.details?.changedFiles?.filter((file) => file.startsWith(type)) || []; if (filesOfType.length === 0) { return null; } return (
{type} ({filesOfType.length})
{filesOfType.map((file, index) => { const fileName = file.split(': ')[1]; return (
{fileName}
); })}
); })}
)} {progress.details.totalSize &&
Total size: {progress.details.totalSize}
} {progress.details.additions !== undefined && progress.details.deletions !== undefined && (
Changes: +{progress.details.additions}{' '} -{progress.details.deletions}
)} {progress.details.currentCommit && progress.details.remoteCommit && (
Updating from {progress.details.currentCommit} to {progress.details.remoteCommit}
)}
)}
); const UpdateTab = () => { const { isLatestBranch } = useSettings(); const [isChecking, setIsChecking] = useState(false); const [error, setError] = useState(null); const [updateSettings, setUpdateSettings] = useState(() => { const stored = localStorage.getItem('update_settings'); return stored ? JSON.parse(stored) : { autoUpdate: false, notifyInApp: true, checkInterval: 24, }; }); const [showUpdateDialog, setShowUpdateDialog] = useState(false); const [updateProgress, setUpdateProgress] = useState(null); useEffect(() => { localStorage.setItem('update_settings', JSON.stringify(updateSettings)); }, [updateSettings]); const checkForUpdates = async () => { console.log('Starting update check...'); setIsChecking(true); setError(null); setUpdateProgress(null); try { const branchToCheck = isLatestBranch ? 'main' : 'stable'; // Start the update check with streaming progress const response = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ branch: branchToCheck, autoUpdate: updateSettings.autoUpdate, }), }); if (!response.ok) { throw new Error(`Update check failed: ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('No response stream available'); } // Read the stream while (true) { const { done, value } = await reader.read(); if (done) { break; } // Convert the chunk to text and parse the JSON const chunk = new TextDecoder().decode(value); const lines = chunk.split('\n').filter(Boolean); for (const line of lines) { try { const progress = JSON.parse(line) as UpdateProgress; setUpdateProgress(progress); if (progress.error) { setError(progress.error); } // If we're done, update the UI accordingly if (progress.stage === 'complete') { setIsChecking(false); if (!progress.error) { // Update check completed toast.success('Update check completed'); // Show update dialog only if there are changes and auto-update is disabled if (progress.details?.changedFiles?.length && progress.details.updateReady) { setShowUpdateDialog(true); } } } } catch (e) { console.error('Error parsing progress update:', e); } } } } catch (error) { setError(error instanceof Error ? error.message : 'Unknown error occurred'); logStore.logWarning('Update Check Failed', { type: 'update', message: error instanceof Error ? error.message : 'Unknown error occurred', }); } finally { setIsChecking(false); } }; const handleUpdate = async () => { setShowUpdateDialog(false); try { const branchToCheck = isLatestBranch ? 'main' : 'stable'; // Start the update with autoUpdate set to true to force the update const response = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ branch: branchToCheck, autoUpdate: true, }), }); if (!response.ok) { throw new Error(`Update failed: ${response.statusText}`); } // Handle the update progress stream const reader = response.body?.getReader(); if (!reader) { throw new Error('No response stream available'); } while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunk = new TextDecoder().decode(value); const lines = chunk.split('\n').filter(Boolean); for (const line of lines) { try { const progress = JSON.parse(line) as UpdateProgress; setUpdateProgress(progress); if (progress.error) { setError(progress.error); toast.error('Update failed'); } if (progress.stage === 'complete' && !progress.error) { toast.success('Update completed successfully'); } } catch (e) { console.error('Error parsing update progress:', e); } } } } catch (error) { setError(error instanceof Error ? error.message : 'Unknown error occurred'); toast.error('Update failed'); } }; 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 */}

Update Status

{updateProgress?.details?.updateReady && !updateSettings.autoUpdate && ( )}
{/* Show progress information */} {updateProgress && } {error &&
{error}
} {/* Show update source information */} {updateProgress?.details?.currentCommit && updateProgress?.details?.remoteCommit && (

Updates are fetched from: stackblitz-labs/bolt.diy ( {isLatestBranch ? 'main' : 'stable'} branch)

Current version: {updateProgress.details.currentCommit} Latest version: {updateProgress.details.remoteCommit}

{updateProgress?.details?.compareUrl && ( {updateProgress?.details?.additions !== undefined && updateProgress?.details?.deletions !== undefined && (
Changes: +{updateProgress.details.additions}{' '} -{updateProgress.details.deletions}
)}
)} {/* Add this before the changed files section */} {updateProgress?.details?.changelog && (

Changelog

{updateProgress.details.changelog}
)} {/* Add this in the update status card, after the commit info */} {updateProgress?.details?.compareUrl && (
)} {updateProgress?.details?.commitMessages && updateProgress.details.commitMessages.length > 0 && (

Changes in this Update:

{updateProgress.details.commitMessages.map((section, index) => ( {section} ))}
)} {/* Update dialog */} Update Available

A new version is available from stackblitz-labs/bolt.diy ( {isLatestBranch ? 'main' : 'stable'} branch)

{updateProgress?.details?.compareUrl && (
)} {updateProgress?.details?.commitMessages && updateProgress.details.commitMessages.length > 0 && (

Commit Messages:

{updateProgress.details.commitMessages.map((msg, index) => (
{msg}
))}
)} {updateProgress?.details?.totalSize && (
Total size: {updateProgress.details.totalSize}
{updateProgress?.details?.additions !== undefined && updateProgress?.details?.deletions !== undefined && (
Changes: +{updateProgress.details.additions}{' '} -{updateProgress.details.deletions}
)}
)}
setShowUpdateDialog(false)}> Cancel Update Now
); }; export default UpdateTab;