chore: format

This commit is contained in:
Timothy Jaeryang Baek
2025-01-11 18:13:43 -08:00
parent 1825957bd3
commit e91a8ab4a7
24 changed files with 14064 additions and 13851 deletions

View File

@@ -1,219 +1,220 @@
import {
app,
nativeImage,
Tray,
Menu,
MenuItem,
BrowserWindow,
globalShortcut,
ipcMain,
} from "electron";
import path from "path";
import started from "electron-squirrel-startup";
app,
nativeImage,
Tray,
Menu,
MenuItem,
BrowserWindow,
globalShortcut,
ipcMain
} from 'electron';
import path from 'path';
import started from 'electron-squirrel-startup';
import {
installPackage,
removePackage,
startServer,
stopAllServers,
validateInstallation,
} from "./utils";
installPackage,
removePackage,
startServer,
stopAllServers,
validateInstallation
} from './utils';
// Restrict app to a single instance
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit(); // Quit if another instance is already running
app.quit(); // Quit if another instance is already running
} else {
// Handle second-instance logic
app.on("second-instance", (event, argv, workingDirectory) => {
// This event happens if a second instance is launched
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore(); // Restore if minimized
mainWindow.show(); // Show existing window
mainWindow.focus(); // Focus the existing window
}
});
// Handle second-instance logic
app.on('second-instance', (event, argv, workingDirectory) => {
// This event happens if a second instance is launched
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore(); // Restore if minimized
mainWindow.show(); // Show existing window
mainWindow.focus(); // Focus the existing window
}
});
// Handle creating/removing shortcuts on Windows during installation/uninstallation
if (started) {
app.quit();
}
// Handle creating/removing shortcuts on Windows during installation/uninstallation
if (started) {
app.quit();
}
app.setAboutPanelOptions({
applicationName: "Open WebUI",
iconPath: path.join(__dirname, "assets/icon.png"),
applicationVersion: app.getVersion(),
version: app.getVersion(),
website: "https://openwebui.com",
copyright: `© ${new Date().getFullYear()} Open WebUI (Timothy Jaeryang Baek)`,
});
app.setAboutPanelOptions({
applicationName: 'Open WebUI',
iconPath: path.join(__dirname, 'assets/icon.png'),
applicationVersion: app.getVersion(),
version: app.getVersion(),
website: 'https://openwebui.com',
copyright: `© ${new Date().getFullYear()} Open WebUI (Timothy Jaeryang Baek)`
});
// Main application logic
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
// Main application logic
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
const loadDefaultView = () => {
// Load index.html or dev server URL
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
);
}
};
const loadDefaultView = () => {
// Load index.html or dev server URL
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
);
}
};
const onReady = async () => {
console.log(process.resourcesPath);
console.log(app.getName());
console.log(app.getPath("userData"));
console.log(app.getPath("appData"));
const onReady = async () => {
console.log(process.resourcesPath);
console.log(app.getName());
console.log(app.getPath('userData'));
console.log(app.getPath('appData'));
mainWindow = new BrowserWindow({
width: 800,
height: 600,
icon: path.join(__dirname, "assets/icon.png"),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
titleBarStyle: "hidden",
});
mainWindow.setIcon(path.join(__dirname, "assets/icon.png"));
mainWindow = new BrowserWindow({
width: 800,
height: 600,
icon: path.join(__dirname, 'assets/icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hidden',
trafficLightPosition: { x: 10, y: 10 },
// expose window controlls in Windows/Linux
...(process.platform !== 'darwin' ? { titleBarOverlay: true } : {})
});
mainWindow.setIcon(path.join(__dirname, 'assets/icon.png'));
loadDefaultView();
if (!app.isPackaged) {
mainWindow.webContents.openDevTools();
}
loadDefaultView();
if (!app.isPackaged) {
mainWindow.webContents.openDevTools();
}
if (validateInstallation()) {
const serverUrl = await startServer();
mainWindow.loadURL(serverUrl);
}
if (validateInstallation()) {
const serverUrl = await startServer();
mainWindow.loadURL(serverUrl);
}
globalShortcut.register("Alt+CommandOrControl+O", () => {
mainWindow?.show();
globalShortcut.register('Alt+CommandOrControl+O', () => {
mainWindow?.show();
if (mainWindow?.isMinimized()) mainWindow?.restore();
mainWindow?.focus();
});
if (mainWindow?.isMinimized()) mainWindow?.restore();
mainWindow?.focus();
});
const defaultMenu = Menu.getApplicationMenu();
const defaultMenu = Menu.getApplicationMenu();
console.log(defaultMenu);
// Convert the default menu to a template we can modify
let menuTemplate = defaultMenu ? defaultMenu.items.map((item) => item) : [];
console.log(defaultMenu);
// Convert the default menu to a template we can modify
let menuTemplate = defaultMenu ? defaultMenu.items.map((item) => item) : [];
// Add your own custom menu items
menuTemplate.push({
label: "Action",
submenu: [
{
label: "Home",
accelerator: process.platform === "darwin" ? "Cmd+H" : "Ctrl+H",
click: () => {
loadDefaultView();
},
},
],
});
// Add your own custom menu items
menuTemplate.push({
label: 'Action',
submenu: [
{
label: 'Home',
accelerator: process.platform === 'darwin' ? 'Cmd+H' : 'Ctrl+H',
click: () => {
loadDefaultView();
}
}
]
});
// Build the updated menu and set it as the application menu
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(updatedMenu);
// Build the updated menu and set it as the application menu
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(updatedMenu);
// Create a system tray icon
const image = nativeImage.createFromPath(
path.join(__dirname, "assets/tray.png")
);
tray = new Tray(image.resize({ width: 16, height: 16 }));
// Create a system tray icon
const image = nativeImage.createFromPath(path.join(__dirname, 'assets/tray.png'));
tray = new Tray(image.resize({ width: 16, height: 16 }));
const trayMenu = Menu.buildFromTemplate([
{
label: "Show Application",
click: () => {
mainWindow.show(); // Show the main window when clicked
},
},
{
label: "Quit Open WebUI",
accelerator: "CommandOrControl+Q",
click: () => {
app.isQuiting = true; // Mark as quitting
app.quit(); // Quit the application
},
},
]);
const trayMenu = Menu.buildFromTemplate([
{
label: 'Show Application',
click: () => {
mainWindow.show(); // Show the main window when clicked
}
},
{
label: 'Quit Open WebUI',
accelerator: 'CommandOrControl+Q',
click: () => {
app.isQuiting = true; // Mark as quitting
app.quit(); // Quit the application
}
}
]);
tray.setToolTip("Open WebUI");
tray.setContextMenu(trayMenu);
tray.setToolTip('Open WebUI');
tray.setContextMenu(trayMenu);
// Handle the close event
mainWindow.on("close", (event) => {
if (!app.isQuiting) {
event.preventDefault(); // Prevent the default close behavior
mainWindow.hide(); // Hide the window instead of closing it
}
});
};
// Handle the close event
mainWindow.on('close', (event) => {
if (!app.isQuiting) {
event.preventDefault(); // Prevent the default close behavior
mainWindow.hide(); // Hide the window instead of closing it
}
});
};
ipcMain.handle("install", async (event) => {
console.log("Installing package...");
installPackage();
});
ipcMain.handle('install', async (event) => {
console.log('Installing package...');
installPackage();
});
ipcMain.handle("remove", async (event) => {
console.log("Resetting package...");
removePackage();
});
ipcMain.handle('remove', async (event) => {
console.log('Resetting package...');
removePackage();
});
ipcMain.handle("server:start", async (event) => {
console.log("Starting server...");
ipcMain.handle('server:start', async (event) => {
console.log('Starting server...');
startServer();
});
startServer();
});
ipcMain.handle("server:stop", async (event) => {
console.log("Stopping server...");
ipcMain.handle('server:stop', async (event) => {
console.log('Stopping server...');
stopAllServers();
});
stopAllServers();
});
ipcMain.handle("load-webui", async (event, arg) => {
console.log(arg); // prints "ping"
mainWindow.loadURL("http://localhost:8080");
ipcMain.handle('load-webui', async (event, arg) => {
console.log(arg); // prints "ping"
mainWindow.loadURL('http://localhost:8080');
mainWindow.webContents.once("did-finish-load", () => {
mainWindow.webContents.send("main:data", {
type: "ping", // This is the same type you're listening for in the renderer
});
});
mainWindow.webContents.once('did-finish-load', () => {
mainWindow.webContents.send('main:data', {
type: 'ping' // This is the same type you're listening for in the renderer
});
});
ipcMain.on("send-ping", (event) => {
console.log("Received PING from renderer process");
mainWindow.webContents.send("ping-reply", "PONG from Main Process!");
});
});
ipcMain.on('send-ping', (event) => {
console.log('Received PING from renderer process');
mainWindow.webContents.send('ping-reply', 'PONG from Main Process!');
});
});
app.on("before-quit", () => {
app.isQuiting = true; // Ensure quit flag is set
stopAllServers();
});
app.on('before-quit', () => {
app.isQuiting = true; // Ensure quit flag is set
stopAllServers();
});
// Quit when all windows are closed, except on macOS
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.isQuitting = true;
app.quit();
}
});
// Quit when all windows are closed, except on macOS
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.isQuitting = true;
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
onReady();
} else {
mainWindow?.show();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
onReady();
} else {
mainWindow?.show();
}
});
app.on("ready", onReady);
app.on('ready', onReady);
}

