mirror of
				https://github.com/open-webui/desktop
				synced 2025-06-26 18:15:59 +00:00 
			
		
		
		
	chore: format
This commit is contained in:
		
							parent
							
								
									1825957bd3
								
							
						
					
					
						commit
						e91a8ab4a7
					
				| @ -1,16 +1,16 @@ | ||||
| { | ||||
|   "env": { | ||||
|     "browser": true, | ||||
|     "es6": true, | ||||
|     "node": true | ||||
|   }, | ||||
|   "extends": [ | ||||
|     "eslint:recommended", | ||||
|     "plugin:@typescript-eslint/eslint-recommended", | ||||
|     "plugin:@typescript-eslint/recommended", | ||||
|     "plugin:import/recommended", | ||||
|     "plugin:import/electron", | ||||
|     "plugin:import/typescript" | ||||
|   ], | ||||
|   "parser": "@typescript-eslint/parser" | ||||
| 	"env": { | ||||
| 		"browser": true, | ||||
| 		"es6": true, | ||||
| 		"node": true | ||||
| 	}, | ||||
| 	"extends": [ | ||||
| 		"eslint:recommended", | ||||
| 		"plugin:@typescript-eslint/eslint-recommended", | ||||
| 		"plugin:@typescript-eslint/recommended", | ||||
| 		"plugin:import/recommended", | ||||
| 		"plugin:import/electron", | ||||
| 		"plugin:import/typescript" | ||||
| 	], | ||||
| 	"parser": "@typescript-eslint/parser" | ||||
| } | ||||
|  | ||||
							
								
								
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| { | ||||
| 	"useTabs": true, | ||||
| 	"singleQuote": true, | ||||
| 	"trailingComma": "none", | ||||
| 	"printWidth": 100, | ||||
| 	"plugins": ["prettier-plugin-svelte"], | ||||
| 	"pluginSearchDirs": ["."], | ||||
| 	"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }], | ||||
| 	"tabWidth": 4 | ||||
| } | ||||
							
								
								
									
										10
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								LICENSE
									
									
									
									
									
								
							| @ -5,15 +5,15 @@ Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this | ||||
|    list of conditions and the following disclaimer. | ||||
|      list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|    this list of conditions and the following disclaimer in the documentation | ||||
|    and/or other materials provided with the distribution. | ||||
|      this list of conditions and the following disclaimer in the documentation | ||||
|      and/or other materials provided with the distribution. | ||||
| 
 | ||||
| 3. Neither the name of the copyright holder nor the names of its | ||||
|    contributors may be used to endorse or promote products derived from | ||||
|    this software without specific prior written permission. | ||||
|      contributors may be used to endorse or promote products derived from | ||||
|      this software without specific prior written permission. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| # Open WebUI App (Experimental) 🚀 | ||||
| 
 | ||||
| **Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It provides a streamlined, all-in-one experience for installing and managing Open WebUI directly on your personal computer or connecting to remote Open WebUI servers.   | ||||
| **Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It provides a streamlined, all-in-one experience for installing and managing Open WebUI directly on your personal computer or connecting to remote Open WebUI servers. | ||||
| 
 | ||||
| This project is still **super experimental** and under active development. 🛠️ Expect rapid iteration and potential breaking changes during this phase. | ||||
| 
 | ||||
| @ -25,4 +25,4 @@ This project is licensed under the [BSD-3-Clause License](LICENSE). | ||||
| 
 | ||||
| We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved. | ||||
| 
 | ||||
| Let's build something amazing together! 💪 | ||||
| Let's build something amazing together! 💪 | ||||
|  | ||||
							
								
								
									
										34
									
								
								dev.md
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								dev.md
									
									
									
									
									
								
							| @ -15,27 +15,31 @@ Ensure these tools are properly installed and configured before proceeding. | ||||
| ## Getting Started | ||||
| 
 | ||||
| 1. **Clone the repository**: | ||||
|    ```bash | ||||
|    git clone https://github.com/open-webui/app | ||||
|    cd app | ||||
|    ``` | ||||
| 
 | ||||
