diff --git a/app/assets/IconTemplate.png b/app/assets/IconTemplate.png new file mode 100644 index 0000000..90157f3 Binary files /dev/null and b/app/assets/IconTemplate.png differ diff --git a/app/assets/icon.icns b/app/assets/icon.icns new file mode 100644 index 0000000..54f3451 Binary files /dev/null and b/app/assets/icon.icns differ diff --git a/app/main/main.ts b/app/main/main.ts index 7dfcd50..cb603e2 100644 --- a/app/main/main.ts +++ b/app/main/main.ts @@ -10,7 +10,7 @@ import { nativeImage, Tray, } from 'electron'; - +import Store from 'electron-store'; import * as net from 'net'; const path = require('path'); @@ -72,6 +72,16 @@ class ServerManager { console.log(`Starting server for model: ${model} on port: ${port}`); this.serverProcess = this.runPythonServer(model, port); + this.serverProcess.stdout.on('data', (data: Buffer) => { + const output = data.toString('utf8'); + console.log('Server output:', output); + + // Check if the server is ready + if (output.includes('starting server on')) { + resolve(); // Resolve the promise when the server is ready + } + }); + this.serverProcess.on('close', (code: number | null) => { console.log(`Server process exited with code ${code}`); this.serverProcess = null; @@ -82,8 +92,6 @@ class ServerManager { this.serverProcess = null; reject(err); }); - - resolve(); }); }); } @@ -130,12 +138,52 @@ if (isProd) { app.setPath('userData', `${app.getPath('userData')} (development)`); } -let directoryOpen = false; +let openModal: 'settings' | 'directory' | null = null; + +let globalWindow: BrowserWindow | null = null; + +const triggerShortcut = () => { + if (openModal || !globalWindow) { + return; + } + if (globalWindow.isFocused()) { + globalWindow.blur(); + return; + } + globalWindow.show(); +}; + +const store = new Store({ + schema: { + keybind: { + type: 'string', + default: 'Cmd+O', + }, + model: { + type: 'string', + default: 'mlx-community/quantized-gemma-7b-it', + }, + customInstructions: { + type: 'string', + default: '', + }, + }, +}); + +const serverManager = new ServerManager(); const createWindow = () => { - const icon = nativeImage.createFromPath('path/to/asset.png'); - let tray = new Tray(icon); - tray.setTitle('M'); + const icon = nativeImage.createFromPath( + !isProd + ? '../assets/IconTemplate.png' + : path.join(process.resourcesPath, 'IconTemplate.png'), + ); + // if you want to resize it, be careful, it creates a copy + const trayIcon = icon.resize({ width: 16 }); + // here is the important part (has to be set on the resized version) + trayIcon.setTemplateImage(true); + let tray = new Tray(trayIcon); + tray.setTitle(isProd ? '' : 'M'); const win = new BrowserWindow({ webPreferences: { @@ -152,14 +200,13 @@ const createWindow = () => { autoHideMenuBar: true, vibrancy: 'under-window', // on MacOS backgroundMaterial: 'acrylic', + icon: __dirname + '../../assets/public/icon.icns', }); - app.dock.hide(); + globalWindow = win; win.setWindowButtonVisibility(false); win.setAlwaysOnTop(true, 'floating'); win.setVisibleOnAllWorkspaces(true); - // win.webContents.openDevTools(); - // Expose URL if (isProd) { win.loadURL('app://./home.html'); @@ -176,30 +223,30 @@ const createWindow = () => { win.show(); }); - win.webContents.on('did-finish-load', () => { + win.webContents.on('did-finish-load', async () => { + await serverManager.start(store.get('model') as string); /// then close the loading screen window and show the main window if (splash) { splash.close(); } win.show(); - globalShortcut.register('Cmd+O', () => { - if (win.isFocused()) { - win.blur(); - return; - } - win.show(); - }); + globalShortcut.register(store.get('keybind') as string, triggerShortcut.bind(null)); }); // @ts-expect-error -- We don't have types for electron win.on('blur', (event) => { - if (directoryOpen) { + if (openModal) { win.setAlwaysOnTop(false); + } + if (openModal === 'directory') { return; } - globalShortcut.unregister('Escape'); win.hide(); + if (openModal) { + return; + } + Menu.sendActionToFirstResponder('hide:'); }); @@ -216,26 +263,79 @@ const createWindow = () => { event.preventDefault(); win.hide(); }); + let settingsModal: BrowserWindow | null = null; + + const createSettings = () => { + settingsModal = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + width: 500, + height: 500, + resizable: false, + minimizable: false, + titleBarStyle: 'hidden', + show: false, + backgroundColor: '#000', + }); + + if (isProd) { + settingsModal.loadURL('app://./home.html'); + } else { + // const port = process.argv[2]; + settingsModal.loadURL('http://localhost:3000/settings'); + } + + settingsModal.on('closed', () => { + openModal = null; + settingsModal?.destroy(); + settingsModal = null; + }); + + settingsModal.on('ready-to-show', () => { + settingsModal?.show(); + }); + + return settingsModal; + }; + + const nativeMenus: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [ + { + label: 'MLX Chat', + submenu: [ + { + label: 'Settings', + click() { + openModal = 'settings'; + if (settingsModal !== null) { + settingsModal.close(); + } + createSettings(); + }, + accelerator: 'Cmd+,', + }, + ], + }, + ]; + + const menu = Menu.buildFromTemplate(nativeMenus); + Menu.setApplicationMenu(menu); }; app.whenReady().then(() => { - if (process.platform == 'darwin') { - app.dock.hide(); - } ipcMain.on('set-title', handleSetTitle); ipcMain.on('select-directory', (event: any) => { - directoryOpen = true; + openModal = 'directory'; dialog.showOpenDialog({ properties: ['openDirectory'] }).then((result: any) => { const win = BrowserWindow.fromWebContents(event.sender); // Weird hack to bring the window to the front after allowing windows in front of it win?.setAlwaysOnTop(true, 'floating'); - directoryOpen = false; + openModal = null; event.sender.send('selected-directory', result.filePaths); }); }); - const serverManager = new ServerManager(); ipcMain.on('start-server', (event: any, model: string) => { event; serverManager.start(model) @@ -254,11 +354,23 @@ app.whenReady().then(() => { win.center(); }); + ipcMain.on('fetch-setting', (event, arg) => { + event.returnValue = store.get(arg); + }); + + ipcMain.on('update-setting', (_event, arg) => { + if (arg.key === 'keybind') { + globalShortcut.unregister(store.get('keybind') as string); + globalShortcut.register(arg.value, triggerShortcut.bind(null)); + } + store.set(arg.key, arg.value); + }); + createSplashScreen(); setTimeout(() => { createWindow(); - }, 2000); + }, 500); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } diff --git a/app/main/preload.ts b/app/main/preload.ts index abb2649..acfc990 100644 --- a/app/main/preload.ts +++ b/app/main/preload.ts @@ -16,6 +16,8 @@ export const electronAPI = { }, startServer: (model: string) => ipcRenderer.send('start-server', model), resizeWindow: (height: number) => ipcRenderer.send('resize-window', { height }), + fetchSetting: (key: string) => ipcRenderer.sendSync('fetch-setting', key), + updateSetting: (key: string, value: any) => ipcRenderer.send('update-setting', { key, value }), }; contextBridge.exposeInMainWorld('electronAPI', electronAPI); diff --git a/app/main/public/icon.png b/app/main/public/icon.png new file mode 100644 index 0000000..3f3cd67 Binary files /dev/null and b/app/main/public/icon.png differ diff --git a/app/main/tsconfig.json b/app/main/tsconfig.json index 533b9e0..b312b43 100644 --- a/app/main/tsconfig.json +++ b/app/main/tsconfig.json @@ -17,9 +17,9 @@ "skipLibCheck": true, "strict": true, "target": "esnext", - "outDir": "./out" + "outDir": "./out", }, "compileOnSave": true, "exclude": ["node_modules", "./out/**/*"], - "include": ["**/*.ts", "**/*.tsx", "**/*.js"] + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "public/**.icns"], } diff --git a/app/package-lock.json b/app/package-lock.json index fb7c829..4595c02 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -30,6 +30,7 @@ "dprint": "^0.45.0", "electron-serve": "^1.1.0", "electron-squirrel-startup": "^1.0.0", + "electron-store": "^8.1.0", "eslint": "8.41.0", "eslint-config-next": "13.4.3", "markdown-to-jsx": "^7.4.1", @@ -2143,6 +2144,42 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -2509,6 +2546,14 @@ "node": ">= 4.0.0" } }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", @@ -3213,6 +3258,71 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/conf/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/conf/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/config-file-ts": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", @@ -3325,6 +3435,28 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3601,6 +3733,20 @@ "node": ">=6.0.0" } }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", @@ -3804,6 +3950,29 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/electron-store": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.1.0.tgz", + "integrity": "sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==", + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-store/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.513", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", @@ -5804,6 +5973,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -6062,6 +6239,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -6783,6 +6965,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6868,6 +7058,73 @@ "node": ">= 6" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -7341,6 +7598,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -10158,6 +10423,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -10447,6 +10738,11 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, + "atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==" + }, "autoprefixer": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", @@ -10912,6 +11208,54 @@ } } }, + "conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "requires": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + } + } + }, "config-file-ts": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", @@ -10994,6 +11338,21 @@ "@babel/runtime": "^7.21.0" } }, + "debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "requires": { + "mimic-fn": "^3.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" + } + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11190,6 +11549,14 @@ "esutils": "^2.0.2" } }, + "dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "requires": { + "is-obj": "^2.0.0" + } + }, "dotenv": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", @@ -11363,6 +11730,22 @@ } } }, + "electron-store": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.1.0.tgz", + "integrity": "sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==", + "requires": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + } + } + }, "electron-to-chromium": { "version": "1.4.513", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", @@ -12743,6 +13126,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -12922,6 +13310,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13410,6 +13803,11 @@ "p-limit": "^3.0.2" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13468,6 +13866,54 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==" }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + } + } + }, "plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -13749,6 +14195,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", diff --git a/app/package.json b/app/package.json index 5f81d9d..da7decb 100644 --- a/app/package.json +++ b/app/package.json @@ -45,6 +45,7 @@ "dprint": "^0.45.0", "electron-serve": "^1.1.0", "electron-squirrel-startup": "^1.0.0", + "electron-store": "^8.1.0", "eslint": "8.41.0", "eslint-config-next": "13.4.3", "markdown-to-jsx": "^7.4.1", @@ -99,10 +100,20 @@ "category": "your.app.category.type", "target": [ "dmg" - ] + ], + "icon": "assets/icon.icns" }, "dmg": { "title": "MLX Chat Installer" - } + }, + "extraFiles": [ + { + "from": "assets", + "to": "resources", + "filter": [ + "**/*" + ] + } + ] } } diff --git a/app/src/app/globals.css b/app/src/app/globals.css index 3291949..bcf59ed 100644 --- a/app/src/app/globals.css +++ b/app/src/app/globals.css @@ -123,3 +123,11 @@ menu { margin: 0; padding-left: 20px; } + +.drag { + -webkit-app-region: drag; +} + +.no-drag { + -webkit-app-region: no-drag; +} diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 6a52445..7d0786f 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -2,6 +2,13 @@ import StoreProvider from '../AppProvider'; import './globals.css'; +import '@fortawesome/fontawesome-svg-core/styles.css'; +// Prevent fontawesome from adding its CSS since we did it manually above: +import { + config, +} from '@fortawesome/fontawesome-svg-core'; + +config.autoAddCss = false; export default function RootLayout({ children, diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index b3a87b3..9c75f83 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -6,7 +6,6 @@ import React, { } from 'react'; import Chat from '../components/chat/Chat'; import SelectDirectory from '../components/options/SelectDirectory'; -import SelectModel from '../components/options/SelectModel'; import { useAppDispatch, } from '../lib/hooks'; @@ -17,7 +16,6 @@ import { export default function Home() { const [selectedDirectory, setSelectedDirectory] = useState(null); - const [selectedModel, setSelectedModel] = useState(null); const dispatch = useAppDispatch(); @@ -51,19 +49,11 @@ export default function Home() { }); }, []); - const handleModelChange = (model: string | null) => { - setSelectedModel(model); - if (typeof window !== 'undefined' && model) { - window.electronAPI.startServer(model); - } - }; - return (
-
- +
diff --git a/app/src/app/settings/page.tsx b/app/src/app/settings/page.tsx new file mode 100644 index 0000000..663458b --- /dev/null +++ b/app/src/app/settings/page.tsx @@ -0,0 +1,188 @@ +'use client'; + +import type { + IconProp, +} from '@fortawesome/fontawesome-svg-core'; +import { + faCog, + faMessage, +} from '@fortawesome/free-solid-svg-icons'; +import { + FontAwesomeIcon, +} from '@fortawesome/react-fontawesome'; +import React, { + useEffect, +} from 'react'; +import SelectModel from '../../components/options/SelectModel'; +import { + Textarea, +} from '../../components/ui/textarea'; +import { + convertToNiceShortcut, + useKeyboardShortcut, +} from '../../lib/hooks'; +import { + cn, +} from '../../lib/utils'; + +enum SETTINGS { + GENERAL, + PROMPTS, +} + +function SettingsOption({ + title, + icon, + onClick, + selected, +}: { + title: string; + icon: IconProp; + onClick: () => void; + selected?: boolean; +}) { + return ( +
+ +

+ {title} +

+
+ ); +} + +function GeneralSettings() { + const { + startListening, + stopListening, + shortcut, + } = useKeyboardShortcut(); + + const [keybind, setKeybind] = React.useState( + typeof window !== 'undefined' ? window.electronAPI.fetchSetting('keybind') : '⌘O', + ); + const [model, setModel] = React.useState( + typeof window !== 'undefined' + ? window.electronAPI.fetchSetting('model') + : 'mlx-community/quantized-gemma-7b-it', + ); + + useEffect(() => { + if (!shortcut) { + return; + } + setKeybind(shortcut); + }, [shortcut]); + + return ( +
+
+

Launch keybind:

+ { + stopListening(); + if (typeof window !== 'undefined') { + window.electronAPI.updateSetting('keybind', shortcut); + } + }} + /> +
+
+

Default model:

+ { + setModel(selectedModel); + if (typeof window !== 'undefined' && selectedModel) { + window.electronAPI.startServer(selectedModel); + window.electronAPI.updateSetting('model', selectedModel); + } + }} + /> +
+
+ ); +} + +function PromptSettings() { + const [instructions, setInstructions] = React.useState( + typeof window !== 'undefined' ? window.electronAPI.fetchSetting('customInstructions') : '', + ); + + return ( +
+
+

Custom Instructions

+