ui fix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user