|     ```bash | ||||
|     git clone https://github.com/open-webui/app | ||||
|     cd app | ||||
|     ``` | ||||
| 
 | ||||
| 2. **Install Node.js dependencies**: | ||||
|    ```bash | ||||
|    npm i | ||||
|    ``` | ||||
| 
 | ||||
|     ```bash | ||||
|     npm i | ||||
|     ``` | ||||
| 
 | ||||
| 3. **Generate the Python environment tarball**: | ||||
|    ```bash | ||||
|    npm run create:python-tar | ||||
|    ``` | ||||
| 
 | ||||
|     ```bash | ||||
|     npm run create:python-tar | ||||
|     ``` | ||||
| 
 | ||||
| 4. **Start the development environment**: | ||||
|    ```bash | ||||
|    npm run start | ||||
|    ``` | ||||
| 
 | ||||
|    This will launch the project in development mode. | ||||
|     ```bash | ||||
|     npm run start | ||||
|     ``` | ||||
| 
 | ||||
|     This will launch the project in development mode. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| @ -56,4 +60,4 @@ This will create the necessary files for distribution in the `out` directory. | ||||
| - Make sure you have the required versions of **conda**, **conda-pack**, and **conda-lock** to avoid compatibility issues. | ||||
| - If you encounter any issues, check the project-specific scripts in the `package.json` file. | ||||
| 
 | ||||
| Enjoy developing! 🚀 | ||||
| Enjoy developing! 🚀 | ||||
|  | ||||
							
								
								
									
										112
									
								
								forge.config.ts
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								forge.config.ts
									
									
									
									
									
								
							| @ -1,61 +1,61 @@ | ||||
| import type { ForgeConfig } from "@electron-forge/shared-types"; | ||||
| import { MakerSquirrel } from "@electron-forge/maker-squirrel"; | ||||
| import { MakerZIP } from "@electron-forge/maker-zip"; | ||||
| import { MakerDeb } from "@electron-forge/maker-deb"; | ||||
| import { MakerRpm } from "@electron-forge/maker-rpm"; | ||||
| import { VitePlugin } from "@electron-forge/plugin-vite"; | ||||
| import { FusesPlugin } from "@electron-forge/plugin-fuses"; | ||||
| import { FuseV1Options, FuseVersion } from "@electron/fuses"; | ||||
| import type { ForgeConfig } from '@electron-forge/shared-types'; | ||||
| import { MakerSquirrel } from '@electron-forge/maker-squirrel'; | ||||
| import { MakerZIP } from '@electron-forge/maker-zip'; | ||||
| import { MakerDeb } from '@electron-forge/maker-deb'; | ||||
| import { MakerRpm } from '@electron-forge/maker-rpm'; | ||||
| import { VitePlugin } from '@electron-forge/plugin-vite'; | ||||
| import { FusesPlugin } from '@electron-forge/plugin-fuses'; | ||||
| import { FuseV1Options, FuseVersion } from '@electron/fuses'; | ||||
| 
 | ||||
