diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef42..2c63c085 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,2 @@ -{} +{ +} diff --git a/electron/lib/sdGenerator.ts b/electron/lib/sdGenerator.ts index 20501e1d..f6c9a1a8 100644 --- a/electron/lib/sdGenerator.ts +++ b/electron/lib/sdGenerator.ts @@ -80,14 +80,14 @@ export class SDGenerator { clipOnCpu, vaeTiling, onProgress, - onLog, + onLog } = options; // Validate binary exists if (!existsSync(binaryPath)) { return { success: false, - error: `Binary not found at: ${binaryPath}`, + error: `Binary not found at: ${binaryPath}` }; } @@ -121,7 +121,7 @@ export class SDGenerator { outputPath, "-v", // Verbose "--offload-to-cpu", // Offload to CPU when needed - "--diffusion-fa", // Use Flash Attention + "--diffusion-fa" // Use Flash Attention ]; if (clipOnCpu) { @@ -169,7 +169,7 @@ export class SDGenerator { // Spawn child process const childProcess = spawn(binaryPath, args, { cwd: outputDir, - env: { ...process.env, LD_LIBRARY_PATH: ldLibraryPath }, + env: { ...process.env, LD_LIBRARY_PATH: ldLibraryPath } }); // Track active process for cancellation @@ -179,7 +179,7 @@ export class SDGenerator { let stdoutData = ""; // Listen to stdout and send logs + parse progress - childProcess.stdout.on("data", (data) => { + childProcess.stdout.on("data", data => { const log = data.toString(); stdoutData += log; @@ -187,7 +187,7 @@ export class SDGenerator { if (onLog) { onLog({ type: "stdout", - message: log, + message: log }); } @@ -201,14 +201,14 @@ export class SDGenerator { detail: { current: progressInfo.current, total: progressInfo.total, - unit: "steps", - }, + unit: "steps" + } }); } }); // Listen to stderr and send logs + parse progress - childProcess.stderr.on("data", (data) => { + childProcess.stderr.on("data", data => { const log = data.toString(); stderrData += log; @@ -216,7 +216,7 @@ export class SDGenerator { if (onLog) { onLog({ type: "stderr", - message: log, + message: log }); } @@ -230,14 +230,14 @@ export class SDGenerator { detail: { current: progressInfo.current, total: progressInfo.total, - unit: "steps", - }, + unit: "steps" + } }); } }); // Wait for process to end - return new Promise((resolve) => { + return new Promise(resolve => { childProcess.on("close", (code, signal) => { this.activeProcess = null; const wasCancelled = @@ -247,7 +247,7 @@ export class SDGenerator { if (wasCancelled) { resolve({ success: false, - error: "Cancelled", + error: "Cancelled" }); return; } @@ -257,18 +257,16 @@ export class SDGenerator { if (onProgress) { onProgress({ phase: "generate", - progress: 100, + progress: 100 }); } resolve({ success: true, - outputPath: outputPath, + outputPath: outputPath }); } else { // Extract error information - const errorLines = stderrData - .split("\n") - .filter((line) => line.trim()); + const errorLines = stderrData.split("\n").filter(line => line.trim()); const errorMsg = errorLines.length > 0 ? errorLines[errorLines.length - 1] @@ -277,17 +275,17 @@ export class SDGenerator { console.error("[SDGenerator] Generation failed:", errorMsg); resolve({ success: false, - error: errorMsg, + error: errorMsg }); } }); - childProcess.on("error", (err) => { + childProcess.on("error", err => { this.activeProcess = null; console.error("[SDGenerator] Process error:", err.message); resolve({ success: false, - error: err.message, + error: err.message }); }); }); @@ -301,7 +299,9 @@ export class SDGenerator { * - "sampling: 18/20" * - "|==================================================| 12/12 - 7.28s/it" */ - private parseProgress(log: string): { + private parseProgress( + log: string + ): { current: number; total: number; progress: number; diff --git a/electron/main.ts b/electron/main.ts index d8bc81c9..33f5c0ec 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -7,7 +7,7 @@ import { Menu, clipboard, protocol, - net, + net } from "electron"; import { join, dirname } from "path"; import { @@ -19,7 +19,7 @@ import { unlinkSync, statSync, readdirSync, - copyFileSync, + copyFileSync } from "fs"; import { readdir, stat } from "fs/promises"; import AdmZip from "adm-zip"; @@ -103,8 +103,8 @@ let binaryPathLoggedOnce = false; function parseMaxNumberFromOutput(output: string): number | null { const values = output .split(/\r?\n/) - .map((line) => parseInt(line.replace(/[^\d]/g, "").trim(), 10)) - .filter((value) => Number.isFinite(value) && value > 0); + .map(line => parseInt(line.replace(/[^\d]/g, "").trim(), 10)) + .filter(value => Number.isFinite(value) && value > 0); if (values.length === 0) { return null; @@ -117,7 +117,7 @@ function getNvidiaVramMb(): number | null { try { const output = execSync( "nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits", - { encoding: "utf8", timeout: 3000 }, + { encoding: "utf8", timeout: 3000 } ); return parseMaxNumberFromOutput(output); } catch (error) { @@ -129,7 +129,7 @@ function getWindowsGpuVramMb(): number | null { try { const output = execSync( 'powershell -NoProfile -Command "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty AdapterRAM"', - { encoding: "utf8", timeout: 3000 }, + { encoding: "utf8", timeout: 3000 } ); const bytes = parseMaxNumberFromOutput(output); if (!bytes) return null; @@ -138,7 +138,7 @@ function getWindowsGpuVramMb(): number | null { try { const output = execSync( "wmic path win32_VideoController get AdapterRAM", - { encoding: "utf8", timeout: 3000 }, + { encoding: "utf8", timeout: 3000 } ); const bytes = parseMaxNumberFromOutput(output); if (!bytes) return null; @@ -224,7 +224,7 @@ const defaultSettings: Settings = { autoCheckUpdate: true, autoSaveAssets: true, assetsDirectory: defaultAssetsDirectory, - language: "auto", + language: "auto" }; function loadSettings(): Settings { @@ -270,8 +270,8 @@ function createWindow(): void { titleBarOverlay: { color: "#080c16", symbolColor: "#6b7280", - height: 32, - }, + height: 32 + } } : {}), webPreferences: { @@ -279,8 +279,8 @@ function createWindow(): void { sandbox: false, contextIsolation: true, nodeIntegration: false, - webSecurity: !is.dev, // Disable web security in dev mode to bypass CORS - }, + webSecurity: !is.dev // Disable web security in dev mode to bypass CORS + } }); mainWindow.on("ready-to-show", () => { @@ -290,7 +290,7 @@ function createWindow(): void { // macOS: Hide window instead of closing when clicking the red button // The app will only quit when user presses Cmd+Q if (process.platform === "darwin") { - mainWindow.on("close", (event) => { + mainWindow.on("close", event => { if (!(app as typeof app & { isQuitting?: boolean }).isQuitting) { event.preventDefault(); if (mainWindow?.isFullScreen()) { @@ -306,7 +306,7 @@ function createWindow(): void { }); } - mainWindow.webContents.setWindowOpenHandler((details) => { + mainWindow.webContents.setWindowOpenHandler(details => { shell.openExternal(details.url); return { action: "deny" }; }); @@ -319,9 +319,9 @@ function createWindow(): void { "Failed to load:", errorCode, errorDescription, - validatedURL, + validatedURL ); - }, + } ); mainWindow.webContents.on("render-process-gone", (_, details) => { @@ -364,7 +364,7 @@ function createWindow(): void { { label: "Copy", role: "copy", enabled: params.editFlags.canCopy }, { label: "Paste", role: "paste", enabled: params.editFlags.canPaste }, { type: "separator" }, - { label: "Select All", role: "selectAll" }, + { label: "Select All", role: "selectAll" } ); } else if (params.selectionText) { // Add copy option when text is selected @@ -377,12 +377,12 @@ function createWindow(): void { menuItems.push( { label: "Open Link in Browser", - click: () => shell.openExternal(params.linkURL), + click: () => shell.openExternal(params.linkURL) }, { label: "Copy Link", - click: () => clipboard.writeText(params.linkURL), - }, + click: () => clipboard.writeText(params.linkURL) + } ); } @@ -392,12 +392,12 @@ function createWindow(): void { menuItems.push( { label: "Copy Image", - click: () => mainWindow?.webContents.copyImageAt(params.x, params.y), + click: () => mainWindow?.webContents.copyImageAt(params.x, params.y) }, { label: "Open Image in Browser", - click: () => shell.openExternal(params.srcURL), - }, + click: () => shell.openExternal(params.srcURL) + } ); } @@ -417,7 +417,7 @@ ipcMain.handle("update-titlebar-theme", (_, isDark: boolean) => { mainWindow.setTitleBarOverlay({ color: isDark ? "#080c16" : "#f6f7f9", symbolColor: isDark ? "#9ca3af" : "#6b7280", - height: 32, + height: 32 }); } catch { // setTitleBarOverlay may not be available on all platforms @@ -442,7 +442,7 @@ ipcMain.handle("get-settings", () => { defaultTimeout: settings.defaultTimeout, updateChannel: settings.updateChannel, autoCheckUpdate: settings.autoCheckUpdate, - language: settings.language, + language: settings.language }; }); @@ -498,8 +498,8 @@ ipcMain.handle( filters: [ { name: "All Files", extensions: ["*"] }, { name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp"] }, - { name: "Videos", extensions: ["mp4", "webm", "mov"] }, - ], + { name: "Videos", extensions: ["mp4", "webm", "mov"] } + ] }); if (result.canceled || !result.filePath) { @@ -521,12 +521,12 @@ ipcMain.handle( } // Download the file from http/https - return new Promise((resolve) => { + return new Promise(resolve => { const httpProtocol = url.startsWith("https") ? https : http; const file = createWriteStream(result.filePath!); httpProtocol - .get(url, (response) => { + .get(url, response => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; @@ -535,14 +535,14 @@ ipcMain.handle( ? https : http; redirectProtocol - .get(redirectUrl, (redirectResponse) => { + .get(redirectUrl, redirectResponse => { redirectResponse.pipe(file); file.on("finish", () => { file.close(); resolve({ success: true, filePath: result.filePath }); }); }) - .on("error", (err) => { + .on("error", err => { resolve({ success: false, error: err.message }); }); return; @@ -555,11 +555,11 @@ ipcMain.handle( resolve({ success: true, filePath: result.filePath }); }); }) - .on("error", (err) => { + .on("error", err => { resolve({ success: false, error: err.message }); }); }); - }, + } ); // Silent file save handler — saves a remote URL to a local directory without dialog @@ -592,23 +592,23 @@ ipcMain.handle( } // Download from http/https - return new Promise((resolve) => { + return new Promise(resolve => { const httpProtocol = url.startsWith("https") ? https : http; const file = createWriteStream(filePath); httpProtocol - .get(url, (response) => { + .get(url, response => { if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { const rp = redirectUrl.startsWith("https") ? https : http; - rp.get(redirectUrl, (rr) => { + rp.get(redirectUrl, rr => { rr.pipe(file); file.on("finish", () => { file.close(); resolve({ success: true, filePath }); }); - }).on("error", (err) => - resolve({ success: false, error: err.message }), + }).on("error", err => + resolve({ success: false, error: err.message }) ); return; } @@ -619,14 +619,12 @@ ipcMain.handle( resolve({ success: true, filePath }); }); }) - .on("error", (err) => - resolve({ success: false, error: err.message }), - ); + .on("error", err => resolve({ success: false, error: err.message })); }); } catch (err) { return { success: false, error: (err as Error).message }; } - }, + } ); // Assets metadata helpers @@ -658,7 +656,7 @@ ipcMain.handle("get-assets-settings", () => { const settings = loadSettings(); return { autoSaveAssets: settings.autoSaveAssets, - assetsDirectory: settings.assetsDirectory || defaultAssetsDirectory, + assetsDirectory: settings.assetsDirectory || defaultAssetsDirectory }; }); @@ -667,7 +665,7 @@ ipcMain.handle( (_, newSettings: { autoSaveAssets?: boolean; assetsDirectory?: string }) => { saveSettings(newSettings); return true; - }, + } ); ipcMain.handle("get-default-assets-directory", () => { @@ -677,7 +675,10 @@ ipcMain.handle("get-default-assets-directory", () => { ipcMain.handle("get-zimage-output-path", () => { // Use same ID format as other assets: base36 timestamp + random suffix const id = - Date.now().toString(36) + Math.random().toString(36).substring(2, 6); + Date.now().toString(36) + + Math.random() + .toString(36) + .substring(2, 6); const settings = loadSettings(); const assetsDir = settings.assetsDirectory || defaultAssetsDirectory; const imagesDir = join(assetsDir, "images"); @@ -695,7 +696,7 @@ ipcMain.handle("select-directory", async () => { const result = await dialog.showOpenDialog(focusedWindow, { properties: ["openDirectory", "createDirectory"], - title: "Select Assets Directory", + title: "Select Assets Directory" }); if (result.canceled || !result.filePaths[0]) { @@ -735,7 +736,7 @@ ipcMain.handle( } // Download file from http/https - return new Promise((resolve) => { + return new Promise(resolve => { const httpProtocol = url.startsWith("https") ? https : http; const file = createWriteStream(filePath); @@ -748,10 +749,10 @@ ipcMain.handle( ? https : http; redirectProtocol - .get(redirectUrl, (redirectResponse) => { + .get(redirectUrl, redirectResponse => { handleResponse(redirectResponse); }) - .on("error", (err) => { + .on("error", err => { resolve({ success: false, error: err.message }); }); return; @@ -770,11 +771,11 @@ ipcMain.handle( }); }; - httpProtocol.get(url, handleResponse).on("error", (err) => { + httpProtocol.get(url, handleResponse).on("error", err => { resolve({ success: false, error: err.message }); }); }); - }, + } ); ipcMain.handle("delete-asset", async (_, filePath: string) => { @@ -858,7 +859,7 @@ ipcMain.handle( } catch (error) { return { success: false, error: (error as Error).message }; } - }, + } ); ipcMain.handle("file-rename", (_, oldPath: string, newPath: string) => { @@ -920,19 +921,19 @@ ipcMain.handle("scan-assets-directory", async () => { images: "image", videos: "video", audio: "audio", - text: "text", + text: "text" }; // Process directories in parallel for better performance await Promise.all( - subDirs.map(async (subDir) => { + subDirs.map(async subDir => { const dirPath = join(assetsDir, subDir); if (!existsSync(dirPath)) return; try { const entries = await readdir(dirPath); // Process files in parallel batches - const filePromises = entries.map(async (entry) => { + const filePromises = entries.map(async entry => { const filePath = join(dirPath, entry); try { const stats = await stat(filePath); @@ -942,7 +943,7 @@ ipcMain.handle("scan-assets-directory", async () => { fileName: entry, type: typeMap[subDir], fileSize: stats.size, - createdAt: stats.birthtime.toISOString(), + createdAt: stats.birthtime.toISOString() }; } } catch { @@ -952,12 +953,12 @@ ipcMain.handle("scan-assets-directory", async () => { }); const results = await Promise.all(filePromises); files.push( - ...results.filter((f): f is NonNullable => f !== null), + ...results.filter((f): f is NonNullable => f !== null) ); } catch { // Skip directories we can't read } - }), + }) ); return files; @@ -1005,7 +1006,7 @@ ipcMain.handle( } catch (error) { return { success: false, error: (error as Error).message }; } - }, + } ); ipcMain.handle("sd-get-models-dir", () => { @@ -1069,7 +1070,9 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { const found = findBinary(tempExtractDir); if (!found) { throw new Error( - `Binary not found in extracted files. Looked for: ${possibleNames.join(", ")}`, + `Binary not found in extracted files. Looked for: ${possibleNames.join( + ", " + )}` ); } @@ -1123,7 +1126,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { // Create a copy with the old name for compatibility require("fs").copyFileSync(sdCliPath, finalBinaryPath); console.log( - `[SD Extract] Created ${targetBinaryName} copy for compatibility`, + `[SD Extract] Created ${targetBinaryName} copy for compatibility` ); } } @@ -1149,13 +1152,13 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { console.log("[SD Extract] Fixing macOS dynamic library paths..."); // Find all dylib files - const dylibFiles = readdirSync(destDir).filter((f) => - f.endsWith(".dylib"), + const dylibFiles = readdirSync(destDir).filter(f => + f.endsWith(".dylib") ); // Fix binary files const binaryFiles = [targetBinaryName, actualBinaryName].filter( - (name) => name && name !== null, + name => name && name !== null ); for (const binaryFile of binaryFiles) { const binaryFullPath = join(destDir, binaryFile); @@ -1165,7 +1168,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { try { execSync( `install_name_tool -delete_rpath "/Users/runner/work/stable-diffusion.cpp/stable-diffusion.cpp/build/bin" "${binaryFullPath}" 2>/dev/null || true`, - { stdio: "ignore" }, + { stdio: "ignore" } ); } catch (e) { // Ignore if rpath doesn't exist @@ -1175,7 +1178,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { try { execSync( `install_name_tool -add_rpath "@executable_path" "${binaryFullPath}" 2>/dev/null || true`, - { stdio: "ignore" }, + { stdio: "ignore" } ); } catch (e) { // Ignore if rpath already exists @@ -1186,7 +1189,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { try { execSync( `install_name_tool -change "/Users/runner/work/stable-diffusion.cpp/stable-diffusion.cpp/build/bin/${dylibFile}" "@executable_path/${dylibFile}" "${binaryFullPath}" 2>/dev/null || true`, - { stdio: "ignore" }, + { stdio: "ignore" } ); } catch (e) { // Ignore if reference doesn't exist @@ -1197,7 +1200,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { } catch (err) { console.warn( `[SD Extract] Could not fully fix rpath for ${binaryFile}:`, - (err as Error).message, + (err as Error).message ); } } @@ -1210,7 +1213,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { // Update the dylib's install name to use @rpath execSync( `install_name_tool -id "@rpath/${dylibFile}" "${dylibFullPath}" 2>/dev/null || true`, - { stdio: "ignore" }, + { stdio: "ignore" } ); // Update references to other dylibs @@ -1219,7 +1222,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { try { execSync( `install_name_tool -change "/Users/runner/work/stable-diffusion.cpp/stable-diffusion.cpp/build/bin/${otherDylib}" "@rpath/${otherDylib}" "${dylibFullPath}" 2>/dev/null || true`, - { stdio: "ignore" }, + { stdio: "ignore" } ); } catch (e) { // Ignore @@ -1231,7 +1234,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { } catch (err) { console.warn( `[SD Extract] Could not fix install name for ${dylibFile}:`, - (err as Error).message, + (err as Error).message ); } } @@ -1240,7 +1243,7 @@ ipcMain.handle("sd-extract-binary", (_, zipPath: string, destPath: string) => { } catch (err) { console.warn( "[SD Extract] Failed to fix macOS library paths:", - (err as Error).message, + (err as Error).message ); } } @@ -1298,7 +1301,7 @@ function setupAutoUpdater() { if (!existsSync(updateConfigPath)) { console.warn( "[AutoUpdater] app-update.yml not found, skipping auto-updater setup:", - updateConfigPath, + updateConfigPath ); return; } @@ -1313,7 +1316,8 @@ function setupAutoUpdater() { // Use generic provider pointing to nightly release assets autoUpdater.setFeedURL({ provider: "generic", - url: "https://github.com/WaveSpeedAI/wavespeed-desktop/releases/download/nightly", + url: + "https://github.com/WaveSpeedAI/wavespeed-desktop/releases/download/nightly" }); } else { autoUpdater.allowPrerelease = false; @@ -1328,7 +1332,7 @@ function setupAutoUpdater() { sendUpdateStatus("available", { version: info.version, releaseNotes: info.releaseNotes, - releaseDate: info.releaseDate, + releaseDate: info.releaseDate }); }); @@ -1336,23 +1340,23 @@ function setupAutoUpdater() { sendUpdateStatus("not-available", { version: info.version }); }); - autoUpdater.on("download-progress", (progress) => { + autoUpdater.on("download-progress", progress => { sendUpdateStatus("downloading", { percent: progress.percent, bytesPerSecond: progress.bytesPerSecond, transferred: progress.transferred, - total: progress.total, + total: progress.total }); }); autoUpdater.on("update-downloaded", (info: UpdateInfo) => { sendUpdateStatus("downloaded", { version: info.version, - releaseNotes: info.releaseNotes, + releaseNotes: info.releaseNotes }); }); - autoUpdater.on("error", (error) => { + autoUpdater.on("error", error => { sendUpdateStatus("error", { message: error.message }); }); } @@ -1362,7 +1366,7 @@ ipcMain.handle("check-for-updates", async () => { if (is.dev) { return { status: "dev-mode", - message: "Auto-update disabled in development", + message: "Auto-update disabled in development" }; } try { @@ -1412,7 +1416,8 @@ ipcMain.handle("set-update-channel", (_, channel: "stable" | "nightly") => { // Use generic provider pointing to nightly release assets autoUpdater.setFeedURL({ provider: "generic", - url: "https://github.com/WaveSpeedAI/wavespeed-desktop/releases/download/nightly", + url: + "https://github.com/WaveSpeedAI/wavespeed-desktop/releases/download/nightly" }); } else { autoUpdater.allowPrerelease = false; @@ -1421,7 +1426,7 @@ ipcMain.handle("set-update-channel", (_, channel: "stable" | "nightly") => { provider: "github", owner: "WaveSpeedAI", repo: "wavespeed-desktop", - releaseType: "release", + releaseType: "release" }); } return true; @@ -1471,12 +1476,12 @@ ipcMain.handle("sd-get-binary-path", () => { // Binary not found in any location return { success: false, - error: `Binary not found. Checked: ${userDataBinaryPath}, ${resourceBinaryPath}`, + error: `Binary not found. Checked: ${userDataBinaryPath}, ${resourceBinaryPath}` }; } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1499,7 +1504,7 @@ function checkMetalSupport(): boolean { // Metal was introduced in OS X 10.11 if (majorVersion < 15) { console.log( - "[Metal Check] macOS version too old for Metal (Darwin kernel < 15)", + "[Metal Check] macOS version too old for Metal (Darwin kernel < 15)" ); metalSupportCache = false; return false; @@ -1509,7 +1514,7 @@ function checkMetalSupport(): boolean { try { const output = execSync("system_profiler SPDisplaysDataType", { encoding: "utf8", - timeout: 5000, + timeout: 5000 }); // Check if output contains "Metal" support indication @@ -1566,7 +1571,7 @@ ipcMain.handle("sd-get-system-info", () => { try { const output = execSync("lspci 2>/dev/null | grep -i nvidia", { encoding: "utf8", - timeout: 3000, + timeout: 3000 }); if (output.toLowerCase().includes("nvidia")) { acceleration = "CUDA"; @@ -1588,7 +1593,7 @@ ipcMain.handle("sd-get-system-info", () => { } console.log( - `[System Info] Platform: ${platform}, Acceleration: ${acceleration}`, + `[System Info] Platform: ${platform}, Acceleration: ${acceleration}` ); // Cache the result @@ -1596,7 +1601,7 @@ ipcMain.handle("sd-get-system-info", () => { platform, arch, acceleration, - supported: true, + supported: true }; return systemInfoCache; @@ -1612,7 +1617,7 @@ ipcMain.handle("sd-get-gpu-vram", () => { return { success: false, vramMb: null, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1640,7 +1645,7 @@ ipcMain.handle( samplingMethod?: string; scheduler?: string; outputPath: string; - }, + } ) => { try { // Get binary path using the same logic as sd-get-binary-path @@ -1667,7 +1672,7 @@ ipcMain.handle( const resourceBinaryPath = join( basePath, `${platform}-${arch}`, - binaryName, + binaryName ); if (existsSync(resourceBinaryPath)) { binaryPath = resourceBinaryPath; @@ -1721,21 +1726,21 @@ ipcMain.handle( samplingMethod: params.samplingMethod, scheduler: params.scheduler, outputPath: params.outputPath, - onProgress: (progress) => { + onProgress: progress => { // Send progress to frontend event.sender.send("sd-progress", { phase: progress.phase, progress: progress.progress, - detail: progress.detail, + detail: progress.detail }); }, - onLog: (log) => { + onLog: log => { // Send logs to frontend event.sender.send("sd-log", { type: log.type, - message: log.message, + message: log.message }); - }, + } }); // Also track via legacy activeSDProcess for backward compatibility @@ -1745,10 +1750,10 @@ ipcMain.handle( } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } - }, + } ); /** @@ -1764,15 +1769,15 @@ ipcMain.handle("sd-list-models", () => { const files = readdirSync(modelsDir); const models = files - .filter((f) => f.endsWith(".gguf") && !f.endsWith(".part")) // Exclude .part files - .map((f) => { + .filter(f => f.endsWith(".gguf") && !f.endsWith(".part")) // Exclude .part files + .map(f => { const filePath = join(modelsDir, f); const stats = statSync(filePath); return { name: f, path: filePath, size: stats.size, - createdAt: stats.birthtime.toISOString(), + createdAt: stats.birthtime.toISOString() }; }); @@ -1780,7 +1785,7 @@ ipcMain.handle("sd-list-models", () => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1797,7 +1802,7 @@ ipcMain.handle("sd-delete-model", (_, modelPath: string) => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1848,7 +1853,7 @@ ipcMain.handle("sd-delete-binary", () => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1882,12 +1887,12 @@ ipcMain.handle("sd-check-auxiliary-models", () => { llmExists: existsSync(llmPath), vaeExists: existsSync(vaePath), llmPath, - vaePath, + vaePath }; } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1918,7 +1923,7 @@ ipcMain.handle("sd-list-auxiliary-models", () => { name: "Qwen3-4B-Instruct LLM", path: llmPath, size: stats.size, - type: "llm", + type: "llm" }); } @@ -1928,7 +1933,7 @@ ipcMain.handle("sd-list-auxiliary-models", () => { name: "Z-Image VAE", path: vaePath, size: stats.size, - type: "vae", + type: "vae" }); } @@ -1936,7 +1941,7 @@ ipcMain.handle("sd-list-auxiliary-models", () => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1963,7 +1968,7 @@ ipcMain.handle("sd-delete-auxiliary-model", (_, type: "llm" | "vae") => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -1988,7 +1993,7 @@ ipcMain.handle("sd-cancel-generation", async () => { } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } }); @@ -2002,7 +2007,7 @@ ipcMain.handle( _, fileName: string, data: Uint8Array, - type: "llm" | "vae" | "model", + type: "llm" | "vae" | "model" ) => { try { let destPath: string; @@ -2028,15 +2033,15 @@ ipcMain.handle( return { success: true, - filePath: destPath, + filePath: destPath }; } catch (error) { return { success: false, - error: (error as Error).message, + error: (error as Error).message }; } - }, + } ); // Register custom protocol for local asset files (must be before app.whenReady) @@ -2047,9 +2052,9 @@ protocol.registerSchemesAsPrivileged([ secure: true, supportFetchAPI: true, stream: true, - bypassCSP: true, - }, - }, + bypassCSP: true + } + } ]); // App lifecycle @@ -2057,9 +2062,9 @@ app.whenReady().then(() => { electronApp.setAppUserModelId("com.wavespeed.desktop"); // Handle local-asset:// protocol for loading local files (videos, images, etc.) - protocol.handle("local-asset", (request) => { + protocol.handle("local-asset", request => { const filePath = decodeURIComponent( - request.url.replace("local-asset://", ""), + request.url.replace("local-asset://", "") ); return net.fetch(pathToFileURL(filePath).href); }); @@ -2071,7 +2076,7 @@ app.whenReady().then(() => { createWindow(); // Initialize workflow module (sql.js DB, node registry, IPC handlers) - initWorkflowModule().catch((err) => { + initWorkflowModule().catch(err => { console.error("[Workflow] Failed to initialize:", err); }); @@ -2083,14 +2088,14 @@ app.whenReady().then(() => { const settings = loadSettings(); if (settings.autoCheckUpdate !== false) { setTimeout(() => { - autoUpdater.checkForUpdates().catch((err) => { + autoUpdater.checkForUpdates().catch(err => { console.error("Failed to check for updates:", err); }); }, 3000); } } - app.on("activate", function () { + app.on("activate", function() { // macOS: Show the hidden window when clicking dock icon if (mainWindow) { mainWindow.show(); diff --git a/electron/preload.ts b/electron/preload.ts index b3013a08..4a7d4477 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -91,13 +91,13 @@ const electronAPI = { clearAllData: (): Promise => ipcRenderer.invoke("clear-all-data"), downloadFile: ( url: string, - defaultFilename: string, + defaultFilename: string ): Promise => ipcRenderer.invoke("download-file", url, defaultFilename), saveFileSilent: ( url: string, dir: string, - fileName: string, + fileName: string ): Promise => ipcRenderer.invoke("save-file-silent", url, dir, fileName), openExternal: (url: string): Promise => @@ -143,7 +143,7 @@ const electronAPI = { url: string, type: string, fileName: string, - subDir: string, + subDir: string ): Promise => ipcRenderer.invoke("save-asset", url, type, fileName, subDir), deleteAsset: (filePath: string): Promise => @@ -206,7 +206,7 @@ const electronAPI = { error?: string; }> => ipcRenderer.invoke("sd-list-auxiliary-models"), sdDeleteAuxiliaryModel: ( - type: "llm" | "vae", + type: "llm" | "vae" ): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke("sd-delete-auxiliary-model", type), sdGenerateImage: (params: { @@ -230,7 +230,7 @@ const electronAPI = { sdSaveModelFromCache: ( filename: string, data: Uint8Array, - type: "model" | "llm" | "vae", + type: "model" | "llm" | "vae" ): Promise<{ success: boolean; filePath?: string; error?: string }> => ipcRenderer.invoke("sd-save-model-from-cache", filename, data, type), sdListModels: (): Promise<{ @@ -244,7 +244,7 @@ const electronAPI = { error?: string; }> => ipcRenderer.invoke("sd-list-models"), sdDeleteModel: ( - modelPath: string, + modelPath: string ): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke("sd-delete-model", modelPath), sdDeleteBinary: (): Promise<{ success: boolean; error?: string }> => @@ -256,7 +256,7 @@ const electronAPI = { phase: string; progress: number; detail?: unknown; - }) => void, + }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { phase: string; progress: number; detail?: unknown }); @@ -264,7 +264,7 @@ const electronAPI = { return () => ipcRenderer.removeListener("sd-progress", handler); }, onSdLog: ( - callback: (data: { type: "stdout" | "stderr"; message: string }) => void, + callback: (data: { type: "stdout" | "stderr"; message: string }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { type: "stdout" | "stderr"; message: string }); @@ -276,7 +276,7 @@ const electronAPI = { phase: string; progress: number; detail?: unknown; - }) => void, + }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { phase: string; progress: number; detail?: unknown }); @@ -288,7 +288,7 @@ const electronAPI = { phase: string; progress: number; detail?: unknown; - }) => void, + }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { phase: string; progress: number; detail?: unknown }); @@ -301,7 +301,7 @@ const electronAPI = { phase: string; progress: number; detail?: unknown; - }) => void, + }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { phase: string; progress: number; detail?: unknown }); @@ -314,7 +314,7 @@ const electronAPI = { phase: string; progress: number; detail?: unknown; - }) => void, + }) => void ): (() => void) => { const handler = (_: unknown, data: unknown) => callback(data as { phase: string; progress: number; detail?: unknown }); @@ -325,21 +325,21 @@ const electronAPI = { // File operations for chunked downloads fileGetSize: ( - filePath: string, + filePath: string ): Promise<{ success: boolean; size?: number; error?: string }> => ipcRenderer.invoke("file-get-size", filePath), fileAppendChunk: ( filePath: string, - chunk: ArrayBuffer, + chunk: ArrayBuffer ): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke("file-append-chunk", filePath, chunk), fileRename: ( oldPath: string, - newPath: string, + newPath: string ): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke("file-rename", oldPath, newPath), fileDelete: ( - filePath: string, + filePath: string ): Promise<{ success: boolean; error?: string }> => ipcRenderer.invoke("file-delete", filePath), @@ -350,7 +350,7 @@ const electronAPI = { error?: string; }> => ipcRenderer.invoke("sd-get-binary-download-path"), sdGetAuxiliaryModelDownloadPath: ( - type: "llm" | "vae", + type: "llm" | "vae" ): Promise<{ success: boolean; path?: string; error?: string }> => ipcRenderer.invoke("sd-get-auxiliary-model-download-path", type), sdGetModelsDir: (): Promise<{ @@ -360,7 +360,7 @@ const electronAPI = { }> => ipcRenderer.invoke("sd-get-models-dir"), sdExtractBinary: ( zipPath: string, - destPath: string, + destPath: string ): Promise<{ success: boolean; path?: string; error?: string }> => ipcRenderer.invoke("sd-extract-binary", zipPath, destPath), @@ -377,7 +377,7 @@ const electronAPI = { const handler = (_: unknown, asset: unknown) => callback(asset); ipcRenderer.on("assets:new-asset", handler); return () => ipcRenderer.removeListener("assets:new-asset", handler); - }, + } }; // ─── Workflow API (isolated namespace to avoid collision with electronAPI) ──── @@ -394,11 +394,11 @@ const workflowAPI = { }, removeListener: ( channel: string, - _callback: (...args: unknown[]) => void, + _callback: (...args: unknown[]) => void ): void => { // Best-effort removal — remove all listeners for this channel ipcRenderer.removeAllListeners(channel); - }, + } }; if (process.contextIsolated) { diff --git a/electron/workflow/db/index.ts b/electron/workflow/db/index.ts index d053bf16..0c248c5c 100644 --- a/electron/workflow/db/index.ts +++ b/electron/workflow/db/index.ts @@ -3,7 +3,7 @@ export { getDatabase, persistDatabase, closeDatabase, - transaction, + transaction } from "./connection"; export * from "./workflow.repo"; export * from "./node.repo"; diff --git a/electron/workflow/db/sql-js.d.ts b/electron/workflow/db/sql-js.d.ts index ac470099..4066603c 100644 --- a/electron/workflow/db/sql-js.d.ts +++ b/electron/workflow/db/sql-js.d.ts @@ -20,6 +20,6 @@ declare module "sql.js" { } export default function initSqlJs( - config?: Record, + config?: Record ): Promise; } diff --git a/electron/workflow/engine/dag-utils.ts b/electron/workflow/engine/dag-utils.ts index 52957af1..9f6be86e 100644 --- a/electron/workflow/engine/dag-utils.ts +++ b/electron/workflow/engine/dag-utils.ts @@ -34,7 +34,7 @@ export function hasCycle(nodeIds: string[], edges: SimpleEdge[]): boolean { export function wouldCreateCycle( nodeIds: string[], edges: SimpleEdge[], - newEdge: SimpleEdge, + newEdge: SimpleEdge ): boolean { return hasCycle(nodeIds, [...edges, newEdge]); } diff --git a/electron/workflow/engine/scheduler.ts b/electron/workflow/engine/scheduler.ts index b63e8a23..b0978548 100644 --- a/electron/workflow/engine/scheduler.ts +++ b/electron/workflow/engine/scheduler.ts @@ -8,7 +8,7 @@ interface SimpleEdge { export function topologicalLevels( nodeIds: string[], - edges: SimpleEdge[], + edges: SimpleEdge[] ): string[][] { const inDegree = new Map(); const adj = new Map(); @@ -21,7 +21,7 @@ export function topologicalLevels( inDegree.set(e.targetNodeId, (inDegree.get(e.targetNodeId) ?? 0) + 1); } const levels: string[][] = []; - let queue = nodeIds.filter((id) => inDegree.get(id) === 0); + let queue = nodeIds.filter(id => inDegree.get(id) === 0); while (queue.length > 0) { levels.push([...queue]); const nextQueue: string[] = []; @@ -40,7 +40,7 @@ export function topologicalLevels( export function downstreamNodes( startNodeId: string, nodeIds: string[], - edges: SimpleEdge[], + edges: SimpleEdge[] ): string[] { const adj = new Map(); for (const id of nodeIds) adj.set(id, []); diff --git a/electron/workflow/index.ts b/electron/workflow/index.ts index 996ba31c..6156c668 100644 --- a/electron/workflow/index.ts +++ b/electron/workflow/index.ts @@ -16,7 +16,7 @@ import { registerExecutionIpc, emitNodeStatus, emitProgress, - emitEdgeStatus, + emitEdgeStatus } from "./ipc/execution.ipc"; import { registerWorkflowIpc } from "./ipc/workflow.ipc"; import { registerHistoryIpc, setMarkDownstreamStale } from "./ipc/history.ipc"; @@ -50,7 +50,7 @@ export async function initWorkflowModule(): Promise { fileStorage.registerWorkflowName(wf.id, wf.name); } console.log( - `[Workflow] Registered ${workflows.length} workflow name mappings`, + `[Workflow] Registered ${workflows.length} workflow name mappings` ); } catch (err) { console.error("[Workflow] Failed to load workflow names:", err); @@ -73,15 +73,15 @@ export async function initWorkflowModule(): Promise { { onNodeStatus: emitNodeStatus, onProgress: emitProgress, - onEdgeStatus: emitEdgeStatus, - }, + onEdgeStatus: emitEdgeStatus + } ); // 5. Wire up singletons setExecutionEngine(engine); setCostDeps(costService, nodeRegistry); setMarkDownstreamStale((workflowId, nodeId) => - engine.markDownstreamStale(workflowId, nodeId), + engine.markDownstreamStale(workflowId, nodeId) ); // 6. Register all IPC handlers diff --git a/electron/workflow/ipc/storage.ipc.ts b/electron/workflow/ipc/storage.ipc.ts index 7ab17963..c0375933 100644 --- a/electron/workflow/ipc/storage.ipc.ts +++ b/electron/workflow/ipc/storage.ipc.ts @@ -17,31 +17,31 @@ export function registerStorageIpc(): void { "storage:get-workflow-snapshot", async (_event, args: { workflowId: string }) => { return getStorage().loadWorkflowSnapshot(args.workflowId); - }, + } ); ipcMain.handle( "storage:get-execution-cache", async ( _event, - args: { workflowId: string; nodeId: string; executionId: string }, + _args: { workflowId: string; nodeId: string; executionId: string } ) => { return null; // simplified — execution cache not used in new structure - }, + } ); ipcMain.handle( "storage:list-node-executions", async (_event, args: { workflowId: string; nodeId: string }) => { return getStorage().listNodeExecutions(args.workflowId, args.nodeId); - }, + } ); ipcMain.handle( "storage:list-uploaded-files", async (_event, args: { workflowId: string; nodeId: string }) => { return getStorage().listUploadedFiles(args.workflowId, args.nodeId); - }, + } ); ipcMain.handle( @@ -53,15 +53,15 @@ export function registerStorageIpc(): void { nodeId: string; filename: string; data: Buffer; - }, + } ) => { return getStorage().saveUploadedFile( args.workflowId, args.nodeId, args.filename, - Buffer.from(args.data), + Buffer.from(args.data) ); - }, + } ); ipcMain.handle( @@ -74,51 +74,51 @@ export function registerStorageIpc(): void { prefix: string; ext: string; data: Buffer; - }, + } ) => { return getStorage().saveNodeOutput( args.workflowId, args.nodeId, args.prefix, args.ext, - Buffer.from(args.data), + Buffer.from(args.data) ); - }, + } ); ipcMain.handle( "storage:copy-uploaded-file", async ( _event, - args: { workflowId: string; nodeId: string; sourcePath: string }, + args: { workflowId: string; nodeId: string; sourcePath: string } ) => { return getStorage().copyUploadedFile( args.workflowId, args.nodeId, - args.sourcePath, + args.sourcePath ); - }, + } ); ipcMain.handle( "storage:get-workflow-disk-usage", async (_event, args: { workflowId: string }) => { return getStorage().getWorkflowDiskUsage(args.workflowId); - }, + } ); ipcMain.handle( "storage:delete-workflow-files", async (_event, args: { workflowId: string }) => { getStorage().deleteWorkflowFiles(args.workflowId); - }, + } ); ipcMain.handle( "storage:artifact-exists", async (_event, args: { artifactPath: string }) => { return getStorage().artifactExists(args.artifactPath); - }, + } ); ipcMain.handle( @@ -129,15 +129,15 @@ export function registerStorageIpc(): void { workflowId: string; workflowName: string; graphDefinition: unknown; - }, + } ) => { const result = await dialog.showSaveDialog({ title: "Save Workflow", defaultPath: `${args.workflowName}.json`, filters: [ { name: "JSON", extensions: ["json"] }, - { name: "All Files", extensions: ["*"] }, - ], + { name: "All Files", extensions: ["*"] } + ] }); if (result.canceled || !result.filePath) return null; const data = { @@ -145,11 +145,11 @@ export function registerStorageIpc(): void { id: args.workflowId, name: args.workflowName, exportedAt: new Date().toISOString(), - graphDefinition: args.graphDefinition, + graphDefinition: args.graphDefinition }; writeFileSync(result.filePath, JSON.stringify(data, null, 2), "utf-8"); return result.filePath; - }, + } ); ipcMain.handle("storage:open-artifacts-folder", async () => { @@ -165,7 +165,7 @@ export function registerStorageIpc(): void { if (!existsSync(workflowPath)) mkdirSync(workflowPath, { recursive: true }); shell.openPath(workflowPath); - }, + } ); ipcMain.handle("storage:import-workflow-json", async () => { @@ -173,9 +173,9 @@ export function registerStorageIpc(): void { title: "Import Workflow", filters: [ { name: "JSON", extensions: ["json"] }, - { name: "All Files", extensions: ["*"] }, + { name: "All Files", extensions: ["*"] } ], - properties: ["openFile"], + properties: ["openFile"] }); if (result.canceled || result.filePaths.length === 0) return null; try { @@ -205,7 +205,7 @@ export function registerStorageIpc(): void { const nodeType = String(n.nodeType ?? "ai-task/run"); const position = (n.position as { x: number; y: number }) ?? { x: 200, - y: 200, + y: 200 }; let label = nodeType; @@ -230,9 +230,9 @@ export function registerStorageIpc(): void { nodeType, position, params: { ...params, __meta: { label, modelInputSchema } }, - currentOutputId: null, + currentOutputId: null }; - }, + } ); const enrichedEdges = (rawGraphDef.edges ?? []).map( @@ -244,8 +244,8 @@ export function registerStorageIpc(): void { sourceOutputKey: String(e.sourceOutputKey ?? "output"), targetNodeId: idMap.get(String(e.targetNodeId)) ?? String(e.targetNodeId), - targetInputKey: String(e.targetInputKey ?? "input"), - }), + targetInputKey: String(e.targetInputKey ?? "input") + }) ); const graphDef = { nodes: enrichedNodes, edges: enrichedEdges }; @@ -262,7 +262,7 @@ export function registerStorageIpc(): void { "storage:delete-node-outputs", async (_event, args: { workflowId: string; nodeId: string }) => { getStorage().deleteNodeOutputs(args.workflowId, args.nodeId); - }, + } ); ipcMain.handle( @@ -272,6 +272,6 @@ export function registerStorageIpc(): void { if (existsSync(mediaDir)) { require("fs").rmSync(mediaDir, { recursive: true, force: true }); } - }, + } ); } diff --git a/electron/workflow/ipc/upload.ipc.ts b/electron/workflow/ipc/upload.ipc.ts index dfdf0ae9..56e3ae65 100644 --- a/electron/workflow/ipc/upload.ipc.ts +++ b/electron/workflow/ipc/upload.ipc.ts @@ -9,7 +9,7 @@ export function registerUploadIpc(): void { "upload:file", async ( _event, - args: { fileData: ArrayBuffer; filename: string }, + args: { fileData: ArrayBuffer; filename: string } ): Promise => { const ws = getWaveSpeedClient(); const buffer = Buffer.from(args.fileData); @@ -17,6 +17,6 @@ export function registerUploadIpc(): void { const file = new File([blob], args.filename); const url = await ws.uploadFile(file, args.filename); return url; - }, + } ); } diff --git a/electron/workflow/nodes/ai-task/run.ts b/electron/workflow/nodes/ai-task/run.ts index 88ade344..2696e8c2 100644 --- a/electron/workflow/nodes/ai-task/run.ts +++ b/electron/workflow/nodes/ai-task/run.ts @@ -11,6 +11,8 @@ import type { NodeTypeDefinition } from "../../../../src/workflow/types/node-def import { getWaveSpeedClient } from "../../services/service-locator"; import { getModelById } from "../../services/model-list"; import { normalizePayloadArrays } from "../../../../src/lib/schemaToForm"; +import { existsSync, readFileSync } from "fs"; +import { basename } from "path"; export const aiTaskDef: NodeTypeDefinition = { type: "ai-task/run", @@ -52,12 +54,14 @@ export class AITaskHandler extends BaseNodeHandler { } const apiParams = this.buildApiParams(ctx); + // Upload any local-asset:// URLs to CDN before sending to API + const resolvedParams = await this.uploadLocalAssets(apiParams); ctx.onProgress(5, `Running ${modelId}...`); try { const client = getWaveSpeedClient(); // Use Desktop's apiClient.run() which handles submit + poll; pass abortSignal so Stop cancels in-flight request/polling - const result = await client.run(modelId, apiParams, { + const result = await client.run(modelId, resolvedParams, { signal: ctx.abortSignal, }); @@ -123,6 +127,52 @@ export class AITaskHandler extends BaseNodeHandler { return model?.costPerRun ?? 0; } + /** + * Upload any local-asset:// URLs in params to CDN so the API receives valid HTTP URLs. + * This handles the case where upstream nodes (e.g. concat) pass through local file paths. + */ + private async uploadLocalAssets( + params: Record, + ): Promise> { + const out = { ...params }; + const client = getWaveSpeedClient(); + + const uploadOne = async (url: string): Promise => { + if (!/^local-asset:\/\//i.test(url)) return url; + const localPath = decodeURIComponent( + url.replace(/^local-asset:\/\//i, ""), + ); + if (!existsSync(localPath)) { + throw new Error(`Local file not found: ${localPath}`); + } + const buffer = readFileSync(localPath); + const filename = basename(localPath); + const blob = new Blob([buffer]); + const file = new File([blob], filename); + return client.uploadFile(file, filename); + }; + + for (const [key, value] of Object.entries(out)) { + if (typeof value === "string" && /^local-asset:\/\//i.test(value)) { + out[key] = await uploadOne(value); + } else if (Array.isArray(value)) { + const hasLocal = value.some( + (v) => typeof v === "string" && /^local-asset:\/\//i.test(v), + ); + if (hasLocal) { + out[key] = await Promise.all( + value.map((v) => + typeof v === "string" && /^local-asset:\/\//i.test(v) + ? uploadOne(v) + : v, + ), + ); + } + } + } + return out; + } + private buildApiParams(ctx: NodeExecutionContext): Record { const params: Record = {}; // Internal keys to skip diff --git a/electron/workflow/nodes/free-tool/shared/media-utils.ts b/electron/workflow/nodes/free-tool/shared/media-utils.ts index 00c45115..4a6d66cf 100644 --- a/electron/workflow/nodes/free-tool/shared/media-utils.ts +++ b/electron/workflow/nodes/free-tool/shared/media-utils.ts @@ -35,14 +35,16 @@ function extFromUrl(input: string): string { function tempFilePath( workflowId: string, nodeId: string, - suffix: string, + suffix: string ): string { const storage = getFileStorageInstance(); const dir = path.join(storage.getNodeOutputDir(workflowId, nodeId), "_tmp"); ensureDir(dir); return path.join( dir, - `${Date.now()}_${Math.random().toString(36).slice(2, 8)}${suffix}`, + `${Date.now()}_${Math.random() + .toString(36) + .slice(2, 8)}${suffix}` ); } @@ -50,7 +52,7 @@ function downloadToFile(url: string, filePath: string): Promise { return new Promise((resolve, reject) => { const proto = url.startsWith("https://") ? https : http; const file = fs.createWriteStream(filePath); - const req = proto.get(url, (res) => { + const req = proto.get(url, res => { if ( res.statusCode && res.statusCode >= 300 && @@ -78,7 +80,7 @@ function downloadToFile(url: string, filePath: string): Promise { file.close(); resolve(); }); - file.on("error", (err) => { + file.on("error", err => { file.close(); reject(err); }); @@ -91,7 +93,7 @@ function downloadToFile(url: string, filePath: string): Promise { export async function resolveInputToLocalFile( input: string, workflowId: string, - nodeId: string, + nodeId: string ): Promise { if (!input) throw new Error("Input is empty."); @@ -114,7 +116,7 @@ export async function resolveInputToLocalFile( } catch { // ignore cleanup errors } - }, + } }; } @@ -129,7 +131,7 @@ export function createOutputPath( workflowId: string, nodeId: string, prefix: string, - ext: string, + ext: string ): string { const storage = getFileStorageInstance(); const outDir = storage.getNodeOutputDir(workflowId, nodeId); @@ -150,10 +152,10 @@ export async function ensureFfmpegAvailable(): Promise { } ffmpegChecked = true; - hasFfmpegBinary = await new Promise((resolve) => { + hasFfmpegBinary = await new Promise(resolve => { const proc = spawn("ffmpeg", ["-version"]); proc.on("error", () => resolve(false)); - proc.on("exit", (code) => resolve(code === 0)); + proc.on("exit", code => resolve(code === 0)); }); if (!hasFfmpegBinary) { @@ -170,7 +172,7 @@ export async function runFfmpeg(args: string[]): Promise { stderr += chunk.toString("utf-8"); }); proc.on("error", reject); - proc.on("exit", (code) => { + proc.on("exit", code => { if (code === 0) { resolve(); } else { diff --git a/electron/workflow/nodes/register-all.ts b/electron/workflow/nodes/register-all.ts index 228c1974..3f1ab3ea 100644 --- a/electron/workflow/nodes/register-all.ts +++ b/electron/workflow/nodes/register-all.ts @@ -18,6 +18,6 @@ export function registerAllNodes(): void { nodeRegistry.register(concatDef, new ConcatHandler()); nodeRegistry.register(selectDef, new SelectHandler()); console.log( - `[Registry] Registered ${nodeRegistry.getAll().length} node types`, + `[Registry] Registered ${nodeRegistry.getAll().length} node types` ); } diff --git a/electron/workflow/services/retry.ts b/electron/workflow/services/retry.ts index d114b8fe..bcf39ae5 100644 --- a/electron/workflow/services/retry.ts +++ b/electron/workflow/services/retry.ts @@ -10,14 +10,11 @@ export interface RetryConfig { const DEFAULT_CONFIG: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, - skipClientErrors: true, + skipClientErrors: true }; export class ApiError extends Error { - constructor( - message: string, - public statusCode: number, - ) { + constructor(message: string, public statusCode: number) { super(message); this.name = "ApiError"; } @@ -25,7 +22,7 @@ export class ApiError extends Error { export async function withRetry( fn: () => Promise, - config: Partial = {}, + config: Partial = {} ): Promise<{ result: T; attempts: number; delays: number[] }> { const cfg = { ...DEFAULT_CONFIG, ...config }; let attempts = 0; @@ -48,7 +45,7 @@ export async function withRetry( if (attempts >= cfg.maxRetries) throw error; const delay = cfg.baseDelayMs * Math.pow(2, attempts - 1); delays.push(delay); - await new Promise((resolve) => setTimeout(resolve, delay)); + await new Promise(resolve => setTimeout(resolve, delay)); } } } diff --git a/electron/workflow/services/service-locator.ts b/electron/workflow/services/service-locator.ts index 63fdf34d..b4339837 100644 --- a/electron/workflow/services/service-locator.ts +++ b/electron/workflow/services/service-locator.ts @@ -24,11 +24,11 @@ function loadApiKeyFromDesktopSettings(): string { } catch (error) { console.error( "[ServiceLocator] Failed to load API key from Desktop settings:", - error, + error ); } throw new Error( - "WaveSpeed API key not configured. Go to Settings to add it.", + "WaveSpeed API key not configured. Go to Settings to add it." ); } @@ -41,7 +41,7 @@ export interface WaveSpeedMainClient { run( model: string, input: Record, - options?: { signal?: AbortSignal }, + options?: { signal?: AbortSignal } ): Promise<{ outputs: unknown[]; [key: string]: unknown }>; uploadFile(file: File, filename: string): Promise; } @@ -58,7 +58,7 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { async run( model: string, input: Record, - options?: { signal?: AbortSignal }, + options?: { signal?: AbortSignal } ) { const signal = options?.signal; @@ -75,7 +75,7 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { signal.addEventListener( "abort", () => reject(new DOMException("Cancelled", "AbortError")), - { once: true }, + { once: true } ); }); return Promise.race([p, abortPromise]); @@ -88,11 +88,11 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, - "X-Client-Name": "wavespeed-desktop-workflow", + "X-Client-Name": "wavespeed-desktop-workflow" }, body: JSON.stringify(input), - ...(signal && { signal }), - }), + ...(signal && { signal }) + }) ); const submitData = (await submitRes.json()) as { code: number; @@ -116,8 +116,8 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { const pollRes = await withAbort( fetch(`${BASE_URL}/api/v3/predictions/${requestId}/result`, { headers: { Authorization: `Bearer ${apiKey}` }, - ...(signal && { signal }), - }), + ...(signal && { signal }) + }) ); const pollData = (await pollRes.json()) as { code: number; @@ -131,7 +131,7 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { throw new Error(pollData.data.error || "Prediction failed"); // Wait 1s but bail out immediately if aborted - await withAbort(new Promise((r) => setTimeout(r, 1000))); + await withAbort(new Promise(r => setTimeout(r, 1000))); } }, @@ -142,9 +142,9 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { const res = await fetch(`${BASE_URL}/api/v3/media/upload/binary`, { method: "POST", headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${apiKey}` }, - body: formData, + body: formData }); const data = (await res.json()) as { code: number; @@ -154,7 +154,7 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { if (data.code !== 200) throw new Error(data.message || "Failed to upload file"); return data.data.download_url; - }, + } }; return _wsClient; @@ -163,5 +163,5 @@ export function getWaveSpeedClient(): WaveSpeedMainClient { /** Reset cached clients (call when API keys change) */ export function resetClients(): void { _wsClient = null; - _apiKey = null; + _apiKey = ""; } diff --git a/electron/workflow/services/template-migration.ts b/electron/workflow/services/template-migration.ts index 93ecdc75..4d7719de 100644 --- a/electron/workflow/services/template-migration.ts +++ b/electron/workflow/services/template-migration.ts @@ -1,8 +1,5 @@ import * as templateRepo from "../db/template.repo"; -const TEMPLATES_STORAGE_KEY = "wavespeed_templates"; -const MIGRATION_FLAG_KEY = "wavespeed_templates_migrated"; - interface LegacyTemplate { id: string; name: string; @@ -20,14 +17,14 @@ export async function migrateTemplatesFromLocalStorage(): Promise<{ // Note: This function runs in main process, so we can't access localStorage directly // Migration will be triggered from renderer process via IPC console.log( - "[Template Migration] Migration should be triggered from renderer process", + "[Template Migration] Migration should be triggered from renderer process" ); return { migrated: 0, skipped: 0 }; } export function migrateTemplatesSync( legacyTemplatesJson: string, - migrationComplete: boolean, + migrationComplete: boolean ): { migrated: number; skipped: number } { if (migrationComplete) { console.log("[Template Migration] Already completed, skipping"); @@ -45,7 +42,7 @@ export function migrateTemplatesSync( const legacyTemplates: LegacyTemplate[] = JSON.parse(legacyTemplatesJson); console.log( - `[Template Migration] Found ${legacyTemplates.length} legacy templates`, + `[Template Migration] Found ${legacyTemplates.length} legacy templates` ); // Migrate each template @@ -62,22 +59,22 @@ export function migrateTemplatesSync( playgroundData: { modelId: legacy.modelId, modelName: legacy.modelName, - values: legacy.values, + values: legacy.values }, - workflowData: null, + workflowData: null }); migrated++; } catch (error) { console.error( `[Template Migration] Failed to migrate template ${legacy.id}:`, - error, + error ); skipped++; } } console.log( - `[Template Migration] Complete: ${migrated} migrated, ${skipped} skipped`, + `[Template Migration] Complete: ${migrated} migrated, ${skipped} skipped` ); } catch (error) { console.error("[Template Migration] Migration failed:", error); diff --git a/electron/workflow/utils/save-to-assets.ts b/electron/workflow/utils/save-to-assets.ts index 00d600ab..030c7c32 100644 --- a/electron/workflow/utils/save-to-assets.ts +++ b/electron/workflow/utils/save-to-assets.ts @@ -12,7 +12,7 @@ import { statSync, createWriteStream, readFileSync, - writeFileSync, + writeFileSync } from "fs"; import { join, extname } from "path"; import https from "https"; @@ -49,7 +49,7 @@ function loadSettings(): { autoSaveAssets: boolean; assetsDirectory: string } { const data = JSON.parse(readFileSync(settingsPath, "utf-8")); return { autoSaveAssets: data.autoSaveAssets ?? true, - assetsDirectory: data.assetsDirectory || defaultAssetsDirectory, + assetsDirectory: data.assetsDirectory || defaultAssetsDirectory }; } } catch { @@ -79,7 +79,12 @@ function saveAssetsMetadata(metadata: AssetMetadata[]): void { } function generateId(): string { - return Date.now().toString(36) + Math.random().toString(36).substring(2, 8); + return ( + Date.now().toString(36) + + Math.random() + .toString(36) + .substring(2, 8) + ); } function getSubDir(type: "image" | "video" | "audio"): string { @@ -114,7 +119,7 @@ function guessExt(url: string): string { /** Download a remote URL to a local file path. Returns true on success. */ function downloadToFile(url: string, destPath: string): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { const proto = url.startsWith("https") ? https : http; const file = createWriteStream(destPath); const handleResponse = (response: http.IncomingMessage) => { @@ -154,7 +159,7 @@ export interface SaveToAssetsOptions { * Notifies all renderer windows so the assets list refreshes. */ export async function saveWorkflowResultToAssets( - options: SaveToAssetsOptions, + options: SaveToAssetsOptions ): Promise { const settings = loadSettings(); if (!settings.autoSaveAssets) return; @@ -165,7 +170,7 @@ export async function saveWorkflowResultToAssets( // Check for duplicate by executionId + resultIndex const existing = loadAssetsMetadata(); const isDuplicate = existing.some( - (a) => a.executionId === options.executionId && a.source === "workflow", + a => a.executionId === options.executionId && a.source === "workflow" ); if (isDuplicate) return; @@ -191,7 +196,7 @@ export async function saveWorkflowResultToAssets( if (/^local-asset:\/\//i.test(options.url)) { try { const localPath = decodeURIComponent( - options.url.replace(/^local-asset:\/\//i, ""), + options.url.replace(/^local-asset:\/\//i, "") ); if (existsSync(localPath)) { copyFileSync(localPath, filePath); @@ -231,7 +236,7 @@ export async function saveWorkflowResultToAssets( workflowId: options.workflowId, workflowName: options.workflowName, nodeId: options.nodeId, - executionId: options.executionId, + executionId: options.executionId }; // Append to metadata file diff --git a/package.json b/package.json index fae56f5d..6bbbb1a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wavespeed-desktop", - "version": "2.0.13", + "version": "2.0.14", "description": "WaveSpeedAI Desktop Application - A playground for AI models", "main": "./out/main/index.js", "author": { diff --git a/src/App.tsx b/src/App.tsx index a2e47b93..2d599003 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,8 +39,8 @@ function App() { if (storedLanguage === "auto") return; const supportedLangs = languages - .map((lang) => lang.code) - .filter((code) => code !== "auto"); + .map(lang => lang.code) + .filter(code => code !== "auto"); if (!supportedLangs.includes(storedLanguage)) return; if (i18n.language !== storedLanguage) { diff --git a/src/components/ffmpeg/TimeRangeSlider.tsx b/src/components/ffmpeg/TimeRangeSlider.tsx index e3d82587..1147a6db 100644 --- a/src/components/ffmpeg/TimeRangeSlider.tsx +++ b/src/components/ffmpeg/TimeRangeSlider.tsx @@ -17,11 +17,11 @@ export function TimeRangeSlider({ endTime, onStartChange, onEndChange, - className, + className }: TimeRangeSliderProps) { const trackRef = useRef(null); const [dragging, setDragging] = useState<"start" | "end" | "range" | null>( - null, + null ); const dragStartRef = useRef({ x: 0, startTime: 0, endTime: 0 }); @@ -33,7 +33,7 @@ export function TimeRangeSlider({ const percentage = Math.max(0, Math.min(1, x / rect.width)); return percentage * duration; }, - [duration], + [duration] ); const handleMouseDown = useCallback( @@ -43,10 +43,10 @@ export function TimeRangeSlider({ dragStartRef.current = { x: e.clientX, startTime, - endTime, + endTime }; }, - [startTime, endTime], + [startTime, endTime] ); const handleMouseMove = useCallback( @@ -92,8 +92,8 @@ export function TimeRangeSlider({ endTime, duration, onStartChange, - onEndChange, - ], + onEndChange + ] ); const handleMouseUp = useCallback(() => { @@ -141,13 +141,13 @@ export function TimeRangeSlider({
handleMouseDown(e, "range")} + onMouseDown={e => handleMouseDown(e, "range")} /> {/* Unselected region (after) */} @@ -160,10 +160,10 @@ export function TimeRangeSlider({
handleMouseDown(e, "start")} + onMouseDown={e => handleMouseDown(e, "start")} >
@@ -174,10 +174,10 @@ export function TimeRangeSlider({
handleMouseDown(e, "end")} + onMouseDown={e => handleMouseDown(e, "end")} >
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 1d7b3340..6ec1bd1f 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -10,7 +10,7 @@ import { TooltipProvider, Tooltip, TooltipTrigger, - TooltipContent, + TooltipContent } from "@/components/ui/tooltip"; import { ToastAction } from "@/components/ui/toast"; import { toast } from "@/hooks/useToast"; @@ -27,7 +27,7 @@ import { Zap, ExternalLink, Globe, - FileText, + FileText } from "lucide-react"; import { VideoEnhancerPage } from "@/pages/VideoEnhancerPage"; import { ImageEnhancerPage } from "@/pages/ImageEnhancerPage"; @@ -55,8 +55,18 @@ const nextKey = () => ++keyCounter; export function Layout() { const { t } = useTranslation(); - const [sidebarCollapsed, setSidebarCollapsed] = useState(true); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { + const stored = localStorage.getItem("sidebarCollapsed"); + return stored !== null ? stored === "true" : false; + }); + + const toggleSidebar = useCallback(() => { + setSidebarCollapsed((prev) => { + const next = !prev; + localStorage.setItem("sidebarCollapsed", String(next)); + return next; + }); + }, []); const navigate = useNavigate(); const location = useLocation(); const hasShownUpdateToast = useRef(false); @@ -68,16 +78,16 @@ export function Layout() { const [visitedPages, setVisitedPages] = useState>(new Set()); // Track the last visited free-tools sub-page for navigation const [lastFreeToolsPage, setLastFreeToolsPage] = useState( - null, + null ); // Track keys for each page to force remount when reset const [pageKeys, setPageKeys] = useState>({}); // Reset a persistent page by changing its key (forces remount) const resetPage = useCallback((path: string) => { - setPageKeys((prev) => ({ + setPageKeys(prev => ({ ...prev, - [path]: nextKey(), + [path]: nextKey() })); }, []); @@ -86,7 +96,7 @@ export function Layout() { isValidating, loadApiKey, hasAttemptedLoad, - isLoading: isLoadingApiKey, + isLoading: isLoadingApiKey } = useApiKeyStore(); const [inputKey, setInputKey] = useState(""); const [showKey, setShowKey] = useState(false); @@ -124,12 +134,12 @@ export function Layout() { "/free-tools/media-trimmer", "/free-tools/media-merger", "/z-image", - "/workflow", + "/workflow" ]; if (persistentPaths.includes(location.pathname)) { // Track for lazy mounting if (!visitedPages.has(location.pathname)) { - setVisitedPages((prev) => new Set(prev).add(location.pathname)); + setVisitedPages(prev => new Set(prev).add(location.pathname)); } // Track last visited for sidebar navigation (only for free-tools sub-pages) if (location.pathname.startsWith("/free-tools/")) { @@ -152,19 +162,19 @@ export function Layout() { "/templates", "/assets", "/free-tools", - "/z-image", + "/z-image" ]; - const isPublicPage = publicPaths.some((path) => + const isPublicPage = publicPaths.some(path => path === "/" ? location.pathname === "/" - : location.pathname === path || location.pathname.startsWith(path + "/"), + : location.pathname === path || location.pathname.startsWith(path + "/") ); // Listen for update availability on startup useEffect(() => { if (!window.electronAPI?.onUpdateStatus) return; - const unsubscribe = window.electronAPI.onUpdateStatus((status) => { + const unsubscribe = window.electronAPI.onUpdateStatus(status => { if (status.status === "available" && !hasShownUpdateToast.current) { hasShownUpdateToast.current = true; const version = (status as { version?: string }).version; @@ -177,7 +187,7 @@ export function Layout() { navigate("/settings")}> View - ), + ) }); } }); @@ -207,7 +217,7 @@ export function Layout() { toast({ title: t("settings.apiKey.saved"), - description: t("settings.apiKey.savedDesc"), + description: t("settings.apiKey.savedDesc") }); } catch { // Validation failed - clear the temporary key from client @@ -258,11 +268,11 @@ export function Layout() { id="apiKey" type={showKey ? "text" : "password"} value={inputKey} - onChange={(e) => { + onChange={e => { setInputKey(e.target.value); setError(""); }} - onKeyDown={(e) => e.key === "Enter" && handleSaveApiKey()} + onKeyDown={e => e.key === "Enter" && handleSaveApiKey()} placeholder={t("settings.apiKey.placeholder")} className="pr-10" /> @@ -391,7 +401,7 @@ export function Layout() {
setSidebarCollapsed((prev) => !prev)} + onToggle={toggleSidebar} lastFreeToolsPage={lastFreeToolsPage} isMobileOpen={false} onMobileClose={() => {}} @@ -424,7 +434,7 @@ export function Layout() { "/free-tools/media-trimmer", "/free-tools/media-merger", "/z-image", - "/workflow", + "/workflow" ].includes(location.pathname) ? "hidden" : "h-full overflow-auto" diff --git a/src/components/layout/PageResetContext.ts b/src/components/layout/PageResetContext.ts index f218ab55..87ffc412 100644 --- a/src/components/layout/PageResetContext.ts +++ b/src/components/layout/PageResetContext.ts @@ -6,5 +6,5 @@ import { createContext } from "react"; export const PageResetContext = createContext<{ resetPage: (path: string) => void; }>({ - resetPage: () => {}, + resetPage: () => {} }); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index cc9b09e9..e60d4b54 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -7,7 +7,7 @@ import { Button, buttonVariants } from "@/components/ui/button"; import { Tooltip, TooltipContent, - TooltipTrigger, + TooltipTrigger } from "@/components/ui/tooltip"; import { Home, @@ -22,7 +22,7 @@ import { Sparkles, GitBranch, Layers, - X, + X } from "lucide-react"; interface NavItem { @@ -45,7 +45,7 @@ export function Sidebar({ onToggle, lastFreeToolsPage, isMobileOpen, - onMobileClose, + onMobileClose }: SidebarProps) { const { t } = useTranslation(); const location = useLocation(); @@ -92,37 +92,37 @@ export function Sidebar({ { titleKey: "nav.home", href: "/", - icon: Home, + icon: Home }, { titleKey: "nav.models", href: "/models", - icon: Layers, + icon: Layers }, { titleKey: "nav.playground", href: "/playground", icon: PlayCircle, - matchPrefix: true, - }, + matchPrefix: true + } ]; const manageItems: NavItem[] = [ { titleKey: "nav.templates", href: "/templates", - icon: FolderOpen, + icon: FolderOpen }, { titleKey: "nav.history", href: "/history", - icon: History, + icon: History }, { titleKey: "nav.assets", href: "/assets", - icon: FolderHeart, - }, + icon: FolderHeart + } ]; const toolsItems: NavItem[] = [ @@ -130,19 +130,19 @@ export function Sidebar({ titleKey: "nav.workflow", href: "/workflow", icon: GitBranch, - matchPrefix: true, + matchPrefix: true }, { titleKey: "nav.freeTools", href: "/free-tools", icon: Sparkles, - matchPrefix: true, + matchPrefix: true }, { titleKey: "nav.zImage", href: "/z-image", - icon: Zap, - }, + icon: Zap + } ]; // Check if a nav item is active @@ -159,21 +159,21 @@ export function Sidebar({ const navGroups = [ { key: "create", label: "Create", items: createItems }, { key: "manage", label: "Manage", items: manageItems }, - { key: "tools", label: "Tools", items: toolsItems }, + { key: "tools", label: "Tools", items: toolsItems } ]; const bottomNavItems = [ { titleKey: "nav.settings", href: "/settings", - icon: Settings, - }, + icon: Settings + } ]; // Sliding active-indicator for main nav const navRef = useRef(null); const [indicatorStyle, setIndicatorStyle] = useState({ - opacity: 0, + opacity: 0 }); const hasPositioned = useRef(false); @@ -182,10 +182,10 @@ export function Sidebar({ const nav = navRef.current; if (!nav) return; const activeBtn = nav.querySelector( - "[data-nav-active]", + "[data-nav-active]" ) as HTMLElement | null; if (!activeBtn) { - setIndicatorStyle((s) => ({ ...s, opacity: 0 })); + setIndicatorStyle(s => ({ ...s, opacity: 0 })); return; } const nr = nav.getBoundingClientRect(); @@ -195,7 +195,7 @@ export function Sidebar({ left: br.left - nr.left, width: br.width, height: br.height, - opacity: 1, + opacity: 1 }); hasPositioned.current = true; }; @@ -216,7 +216,7 @@ export function Sidebar({ "flex h-full flex-col bg-background/95 backdrop-blur transition-all duration-300 shrink-0 electron-drag", collapsed ? "w-12" : "w-48", // Mobile overlay when hamburger opens - isMobileOpen && "!fixed inset-y-0 left-0 z-50 w-72 shadow-2xl", + isMobileOpen && "!fixed inset-y-0 left-0 z-50 w-72 shadow-2xl" )} > {/* Mobile close button */} @@ -241,11 +241,11 @@ export function Sidebar({ className={cn( "absolute rounded-lg bg-primary shadow-sm pointer-events-none", hasPositioned.current && - "transition-[top,left,width,height,opacity] duration-300 ease-out", + "transition-[top,left,width,height,opacity] duration-300 ease-out" )} style={indicatorStyle} /> - {navGroups.map((group) => ( + {navGroups.map(group => (
)} - {group.items.map((item) => { + {group.items.map(item => { const active = isActive(item); const showTooltip = collapsed && !isMobileOpen && tooltipReady; const isNewFeature = item.href === "/workflow"; @@ -296,7 +296,7 @@ export function Sidebar({ : "text-muted-foreground hover:bg-muted hover:text-foreground", isNewFeature && !active && - "ring-2 ring-blue-500/20 hover:ring-blue-500/30", + "ring-2 ring-blue-500/20 hover:ring-blue-500/30" )} > {/* Glow effect for new feature */} @@ -318,15 +318,12 @@ export function Sidebar({ )} {/* Blue dot for collapsed state — only when not active */} - {isNewFeature && - !active && - collapsed && - !isMobileOpen && ( - - - - - )} + {isNewFeature && !active && collapsed && !isMobileOpen && ( + + + + + )} {showTooltip && ( @@ -353,7 +350,7 @@ export function Sidebar({ {/* Bottom Navigation */}