View File

@@ -1,75 +1,75 @@
import { ipcRenderer, contextBridge } from "electron";
import { ipcRenderer, contextBridge } from 'electron';
const isLocalSource = () => {
// Check if the execution environment is local
const origin = window.location.origin;
// Check if the execution environment is local
const origin = window.location.origin;
// Allow local sources: file protocol, localhost, or 0.0.0.0
return (
origin.startsWith("file://") ||
origin.includes("localhost") ||
origin.includes("127.0.0.1") ||
origin.includes("0.0.0.0")
);
// Allow local sources: file protocol, localhost, or 0.0.0.0
return (
origin.startsWith('file://') ||
origin.includes('localhost') ||
origin.includes('127.0.0.1') ||
origin.includes('0.0.0.0')
);
};
window.addEventListener("DOMContentLoaded", () => {
// Listen for messages from the main process
ipcRenderer.on("main:data", (event, data) => {
// Forward the message to the renderer using window.postMessage
window.postMessage(
{
type: `electron:${data.type}`,
data: data,
},
window.location.origin
);
});
window.addEventListener('DOMContentLoaded', () => {
// Listen for messages from the main process
ipcRenderer.on('main:data', (event, data) => {
// Forward the message to the renderer using window.postMessage
window.postMessage(
{
type: `electron:${data.type}`,
data: data
},
window.location.origin
);
});
});
contextBridge.exposeInMainWorld("electronAPI", {
sendPing: async () => {
console.log("Sending PING to main process...");
await ipcRenderer.invoke("send-ping"); // Send the ping back to the main process
},
contextBridge.exposeInMainWorld('electronAPI', {
sendPing: async () => {
console.log('Sending PING to main process...');
await ipcRenderer.invoke('send-ping'); // Send the ping back to the main process
},
installPackage: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
installPackage: async () => {
if (!isLocalSource()) {
throw new Error(
'Access restricted: This operation is only allowed in a local environment.'
);
}
await ipcRenderer.invoke("install");
},
await ipcRenderer.invoke('install');
},
removePackage: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
removePackage: async () => {
if (!isLocalSource()) {
throw new Error(
'Access restricted: This operation is only allowed in a local environment.'
);
}
await ipcRenderer.invoke("remove");
},
await ipcRenderer.invoke('remove');
},
startServer: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
startServer: async () => {
if (!isLocalSource()) {
throw new Error(
'Access restricted: This operation is only allowed in a local environment.'
);
}
await ipcRenderer.invoke("server:start");
},
await ipcRenderer.invoke('server:start');
},
stopServer: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
stopServer: async () => {
if (!isLocalSource()) {
throw new Error(
'Access restricted: This operation is only allowed in a local environment.'
);
}
await ipcRenderer.invoke("server:stop");
},
await ipcRenderer.invoke('server:stop');
}
});

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import Main from "./lib/components/Main.svelte";
onMount(() => {});
import { onMount } from 'svelte';
import Main from './lib/components/Main.svelte';
onMount(() => {});
</script>
<main class="w-screen h-screen bg-gray-900">
<Main />
<Main />
</main>

