mirror of
https://github.com/open-webui/desktop.git
synced 2026-07-09 06:46:04 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eebd30e29e | |||
| 8b62c3c993 | |||
| c6189bb7cc | |||
| ce3766d3c2 |
+15
-9
@@ -16,6 +16,7 @@ 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";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
isPythonInstalled,
|
||||
isUvInstalled,
|
||||
openUrl,
|
||||
resetApp,
|
||||
setConfig,
|
||||
startServer,
|
||||
stopAllServers,
|
||||
@@ -267,7 +269,7 @@ const startServerHandler = async () => {
|
||||
|
||||
updateTrayMenu("Open WebUI: Starting...", null);
|
||||
|
||||
console.log("Server started successfully:", SERVER_URL, SERVER_PID);
|
||||
log.info("Server started successfully:", SERVER_URL, SERVER_PID);
|
||||
SERVER_STATUS = "started";
|
||||
|
||||
mainWindow?.webContents.send("main:data", {
|
||||
@@ -374,7 +376,7 @@ if (!gotTheLock) {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
CONFIG = await getConfig(); // Load initial config
|
||||
console.log("Initial Config:", CONFIG);
|
||||
log.info("Initial Config:", CONFIG);
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId("com.openwebui.desktop");
|
||||
@@ -386,14 +388,14 @@ if (!gotTheLock) {
|
||||
});
|
||||
|
||||
// IPC test
|
||||
ipcMain.on("ping", () => console.log("pong"));
|
||||
ipcMain.on("ping", () => log.info("pong"));
|
||||
|
||||
ipcMain.handle("get:version", async () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle("install:python", async (event) => {
|
||||
console.log("Installing package...");
|
||||
log.info("Installing package...");
|
||||
try {
|
||||
const res = await installPython();
|
||||
if (res) {
|
||||
@@ -425,7 +427,7 @@ if (!gotTheLock) {
|
||||
});
|
||||
|
||||
ipcMain.handle("install:package", async (event) => {
|
||||
console.log("Installing package...");
|
||||
log.info("Installing package...");
|
||||
try {
|
||||
const res = await installPackage("open-webui");
|
||||
if (res) {
|
||||
@@ -449,7 +451,7 @@ if (!gotTheLock) {
|
||||
ipcMain.handle("status:package", async (event) => {
|
||||
const packageStatus = await isPackageInstalled("open-webui");
|
||||
|
||||
console.log("Package Status:", packageStatus);
|
||||
log.info("Package Status:", packageStatus);
|
||||
return packageStatus;
|
||||
});
|
||||
|
||||
@@ -482,6 +484,10 @@ if (!gotTheLock) {
|
||||
return SERVER_STATUS;
|
||||
});
|
||||
|
||||
ipcMain.handle("app:reset", async (event) => {
|
||||
return await resetApp();
|
||||
});
|
||||
|
||||
ipcMain.handle("get:config", async (event) => {
|
||||
return await getConfig();
|
||||
});
|
||||
@@ -494,7 +500,7 @@ if (!gotTheLock) {
|
||||
if (!url) {
|
||||
throw new Error("No URL provided to open in browser.");
|
||||
}
|
||||
console.log("Opening URL in browser:", url);
|
||||
log.info("Opening URL in browser:", url);
|
||||
if (url.startsWith("http://0.0.0.0")) {
|
||||
url = url.replace("http://0.0.0.0", "http://localhost");
|
||||
}
|
||||
@@ -503,7 +509,7 @@ if (!gotTheLock) {
|
||||
});
|
||||
|
||||
ipcMain.handle("notification", async (event, { title, body }) => {
|
||||
console.log("Received notification:", title, body);
|
||||
log.info("Received notification:", title, body);
|
||||
const notification = new Notification({
|
||||
title: title,
|
||||
body: body,
|
||||
@@ -515,7 +521,7 @@ if (!gotTheLock) {
|
||||
if (isPackageInstalled("open-webui")) {
|
||||
if (CONFIG?.autoUpdate ?? true) {
|
||||
try {
|
||||
console.log("Checking for updates...");
|
||||
log.info("Checking for updates...");
|
||||
updateTrayMenu(
|
||||
"Open WebUI: Checking for updates...",
|
||||
null
|
||||
|
||||
+36
-31
@@ -59,7 +59,7 @@ export const openUrl = (url: string) => {
|
||||
throw new Error("No URL provided to open in browser.");
|
||||
}
|
||||
|
||||
console.log("Opening URL in browser:", url);
|
||||
log.info("Opening URL in browser:", url);
|
||||
if (url.startsWith("http://0.0.0.0")) {
|
||||
url = url.replace("http://0.0.0.0", "http://localhost");
|
||||
}
|
||||
@@ -215,7 +215,7 @@ export const downloadFileWithProgress = async (
|
||||
// Write to file
|
||||
fs.writeFileSync(downloadPath, buffer);
|
||||
|
||||
console.log("File downloaded successfully:", downloadPath);
|
||||
log.info("File downloaded successfully:", downloadPath);
|
||||
return downloadPath;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -257,13 +257,13 @@ const downloadPython = async (onProgress = null) => {
|
||||
const url = generateDownloadUrl();
|
||||
const downloadPath = getPythonDownloadPath();
|
||||
|
||||
console.log(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
|
||||
console.log(`📁 Download path: ${downloadPath}`);
|
||||
console.log(`🔗 URL: ${url}`);
|
||||
log.info(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
|
||||
log.info(`📁 Download path: ${downloadPath}`);
|
||||
log.info(`🔗 URL: ${url}`);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(downloadPath)) {
|
||||
console.log(`✅ File already exists: ${downloadPath}`);
|
||||
log.info(`✅ File already exists: ${downloadPath}`);
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ const downloadPython = async (onProgress = null) => {
|
||||
downloadPath,
|
||||
onProgress
|
||||
);
|
||||
console.log(`✅ Python downloaded successfully to: ${result}`);
|
||||
log.info(`✅ Python downloaded successfully to: ${result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`❌ Download failed: ${error?.message}`);
|
||||
@@ -311,7 +311,7 @@ export const installPython = async (
|
||||
}
|
||||
|
||||
await downloadPython((progress, downloaded, total) => {
|
||||
console.log(
|
||||
log.info(
|
||||
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
|
||||
);
|
||||
log.info(
|
||||
@@ -325,7 +325,7 @@ export const installPython = async (
|
||||
}
|
||||
|
||||
installationPath = installationPath || getPythonInstallationPath();
|
||||
console.log(installationPath, pythonDownloadPath);
|
||||
log.info(installationPath, pythonDownloadPath);
|
||||
|
||||
try {
|
||||
const userDataPath = getUserDataPath();
|
||||
@@ -348,7 +348,7 @@ export const installPython = async (
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
console.log("Successfully installed uv package");
|
||||
log.info("Successfully installed uv package");
|
||||
|
||||
return true; // Return true to indicate success
|
||||
} else {
|
||||
@@ -389,7 +389,7 @@ export const isPythonInstalled = (installationPath?: string) => {
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
console.log("Installed Python Version:", pythonVersion.trim());
|
||||
log.info("Installed Python Version:", pythonVersion.trim());
|
||||
|
||||
return true; // Return true to indicate success
|
||||
} catch (error) {
|
||||
@@ -409,7 +409,7 @@ export const isUvInstalled = (installationPath?: string) => {
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Installed uv Version:", result.trim());
|
||||
log.info("Installed uv Version:", result.trim());
|
||||
return true; // Return true if uv is installed
|
||||
} catch (error) {
|
||||
log.error(
|
||||
@@ -446,6 +446,11 @@ export const uninstallPython = (installationPath?: string): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const resetApp = async (): Promise<void> => {
|
||||
await uninstallPython();
|
||||
log.info("Uninstalled Python environment");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Fixes code-signing issues in macOS by applying ad-hoc signatures to extracted environment files.
|
||||
@@ -519,7 +524,7 @@ export const installPackage = (
|
||||
|
||||
// Function to handle logging output
|
||||
const onLog = (data: any) => {
|
||||
console.log(data);
|
||||
log.info(data);
|
||||
};
|
||||
|
||||
// Listen to stdout and stderr for logging
|
||||
@@ -528,7 +533,7 @@ export const installPackage = (
|
||||
|
||||
// Handle the exit event
|
||||
commandProcess.on("exit", (code) => {
|
||||
console.log(`Child exited with code ${code}`);
|
||||
log.info(`Child exited with code ${code}`);
|
||||
|
||||
if (code !== 0) {
|
||||
log.error(`Failed to install open-webui: ${code}`);
|
||||
@@ -568,14 +573,14 @@ export const isPackageInstalled = (packageName: string): boolean => {
|
||||
);
|
||||
|
||||
if (info.includes(`Name: ${packageName}`)) {
|
||||
console.log(`Package ${packageName} is installed.`);
|
||||
log.info(`Package ${packageName} is installed.`);
|
||||
return true; // Return true to indicate success
|
||||
} else {
|
||||
console.log(`Package ${packageName} is not installed.`);
|
||||
log.info(`Package ${packageName} is not installed.`);
|
||||
return false; // Return false to indicate failure
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Failed to execute Python binary");
|
||||
log.info("Failed to execute Python binary");
|
||||
return false; // Return false to indicate failure
|
||||
}
|
||||
};
|
||||
@@ -603,7 +608,7 @@ export const startServer = async (
|
||||
throw new Error("open-webui package is not installed");
|
||||
|
||||
const pythonPath = getPythonPath();
|
||||
console.log(`Using Python at: ${pythonPath}`);
|
||||
log.info(`Using Python at: ${pythonPath}`);
|
||||
|
||||
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
|
||||
|
||||
@@ -622,17 +627,17 @@ export const startServer = async (
|
||||
let desiredPort = port || 8080;
|
||||
let availablePort = desiredPort;
|
||||
|
||||
console.log(`Checking port availability starting from ${availablePort}...`);
|
||||
log.info(`Checking port availability starting from ${availablePort}...`);
|
||||
while (await portInUse(availablePort, host)) {
|
||||
availablePort++;
|
||||
|
||||
console.log(`Port ${availablePort} is in use, trying next port...`);
|
||||
log.info(`Port ${availablePort} is in use, trying next port...`);
|
||||
if (availablePort > desiredPort + 100) {
|
||||
throw new Error("No available ports found");
|
||||
}
|
||||
}
|
||||
commandArgs.push("--port", availablePort.toString());
|
||||
console.log(
|
||||
log.info(
|
||||
"Starting Open-WebUI server...",
|
||||
pythonPath,
|
||||
commandArgs.join(" ")
|
||||
@@ -656,7 +661,7 @@ export const startServer = async (
|
||||
const tag = `[${source}][PID:${childProcess.pid}]:`;
|
||||
logLines.push(`${tag} ${logLine}`);
|
||||
// (Optional) also log to main process console
|
||||
// console.log(`${tag} ${logLine}`);
|
||||
// log.info(`${tag} ${logLine}`);
|
||||
};
|
||||
childProcess.stdout?.on("data", appendLog("stdout"));
|
||||
childProcess.stderr?.on("data", appendLog("stderr"));
|
||||
@@ -677,7 +682,7 @@ export const startServer = async (
|
||||
let effectiveHost = host;
|
||||
if (!expose && host === "0.0.0.0") effectiveHost = "127.0.0.1";
|
||||
const url = `http://${effectiveHost}:${availablePort}`;
|
||||
console.log(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
|
||||
log.info(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
|
||||
|
||||
return { url, pid: childProcess.pid };
|
||||
};
|
||||
@@ -686,7 +691,7 @@ export const startServer = async (
|
||||
* Terminates all server processes.
|
||||
*/
|
||||
export async function stopAllServers(): Promise<void> {
|
||||
console.log("Stopping all servers...");
|
||||
log.info("Stopping all servers...");
|
||||
for (const pid of Array.from(serverPIDs)) {
|
||||
try {
|
||||
terminateProcessTree(pid);
|
||||
@@ -696,7 +701,7 @@ export async function stopAllServers(): Promise<void> {
|
||||
console.error(`Error stopping server with PID ${pid}:`, error);
|
||||
}
|
||||
}
|
||||
console.log("All servers stopped successfully.");
|
||||
log.info("All servers stopped successfully.");
|
||||
}
|
||||
/**
|
||||
* Kills a process tree by PID.
|
||||
@@ -705,7 +710,7 @@ function terminateProcessTree(pid: number): void {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`);
|
||||
console.log(
|
||||
log.info(
|
||||
`Terminated server process tree (PID: ${pid}) on Windows.`
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -717,7 +722,7 @@ function terminateProcessTree(pid: number): void {
|
||||
} else {
|
||||
try {
|
||||
process.kill(-pid, "SIGKILL");
|
||||
console.log(
|
||||
log.info(
|
||||
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -760,20 +765,20 @@ export const checkUrlAndOpen = async (
|
||||
const pollUrl = async () => {
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
console.log(
|
||||
log.info(
|
||||
`Checking URL availability (attempt ${attempts}/${maxAttempts}): ${url}`
|
||||
);
|
||||
|
||||
try {
|
||||
const isAvailable = await checkUrl();
|
||||
if (isAvailable) {
|
||||
console.log("URL is now available, opening browser...");
|
||||
log.info("URL is now available, opening browser...");
|
||||
await openUrl(url);
|
||||
await callback(); // Call the provided callback function
|
||||
return; // Exit the polling loop
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error checking URL: ${error}`);
|
||||
log.info(`Error checking URL: ${error}`);
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
@@ -782,7 +787,7 @@ export const checkUrlAndOpen = async (
|
||||
|
||||
// If we exit the loop without success
|
||||
if (attempts >= maxAttempts) {
|
||||
console.log("URL check timed out after 5 minutes");
|
||||
log.info("URL check timed out after 5 minutes");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -132,6 +132,16 @@ const api = {
|
||||
return await ipcRenderer.invoke("server:info");
|
||||
},
|
||||
|
||||
resetApp: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
|
||||
return await ipcRenderer.invoke("app:reset");
|
||||
},
|
||||
|
||||
startServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,5 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
pre {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Inter", "Vazirmatn",
|
||||
ui-sans-serif, system-ui, "Segoe UI", Roboto, Ubuntu, Cantarell,
|
||||
"Noto Sans", sans-serif, "Helvetica Neue", Arial,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: theme(--color-gray-400);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@apply appearance-none size-3.5 align-middle bg-white border border-gray-300 rounded transition cursor-pointer focus:ring-2 focus:ring-blue-500 focus:outline-none dark:bg-gray-800 dark:border-gray-600 self-center;
|
||||
/* Center the custom mark */
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
input[type="checkbox"]:checked {
|
||||
@apply bg-blue-600 border-blue-600;
|
||||
}
|
||||
input[type="checkbox"]:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Hide by default */
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
/* SVG checkmark as background image */
|
||||
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 8l3 3l5-5"/></svg>')
|
||||
center/80% no-repeat;
|
||||
}
|
||||
input[type="checkbox"]:checked:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
src: url("./lib/assets/fonts/Archivo-Variable.ttf");
|
||||
|
||||
@@ -48,16 +48,18 @@
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:showConfirm
|
||||
bind:show={showConfirm}
|
||||
title="Factory Reset"
|
||||
message="Are you sure you want to reset the app? This will remove all configurations, user data, and the bundled Python environment, restoring the app to its original state."
|
||||
confirmLabel="Reset"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
config = null;
|
||||
await window.electronAPI.resetApp();
|
||||
toast.success(
|
||||
"App has been reset successfully. Please restart the app."
|
||||
);
|
||||
toast.success("App has been reset successfully.");
|
||||
|
||||
// refresh the page to apply changes
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
toast.error("Failed to reset the app");
|
||||
}
|
||||
|
||||
@@ -8,23 +8,43 @@
|
||||
import { flyAndScale } from "../../utils/transitions";
|
||||
import { marked } from "marked";
|
||||
|
||||
export let title = "";
|
||||
export let message = "";
|
||||
let {
|
||||
title,
|
||||
message,
|
||||
cancelLabel = "Cancel",
|
||||
confirmLabel = "Confirm",
|
||||
onConfirm = () => {},
|
||||
input,
|
||||
inputPlaceholder,
|
||||
inputValue,
|
||||
show = $bindable(false),
|
||||
} = $props();
|
||||
|
||||
export let cancelLabel = "Cancel";
|
||||
export let confirmLabel = "Confirm";
|
||||
$effect(() => {
|
||||
if (show) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
export let onConfirm = () => {};
|
||||
$effect(() => {
|
||||
if (mounted) {
|
||||
if (show && modalElement) {
|
||||
document.body.appendChild(modalElement);
|
||||
focusTrap = FocusTrap.createFocusTrap(modalElement);
|
||||
focusTrap.activate();
|
||||
|
||||
export let input = false;
|
||||
export let inputPlaceholder = "";
|
||||
export let inputValue = "";
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
} else if (modalElement) {
|
||||
focusTrap.deactivate();
|
||||
|
||||
export let show = false;
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
document.body.removeChild(modalElement);
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
@@ -57,24 +77,6 @@
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (mounted) {
|
||||
if (show && modalElement) {
|
||||
document.body.appendChild(modalElement);
|
||||
focusTrap = FocusTrap.createFocusTrap(modalElement);
|
||||
focusTrap.activate();
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
} else if (modalElement) {
|
||||
focusTrap.deactivate();
|
||||
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
document.body.removeChild(modalElement);
|
||||
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
show = false;
|
||||
if (focusTrap) {
|
||||
|
||||
Reference in New Issue
Block a user