Compare commits

...

16 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 238cce6dc8 refac 2025-07-29 14:31:03 +04:00
Timothy Jaeryang Baek b255f22fe7 refac 2025-07-29 14:30:09 +04:00
Timothy Jaeryang Baek d205c176ef refac 2025-07-29 14:19:48 +04:00
Timothy Jaeryang Baek 2a223e62b6 refac 2025-07-29 14:16:54 +04:00
Timothy Jaeryang Baek eb2ebd2c1d refac 2025-07-29 14:15:34 +04:00
Timothy Jaeryang Baek a6a944c847 refac 2025-07-29 14:14:34 +04:00
Timothy Jaeryang Baek 03fea4a7cc refac 2025-07-29 14:09:52 +04:00
Timothy Jaeryang Baek 6b159246d1 refac: styling 2025-07-29 13:44:42 +04:00
Timothy Jaeryang Baek 398dc3128e refac 2025-07-29 13:39:05 +04:00
Timothy Jaeryang Baek d991908205 refac 2025-07-29 13:34:49 +04:00
Timothy Jaeryang Baek 2528096e07 fix: windows utf-8 issue 2025-07-29 13:32:38 +04:00
Timothy Jaeryang Baek aeba0e7c22 refac: logging 2025-07-29 13:23:30 +04:00
Timothy Jaeryang Baek ffbbb8f5b2 refac 2025-07-29 13:14:06 +04:00
Timothy Jaeryang Baek 119409a2dc refac 2025-07-29 13:08:39 +04:00
Timothy Jaeryang Baek cb564555dc refac 2025-07-29 12:54:24 +04:00
Timothy Jaeryang Baek 00d739d0d0 refac 2025-07-29 12:51:54 +04:00
8 changed files with 195 additions and 67 deletions
+38 -5
View File
@@ -16,12 +16,9 @@ import {
} from "electron";
import path, { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import log from "electron-log";
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
import {
getLogFilePath,
checkUrlAndOpen,
getConfig,
getServerLog,
@@ -38,6 +35,12 @@ import {
uninstallPython,
} from "./utils";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
// Main application logic
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
@@ -122,6 +125,13 @@ function createWindow(show = true): void {
uninstallHandler();
},
},
{
label: "Reset",
click: async () => {
await resetAppHandler();
},
},
],
});
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
@@ -348,6 +358,29 @@ const stopServerHandler = async () => {
}
};
const resetAppHandler = async () => {
try {
await stopServerHandler(); // Stop the server if running
SERVER_STATUS = null;
await resetApp(); // Reset the application state
// Show success notification
const notification = new Notification({
title: "Open WebUI",
body: "Application has been reset successfully.",
});
notification.show();
} catch (error) {
log.error("Failed to reset application:", error);
// Show error notification
const notification = new Notification({
title: "Open WebUI",
body: `Failed to reset application: ${error.message}`,
});
notification.show();
}
};
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit(); // Quit if another instance is already running
@@ -485,7 +518,7 @@ if (!gotTheLock) {
});
ipcMain.handle("app:reset", async (event) => {
return await resetApp();
return await resetAppHandler();
});
ipcMain.handle("get:config", async (event) => {
+95 -34
View File
@@ -12,6 +12,18 @@ import { app, shell, Notification } from "electron";
import { execFileSync, exec, spawn, execSync, execFile } from "child_process";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
const serverLogger = log.create({ logId: "server" });
serverLogger.transports.file.resolvePath = () => getLogFilePath(`server`);
export const getLogFilePath = (name: string = "main"): string => {
const logDir = path.join(getUserDataPath(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return path.join(logDir, `${name}.log`);
};
export const getAppPath = (): string => {
let appPath = app.getAppPath();
@@ -237,7 +249,7 @@ export const getPythonDownloadPath = (): string => {
return downloadPath;
};
export const getPythonInstallationPath = (): string => {
export const getPythonInstallationDir = (): string => {
const installDir = path.join(app.getPath("userData"), "python");
if (!fs.existsSync(installDir)) {
@@ -299,7 +311,7 @@ const checkInternet = async () => {
};
export const installPython = async (
installationPath?: string
installationDir?: string
): Promise<boolean> => {
let pythonDownloadPath = getPythonDownloadPath();
if (!isPythonDownloaded()) {
@@ -314,9 +326,6 @@ export const installPython = async (
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
});
}
if (!fs.existsSync(pythonDownloadPath)) {
@@ -324,8 +333,8 @@ export const installPython = async (
return false;
}
installationPath = installationPath || getPythonInstallationPath();
log.info(installationPath, pythonDownloadPath);
installationDir = installationDir || getPythonInstallationDir();
log.info(installationDir, pythonDownloadPath);
try {
const userDataPath = getUserDataPath();
@@ -339,13 +348,16 @@ export const installPython = async (
}
// Get the path to the installed Python binary
if (isPythonInstalled(installationPath)) {
const pythonPath = getPythonPath(installationPath);
if (isPythonInstalled(installationDir)) {
const pythonPath = getPythonPath(installationDir);
execFileSync(pythonPath, ["-m", "pip", "install", "uv"], {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
log.info("Successfully installed uv package");
@@ -367,14 +379,14 @@ export const getPythonExecutablePath = (envPath: string) => {
}
};
export const getPythonPath = (installationPath?: string) => {
export const getPythonPath = (installationDir?: string) => {
return path.normalize(
getPythonExecutablePath(installationPath || getPythonInstallationPath())
getPythonExecutablePath(installationDir || getPythonInstallationDir())
);
};
export const isPythonInstalled = (installationPath?: string) => {
const pythonPath = getPythonPath(installationPath);
export const isPythonInstalled = (installationDir?: string) => {
const pythonPath = getPythonPath(installationDir);
if (!fs.existsSync(pythonPath)) {
log.error("Python binary not found in install path");
@@ -387,6 +399,9 @@ export const isPythonInstalled = (installationPath?: string) => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
log.info("Installed Python Version:", pythonVersion.trim());
@@ -398,14 +413,17 @@ export const isPythonInstalled = (installationPath?: string) => {
}
};
export const isUvInstalled = (installationPath?: string) => {
const pythonPath = getPythonPath(installationPath);
export const isUvInstalled = (installationDir?: string) => {
const pythonPath = getPythonPath(installationDir);
try {
// Check if uv is installed by running the command
const result = execFileSync(pythonPath, ["-m", "uv", "--version"], {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
@@ -420,16 +438,17 @@ export const isUvInstalled = (installationPath?: string) => {
}
};
export const uninstallPython = (installationPath?: string): boolean => {
installationPath = installationPath || getPythonInstallationPath();
export const uninstallPython = (installationDir?: string): boolean => {
installationDir = installationDir || getPythonInstallationDir();
if (!fs.existsSync(installationPath)) {
if (!fs.existsSync(installationDir)) {
log.error("Python installation not found");
return false;
}
try {
fs.rmSync(installationPath, { recursive: true });
fs.rmSync(installationDir, { recursive: true, force: true });
log.info("Python installation removed successfully:", installationDir);
} catch (error) {
log.error("Failed to remove Python installation", error);
return false;
@@ -449,6 +468,39 @@ export const uninstallPython = (installationPath?: string): boolean => {
export const resetApp = async (): Promise<void> => {
await uninstallPython();
log.info("Uninstalled Python environment");
// remove config file
const configPath = path.join(getUserDataPath(), "config.json");
if (fs.existsSync(configPath)) {
try {
fs.unlinkSync(configPath);
log.info("Removed config file:", configPath);
} catch (error) {
log.error("Failed to remove config file:", error);
}
}
// remove secret key file
const secretKeyPath = path.join(getOpenWebUIDataPath(), ".key");
if (fs.existsSync(secretKeyPath)) {
try {
fs.unlinkSync(secretKeyPath);
log.info("Removed secret key file:", secretKeyPath);
} catch (error) {
log.error("Failed to remove secret key file:", error);
}
}
// remove /data folder
const dataPath = getOpenWebUIDataPath();
if (fs.existsSync(dataPath)) {
try {
fs.rmSync(dataPath, { recursive: true, force: true });
log.info("Removed data directory:", dataPath);
} catch (error) {
log.error("Failed to remove data directory:", error);
}
}
};
////////////////////////////////////////////////
@@ -518,6 +570,9 @@ export const installPackage = (
{
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
@@ -568,6 +623,9 @@ export const isPackageInstalled = (packageName: string): boolean => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
@@ -610,16 +668,15 @@ export const startServer = async (
const pythonPath = getPythonPath();
log.info(`Using Python at: ${pythonPath}`);
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
let commandArgs: string[];
commandArgs = ["-m", "uv", "run", "open-webui", "serve", "--host", host];
const dataDir = path.join(app.getPath("userData"), "data");
const dataDir = getOpenWebUIDataPath();
const secretKey = getSecretKey();
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
process.env.DATA_DIR = dataDir;
process.env.WEBUI_SECRET_KEY = secretKey;
@@ -645,7 +702,12 @@ export const startServer = async (
const childProcess = spawn(pythonPath, commandArgs, {
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
if (!childProcess.pid) {
@@ -658,24 +720,23 @@ export const startServer = async (
const appendLog = (source: string) => (data: Buffer) => {
const logLine = data.toString().trim();
const tag = `[${source}][PID:${childProcess.pid}]:`;
logLines.push(`${tag} ${logLine}`);
// (Optional) also log to main process console
// log.info(`${tag} ${logLine}`);
const line = `[${source}][PID:${childProcess.pid}]: ${logLine}`;
logLines.push(line);
serverLogger.info(line); // Log to console
};
childProcess.stdout?.on("data", appendLog("stdout"));
childProcess.stderr?.on("data", appendLog("stderr"));
childProcess.on("close", (code, signal) => {
logLines.push(
`[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`
);
const line = `[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`;
serverLogger.info(line);
logLines.push(line);
serverPIDs.delete(childProcess.pid);
// Note: we keep the logs available until manually cleared
});
childProcess.on("error", (err) => {
logLines.push(
`[process][PID:${childProcess.pid}] Error: ${err.message}`
);
const line = `[process][PID:${childProcess.pid}] Error: ${err.message}`;
serverLogger.error(line);
logLines.push(line);
});
// Compute URL directly, do not try to parse logs
@@ -746,7 +807,7 @@ export const checkUrlAndOpen = async (
url: string,
callback: Function = async () => {}
) => {
const maxAttempts = 150; // 5 minutes with 2-second intervals
const maxAttempts = 1800; // 60 minutes with 2-second intervals
const interval = 2000; // 2 seconds
let attempts = 0;
+1 -1
View File
@@ -57,7 +57,7 @@
{:else if installed === false}
<Installation bind:installed />
{:else}
<Controls />
<Controls bind:installed />
{/if}
</main>
+37 -1
View File
@@ -154,7 +154,6 @@ html {
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-hidden::-webkit-scrollbar-thumb {
visibility: hidden;
}
@@ -162,3 +161,40 @@ html {
.scrollbar-hidden::-webkit-scrollbar-corner {
display: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.scrollbar-none::-webkit-scrollbar-corner {
display: none;
}
.scrollbar-none {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
::-webkit-scrollbar-thumb {
--tw-border-opacity: 1;
background-color: rgba(215, 215, 215, 0.8);
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
border-radius: 9999px;
border-width: 1px;
}
/* Dark theme scrollbar styles */
.dark ::-webkit-scrollbar-thumb {
background-color: rgba(67, 67, 67, 0.8); /* Darker color for dark theme */
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
}
::-webkit-scrollbar {
height: 0.6rem;
width: 0.4rem;
}
::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 9999px;
}
@@ -7,6 +7,8 @@
import About from "./Controls/About.svelte";
import { info } from "../stores";
let { installed = $bindable(false) } = $props();
let selectedTab = $state("general");
onMount(async () => {});
@@ -104,7 +106,7 @@
class="flex-1 pt-1 sm:mt-0 overflow-y-scroll pr-1 scrollbar-hidden"
>
{#if selectedTab === "general"}
<General info={$info} />
<General bind:installed info={$info} />
{:else if selectedTab === "about"}
<About />
{/if}
@@ -7,7 +7,7 @@
import Spinner from "../common/Spinner.svelte";
import ConfirmDialog from "../common/ConfirmDialog.svelte";
let { info } = $props();
let { info, installed = $bindable(false) } = $props();
let config = $state(null);
@@ -54,12 +54,14 @@
confirmLabel="Reset"
onConfirm={async () => {
try {
installed = null;
config = null;
await window.electronAPI.resetApp();
toast.success("App has been reset successfully.");
// refresh the page to apply changes
window.location.reload();
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
toast.error("Failed to reset the app");
}
@@ -11,20 +11,12 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
let { installed = $bindable() } = $props();
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let mounted = $state(false);
let currentTime = Date.now();
@@ -57,10 +49,12 @@
(await window.electronAPI.getPythonStatus()) &&
(await window.electronAPI.getPackageStatus())
) {
// Notify the user that the installation is complete
// Start the server if it's not already running
if (!(await window.electronAPI.getServerStatus())) {
await window.electronAPI.startServer();
}
// Notify the user that the installation is complete
await window.electronAPI.notification(
"Installation Complete",
"Open WebUI is now ready to use."
@@ -218,7 +212,7 @@
{`Continue`}
</div>
<!-- <button
<div
class="text-xs mt-3 text-gray-500 cursor-pointer"
in:fly={{
delay: 500,
@@ -226,8 +220,16 @@
y: 10,
}}
>
To connect to an existing server, click here.
</button> -->
By continuing, you agree to our
<button
class="underline"
onclick={() => {
window.electronAPI.openInBrowser(
"https://github.com/open-webui/desktop/blob/main/LICENSE"
);
}}>license agreement</button
>.
</div>
{/if}
</div>
</div>
@@ -7,19 +7,11 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
import { fly } from "svelte/transition";
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let startTime = $state(null);
let currentTime = $state(null);