mirror of
https://github.com/open-webui/desktop.git
synced 2026-07-09 14:57:14 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 238cce6dc8 | |||
| b255f22fe7 | |||
| d205c176ef | |||
| 2a223e62b6 | |||
| eb2ebd2c1d | |||
| a6a944c847 | |||
| 03fea4a7cc | |||
| 6b159246d1 | |||
| 398dc3128e | |||
| d991908205 | |||
| 2528096e07 | |||
| aeba0e7c22 | |||
| ffbbb8f5b2 | |||
| 119409a2dc | |||
| cb564555dc | |||
| 00d739d0d0 |
+38
-5
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{:else if installed === false}
|
||||
<Installation bind:installed />
|
||||
{:else}
|
||||
<Controls />
|
||||
<Controls bind:installed />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user