mirror of
https://github.com/open-webui/desktop.git
synced 2026-07-15 12:45:42 -04:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 398dc3128e | |||
| d991908205 | |||
| 2528096e07 | |||
| aeba0e7c22 | |||
| ffbbb8f5b2 | |||
| 119409a2dc | |||
| cb564555dc | |||
| 00d739d0d0 | |||
| eebd30e29e | |||
| 8b62c3c993 | |||
| c6189bb7cc | |||
| ce3766d3c2 | |||
| e40766430a |
+22
-12
@@ -17,10 +17,8 @@ import {
|
||||
import path, { join } from "path";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
|
||||
import icon from "../../resources/icon.png?asset";
|
||||
import trayIconImage from "../../resources/assets/tray.png?asset";
|
||||
|
||||
import {
|
||||
getLogFilePath,
|
||||
checkUrlAndOpen,
|
||||
getConfig,
|
||||
getServerLog,
|
||||
@@ -30,12 +28,19 @@ import {
|
||||
isPythonInstalled,
|
||||
isUvInstalled,
|
||||
openUrl,
|
||||
resetApp,
|
||||
setConfig,
|
||||
startServer,
|
||||
stopAllServers,
|
||||
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;
|
||||
@@ -267,7 +272,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 +379,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 +391,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 +430,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 +454,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 +487,11 @@ if (!gotTheLock) {
|
||||
return SERVER_STATUS;
|
||||
});
|
||||
|
||||
ipcMain.handle("app:reset", async (event) => {
|
||||
await stopServerHandler();
|
||||
return await resetApp();
|
||||
});
|
||||
|
||||
ipcMain.handle("get:config", async (event) => {
|
||||
return await getConfig();
|
||||
});
|
||||
@@ -494,7 +504,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 +513,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 +525,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
|
||||
|
||||
+112
-43
@@ -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();
|
||||
@@ -59,7 +71,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 +227,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 +269,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 +285,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 +323,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 +337,7 @@ export const installPython = async (
|
||||
}
|
||||
|
||||
installationPath = installationPath || getPythonInstallationPath();
|
||||
console.log(installationPath, pythonDownloadPath);
|
||||
log.info(installationPath, pythonDownloadPath);
|
||||
|
||||
try {
|
||||
const userDataPath = getUserDataPath();
|
||||
@@ -346,9 +358,12 @@ export const installPython = async (
|
||||
encoding: "utf-8",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.platform === "win32"
|
||||
? { PYTHONIOENCODING: "utf-8" }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
console.log("Successfully installed uv package");
|
||||
log.info("Successfully installed uv package");
|
||||
|
||||
return true; // Return true to indicate success
|
||||
} else {
|
||||
@@ -361,7 +376,7 @@ export const installPython = async (
|
||||
|
||||
export const getPythonExecutablePath = (envPath: string) => {
|
||||
if (process.platform === "win32") {
|
||||
return path.normalize(path.join(envPath, "Scripts", "python.exe"));
|
||||
return path.normalize(path.join(envPath, "python.exe"));
|
||||
} else {
|
||||
return path.normalize(path.join(envPath, "bin", "python"));
|
||||
}
|
||||
@@ -387,9 +402,12 @@ export const isPythonInstalled = (installationPath?: string) => {
|
||||
encoding: "utf-8",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.platform === "win32"
|
||||
? { PYTHONIOENCODING: "utf-8" }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
console.log("Installed Python Version:", pythonVersion.trim());
|
||||
log.info("Installed Python Version:", pythonVersion.trim());
|
||||
|
||||
return true; // Return true to indicate success
|
||||
} catch (error) {
|
||||
@@ -406,10 +424,13 @@ export const isUvInstalled = (installationPath?: string) => {
|
||||
encoding: "utf-8",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.platform === "win32"
|
||||
? { PYTHONIOENCODING: "utf-8" }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
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 +467,44 @@ export const uninstallPython = (installationPath?: string): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const resetApp = async (): Promise<void> => {
|
||||
await uninstallPython();
|
||||
log.info("Uninstalled Python environment");
|
||||
|
||||
// remove /data folder
|
||||
const dataPath = getOpenWebUIDataPath();
|
||||
if (fs.existsSync(dataPath)) {
|
||||
try {
|
||||
fs.rmSync(dataPath, { recursive: true });
|
||||
log.info("Removed data directory:", dataPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to remove data directory:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
// Fixes code-signing issues in macOS by applying ad-hoc signatures to extracted environment files.
|
||||
@@ -513,13 +572,16 @@ export const installPackage = (
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.platform === "win32"
|
||||
? { PYTHONIOENCODING: "utf-8" }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Function to handle logging output
|
||||
const onLog = (data: any) => {
|
||||
console.log(data);
|
||||
log.info(data);
|
||||
};
|
||||
|
||||
// Listen to stdout and stderr for logging
|
||||
@@ -528,7 +590,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}`);
|
||||
@@ -563,19 +625,22 @@ export const isPackageInstalled = (packageName: string): boolean => {
|
||||
encoding: "utf-8",
|
||||
env: {
|
||||
...process.env,
|
||||
...(process.platform === "win32"
|
||||
? { PYTHONIOENCODING: "utf-8" }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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 +668,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 +687,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(" ")
|
||||
@@ -640,7 +705,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) {
|
||||
@@ -653,31 +723,30 @@ 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
|
||||
// console.log(`${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
|
||||
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 +755,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 +765,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 +774,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 +786,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) {
|
||||
@@ -741,7 +810,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;
|
||||
|
||||
@@ -760,20 +829,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 +851,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(
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{:else if installed === false}
|
||||
<Installation bind:installed />
|
||||
{:else}
|
||||
<Controls />
|
||||
<Controls bind:installed />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -48,16 +48,19 @@
|
||||
</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 {
|
||||
installed = null;
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -218,7 +210,7 @@
|
||||
{`Continue`}
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
<div
|
||||
class="text-xs mt-3 text-gray-500 cursor-pointer"
|
||||
in:fly={{
|
||||
delay: 500,
|
||||
@@ -226,8 +218,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);
|
||||
|
||||
@@ -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