View File

@@ -1,39 +1,44 @@
@import "tailwindcss";
@import 'tailwindcss';
@font-face {
font-family: "Archivo";
src: url("/assets/fonts/Archivo-Variable.ttf");
font-display: swap;
font-family: 'Archivo';
src: url('/assets/fonts/Archivo-Variable.ttf');
font-display: swap;
}
@font-face {
font-family: "InstrumentSerif";
src: url("/assets/fonts/InstrumentSerif-Regular.ttf");
font-display: swap;
font-family: 'InstrumentSerif';
src: url('/assets/fonts/InstrumentSerif-Regular.ttf');
font-display: swap;
}
.font-secondary {
font-family: "InstrumentSerif", sans-serif;
font-family: 'InstrumentSerif', sans-serif;
}
html {
font-family: "Archivo";
font-family: 'Archivo';
}
@theme {
--color-*: initial;
--color-*: initial;
--color-white: #fff;
--color-gray-50: #f9f9f9;
--color-gray-100: #ececec;
--color-gray-200: #e3e3e3;
--color-gray-300: #cdcdcd;
--color-gray-400: #b4b4b4;
--color-gray-500: #9b9b9b;
--color-gray-600: #676767;
--color-gray-700: #4e4e4e;
--color-gray-800: #333;
--color-gray-850: #262626;
--color-gray-900: #171717;
--color-gray-950: #0d0d0d;
--color-white: #fff;
--color-black: #000;
--color-gray-50: #f9f9f9;
--color-gray-100: #ececec;
--color-gray-200: #e3e3e3;
--color-gray-300: #cdcdcd;
--color-gray-400: #b4b4b4;
--color-gray-500: #9b9b9b;
--color-gray-600: #676767;
--color-gray-700: #4e4e4e;
--color-gray-800: #333;
--color-gray-850: #262626;
--color-gray-900: #171717;
--color-gray-950: #0d0d0d;
}
.tippy-box[data-theme~='dark'] {
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
}

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import Tooltip from './common/Tooltip.svelte';
import Plus from './icons/Plus.svelte';
let selected = 'home';
</script>
<div class="min-w-18 bg-gray-950 flex gap-2.5 flex-col pt-9.5">
<div class="flex justify-center relative">
{#if selected === 'home'}
<div class="absolute top-0 left-0 flex h-full">
<div class="my-auto rounded-r-lg w-1 h-8 bg-white"></div>
</div>
{/if}
<Tooltip content="Home" placement="right">
<button
class=" cursor-pointer bg-gray-850 {selected === 'home'
? 'rounded-2xl'
: 'rounded-full'}"
onclick={() => {
selected = 'home';
}}
>
<img
src="./assets/images/splash.png"
class="size-11 dark:invert p-1"
alt="logo"
draggable="false"
/>
</button>
</Tooltip>
</div>
<div class="border-t border-gray-900 mx-3"></div>
<!-- <div class="flex justify-center relative group">
{#if selected === ""}
<div class="absolute top-0 left-0 flex h-full">
<div class="my-auto rounded-r-lg w-1 h-8 bg-white"></div>
</div>
{/if}
<button
class=" cursor-pointer bg-transparent"
onclick={() => {
selected = "";
}}
>
<img
src="./assets/images/adam.jpg"
class="size-11 {selected === '' ? 'rounded-2xl' : 'rounded-full'}"
alt="logo"
draggable="false"
/>
</button>
</div> -->
<div class="flex justify-center relative group text-gray-400">
<button class=" cursor-pointer p-2" onclick={() => {}}>
<Plus className="size-5" strokeWidth="2" />
</button>
</div>
</div>

View File

@@ -1,76 +1,80 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount } from 'svelte';
import Spinner from "./common/Spinner.svelte";
import Spinner from './common/Spinner.svelte';
import ListView from './ListView.svelte';
</script>
<div class="flex flex-row w-full h-full relative dark:text-gray-100">
<div
class="absolute top-0 left-0 w-full h-6 bg-gray-900 bg-opacity-50 draggable"
></div>
<div class="m-auto flex flex-col max-w-xs w-full text-center">
<div class=" flex justify-center mb-3">
<img
src="./assets/images/splash.png"
class=" size-24 dark:invert"
alt="hero"
/>
</div>
<div class="absolute top-0 left-0 w-full h-6 bg-transparent draggable"></div>
<!-- <div class=" text-2xl text-gray-50 font-secondary">Install Open WebUI</div> -->
<ListView />
<div class=" text-gray-500 hover:text-white transition">
<div class="flex justify-center items-center gap-2">
<div>Loading...</div>
<div>
<Spinner className="size-4" />
</div>
</div>
</div>
<div class="flex-1 w-full flex justify-center">
<div class="my-auto flex flex-col max-w-xs w-full">
<div class=" flex justify-center mb-3">
<!-- <img
src="./assets/images/splash.png"
class=" size-24 dark:invert"
alt="hero"
/> -->
</div>
<!-- <button
class=" hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("install clicked");
if (window?.electronAPI) {
window.electronAPI.installPackage();
}
}}
>
<div class="flex justify-center items-center gap-2">
<div>Install</div>
<div>
<Spinner className="size-4" />
</div>
</div>
</button> -->
<!-- <div class=" text-2xl text-gray-50 font-secondary">Install Open WebUI</div> -->
<!--
<div class=" text-gray-500 hover:text-white transition">
<div class="flex justify-center items-center gap-2">
<div>Loading...</div>
<div>
<Spinner className="size-4" />
</div>
</div>
</div>
<button
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("start clicked");
if (window?.electronAPI) {
window.electronAPI.startServer();
}
}}>Start Open WebUI</button
>
<!-- <button
class=" hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("install clicked");
if (window?.electronAPI) {
window.electronAPI.installPackage();
}
}}
>
<div class="flex justify-center items-center gap-2">
<div>Install</div>
<div>
<Spinner className="size-4" />
</div>
</div>
</button> -->
<button
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("stop clicked");
if (window?.electronAPI) {
window.electronAPI.stopServer();
}
}}>Stop Open WebUI</button
> -->
</div>
<!--
<button
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("start clicked");
if (window?.electronAPI) {
window.electronAPI.startServer();
}
}}>Start Open WebUI</button
>
<button
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
onclick={() => {
console.log("stop clicked");
if (window?.electronAPI) {
window.electronAPI.stopServer();
}
}}>Stop Open WebUI</button
> -->
</div>
</div>
</div>
<style>
.draggable {
-webkit-app-region: drag;
}
.draggable {
-webkit-app-region: drag;
}
</style>

