mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-05 20:54:40 +00:00
bug fix and some icons changes
This commit is contained in:
parent
8035a76429
commit
9171cf48aa
@ -11,11 +11,15 @@ import { useFeatures } from '~/lib/hooks/useFeatures';
|
|||||||
import { useNotifications } from '~/lib/hooks/useNotifications';
|
import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||||
import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
|
import {
|
||||||
|
tabConfigurationStore,
|
||||||
|
developerModeStore,
|
||||||
|
setDeveloperMode,
|
||||||
|
resetTabConfiguration,
|
||||||
|
} from '~/lib/stores/settings';
|
||||||
import { profileStore } from '~/lib/stores/profile';
|
import { profileStore } from '~/lib/stores/profile';
|
||||||
import type { TabType, TabVisibilityConfig, DevTabConfig, Profile } from './types';
|
import type { TabType, TabVisibilityConfig, Profile } from './types';
|
||||||
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
||||||
import { resetTabConfiguration } from '~/lib/stores/settings';
|
|
||||||
import { DialogTitle } from '~/components/ui/Dialog';
|
import { DialogTitle } from '~/components/ui/Dialog';
|
||||||
import { AvatarDropdown } from './AvatarDropdown';
|
import { AvatarDropdown } from './AvatarDropdown';
|
||||||
|
|
||||||
@ -43,6 +47,24 @@ interface TabWithDevType extends TabVisibilityConfig {
|
|||||||
isExtraDevTab?: boolean;
|
isExtraDevTab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExtendedTabConfig extends TabVisibilityConfig {
|
||||||
|
isExtraDevTab?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseTabConfig {
|
||||||
|
id: TabType;
|
||||||
|
visible: boolean;
|
||||||
|
window: 'user' | 'developer';
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnimatedSwitchProps {
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange: (checked: boolean) => void;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||||
profile: 'Manage your profile and account settings',
|
profile: 'Manage your profile and account settings',
|
||||||
settings: 'Configure application preferences',
|
settings: 'Configure application preferences',
|
||||||
@ -60,6 +82,65 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|||||||
'tab-management': 'Configure visible tabs and their order',
|
'tab-management': 'Configure visible tabs and their order',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
className={classNames(
|
||||||
|
'relative inline-flex h-6 w-11 items-center rounded-full',
|
||||||
|
'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
|
||||||
|
'bg-gray-200 dark:bg-gray-700',
|
||||||
|
'data-[state=checked]:bg-purple-500',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
|
||||||
|
'cursor-pointer',
|
||||||
|
'group',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className={classNames(
|
||||||
|
'absolute left-[2px] top-[2px]',
|
||||||
|
'inline-block h-5 w-5 rounded-full',
|
||||||
|
'bg-white shadow-lg',
|
||||||
|
'transition-shadow duration-300',
|
||||||
|
'group-hover:shadow-md group-active:shadow-sm',
|
||||||
|
'group-hover:scale-95 group-active:scale-90',
|
||||||
|
)}
|
||||||
|
layout
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 500,
|
||||||
|
damping: 30,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
x: checked ? '1.25rem' : '0rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full bg-white"
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
scale: checked ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
</motion.span>
|
||||||
|
<span className="sr-only">Toggle {label}</span>
|
||||||
|
</Switch>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||||
// State
|
// State
|
||||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||||
@ -78,7 +159,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||||
|
|
||||||
// Add visibleTabs logic using useMemo
|
// Memoize the base tab configurations to avoid recalculation
|
||||||
|
const baseTabConfig = useMemo(() => {
|
||||||
|
return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add visibleTabs logic using useMemo with optimized calculations
|
||||||
const visibleTabs = useMemo(() => {
|
const visibleTabs = useMemo(() => {
|
||||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||||
console.warn('Invalid tab configuration, resetting to defaults');
|
console.warn('Invalid tab configuration, resetting to defaults');
|
||||||
@ -87,64 +173,84 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notificationsDisabled = profile?.preferences?.notifications === false;
|
||||||
|
|
||||||
// In developer mode, show ALL tabs without restrictions
|
// In developer mode, show ALL tabs without restrictions
|
||||||
if (developerMode) {
|
if (developerMode) {
|
||||||
// Combine all unique tabs from both user and developer configurations
|
const seenTabs = new Set<TabType>();
|
||||||
const allTabs = new Set([
|
const devTabs: ExtendedTabConfig[] = [];
|
||||||
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
|
|
||||||
...tabConfiguration.userTabs.map((tab) => tab.id),
|
|
||||||
...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a complete tab list with all tabs visible
|
// Process tabs in order of priority: developer, user, default
|
||||||
const devTabs = Array.from(allTabs).map((tabId) => {
|
const processTab = (tab: BaseTabConfig) => {
|
||||||
// Try to find existing configuration for this tab
|
if (!seenTabs.has(tab.id)) {
|
||||||
const existingTab =
|
seenTabs.add(tab.id);
|
||||||
tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
|
devTabs.push({
|
||||||
tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
|
id: tab.id,
|
||||||
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
|
visible: true,
|
||||||
|
window: 'developer',
|
||||||
|
order: tab.order || devTabs.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
// Process tabs in priority order
|
||||||
id: tabId,
|
tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||||
visible: true,
|
tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||||
window: 'developer' as const,
|
DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||||
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Tab Management tile for developer mode
|
// Add Tab Management tile
|
||||||
const tabManagementConfig: DevTabConfig = {
|
devTabs.push({
|
||||||
id: 'tab-management',
|
id: 'tab-management' as TabType,
|
||||||
visible: true,
|
visible: true,
|
||||||
window: 'developer',
|
window: 'developer',
|
||||||
order: devTabs.length,
|
order: devTabs.length,
|
||||||
isExtraDevTab: true,
|
isExtraDevTab: true,
|
||||||
};
|
});
|
||||||
devTabs.push(tabManagementConfig);
|
|
||||||
|
|
||||||
return devTabs.sort((a, b) => a.order - b.order);
|
return devTabs.sort((a, b) => a.order - b.order);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In user mode, only show visible user tabs
|
// Optimize user mode tab filtering
|
||||||
const notificationsDisabled = profile?.preferences?.notifications === false;
|
|
||||||
|
|
||||||
return tabConfiguration.userTabs
|
return tabConfiguration.userTabs
|
||||||
.filter((tab) => {
|
.filter((tab) => {
|
||||||
if (!tab || typeof tab.id !== 'string') {
|
if (!tab?.id) {
|
||||||
console.warn('Invalid tab entry:', tab);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide notifications tab if notifications are disabled in user preferences
|
|
||||||
if (tab.id === 'notifications' && notificationsDisabled) {
|
if (tab.id === 'notifications' && notificationsDisabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show tabs that are explicitly visible and assigned to the user window
|
|
||||||
return tab.visible && tab.window === 'user';
|
return tab.visible && tab.window === 'user';
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
}, [tabConfiguration, developerMode, profile?.preferences?.notifications]);
|
}, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
|
||||||
|
|
||||||
|
// Optimize animation performance with layout animations
|
||||||
|
const gridLayoutVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.05,
|
||||||
|
delayChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, scale: 0.8 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 20,
|
||||||
|
mass: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@ -328,7 +434,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
|
<div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
|
||||||
<div className="i-ph:robot-fill w-5 h-5 text-gray-400 dark:text-gray-400 transition-colors" />
|
<div className="i-ph:lightning-fill w-5 h-5 text-purple-500 dark:text-purple-400 transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -338,39 +444,14 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
{/* Developer Mode Controls */}
|
{/* Mode Toggle */}
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
|
||||||
{/* Mode Toggle */}
|
<AnimatedSwitch
|
||||||
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
|
id="developer-mode"
|
||||||
<Switch
|
checked={developerMode}
|
||||||
id="developer-mode"
|
onCheckedChange={handleDeveloperModeChange}
|
||||||
checked={developerMode}
|
label={developerMode ? 'Developer Mode' : 'User Mode'}
|
||||||
onCheckedChange={handleDeveloperModeChange}
|
/>
|
||||||
className={classNames(
|
|
||||||
'relative inline-flex h-6 w-11 items-center rounded-full',
|
|
||||||
'bg-gray-200 dark:bg-gray-700',
|
|
||||||
'data-[state=checked]:bg-purple-500',
|
|
||||||
'transition-colors duration-200',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Toggle developer mode</span>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'inline-block h-4 w-4 transform rounded-full bg-white',
|
|
||||||
'transition duration-200',
|
|
||||||
'translate-x-1 data-[state=checked]:translate-x-6',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="developer-mode"
|
|
||||||
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
|
|
||||||
>
|
|
||||||
{developerMode ? 'Developer Mode' : 'User Mode'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar and Dropdown */}
|
{/* Avatar and Dropdown */}
|
||||||
@ -415,24 +496,15 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|||||||
) : activeTab ? (
|
) : activeTab ? (
|
||||||
getTabComponent(activeTab)
|
getTabComponent(activeTab)
|
||||||
) : (
|
) : (
|
||||||
<motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
|
<motion.div
|
||||||
|
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
|
||||||
|
variants={gridLayoutVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
>
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
||||||
<motion.div
|
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
|
||||||
key={tab.id}
|
|
||||||
layout
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 30,
|
|
||||||
mass: 0.8,
|
|
||||||
duration: 0.3,
|
|
||||||
}}
|
|
||||||
className="aspect-[1.5/1]"
|
|
||||||
>
|
|
||||||
<TabTile
|
<TabTile
|
||||||
tab={tab}
|
tab={tab}
|
||||||
onClick={() => handleTabClick(tab.id as TabType)}
|
onClick={() => handleTabClick(tab.id as TabType)}
|
||||||
|
@ -22,6 +22,19 @@ interface GitHubReleaseResponse {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateProgress {
|
||||||
|
stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
|
||||||
|
message: string;
|
||||||
|
progress?: number;
|
||||||
|
error?: string;
|
||||||
|
details?: {
|
||||||
|
changedFiles?: string[];
|
||||||
|
additions?: number;
|
||||||
|
deletions?: number;
|
||||||
|
commitMessages?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateInfo {
|
interface UpdateInfo {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
@ -32,9 +45,7 @@ interface UpdateInfo {
|
|||||||
changelog?: string[];
|
changelog?: string[];
|
||||||
currentCommit?: string;
|
currentCommit?: string;
|
||||||
latestCommit?: string;
|
latestCommit?: string;
|
||||||
downloadProgress?: number;
|
updateProgress?: UpdateProgress;
|
||||||
installProgress?: number;
|
|
||||||
estimatedTimeRemaining?: number;
|
|
||||||
error?: {
|
error?: {
|
||||||
type: string;
|
type: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -47,13 +58,6 @@ interface UpdateSettings {
|
|||||||
checkInterval: number;
|
checkInterval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateResponse {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
message?: string;
|
|
||||||
instructions?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const categorizeChangelog = (messages: string[]) => {
|
const categorizeChangelog = (messages: string[]) => {
|
||||||
const categories = new Map<string, string[]>();
|
const categories = new Map<string, string[]>();
|
||||||
|
|
||||||
@ -168,7 +172,6 @@ const UpdateTab = () => {
|
|||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
|
||||||
const [showChangelog, setShowChangelog] = useState(false);
|
const [showChangelog, setShowChangelog] = useState(false);
|
||||||
const [showManualInstructions, setShowManualInstructions] = useState(false);
|
const [showManualInstructions, setShowManualInstructions] = useState(false);
|
||||||
const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
|
const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
|
||||||
@ -186,6 +189,7 @@ const UpdateTab = () => {
|
|||||||
const [lastChecked, setLastChecked] = useState<Date | null>(null);
|
const [lastChecked, setLastChecked] = useState<Date | null>(null);
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
|
const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
|
||||||
|
const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
||||||
@ -259,78 +263,105 @@ const UpdateTab = () => {
|
|||||||
const initiateUpdate = async () => {
|
const initiateUpdate = async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setUpdateProgress(null);
|
||||||
|
|
||||||
let currentRetry = 0;
|
try {
|
||||||
const maxRetries = 3;
|
const response = await fetch('/api/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
branch: isLatestBranch ? 'main' : 'stable',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const attemptUpdate = async (): Promise<void> => {
|
if (!response.ok) {
|
||||||
try {
|
const errorData = (await response.json()) as { error: string };
|
||||||
const response = await fetch('/api/update', {
|
throw new Error(errorData.error || 'Failed to initiate update');
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
branch: isLatestBranch ? 'main' : 'stable',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = (await response.json()) as { error: string };
|
|
||||||
throw new Error(errorData.error || 'Failed to initiate update');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = (await response.json()) as UpdateResponse;
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logStore.logSuccess('Update instructions ready', {
|
|
||||||
type: 'update',
|
|
||||||
message: result.message || 'Update instructions ready',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show manual update instructions
|
|
||||||
setShowManualInstructions(true);
|
|
||||||
setUpdateChangelog(
|
|
||||||
result.instructions || [
|
|
||||||
'Failed to get update instructions. Please update manually:',
|
|
||||||
'1. git pull origin main',
|
|
||||||
'2. pnpm install',
|
|
||||||
'3. pnpm build',
|
|
||||||
'4. Restart the application',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(result.error || 'Update failed');
|
|
||||||
} 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 get update instructions. Please update manually.');
|
|
||||||
console.error('Update failed:', err);
|
|
||||||
logStore.logSystem('Update failed: ' + errorMessage);
|
|
||||||
toast.error('Update failed: ' + errorMessage);
|
|
||||||
setUpdateFailed(true);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
await attemptUpdate();
|
// Handle streaming response
|
||||||
setIsUpdating(false);
|
const reader = response.body?.getReader();
|
||||||
setRetryCount(0);
|
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Failed to read response stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value);
|
||||||
|
const updates = chunk.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
try {
|
||||||
|
const progress = JSON.parse(update) as UpdateProgress;
|
||||||
|
setUpdateProgress(progress);
|
||||||
|
|
||||||
|
if (progress.error) {
|
||||||
|
throw new Error(progress.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.stage === 'complete') {
|
||||||
|
logStore.logSuccess('Update completed', {
|
||||||
|
type: 'update',
|
||||||
|
message: progress.message,
|
||||||
|
});
|
||||||
|
toast.success(progress.message);
|
||||||
|
setUpdateFailed(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logStore.logInfo(`Update progress: ${progress.stage}`, {
|
||||||
|
type: 'update',
|
||||||
|
message: progress.message,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse update progress:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
|
setError('Failed to complete update. Please try again or update manually.');
|
||||||
|
console.error('Update failed:', err);
|
||||||
|
logStore.logSystem('Update failed: ' + errorMessage);
|
||||||
|
toast.error('Update failed: ' + errorMessage);
|
||||||
|
setUpdateFailed(true);
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRestart = async () => {
|
||||||
|
// Show confirmation dialog
|
||||||
|
if (window.confirm('The application needs to restart to apply the update. Proceed?')) {
|
||||||
|
// Save any necessary state
|
||||||
|
localStorage.setItem('pendingRestart', 'true');
|
||||||
|
|
||||||
|
// Reload the page
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for pending restart on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const pendingRestart = localStorage.getItem('pendingRestart');
|
||||||
|
|
||||||
|
if (pendingRestart === 'true') {
|
||||||
|
localStorage.removeItem('pendingRestart');
|
||||||
|
toast.success('Update applied successfully!');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
|
const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
|
||||||
const intervalId = setInterval(checkForUpdates, checkInterval);
|
const intervalId = setInterval(checkForUpdates, checkInterval);
|
||||||
@ -741,7 +772,7 @@ const UpdateTab = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Update Progress */}
|
{/* Update Progress */}
|
||||||
{isUpdating && updateInfo?.downloadProgress !== undefined && (
|
{isUpdating && updateProgress && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@ -750,18 +781,83 @@ const UpdateTab = () => {
|
|||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-bolt-elements-textPrimary">Downloading Update</span>
|
<div>
|
||||||
<span className="text-sm text-bolt-elements-textSecondary">
|
<span className="text-sm font-medium text-bolt-elements-textPrimary">
|
||||||
{Math.round(updateInfo.downloadProgress)}%
|
{updateProgress.stage.charAt(0).toUpperCase() + updateProgress.stage.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
|
<p className="text-xs text-bolt-elements-textSecondary">{updateProgress.message}</p>
|
||||||
|
</div>
|
||||||
|
{updateProgress.progress !== undefined && (
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">{Math.round(updateProgress.progress)}%</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
{/* Show detailed information when available */}
|
||||||
className="h-full bg-purple-500 transition-all duration-300"
|
{updateProgress.details && (
|
||||||
style={{ width: `${updateInfo.downloadProgress}%` }}
|
<div className="mt-4 space-y-4">
|
||||||
/>
|
{updateProgress.details.commitMessages && updateProgress.details.commitMessages.length > 0 && (
|
||||||
</div>
|
<div>
|
||||||
{retryCount > 0 && <p className="text-sm text-yellow-500">Retry attempt {retryCount}/3...</p>}
|
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Commits to be applied:</h4>
|
||||||
|
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
|
||||||
|
{updateProgress.details.commitMessages.map((msg, i) => (
|
||||||
|
<div key={i} className="text-bolt-elements-textSecondary py-1">
|
||||||
|
{msg}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateProgress.details.changedFiles && updateProgress.details.changedFiles.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changed Files:</h4>
|
||||||
|
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
|
||||||
|
{updateProgress.details.changedFiles.map((file, i) => (
|
||||||
|
<div key={i} className="text-bolt-elements-textSecondary py-1">
|
||||||
|
{file}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(updateProgress.details.additions !== undefined || updateProgress.details.deletions !== undefined) && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{updateProgress.details.additions !== undefined && (
|
||||||
|
<div className="text-green-500">
|
||||||
|
<span className="text-sm">+{updateProgress.details.additions} additions</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{updateProgress.details.deletions !== undefined && (
|
||||||
|
<div className="text-red-500">
|
||||||
|
<span className="text-sm">-{updateProgress.details.deletions} deletions</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateProgress.progress !== undefined && (
|
||||||
|
<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: `${updateProgress.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show restart button when update is complete */}
|
||||||
|
{updateProgress.stage === 'complete' && !updateProgress.error && (
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={handleRestart}
|
||||||
|
className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
|
||||||
|
>
|
||||||
|
Restart Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
import { json } from '@remix-run/node';
|
import { json } from '@remix-run/node';
|
||||||
import type { ActionFunction } from '@remix-run/node';
|
import type { ActionFunction } from '@remix-run/node';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
interface UpdateRequestBody {
|
interface UpdateRequestBody {
|
||||||
branch: string;
|
branch: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateProgress {
|
||||||
|
stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
|
||||||
|
message: string;
|
||||||
|
progress?: number;
|
||||||
|
error?: string;
|
||||||
|
details?: {
|
||||||
|
changedFiles?: string[];
|
||||||
|
additions?: number;
|
||||||
|
deletions?: number;
|
||||||
|
commitMessages?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const action: ActionFunction = async ({ request }) => {
|
export const action: ActionFunction = async ({ request }) => {
|
||||||
if (request.method !== 'POST') {
|
if (request.method !== 'POST') {
|
||||||
return json({ error: 'Method not allowed' }, { status: 405 });
|
return json({ error: 'Method not allowed' }, { status: 405 });
|
||||||
@ -13,24 +30,135 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
// Type guard to check if body has the correct shape
|
|
||||||
if (!body || typeof body !== 'object' || !('branch' in body) || typeof body.branch !== 'string') {
|
if (!body || typeof body !== 'object' || !('branch' in body) || typeof body.branch !== 'string') {
|
||||||
return json({ error: 'Invalid request body: branch is required and must be a string' }, { status: 400 });
|
return json({ error: 'Invalid request body: branch is required and must be a string' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { branch } = body as UpdateRequestBody;
|
const { branch } = body as UpdateRequestBody;
|
||||||
|
|
||||||
// Instead of direct Git operations, we'll return instructions
|
// Create a ReadableStream to send progress updates
|
||||||
return json({
|
const stream = new ReadableStream({
|
||||||
success: true,
|
async start(controller) {
|
||||||
message: 'Please update manually using the following steps:',
|
const encoder = new TextEncoder();
|
||||||
instructions: [
|
const sendProgress = (update: UpdateProgress) => {
|
||||||
`1. git fetch origin ${branch}`,
|
controller.enqueue(encoder.encode(JSON.stringify(update) + '\n'));
|
||||||
`2. git pull origin ${branch}`,
|
};
|
||||||
'3. pnpm install',
|
|
||||||
'4. pnpm build',
|
try {
|
||||||
'5. Restart the application',
|
// Fetch stage
|
||||||
],
|
sendProgress({
|
||||||
|
stage: 'fetch',
|
||||||
|
message: 'Fetching latest changes...',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current commit hash
|
||||||
|
const { stdout: currentCommit } = await execAsync('git rev-parse HEAD');
|
||||||
|
|
||||||
|
// Fetch changes
|
||||||
|
await execAsync('git fetch origin');
|
||||||
|
|
||||||
|
// Get list of changed files
|
||||||
|
const { stdout: diffOutput } = await execAsync(`git diff --name-status origin/${branch}`);
|
||||||
|
const changedFiles = diffOutput
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [status, file] = line.split('\t');
|
||||||
|
return `${status === 'M' ? 'Modified' : status === 'A' ? 'Added' : 'Deleted'}: ${file}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get commit messages
|
||||||
|
const { stdout: logOutput } = await execAsync(`git log --oneline ${currentCommit.trim()}..origin/${branch}`);
|
||||||
|
const commitMessages = logOutput.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
// Get diff stats
|
||||||
|
const { stdout: diffStats } = await execAsync(`git diff --shortstat origin/${branch}`);
|
||||||
|
const stats = diffStats.match(
|
||||||
|
/(\d+) files? changed(?:, (\d+) insertions?\\(\\+\\))?(?:, (\d+) deletions?\\(-\\))?/,
|
||||||
|
);
|
||||||
|
|
||||||
|
sendProgress({
|
||||||
|
stage: 'fetch',
|
||||||
|
message: 'Changes detected',
|
||||||
|
progress: 100,
|
||||||
|
details: {
|
||||||
|
changedFiles,
|
||||||
|
additions: stats?.[2] ? parseInt(stats[2]) : 0,
|
||||||
|
deletions: stats?.[3] ? parseInt(stats[3]) : 0,
|
||||||
|
commitMessages,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pull stage
|
||||||
|
sendProgress({
|
||||||
|
stage: 'pull',
|
||||||
|
message: `Pulling changes from ${branch}...`,
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await execAsync(`git pull origin ${branch}`);
|
||||||
|
|
||||||
|
sendProgress({
|
||||||
|
stage: 'pull',
|
||||||
|
message: 'Changes pulled successfully',
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install stage
|
||||||
|
sendProgress({
|
||||||
|
stage: 'install',
|
||||||
|
message: 'Installing dependencies...',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await execAsync('pnpm install');
|
||||||
|
|
||||||
|
sendProgress({
|
||||||
|
stage: 'install',
|
||||||
|
message: 'Dependencies installed successfully',
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build stage
|
||||||
|
sendProgress({
|
||||||
|
stage: 'build',
|
||||||
|
message: 'Building application...',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await execAsync('pnpm build');
|
||||||
|
|
||||||
|
sendProgress({
|
||||||
|
stage: 'build',
|
||||||
|
message: 'Build completed successfully',
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete
|
||||||
|
sendProgress({
|
||||||
|
stage: 'complete',
|
||||||
|
message: 'Update completed successfully! Click Restart to apply changes.',
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sendProgress({
|
||||||
|
stage: 'complete',
|
||||||
|
message: 'Update failed',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update preparation failed:', error);
|
console.error('Update preparation failed:', error);
|
||||||
|
Loading…
Reference in New Issue
Block a user