diff --git a/electron/main/index.ts b/electron/main/index.ts index 682f3d8c..1365276a 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -16,6 +16,7 @@ import { copyBrowserData } from './copy' import { findAvailablePort } from './init' import kill from 'tree-kill'; import { zipFolder } from './utils/log' +import archiver from 'archiver'; import axios from 'axios'; import FormData from 'form-data'; import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps' @@ -168,6 +169,11 @@ const getBackupLogPath = () => { const userDataPath = app.getPath('userData') return path.join(userDataPath, 'logs', 'main.log') } +// Get app log path +const getAppLogPath = () => { + const eigentBase = path.join(os.homedir(), '.eigent'); + return path.join(eigentBase, 'runtime', 'log') +} // Constants define const BROWSER_PATHS = { win32: { @@ -323,16 +329,27 @@ function registerIpcHandlers() { // ==================== log export handler ==================== ipcMain.handle('export-log', async () => { try { - let targetLogPath = logPath; + // Always use main.log as the target log file + let targetLogPath = getBackupLogPath(); // This returns main.log + let appLogPath = getAppLogPath(); + + // Check if main.log exists, if not try the electron-log path if (!fs.existsSync(targetLogPath)) { - const backupPath = getBackupLogPath(); - if (fs.existsSync(backupPath)) { - targetLogPath = backupPath; - } else { - return { success: false, error: 'no log file' }; - } + targetLogPath = logPath; // fallback to electron-log path + } + + // Check if app log path exists + if (!fs.existsSync(appLogPath)) { + log.warn('App log path does not exist:', appLogPath); + appLogPath = ''; // Set to empty if not exists } + // Debug log paths + log.info('Target log path:', targetLogPath); + log.info('App log path:', appLogPath); + log.info('Target log exists:', fs.existsSync(targetLogPath)); + log.info('App log exists:', fs.existsSync(appLogPath)); + await fsp.access(targetLogPath, fs.constants.R_OK); const stats = await fsp.stat(targetLogPath); if (stats.size === 0) { @@ -342,25 +359,78 @@ function registerIpcHandlers() { const logContent = await fsp.readFile(targetLogPath, 'utf-8'); // Get app version and system version - const appVersion = app.getVersion(); - const platform = process.platform; - const arch = process.arch; - const systemVersion = `${platform}-${arch}`; - const defaultFileName = `eigent-${appVersion}-${systemVersion}-${Date.now()}.log`; - - // Show save dialog - const { canceled, filePath } = await dialog.showSaveDialog({ - title: 'save log file', - defaultPath: defaultFileName, - filters: [{ name: 'log file', extensions: ['log', 'txt'] }] - }); + const appVersion = app.getVersion(); + const platform = process.platform; + const arch = process.arch; + const systemVersion = `${platform}-${arch}`; + const defaultFileName = `eigent-logs-${appVersion}-${systemVersion}-${Date.now()}.zip`; + + // Show save dialog + const { canceled, filePath } = await dialog.showSaveDialog({ + title: 'Save Log Archive', + defaultPath: defaultFileName, + filters: [{ name: 'ZIP Archive', extensions: ['zip'] }] + }); - if (canceled || !filePath) { - return { success: false, error: '' }; - } + if (canceled || !filePath) { + return { success: false, error: '' }; + } + + // Create zip archive + const output = fs.createWriteStream(filePath); + const archive = archiver('zip', { + zlib: { level: 9 } // Maximum compression + }); + + // Handle archive events + archive.on('error', (err) => { + throw err; + }); + + archive.on('warning', (err) => { + if (err.code === 'ENOENT') { + log.warn('Archive warning:', err); + } else { + throw err; + } + }); + + // Pipe archive data to the file + archive.pipe(output); + + // Add the target log file to the root of the zip + if (fs.existsSync(targetLogPath)) { + const logFileName = path.basename(targetLogPath); + archive.file(targetLogPath, { name: logFileName }); + } + + // Add the app log folder contents to the second level + if (appLogPath && fs.existsSync(appLogPath)) { + const appLogStats = await fsp.stat(appLogPath); + if (appLogStats.isDirectory()) { + // Add entire directory to archive with its folder name + const folderName = path.basename(appLogPath); + archive.directory(appLogPath, folderName); + } else if (appLogStats.isFile()) { + // Add single file to archive + const appLogFileName = path.basename(appLogPath); + archive.file(appLogPath, { name: appLogFileName }); + } + } + + // Finalize the archive + await archive.finalize(); + + // Wait for the output stream to finish + await new Promise((resolve, reject) => { + output.on('close', () => { + log.info(`Archive created successfully. Total bytes: ${archive.pointer()}`); + resolve(void 0); + }); + output.on('error', reject); + }); - await fsp.writeFile(filePath, logContent, 'utf-8'); - return { success: true, savedPath: filePath }; + return { success: true, savedPath: filePath }; } catch (error: any) { return { success: false, error: error.message }; } @@ -545,7 +615,7 @@ function registerIpcHandlers() { // ==================== window control handler ==================== ipcMain.on('window-close', (_, data) => { - if(data.isForceQuit) { + if (data.isForceQuit) { return app?.quit() } return win?.close() @@ -848,9 +918,9 @@ function registerIpcHandlers() { // ==================== dependency install handler ==================== ipcMain.handle('install-dependencies', async () => { try { - if(win === null) throw new Error("Window is null"); + if (win === null) throw new Error("Window is null"); //Force installation even if versionFile exists - const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true}); + const isInstalled = await checkAndInstallDepsOnUpdate({ win, forceInstall: true }); return { success: true, isInstalled }; } catch (error) { return { success: false, error: (error as Error).message }; @@ -869,9 +939,9 @@ function registerIpcHandlers() { ipcMain.handle('get-installation-status', async () => { try { const { isInstalling, hasLockFile } = await getInstallationStatus(); - return { - success: true, - isInstalling, + return { + success: true, + isInstalling, hasLockFile, timestamp: Date.now() }; @@ -1115,7 +1185,7 @@ async function createWindow() { }); // Now check and install dependencies - let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); + let res: PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { log.info("[DEPS INSTALL] Dependency Error: ", res.message); win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message }); @@ -1186,15 +1256,14 @@ const checkAndStartBackend = async () => { if (isToolInstalled.success) { log.info('Tool installed, starting backend service...'); - // Notify frontend installation success - if (win && !win.isDestroyed()) { - win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); - } - python_process = await startBackend((port) => { backendPort = port; log.info('Backend service started successfully', { port }); }); + // Notify frontend installation success + if (win && !win.isDestroyed()) { + win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); + } python_process?.on('exit', (code, signal) => { @@ -1274,18 +1343,18 @@ const cleanupPythonProcess = async () => { // before close const handleBeforeClose = () => { - let isQuitting = false; - - app.on('before-quit', () => { - isQuitting = true; - }); - - win?.on("close", (event) => { - if (!isQuitting) { - event.preventDefault(); - win?.webContents.send("before-close"); - } - }) + let isQuitting = false; + + app.on('before-quit', () => { + isQuitting = true; + }); + + win?.on("close", (event) => { + if (!isQuitting) { + event.preventDefault(); + win?.webContents.send("before-close"); + } + }) } // ==================== app event handle ==================== @@ -1339,15 +1408,15 @@ app.whenReady().then(() => { // ==================== window close event ==================== app.on('window-all-closed', () => { log.info('window-all-closed'); - + // Clean up WebView manager if (webViewManager) { webViewManager.destroy(); webViewManager = null; } - + win = null; - + if (process.platform !== 'darwin') { app.quit(); } @@ -1370,35 +1439,35 @@ app.on('activate', () => { app.on('before-quit', async (event) => { log.info('before-quit'); log.info('quit python_process.pid: ' + python_process?.pid); - + // Prevent default quit to ensure cleanup completes event.preventDefault(); - + try { // Clean up resources if (webViewManager) { webViewManager.destroy(); webViewManager = null; } - + if (win && !win.isDestroyed()) { win.destroy(); win = null; } - + // Wait for Python process cleanup await cleanupPythonProcess(); - + // Clean up file reader if exists if (fileReader) { fileReader = null; } - + // Clear any remaining timeouts/intervals if (global.gc) { global.gc(); } - + log.info('All cleanup completed, exiting...'); } catch (error) { log.error('Error during cleanup:', error); diff --git a/src/components/InstallStep/Carousel.tsx b/src/components/InstallStep/Carousel.tsx index 9fe9e6b5..3fb12b1b 100644 --- a/src/components/InstallStep/Carousel.tsx +++ b/src/components/InstallStep/Carousel.tsx @@ -19,6 +19,7 @@ export const CarouselStep: React.FC = () => { const [currentSlide, setCurrentSlide] = useState(0); const [isHovered, setIsHovered] = useState(false); const [api, setApi] = useState(null); + const [isCarouselCompleted, setIsCarouselCompleted] = useState(false); const videoRefs = useRef<(HTMLVideoElement | null)[]>([]); // listen to carousel change useEffect(() => { @@ -95,85 +96,99 @@ export const CarouselStep: React.FC = () => { }, [currentSlide, api]); return (
-
-
- {carouselItems[currentSlide].title} -
+ {!isCarouselCompleted ? ( + <> + {/* Carousel content - title and videos */} +
+
+ {carouselItems[currentSlide].title} +
- - - {carouselItems.map((_, index) => ( - -
- - -
-
- ))} -
-
-
-
-
- {carouselItems.map((item, index) => ( -
handleIndicatorHover(index)} - className={`w-[120px] h-1.5 rounded-full cursor-pointer transition-all duration-300 ${ - index === currentSlide - ? "bg-fill-fill-secondary" - : "bg-white-100% hover:bg-fill-fill-secondary" - }`} - >
- ))} -
-
- - -
-
+ + + {carouselItems.map((_, index) => ( + +
+ + +
+
+ ))} +
+
+
+ + {/* Carousel controls - indicators and buttons */} +
+
+ {carouselItems.map((item, index) => ( +
handleIndicatorHover(index)} + className={`w-[120px] h-1.5 rounded-full cursor-pointer transition-all duration-300 ${ + index === currentSlide + ? "bg-fill-fill-secondary" + : "bg-white-100% hover:bg-fill-fill-secondary" + }`} + >
+ ))} +
+
+ + +
+
+ + ) : ( + // Empty state when carousel is completed - waiting for installation +
+ )}
); }; diff --git a/src/components/InstallStep/Permissions.tsx b/src/components/InstallStep/Permissions.tsx index 5ea60715..1980ddcb 100644 --- a/src/components/InstallStep/Permissions.tsx +++ b/src/components/InstallStep/Permissions.tsx @@ -103,14 +103,18 @@ export const Permissions: React.FC = () => {