| const config: ForgeConfig = { | ||||
|   packagerConfig: { | ||||
|     asar: true, | ||||
|     icon: "public/assets/icon.png", | ||||
|     extraResource: ["public/assets", "resources"], | ||||
|   }, | ||||
|   rebuildConfig: {}, | ||||
|   makers: [ | ||||
|     new MakerSquirrel({}), | ||||
|     new MakerZIP({}, ["darwin"]), | ||||
|     new MakerRpm({}), | ||||
|     new MakerDeb({}), | ||||
|   ], | ||||
|   plugins: [ | ||||
|     new VitePlugin({ | ||||
|       // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
 | ||||
|       // If you are familiar with Vite configuration, it will look really familiar.
 | ||||
|       build: [ | ||||
|         { | ||||
|           // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
 | ||||
|           entry: "src/main.ts", | ||||
|           config: "vite.main.config.ts", | ||||
|           target: "main", | ||||
|         }, | ||||
|         { | ||||
|           entry: "src/preload.ts", | ||||
|           config: "vite.preload.config.ts", | ||||
|           target: "preload", | ||||
|         }, | ||||
|       ], | ||||
|       renderer: [ | ||||
|         { | ||||
|           name: "main_window", | ||||
|           config: "vite.renderer.config.mts", | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|     // Fuses are used to enable/disable various Electron functionality
 | ||||
|     // at package time, before code signing the application
 | ||||
|     new FusesPlugin({ | ||||
|       version: FuseVersion.V1, | ||||
|       [FuseV1Options.RunAsNode]: false, | ||||
|       [FuseV1Options.EnableCookieEncryption]: true, | ||||
|       [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, | ||||
|       [FuseV1Options.EnableNodeCliInspectArguments]: false, | ||||
|       [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, | ||||
|       [FuseV1Options.OnlyLoadAppFromAsar]: true, | ||||
|     }), | ||||
|   ], | ||||
| 	packagerConfig: { | ||||
| 		asar: true, | ||||
| 		icon: 'public/assets/icon.png', | ||||
| 		extraResource: ['public/assets', 'resources'] | ||||
| 	}, | ||||
| 	rebuildConfig: {}, | ||||
| 	makers: [ | ||||
| 		new MakerSquirrel({}), | ||||
| 		new MakerZIP({}, ['darwin']), | ||||
| 		new MakerRpm({}), | ||||
| 		new MakerDeb({}) | ||||
| 	], | ||||
| 	plugins: [ | ||||
| 		new VitePlugin({ | ||||
| 			// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
 | ||||
| 			// If you are familiar with Vite configuration, it will look really familiar.
 | ||||
| 			build: [ | ||||
| 				{ | ||||
| 					// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
 | ||||
| 					entry: 'src/main.ts', | ||||
| 					config: 'vite.main.config.ts', | ||||
| 					target: 'main' | ||||
| 				}, | ||||
| 				{ | ||||
| 					entry: 'src/preload.ts', | ||||
| 					config: 'vite.preload.config.ts', | ||||
| 					target: 'preload' | ||||
| 				} | ||||
| 			], | ||||
| 			renderer: [ | ||||
| 				{ | ||||
| 					name: 'main_window', | ||||
| 					config: 'vite.renderer.config.mts' | ||||
| 				} | ||||
| 			] | ||||
| 		}), | ||||
| 		// Fuses are used to enable/disable various Electron functionality
 | ||||
| 		// at package time, before code signing the application
 | ||||
| 		new FusesPlugin({ | ||||
| 			version: FuseVersion.V1, | ||||
| 			[FuseV1Options.RunAsNode]: false, | ||||
| 			[FuseV1Options.EnableCookieEncryption]: true, | ||||
| 			[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, | ||||
| 			[FuseV1Options.EnableNodeCliInspectArguments]: false, | ||||
| 			[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, | ||||
| 			[FuseV1Options.OnlyLoadAppFromAsar]: true | ||||
| 		}) | ||||
| 	] | ||||
| }; | ||||
| 
 | ||||
| export default config; | ||||
|  | ||||
							
								
								
									
										18
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								index.html
									
									
									
									
									
								
							| @ -1,11 +1,11 @@ | ||||
| <!DOCTYPE html> | ||||
| <!doctype html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <title>Open WebUI</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/renderer.ts"></script> | ||||
|   </body> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<title>Open WebUI</title> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<div id="app"></div> | ||||
| 		<script type="module" src="/src/renderer.ts"></script> | ||||
| 	</body> | ||||
| </html> | ||||
|  | ||||
							
								
								
									
										23450
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23450
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										108
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								package.json
									
									
									
									
									
								
							| @ -1,53 +1,59 @@ | ||||
| { | ||||
|   "name": "open-webui", | ||||
|   "productName": "Open WebUI", | ||||
|   "version": "0.0.1", | ||||
|   "description": "Open WebUI", | ||||
|   "main": ".vite/build/main.js", | ||||
|   "scripts": { | ||||
|     "start": "electron-forge start", | ||||
|     "package": "electron-forge package", | ||||
|     "make": "electron-forge make", | ||||
|     "publish": "electron-forge publish", | ||||
|     "lint": "eslint --ext .ts,.tsx .", | ||||
|     "create:conda-lock": "cd resources && rimraf conda-lock.yml && conda-lock -f environment.yml && cd -", | ||||
|     "create:python-tar": "rimraf ./resources/python.tar.gz && conda-lock install --prefix ./resources/python ./resources/conda-lock.yml && conda pack -p ./resources/python -o ./resources/python.tar.gz" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@electron-forge/cli": "^7.6.0", | ||||
|     "@electron-forge/maker-deb": "^7.6.0", | ||||
|     "@electron-forge/maker-rpm": "^7.6.0", | ||||
|     "@electron-forge/maker-squirrel": "^7.6.0", | ||||
|     "@electron-forge/maker-zip": "^7.6.0", | ||||
|     "@electron-forge/plugin-auto-unpack-natives": "^7.6.0", | ||||
|     "@electron-forge/plugin-fuses": "^7.6.0", | ||||
|     "@electron-forge/plugin-vite": "^7.6.0", | ||||
|     "@electron/fuses": "^1.8.0", | ||||
|     "@sveltejs/vite-plugin-svelte": "^5.0.3", | ||||
|     "@tailwindcss/vite": "^4.0.0-beta.8", | ||||
|     "@tsconfig/svelte": "^5.0.4", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.62.0", | ||||
|     "@typescript-eslint/parser": "^5.62.0", | ||||
|     "electron": "33.3.1", | ||||
|     "eslint": "^8.57.1", | ||||
|     "eslint-plugin-import": "^2.31.0", | ||||
|     "svelte": "^5.17.1", | ||||
|     "svelte-check": "^4.1.3", | ||||
|     "tailwindcss": "^4.0.0-beta.8", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "typescript": "^5.7.3", | ||||
|     "vite": "^6.0.7" | ||||
|   }, | ||||
|   "keywords": [], | ||||
|   "author": { | ||||
|     "name": "Timothy Jaeryang Baek", | ||||
|     "email": "tim@openwebui.com" | ||||
|   }, | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "electron-log": "^5.2.4", | ||||
|     "electron-squirrel-startup": "^1.0.1", | ||||
|     "tar": "^7.4.3", | ||||
|     "update-electron-app": "^3.1.0" | ||||
|   } | ||||
| 	"name": "open-webui", | ||||
| 	"productName": "Open WebUI", | ||||
| 	"version": "0.0.1", | ||||
| 	"description": "Open WebUI", | ||||
| 	"main": ".vite/build/main.js", | ||||
| 	"scripts": { | ||||
| 		"start": "electron-forge start", | ||||
| 		"package": "electron-forge package", | ||||
| 		"make": "electron-forge make", | ||||
| 		"publish": "electron-forge publish", | ||||
| 		"lint": "eslint --ext .ts,.tsx .", | ||||
| 		"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"", | ||||
| 		"create:conda-lock": "cd resources && rimraf conda-lock.yml && conda-lock -f environment.yml && cd -", | ||||
| 		"create:python-tar": "rimraf ./resources/python.tar.gz && conda-lock install --prefix ./resources/python ./resources/conda-lock.yml && conda pack -p ./resources/python -o ./resources/python.tar.gz" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@electron-forge/cli": "^7.6.0", | ||||
| 		"@electron-forge/maker-deb": "^7.6.0", | ||||
| 		"@electron-forge/maker-rpm": "^7.6.0", | ||||
| 		"@electron-forge/maker-squirrel": "^7.6.0", | ||||
| 		"@electron-forge/maker-zip": "^7.6.0", | ||||
| 		"@electron-forge/plugin-auto-unpack-natives": "^7.6.0", | ||||
| 		"@electron-forge/plugin-fuses": "^7.6.0", | ||||
| 		"@electron-forge/plugin-vite": "^7.6.0", | ||||
| 		"@electron/fuses": "^1.8.0", | ||||
| 		"@sveltejs/vite-plugin-svelte": "^5.0.3", | ||||
| 		"@tailwindcss/vite": "^4.0.0-beta.8", | ||||
| 		"@tsconfig/svelte": "^5.0.4", | ||||
| 		"@typescript-eslint/eslint-plugin": "^5.62.0", | ||||
| 		"@typescript-eslint/parser": "^5.62.0", | ||||
| 		"dompurify": "^3.2.3", | ||||
| 		"electron": "33.3.1", | ||||
| 		"eslint": "^8.57.1", | ||||
| 		"eslint-plugin-import": "^2.31.0", | ||||
| 		"marked": "^15.0.6", | ||||
| 		"prettier": "^3.4.2", | ||||
| 		"prettier-plugin-svelte": "^3.3.2", | ||||
| 		"svelte": "^5.17.1", | ||||
| 		"svelte-check": "^4.1.3", | ||||
| 		"tailwindcss": "^4.0.0-beta.8", | ||||
| 		"tippy.js": "^6.3.7", | ||||
| 		"ts-node": "^10.9.2", | ||||
| 		"typescript": "^5.7.3", | ||||
| 		"vite": "^6.0.7" | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": { | ||||
| 		"name": "Timothy Jaeryang Baek", | ||||
| 		"email": "tim@openwebui.com" | ||||
| 	}, | ||||
| 	"license": "MIT", | ||||
| 	"dependencies": { | ||||
| 		"electron-log": "^5.2.4", | ||||
| 		"electron-squirrel-startup": "^1.0.1", | ||||
| 		"tar": "^7.4.3", | ||||
| 		"update-electron-app": "^3.1.0" | ||||
| 	} | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										359
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										359
									
								
								src/main.ts
									
									
									
									
									
								
							| @ -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); | ||||
| } | ||||
|  | ||||
							
								
								
									
										118
									
								
								src/preload.ts
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								src/preload.ts
									
									
									
									
									
								
							| @ -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'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
							
								
								
									
										64
									
								
								src/render/lib/components/ListView.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/render/lib/components/ListView.svelte
									
									
									
									
									
										Normal 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> | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
							
								
								
									
										52
									
								
								src/render/lib/components/common/Tooltip.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/render/lib/components/common/Tooltip.svelte
									
									
									
									
									
										Normal 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> | ||||
| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; | ||||
| import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; | ||||
| 
 | ||||
| export default { | ||||
|   // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | ||||
|   // for more information about preprocessors
 | ||||
|   preprocess: vitePreprocess(), | ||||
| 	// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | ||||
| 	// for more information about preprocessors
 | ||||
| 	preprocess: vitePreprocess() | ||||
| }; | ||||
|  | ||||
| @ -1,15 +1,15 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "ESNext", | ||||
|     "module": "commonjs", | ||||
|     "allowJs": true, | ||||
|     "skipLibCheck": true, | ||||
|     "esModuleInterop": true, | ||||
|     "noImplicitAny": true, | ||||
|     "sourceMap": true, | ||||
|     "baseUrl": ".", | ||||
|     "outDir": "dist", | ||||
|     "moduleResolution": "node", | ||||
|     "resolveJsonModule": true | ||||
|   } | ||||
| 	"compilerOptions": { | ||||
| 		"target": "ESNext", | ||||
| 		"module": "commonjs", | ||||
| 		"allowJs": true, | ||||
| 		"skipLibCheck": true, | ||||
| 		"esModuleInterop": true, | ||||
| 		"noImplicitAny": true, | ||||
| 		"sourceMap": true, | ||||
| 		"baseUrl": ".", | ||||
| 		"outDir": "dist", | ||||
| 		"moduleResolution": "node", | ||||
| 		"resolveJsonModule": true | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { defineConfig } from 'vite'; | ||||
| import { svelte } from '@sveltejs/vite-plugin-svelte' | ||||
| import tailwindcss from '@tailwindcss/vite'; | ||||
| import { defineConfig } from "vite"; | ||||
| import { svelte } from "@sveltejs/vite-plugin-svelte"; | ||||
| import tailwindcss from "@tailwindcss/vite"; | ||||
| 
 | ||||
| // https://vitejs.dev/config
 | ||||
| export default defineConfig({ | ||||
|     plugins: [tailwindcss(), svelte()], | ||||
|   plugins: [tailwindcss(), svelte()], | ||||
| }); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user