View File

@@ -1,29 +1,29 @@
<script lang="ts">
export let className: string = "size-5";
export let className: string = 'size-5';
</script>
<div class="flex justify-center text-center">
<svg
class={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
<svg
class={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { onDestroy } from 'svelte';
import { marked } from 'marked';
import tippy from 'tippy.js';
import { roundArrow } from 'tippy.js';
export let placement = 'top';
export let content = `I'm a tooltip!`;
export let touch = true;
export let className = 'flex';
export let theme = '';
export let offset = [0, 4];
export let allowHTML = true;
export let tippyOptions = {};
let tooltipElement;
let tooltipInstance;
$: if (tooltipElement && content) {
if (tooltipInstance) {
tooltipInstance.setContent(DOMPurify.sanitize(content));
} else {
tooltipInstance = tippy(tooltipElement, {
content: DOMPurify.sanitize(content),
placement: placement,
allowHTML: allowHTML,
touch: touch,
...(theme !== '' ? { theme } : { theme: 'dark' }),
arrow: false,
offset: offset,
...tippyOptions
});
}
} else if (tooltipInstance && content === '') {
if (tooltipInstance) {
tooltipInstance.destroy();
}
}
onDestroy(() => {
if (tooltipInstance) {
tooltipInstance.destroy();
}
});
</script>
<div bind:this={tooltipElement} aria-label={DOMPurify.sanitize(content)} class={className}>
<slot />
</div>

View File

@@ -1,19 +1,15 @@
<script lang="ts">
export let className = "w-4 h-4";
export let strokeWidth = "2";
export let className = 'w-4 h-4';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>

View File

@@ -1,9 +1,11 @@
import { mount } from 'svelte'
import './render/app.css'
import App from './render/App.svelte'
import { mount } from 'svelte';
import './render/app.css';
import 'tippy.js/dist/tippy.css';
import App from './render/App.svelte';
const app = mount(App, {
target: document.getElementById('app'),
})
target: document.getElementById('app')
});
export default app
export default app;

View File

@@ -1,21 +1,21 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
exec,
execFile,
ExecFileOptions,
execFileSync,
execSync,
spawn,
ChildProcess,
} from "child_process";
import net from "net";
exec,
execFile,
ExecFileOptions,
execFileSync,
execSync,
spawn,
ChildProcess
} from 'child_process';
import net from 'net';
import * as tar from "tar";
import log from "electron-log";
import * as tar from 'tar';
import log from 'electron-log';
import { app } from "electron";
import { app } from 'electron';
////////////////////////////////////////////////
//
@@ -24,73 +24,70 @@ import { app } from "electron";
////////////////////////////////////////////////
export function getAppPath(): string {
let appDir = app.getAppPath();
return appDir;
let appDir = app.getAppPath();
return appDir;
}
export function getUserHomePath(): string {
return app.getPath("home");
return app.getPath('home');
}
export function getUserDataPath(): string {
const userDataDir = app.getPath("userData");
const userDataDir = app.getPath('userData');
if (!fs.existsSync(userDataDir)) {
try {
fs.mkdirSync(userDataDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
if (!fs.existsSync(userDataDir)) {
try {
fs.mkdirSync(userDataDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
return userDataDir;
return userDataDir;
}
export function getOpenWebUIDataPath(): string {
const openWebUIDataDir = path.join(getUserDataPath(), "data");
const openWebUIDataDir = path.join(getUserDataPath(), 'data');
if (!fs.existsSync(openWebUIDataDir)) {
try {
fs.mkdirSync(openWebUIDataDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
if (!fs.existsSync(openWebUIDataDir)) {
try {
fs.mkdirSync(openWebUIDataDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
return openWebUIDataDir;
return openWebUIDataDir;
}
export async function portInUse(
port: number,
host: string = "127.0.0.1"
): Promise<boolean> {
return new Promise((resolve) => {
const client = new net.Socket();
export async function portInUse(port: number, host: string = '127.0.0.1'): Promise<boolean> {
return new Promise((resolve) => {
const client = new net.Socket();
// Attempt to connect to the port
client
.setTimeout(1000) // Timeout for the connection attempt
.once("connect", () => {
// If connection succeeds, port is in use
client.destroy();
resolve(true);
})
.once("timeout", () => {
// If no connection after the timeout, port is not in use
client.destroy();
resolve(false);
})
.once("error", (err: any) => {
if (err.code === "ECONNREFUSED") {
// Port is not in use or no listener is accepting connections
resolve(false);
} else {
// Unexpected error
resolve(false);
}
})
.connect(port, host);
});
// Attempt to connect to the port
client
.setTimeout(1000) // Timeout for the connection attempt
.once('connect', () => {
// If connection succeeds, port is in use
client.destroy();
resolve(true);
})
.once('timeout', () => {
// If no connection after the timeout, port is not in use
client.destroy();
resolve(false);
})
.once('error', (err: any) => {
if (err.code === 'ECONNREFUSED') {
// Port is not in use or no listener is accepting connections
resolve(false);
} else {
// Unexpected error
resolve(false);
}
})
.connect(port, host);
});
}
////////////////////////////////////////////////
@@ -100,43 +97,43 @@ export async function portInUse(
////////////////////////////////////////////////
export function getBundledPythonTarPath(): string {
const appPath = getAppPath();
return path.join(appPath, "resources", "python.tar.gz");
const appPath = getAppPath();
return path.join(appPath, 'resources', 'python.tar.gz');
}
export function getBundledPythonInstallationPath(): string {
const installDir = path.join(app.getPath("userData"), "python");
const installDir = path.join(app.getPath('userData'), 'python');
if (!fs.existsSync(installDir)) {
try {
fs.mkdirSync(installDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
return installDir;
if (!fs.existsSync(installDir)) {
try {
fs.mkdirSync(installDir, { recursive: true });
} catch (error) {
log.error(error);
}
}
return installDir;
}
export function isCondaEnv(envPath: string): boolean {
return fs.existsSync(path.join(envPath, "conda-meta"));
return fs.existsSync(path.join(envPath, 'conda-meta'));
}
export function getPythonPath(envPath: string, isConda?: boolean) {
if (process.platform === "win32") {
return isConda ?? isCondaEnv(envPath)
? path.join(envPath, "python.exe")
: path.join(envPath, "Scripts", "python.exe");
} else {
return path.join(envPath, "bin", "python");
}
if (process.platform === 'win32') {
return (isConda ?? isCondaEnv(envPath))
? path.join(envPath, 'python.exe')
: path.join(envPath, 'Scripts', 'python.exe');
} else {
return path.join(envPath, 'bin', 'python');
}
}
export function getBundledPythonPath() {
return getPythonPath(getBundledPythonInstallationPath());
return getPythonPath(getBundledPythonInstallationPath());
}
export function isBundledPythonInstalled() {
return fs.existsSync(getBundledPythonPath());
return fs.existsSync(getBundledPythonPath());
}
////////////////////////////////////////////////
@@ -153,133 +150,131 @@ export function isBundledPythonInstalled() {
////////////////////////////////////////////////
export function createAdHocSignCommand(envPath: string): string {
const appPath = getAppPath();
const appPath = getAppPath();
const signListFile = path.join(
appPath,
"resources",
`sign-osx-${process.arch === "arm64" ? "arm64" : "64"}.txt`
);
const fileContents = fs.readFileSync(signListFile, "utf-8");
const signList: string[] = [];
const signListFile = path.join(
appPath,
'resources',
`sign-osx-${process.arch === 'arm64' ? 'arm64' : '64'}.txt`
);
const fileContents = fs.readFileSync(signListFile, 'utf-8');
const signList: string[] = [];
fileContents.split(/\r?\n/).forEach((line) => {
if (line) {
signList.push(`"${line}"`);
}
});
fileContents.split(/\r?\n/).forEach((line) => {
if (line) {
signList.push(`"${line}"`);
}
});
// sign all binaries with ad-hoc signature
return `cd ${envPath} && codesign -s - -o 0x2 -f ${signList.join(
" "
)} && cd -`;
// sign all binaries with ad-hoc signature
return `cd ${envPath} && codesign -s - -o 0x2 -f ${signList.join(' ')} && cd -`;
}
export async function installOpenWebUI(installationPath: string) {
console.log(installationPath);
let unpackCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && uv pip install open-webui -U`
: `source "${installationPath}/bin/activate" && uv pip install open-webui -U`;
console.log(installationPath);
let unpackCommand =
process.platform === 'win32'
? `${installationPath}\\Scripts\\activate.bat && uv pip install open-webui -U`
: `source "${installationPath}/bin/activate" && uv pip install open-webui -U`;
// only unsign when installing from bundled installer
// if (platform === "darwin") {
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
// }
// only unsign when installing from bundled installer
// if (platform === "darwin") {
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
// }
console.log(unpackCommand);
console.log(unpackCommand);
const commandProcess = exec(unpackCommand, {
shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash",
});
const commandProcess = exec(unpackCommand, {
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'
});
commandProcess.stdout?.on("data", (data) => {
console.log(data);
});
commandProcess.stdout?.on('data', (data) => {
console.log(data);
});
commandProcess.stderr?.on("data", (data) => {
console.error(data);
});
commandProcess.stderr?.on('data', (data) => {
console.error(data);
});
commandProcess.on("exit", (code) => {
console.log(`Child exited with code ${code}`);
});
commandProcess.on('exit', (code) => {
console.log(`Child exited with code ${code}`);
});
}
export async function installBundledPython(installationPath?: string) {
installationPath = installationPath || getBundledPythonInstallationPath();
installationPath = installationPath || getBundledPythonInstallationPath();
const pythonTarPath = getBundledPythonTarPath();
const pythonTarPath = getBundledPythonTarPath();
console.log(installationPath, pythonTarPath);
if (!fs.existsSync(pythonTarPath)) {
log.error("Python tarball not found");
return;
}
console.log(installationPath, pythonTarPath);
if (!fs.existsSync(pythonTarPath)) {
log.error('Python tarball not found');
return;
}
try {
fs.mkdirSync(installationPath, { recursive: true });
await tar.x({
cwd: installationPath,
file: pythonTarPath,
});
} catch (error) {
log.error(error);
}
try {
fs.mkdirSync(installationPath, { recursive: true });
await tar.x({
cwd: installationPath,
file: pythonTarPath
});
} catch (error) {
log.error(error);
}
// Get the path to the installed Python binary
const bundledPythonPath = getBundledPythonPath();
// Get the path to the installed Python binary
const bundledPythonPath = getBundledPythonPath();
if (!fs.existsSync(bundledPythonPath)) {
log.error("Python binary not found in install path");
return;
}
if (!fs.existsSync(bundledPythonPath)) {
log.error('Python binary not found in install path');
return;
}
try {
// Execute the Python binary to print the version
const pythonVersion = execFileSync(bundledPythonPath, ["--version"], {
encoding: "utf-8",
});
console.log("Installed Python Version:", pythonVersion.trim());
} catch (error) {
log.error("Failed to execute Python binary", error);
}
try {
// Execute the Python binary to print the version
const pythonVersion = execFileSync(bundledPythonPath, ['--version'], {
encoding: 'utf-8'
});
console.log('Installed Python Version:', pythonVersion.trim());
} catch (error) {
log.error('Failed to execute Python binary', error);
}
}
export async function installPackage(installationPath?: string) {
installationPath = installationPath || getBundledPythonInstallationPath();
installationPath = installationPath || getBundledPythonInstallationPath();
// if (!isBundledPythonInstalled()) {
// try {
// await installBundledPython(installationPath);
// } catch (error) {
// log.error("Failed to install bundled Python", error);
// return Promise.reject("Failed to install bundled Python");
// }
// }
// if (!isBundledPythonInstalled()) {
// try {
// await installBundledPython(installationPath);
// } catch (error) {
// log.error("Failed to install bundled Python", error);
// return Promise.reject("Failed to install bundled Python");
// }
// }
try {
await installBundledPython(installationPath);
} catch (error) {
log.error("Failed to install bundled Python", error);
return Promise.reject("Failed to install bundled Python");
}
try {
await installBundledPython(installationPath);
} catch (error) {
log.error('Failed to install bundled Python', error);
return Promise.reject('Failed to install bundled Python');
}
try {
await installOpenWebUI(installationPath);
} catch (error) {
log.error("Failed to install open-webui", error);
return Promise.reject("Failed to install open-webui");
}
try {
await installOpenWebUI(installationPath);
} catch (error) {
log.error('Failed to install open-webui', error);
return Promise.reject('Failed to install open-webui');
}
}
export async function removePackage(installationPath?: string) {
installationPath = installationPath || getBundledPythonInstallationPath();
installationPath = installationPath || getBundledPythonInstallationPath();
// remove the python env entirely
if (fs.existsSync(installationPath)) {
fs.rmdirSync(installationPath, { recursive: true });
}
// remove the python env entirely
if (fs.existsSync(installationPath)) {
fs.rmdirSync(installationPath, { recursive: true });
}
}
////////////////////////////////////////////////
@@ -292,25 +287,23 @@ export async function removePackage(installationPath?: string) {
* Validates that Python is installed and the `open-webui` package is present
* within the specified virtual environment.
*/
export async function validateInstallation(
installationPath?: string
): Promise<boolean> {
installationPath = installationPath || getBundledPythonInstallationPath();
const pythonPath = getPythonPath(installationPath);
if (!fs.existsSync(pythonPath)) {
return false;
}
try {
const checkCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && pip show open-webui`
: `source "${installationPath}/bin/activate" && pip show open-webui`;
execSync(checkCommand, { stdio: "ignore" });
} catch (error) {
return false;
}
export async function validateInstallation(installationPath?: string): Promise<boolean> {
installationPath = installationPath || getBundledPythonInstallationPath();
const pythonPath = getPythonPath(installationPath);
if (!fs.existsSync(pythonPath)) {
return false;
}
try {
const checkCommand =
process.platform === 'win32'
? `${installationPath}\\Scripts\\activate.bat && pip show open-webui`
: `source "${installationPath}/bin/activate" && pip show open-webui`;
execSync(checkCommand, { stdio: 'ignore' });
} catch (error) {
return false;
}
return true;
return true;
}
// Tracks all spawned server process PIDs
@@ -319,144 +312,137 @@ const serverPIDs: Set<number> = new Set();
/**
* Spawn the Open-WebUI server process.
*/
export async function startServer(
installationPath?: string,
port?: number
): Promise<string> {
installationPath = path.normalize(
installationPath || getBundledPythonInstallationPath()
);
export async function startServer(installationPath?: string, port?: number): Promise<string> {
installationPath = path.normalize(installationPath || getBundledPythonInstallationPath());
if (!validateInstallation(installationPath)) {
console.error("Failed to validate installation");
return;
}
if (!validateInstallation(installationPath)) {
console.error('Failed to validate installation');
return;
}
let startCommand =
process.platform === "win32"
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
app.getPath("userData"),
"data"
)}" && open-webui serve`
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
app.getPath("userData"),
"data"
)}" && open-webui serve`;
let startCommand =
process.platform === 'win32'
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
app.getPath('userData'),
'data'
)}" && open-webui serve`
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
app.getPath('userData'),
'data'
)}" && open-webui serve`;
port = port || 8080;
while (await portInUse(port)) {
port++;
}
port = port || 8080;
while (await portInUse(port)) {
port++;
}
startCommand += ` --port ${port}`;
startCommand += ` --port ${port}`;
console.log("Starting Open-WebUI server...");
const childProcess = spawn(startCommand, {
shell: true,
detached: true,
stdio: ["ignore", "pipe", "pipe"], // Let us capture logs via stdout/stderr
});
console.log('Starting Open-WebUI server...');
const childProcess = spawn(startCommand, {
shell: true,
detached: true,
stdio: ['ignore', 'pipe', 'pipe'] // Let us capture logs via stdout/stderr
});
let serverCrashed = false;
let detectedURL: string | null = null;
let serverCrashed = false;
let detectedURL: string | null = null;
// Wait for log output to confirm the server has started
async function monitorServerLogs(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const handleLog = (data: Buffer) => {
const logLine = data.toString().trim();
console.log(`[Open-WebUI Log]: ${logLine}`);
// Wait for log output to confirm the server has started
async function monitorServerLogs(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const handleLog = (data: Buffer) => {
const logLine = data.toString().trim();
console.log(`[Open-WebUI Log]: ${logLine}`);
// Look for "Uvicorn running on http://<hostname>:<port>"
const match = logLine.match(
/Uvicorn running on (http:\/\/[^\s]+) \(Press CTRL\+C to quit\)/
);
if (match) {
detectedURL = match[1]; // e.g., "http://0.0.0.0:8081"
resolve();
}
};
// Look for "Uvicorn running on http://<hostname>:<port>"
const match = logLine.match(
/Uvicorn running on (http:\/\/[^\s]+) \(Press CTRL\+C to quit\)/
);
if (match) {
detectedURL = match[1]; // e.g., "http://0.0.0.0:8081"
resolve();
}
};
// Combine stdout and stderr streams as a unified log source
childProcess.stdout?.on("data", handleLog);
childProcess.stderr?.on("data", handleLog);
// Combine stdout and stderr streams as a unified log source
childProcess.stdout?.on('data', handleLog);
childProcess.stderr?.on('data', handleLog);
childProcess.on("close", (code) => {
serverCrashed = true;
if (!detectedURL) {
reject(
new Error(
`Process exited unexpectedly with code ${code}. No server URL detected.`
)
);
}
});
});
}
childProcess.on('close', (code) => {
serverCrashed = true;
if (!detectedURL) {
reject(
new Error(
`Process exited unexpectedly with code ${code}. No server URL detected.`
)
);
}
});
});
}
// Track the child process PID
if (childProcess.pid) {
serverPIDs.add(childProcess.pid);
console.log(`Server started with PID: ${childProcess.pid}`);
} else {
throw new Error("Failed to start server: No PID available");
}
// Track the child process PID
if (childProcess.pid) {
serverPIDs.add(childProcess.pid);
console.log(`Server started with PID: ${childProcess.pid}`);
} else {
throw new Error('Failed to start server: No PID available');
}
// Wait until the server log confirms it's started
try {
await monitorServerLogs();
} catch (error) {
if (serverCrashed) {
throw new Error("Server crashed unexpectedly.");
}
throw error;
}
// Wait until the server log confirms it's started
try {
await monitorServerLogs();
} catch (error) {
if (serverCrashed) {
throw new Error('Server crashed unexpectedly.');
}
throw error;
}
if (!detectedURL) {
throw new Error("Failed to detect server URL from logs.");
}
if (!detectedURL) {
throw new Error('Failed to detect server URL from logs.');
}
console.log(`Server is now running at ${detectedURL}`);
return detectedURL; // Return the detected URL
console.log(`Server is now running at ${detectedURL}`);
return detectedURL; // Return the detected URL
}
/**
* Terminates all server processes.
*/
export async function stopAllServers(): Promise<void> {
console.log("Stopping all servers...");
for (const pid of serverPIDs) {
try {
terminateProcessTree(pid);
serverPIDs.delete(pid); // Remove from tracking set after termination
} catch (error) {
console.error(`Error stopping server with PID ${pid}:`, error);
}
}
console.log("All servers stopped successfully.");
console.log('Stopping all servers...');
for (const pid of serverPIDs) {
try {
terminateProcessTree(pid);
serverPIDs.delete(pid); // Remove from tracking set after termination
} catch (error) {
console.error(`Error stopping server with PID ${pid}:`, error);
}
}
console.log('All servers stopped successfully.');
}
/**
* Kills a process tree by PID.
*/
function terminateProcessTree(pid: number): void {
if (process.platform === "win32") {
// Use `taskkill` on Windows to recursively kill the process and its children
try {
execSync(`taskkill /PID ${pid} /T /F`); // /T -> terminate child processes, /F -> force termination
console.log(`Terminated server process tree (PID: ${pid}) on Windows.`);
} catch (error) {
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
}
} else {
// Use `kill` on Unix-like platforms to terminate the process group (-pid)
try {
process.kill(-pid, "SIGKILL"); // Negative PID (-pid) kills the process group
console.log(
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
);
} catch (error) {
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
}
}
if (process.platform === 'win32') {
// Use `taskkill` on Windows to recursively kill the process and its children
try {
execSync(`taskkill /PID ${pid} /T /F`); // /T -> terminate child processes, /F -> force termination
console.log(`Terminated server process tree (PID: ${pid}) on Windows.`);
} catch (error) {
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
}
} else {
// Use `kill` on Unix-like platforms to terminate the process group (-pid)
try {
process.kill(-pid, 'SIGKILL'); // Negative PID (-pid) kills the process group
console.log(`Terminated server process tree (PID: ${pid}) on Unix-like OS.`);
} catch (error) {
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
}
}
}