├── src ├── assets │ ├── grid.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── logo.png │ └── grid.svg ├── SearchWindow.html ├── SettingsWindow.html ├── OutputWindow.html ├── OutputWindow.tsx ├── SettingsWindow.tsx ├── custom.d.ts ├── SearchWindow.tsx ├── components │ ├── SettingsRenderer.tsx │ ├── ModelButton.tsx │ ├── UserInput.tsx │ ├── OutputRenderer.tsx │ └── ModelSettings.tsx ├── utils.ts ├── preload.ts ├── common │ └── index.css ├── index.ts └── main │ ├── Downloader.ts │ ├── Models.ts │ ├── WindowManager.ts │ └── IpcHandlers.ts ├── postcss.config.js ├── tailwind.config.js ├── webpack.plugins.ts ├── webpack.renderer.config.ts ├── webpack.main.config.ts ├── tsconfig.json ├── webpack.rules.ts ├── README.md ├── forge.config.ts └── package.json /src/assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achsill/orac-interface/HEAD/src/assets/grid.png -------------------------------------------------------------------------------- /src/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achsill/orac-interface/HEAD/src/assets/icon.icns -------------------------------------------------------------------------------- /src/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achsill/orac-interface/HEAD/src/assets/icon.ico -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achsill/orac-interface/HEAD/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achsill/orac-interface/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /src/SearchWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alexandrie 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/SettingsWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alexandrie 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/OutputWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Output Window 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{html,js,ts,jsx,tsx}"], // Adjust this path as necessary 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /src/OutputWindow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./common/index.css"; //added line 4 | import OutputRenderer from "./components/OutputRenderer"; 5 | 6 | const container = document.getElementById("app"); // Ensure you have a div with id="app" in your index.html 7 | const root = createRoot(container); // Create a root. 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/SettingsWindow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./common/index.css"; //added line 4 | import SettingsWindow from "./components/SettingsRenderer"; 5 | 6 | const container = document.getElementById("app"); // Ensure you have a div with id="app" in your index.html 7 | const root = createRoot(container); // Create a root. 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.jpeg" { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module "*.svg" { 17 | const content: string; 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /webpack.plugins.ts: -------------------------------------------------------------------------------- 1 | import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 5 | 6 | export const plugins = [ 7 | new ForkTsCheckerWebpackPlugin({ 8 | logger: 'webpack-infrastructure', 9 | }), 10 | ]; 11 | -------------------------------------------------------------------------------- /src/SearchWindow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./common/index.css"; //added line 4 | import UserInput from "./components/UserInput"; 5 | 6 | const container = document.getElementById("app"); // Ensure you have a div with id="app" in your index.html 7 | const root = createRoot(container); // Create a root. 8 | root.render( 9 |
10 | 11 |
12 | ); 13 | -------------------------------------------------------------------------------- /webpack.renderer.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from "webpack"; 2 | 3 | import { rules } from "./webpack.rules"; 4 | import { plugins } from "./webpack.plugins"; 5 | 6 | rules.push({ 7 | test: /\.css$/, 8 | use: [ 9 | { loader: "style-loader" }, 10 | { loader: "css-loader" }, 11 | { loader: "postcss-loader" }, 12 | ], 13 | }); 14 | 15 | export const rendererConfig: Configuration = { 16 | module: { 17 | rules, 18 | }, 19 | plugins, 20 | resolve: { 21 | extensions: [".js", ".ts", ".jsx", ".tsx", ".css"], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /webpack.main.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'webpack'; 2 | 3 | import { rules } from './webpack.rules'; 4 | import { plugins } from './webpack.plugins'; 5 | 6 | export const mainConfig: Configuration = { 7 | /** 8 | * This is the main entry point for your application, it's the first file 9 | * that runs in the main process. 10 | */ 11 | entry: './src/index.ts', 12 | // Put your normal webpack config below here 13 | module: { 14 | rules, 15 | }, 16 | plugins, 17 | resolve: { 18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/SettingsRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import ModelSettings from "./ModelSettings"; 3 | 4 | const SettingsWindow: React.FC = () => { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | export default SettingsWindow; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "strict": false, 9 | "noImplicitAny": true, 10 | "sourceMap": true, 11 | "baseUrl": ".", 12 | "outDir": "dist", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "paths": { 16 | "*": ["node_modules/*"] 17 | }, 18 | "jsx": "react-jsx" 19 | }, 20 | 21 | "include": [ 22 | "src/main/**/*", 23 | "src/renderer/**/*", 24 | "src/common/**/*", 25 | "src/settings/**/*", 26 | "src/search/**/*", 27 | "src/output/**/*", 28 | "src/**/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from "os"; 2 | import path from "path"; 3 | const fs = require("fs"); 4 | 5 | const Store = require("electron-store"); 6 | const store = new Store(); 7 | 8 | export const getModelPath = (): string | null => { 9 | const homeDirectory = homedir(); 10 | const baseModelPath = path.join(homeDirectory, ".orac", "models"); 11 | 12 | console.log("heheyy2"); 13 | if (store.get("isUsingCustomModel")) { 14 | const customModelPath = store.get("customModelPath"); 15 | if (customModelPath && fs.existsSync(customModelPath)) { 16 | return customModelPath; 17 | } 18 | return null; 19 | } 20 | 21 | const modelNameMap: { [key: string]: string } = { 22 | capybarahermes: "capybarahermes-2.5-mistral-7b.Q5_K_M.gguf", 23 | openchat: "openchat_3.5.Q6_K.gguf", 24 | }; 25 | 26 | const modelName = store.get("selectedModel"); 27 | if (modelName) { 28 | const modelPath = path.join(baseModelPath, modelNameMap[modelName]); 29 | if (fs.existsSync(modelPath)) { 30 | return modelPath; 31 | } 32 | } 33 | 34 | return null; 35 | }; 36 | -------------------------------------------------------------------------------- /webpack.rules.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleOptions } from "webpack"; 2 | 3 | export const rules: Required["rules"] = [ 4 | // Add support for native node modules 5 | { 6 | // We're specifying native_modules in the test because the asset relocator loader generates a 7 | // "fake" .node file which is really a cjs file. 8 | test: /native_modules[/\\].+\.node$/, 9 | use: "node-loader", 10 | }, 11 | { 12 | test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, 13 | parser: { amd: false }, 14 | use: { 15 | loader: "@vercel/webpack-asset-relocator-loader", 16 | options: { 17 | outputAssetBase: "native_modules", 18 | }, 19 | }, 20 | }, 21 | { 22 | test: /\.(png|jpe?g|gif|svg)$/i, 23 | type: "asset/resource", 24 | }, 25 | { 26 | test: /\.tsx?$/, 27 | exclude: /(node_modules|\.webpack)/, 28 | use: { 29 | loader: "ts-loader", 30 | options: { 31 | transpileOnly: true, 32 | }, 33 | }, 34 | }, 35 | { 36 | test: /\.jsx?$/, 37 | use: { 38 | loader: "babel-loader", 39 | options: { 40 | exclude: /node_modules/, 41 | presets: ["@babel/preset-react"], 42 | }, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/preload.ts: -------------------------------------------------------------------------------- 1 | // In your preload.js 2 | const { contextBridge, ipcRenderer } = require("electron"); 3 | 4 | contextBridge.exposeInMainWorld("api", { 5 | send: (channel: any, data: any) => { 6 | let validChannels = [ 7 | "user-input", 8 | "close-output-window", 9 | "close-input-window", 10 | "extend-input-window", 11 | "settings-button-clicked", 12 | "update-model", 13 | "close-setting-window", 14 | "minimize-search-window", 15 | "download-model", 16 | "switch-model-type", 17 | "select-model", 18 | "stop-download", 19 | "open-file-dialog", 20 | ]; 21 | if (validChannels.includes(channel)) { 22 | ipcRenderer.send(channel, data); 23 | } 24 | }, 25 | receive: (channel: any, func: Function) => { 26 | let validChannels = [ 27 | "ia-output", 28 | "ia-output-end", 29 | "ia-input", 30 | "installer-progression", 31 | "check-ia", 32 | "ia-error", 33 | "init-model-name", 34 | "send-clipboard-content", 35 | "download-data", 36 | "download-completed", 37 | "set-custom-model-path", 38 | "get-custom-model-path", 39 | ]; 40 | if (validChannels.includes(channel)) { 41 | ipcRenderer.on(channel, (event, ...args) => func(...args)); 42 | } 43 | }, 44 | removeListener: (channel: any, func: any) => { 45 | ipcRenderer.removeListener(channel, func); 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/common/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | width: 100%; 11 | background-color: transparent; 12 | } 13 | 14 | body { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | #userInput:focus { 21 | outline: none; 22 | } 23 | 24 | #ollamaOutput { 25 | color: rgba(255, 255, 255, 0.796); 26 | } 27 | 28 | /* width */ 29 | ::-webkit-scrollbar { 30 | width: 2px; 31 | height: 2px; 32 | max-height: 2px; 33 | max-width: 2px; 34 | } 35 | 36 | /* Track */ 37 | ::-webkit-scrollbar-track { 38 | background: #000000e6; 39 | } 40 | 41 | /* Handle */ 42 | ::-webkit-scrollbar-thumb { 43 | background: rgb(99 102 241); 44 | border-radius: 20px; 45 | } 46 | 47 | #header { 48 | -webkit-user-select: none; 49 | -webkit-app-region: drag; 50 | } 51 | 52 | form, 53 | input { 54 | -webkit-app-region: no-drag; 55 | } 56 | 57 | #userInput:focus { 58 | outline: none; 59 | } 60 | 61 | #ollamaOutput { 62 | color: white; 63 | overflow-y: auto; 64 | } 65 | 66 | #buttonSettings { 67 | -webkit-app-region: no-drag; 68 | } 69 | 70 | #head { 71 | -webkit-user-select: none; 72 | -webkit-app-region: drag; 73 | } 74 | 75 | input[type="file"]::file-selector-button { 76 | margin-right: 20px; 77 | border: none; 78 | background: plum; 79 | padding: 10px 20px; 80 | border-radius: 10px; 81 | color: #fff; 82 | cursor: pointer; 83 | transition: background 0.2s ease-in-out; 84 | } 85 | 86 | input[type="file"]::file-selector-button:hover { 87 | background: rgb(187, 72, 187); 88 | } 89 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, globalShortcut, clipboard } from "electron"; 2 | import { windowManager } from "./main/WindowManager"; 3 | import { setupIpcHandlers, sendClipboardContent } from "./main/IpcHandlers"; 4 | import { modelInit } from "./main/Models"; 5 | import { updateElectronApp } from "update-electron-app"; 6 | 7 | function registerGlobalShortcuts() { 8 | globalShortcut.register("Ctrl+Space", () => { 9 | if (!windowManager.searchWindow) { 10 | windowManager.createSearchWindow(); 11 | } else { 12 | windowManager.searchWindow.show(); 13 | } 14 | }); 15 | 16 | globalShortcut.register("Ctrl+Option+Space", () => { 17 | const clipboardContent = clipboard.readText("selection"); 18 | if (!windowManager.searchWindow) { 19 | windowManager.createSearchWindow(); 20 | sendClipboardContent(clipboardContent); 21 | } else { 22 | windowManager.searchWindow.show(); 23 | sendClipboardContent(clipboardContent); 24 | } 25 | }); 26 | } 27 | 28 | function setupAppLifecycle() { 29 | app.on("ready", async () => { 30 | windowManager.createSearchWindow(); 31 | setupIpcHandlers(); 32 | registerGlobalShortcuts(); 33 | modelInit(); 34 | }); 35 | 36 | app.on("will-quit", () => { 37 | globalShortcut.unregisterAll(); 38 | }); 39 | 40 | app.on("window-all-closed", () => { 41 | if (process.platform !== "darwin") { 42 | app.quit(); 43 | } 44 | }); 45 | 46 | app.on("activate", () => { 47 | if (BrowserWindow.getAllWindows().length === 0) { 48 | windowManager.createSearchWindow(); 49 | } 50 | }); 51 | } 52 | 53 | updateElectronApp(); 54 | setupAppLifecycle(); 55 | -------------------------------------------------------------------------------- /src/components/ModelButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // Assuming you're using Heroicons for the download icon 3 | import { ArrowDownOnSquareIcon } from "@heroicons/react/24/outline"; // Adjust the import path as necessary 4 | 5 | interface ModelDownloadButtonProps { 6 | modelName: string; 7 | isDownloaded: boolean; 8 | isRecommanded: boolean; 9 | isSelected: boolean; 10 | modelRef: string; 11 | onSelectModel: (modelIdentifier: string) => void; // Function to handle model selection 12 | downloadModel: (modelIdentifier: string) => void; 13 | } 14 | 15 | const ModelDownloadButton: React.FC = ({ 16 | modelName, 17 | isDownloaded, 18 | isSelected, 19 | modelRef, 20 | isRecommanded, 21 | onSelectModel, 22 | downloadModel, 23 | }) => { 24 | return ( 25 |
26 |
27 |

{modelName}

28 | {isDownloaded ? ( 29 | <> 30 | onSelectModel(modelName)} 35 | className="cursor-pointer" 36 | /> 37 | 38 | ) : ( 39 | 42 | )} 43 |
44 |
45 | {isRecommanded ? ( 46 |

Recommanded

47 | ) : ( 48 | "" 49 | )} 50 |

{modelRef}

51 |
52 |
53 | ); 54 | }; 55 | 56 | export default ModelDownloadButton; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orac Interface 2 | 3 | ## About The Project 4 | ![Orac Interface](https://i.ibb.co/GRYs9sL/orac-demo-pic.png) 5 | Easily chat with your local LLM. The Orac Interface is seamlessly integrated into your workflow; just press Ctrl+Space to start a conversation with your AI. Asking questions to your AI won't interrupt your workflow anymore. 6 | 7 | a demo is available on the [website](https://dub.sh/8srNEsu). 8 | ## How To Use 9 | 10 | Orac-Interface is currently compatible with macOS and utilizes Ollama for its operation. Follow the steps below to get started: 11 | 12 | 1. **Download Ollama:** Visit [https://ollama.com/](https://ollama.com/) to download the Ollama software. 13 | 2. **Install a Local LLM:** Use the command `pull model_name` in your terminal to install the local LLM you wish to use. 14 | 3. **Verify Ollama is Running:** Ensure that the Ollama application is running correctly. You should see the Ollama logo displayed in your top bar. 15 | 4. **Download Orac-Interface:** [Download the .dmg](https://dub.sh/OlyKTY8) 16 | 5. **Launch Orac-Interface:** After installation, launch the software and simply press `Ctrl+Space` to open Orac-Interface from anywhere on your system. 17 | 18 | 19 | ## Shortcuts 20 | 1. **Ctrl+Space**: Open the input 21 | 2. **Esc**: Hide the input 22 | 3. **Shift+Enter**: New line 23 | 24 | ## Testing And Building The Project 25 | - **Test**: npm run start 26 | - **Build**: npm run make 27 | 28 | ## What's Next 29 | 30 | - [ ] Linux integration for broader OS support. 31 | - [ ] Multimodal language support, allowing the interface to process images. 32 | - [ ] Eliminate the dependency on Ollama to streamline the installation process. 33 | - [ ] Windows integration to cater to a wider audience. 34 | - [ ] Feature to save LLM responses, enabling easy retrieval for future reference. 35 | - [ ] Allow shortcut modification 36 | 37 | ## Links 38 | [Website](orac-interface.vercel.app)\ 39 | [Discord](https://dub.sh/WkWCKNd) 40 | 41 | Contributions, feedback, and suggestions are more than welcomed.\ 42 | You can also reach out to me on [twitter](https://x.com/achsill) if you have any question. 43 | -------------------------------------------------------------------------------- /src/assets/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 43 | 44 | 45 | 49 | 50 | 51 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /forge.config.ts: -------------------------------------------------------------------------------- 1 | import type { ForgeConfig } from "@electron-forge/shared-types"; 2 | import { MakerSquirrel } from "@electron-forge/maker-squirrel"; 3 | import { MakerZIP } from "@electron-forge/maker-zip"; 4 | import { MakerDeb } from "@electron-forge/maker-deb"; 5 | import { MakerRpm } from "@electron-forge/maker-rpm"; 6 | import { MakerDMG } from "@electron-forge/maker-dmg"; 7 | import { AutoUnpackNativesPlugin } from "@electron-forge/plugin-auto-unpack-natives"; 8 | import { WebpackPlugin } from "@electron-forge/plugin-webpack"; 9 | 10 | import { mainConfig } from "./webpack.main.config"; 11 | import { rendererConfig } from "./webpack.renderer.config"; 12 | 13 | const config: ForgeConfig = { 14 | packagerConfig: { 15 | asar: true, 16 | icon: "./src/assets/icon", 17 | osxSign: {}, 18 | osxNotarize: { 19 | appleId: process.env.APP_ID ?? "", 20 | appleIdPassword: process.env.APP_PWD ?? "", 21 | teamId: process.env.APP_TEAM_ID ?? "", 22 | }, 23 | }, 24 | rebuildConfig: {}, 25 | makers: [ 26 | // new MakerSquirrel({}), 27 | // new MakerZIP({}, ["darwin", "linux"]), 28 | // new MakerRpm({}), 29 | // new MakerDeb({}), 30 | new MakerDMG({}), 31 | ], 32 | publishers: [ 33 | { 34 | name: "@electron-forge/publisher-github", 35 | config: { 36 | repository: { 37 | owner: "hlouar", 38 | name: "orac-interface", 39 | }, 40 | 41 | prerelease: true, 42 | }, 43 | }, 44 | ], 45 | plugins: [ 46 | new AutoUnpackNativesPlugin({}), 47 | new WebpackPlugin({ 48 | mainConfig, 49 | renderer: { 50 | config: rendererConfig, 51 | entryPoints: [ 52 | { 53 | html: "./src/SearchWindow.html", 54 | js: "./src/SearchWindow.tsx", 55 | name: "main_window", 56 | preload: { 57 | js: "./src/preload.ts", 58 | }, 59 | }, 60 | { 61 | html: "./src/OutputWindow.html", 62 | js: "./src/OutputWindow.tsx", 63 | name: "output_window", 64 | preload: { 65 | js: "./src/preload.ts", 66 | }, 67 | }, 68 | { 69 | html: "./src/SettingsWindow.html", 70 | js: "./src/SettingsWindow.tsx", 71 | name: "settings_window", 72 | preload: { 73 | js: "./src/preload.ts", 74 | }, 75 | }, 76 | ], 77 | }, 78 | }), 79 | ], 80 | }; 81 | 82 | export default config; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Orac Interface", 3 | "productName": "Orac Interface", 4 | "version": "0.0.2", 5 | "description": "LLM interface", 6 | "main": ".webpack/main", 7 | "scripts": { 8 | "start": "set NODE_ENV=development&&electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make --universal", 11 | "publish": "electron-forge publish", 12 | "lint": "eslint --ext .ts,.tsx ." 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/achsill/orac-interface" 17 | }, 18 | "keywords": [], 19 | "author": "achsill", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/core": "^7.23.9", 23 | "@babel/preset-react": "^7.23.3", 24 | "@electron-forge/cli": "^7.2.0", 25 | "@electron-forge/maker-deb": "^7.2.0", 26 | "@electron-forge/maker-rpm": "^7.2.0", 27 | "@electron-forge/maker-squirrel": "^7.2.0", 28 | "@electron-forge/maker-zip": "^7.2.0", 29 | "@electron-forge/plugin-auto-unpack-natives": "^7.2.0", 30 | "@electron-forge/plugin-webpack": "^7.2.0", 31 | "@types/progress-stream": "^2.0.5", 32 | "@types/react-dom": "^18.2.19", 33 | "@types/react-highlight": "^0.12.8", 34 | "@typescript-eslint/eslint-plugin": "^5.0.0", 35 | "@typescript-eslint/parser": "^5.0.0", 36 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 37 | "autoprefixer": "^10.4.17", 38 | "babel-loader": "^9.1.3", 39 | "css-loader": "^6.10.0", 40 | "electron": "28.2.1", 41 | "eslint": "^8.0.1", 42 | "eslint-plugin-import": "^2.25.0", 43 | "fork-ts-checker-webpack-plugin": "^7.2.13", 44 | "node-loader": "^2.0.0", 45 | "postcss": "^8.4.33", 46 | "postcss-loader": "^8.1.0", 47 | "postcss-preset-env": "^9.3.0", 48 | "style-loader": "^3.3.4", 49 | "tailwindcss": "^3.4.1", 50 | "ts-loader": "^9.2.2", 51 | "ts-node": "^10.0.0", 52 | "webpack": "^5.90.1", 53 | "webpack-cli": "^5.1.4", 54 | "webpack-dev-server": "^4.15.1" 55 | }, 56 | "dependencies": { 57 | "@electron-forge/maker-dmg": "^7.3.0", 58 | "@electron-forge/publisher-github": "^7.3.0", 59 | "@heroicons/react": "^2.1.1", 60 | "electron-reload": "^2.0.0-alpha.1", 61 | "electron-reloader": "^1.2.3", 62 | "electron-squirrel-startup": "^1.0.0", 63 | "electron-store": "^8.1.0", 64 | "format-duration": "^3.0.2", 65 | "highlight.js": "^11.9.0", 66 | "node-llama-cpp": "^2.8.8", 67 | "ollama": "^0.4.4", 68 | "progress-stream": "^2.0.0", 69 | "react": "^18.2.0", 70 | "react-code-blocks": "^0.1.6", 71 | "react-dom": "^18.2.0", 72 | "react-highlight": "^0.15.0", 73 | "react-markdown": "^9.0.1", 74 | "update-electron-app": "^3.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/Downloader.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import fs from "fs"; 3 | import progress from "progress-stream"; 4 | import { ipcMain } from "electron"; 5 | 6 | class Downloader { 7 | private static instance: Downloader; 8 | private progressStream: progress.ProgressStream | null = null; 9 | private outputPath: string = ""; 10 | private modelName: string; 11 | 12 | private constructor(private windowManager: any) {} 13 | 14 | public static getInstance(windowManager: any): Downloader { 15 | if (!Downloader.instance) { 16 | Downloader.instance = new Downloader(windowManager); 17 | } 18 | return Downloader.instance; 19 | } 20 | 21 | async downloadFile(url: string, outputPath: string, modelName: string) { 22 | this.outputPath = outputPath; 23 | this.modelName = modelName; 24 | const { headers } = await axios.head(url); 25 | const totalSize = headers["content-length"]; 26 | 27 | this.progressStream = progress({ 28 | length: totalSize, 29 | time: 100, 30 | }); 31 | 32 | this.progressStream.on("progress", (progressData: any) => { 33 | this.windowManager.settingsWindow?.webContents.send( 34 | "download-data", 35 | progressData 36 | ); 37 | console.log( 38 | `Downloaded ${progressData.transferred} out of ${ 39 | progressData.length 40 | } bytes (${progressData.percentage.toFixed(2)}%)` 41 | ); 42 | console.log(`Speed: ${progressData.speed} bytes/s`); 43 | console.log(`Remaining: ${progressData.eta} seconds`); 44 | }); 45 | 46 | const response = await axios({ 47 | url, 48 | method: "GET", 49 | responseType: "stream", 50 | }); 51 | 52 | response.data 53 | .pipe(this.progressStream) 54 | .pipe(fs.createWriteStream(outputPath)) 55 | .on("finish", () => { 56 | console.log("Download completed."); 57 | this.progressStream = null; 58 | this.windowManager.settingsWindow?.webContents.send( 59 | "download-completed", 60 | this.modelName 61 | ); 62 | }) 63 | .on("error", (err: any) => console.log("Download error:", err.message)) 64 | .on("close", () => { 65 | console.log("Stream closed.", "Download was interrupted."); 66 | }); 67 | 68 | ipcMain.on("stop-download", async () => { 69 | this.stopDownload(); 70 | }); 71 | } 72 | 73 | stopDownload() { 74 | if (this.progressStream) { 75 | this.progressStream.destroy(); 76 | console.log("Download stopped"); 77 | this.cleanupFile(this.outputPath); 78 | } 79 | } 80 | 81 | private cleanupFile(filePath: string) { 82 | fs.unlink(filePath, (err) => { 83 | if (err) console.error("Error deleting file", err); 84 | else console.log(`Successfully deleted file: ${filePath}`); 85 | }); 86 | } 87 | } 88 | 89 | export default Downloader; 90 | -------------------------------------------------------------------------------- /src/main/Models.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { homedir } from "os"; 4 | import os from "os"; 5 | import Downloader from "./Downloader"; 6 | import { ipcMain } from "electron"; 7 | import { windowManager } from "./WindowManager"; 8 | import nodeLlamaCpp, { LlamaChatSession, LlamaContext } from "node-llama-cpp"; 9 | import { getModelPath } from "../utils"; 10 | import { sendMessageToOutputWindow } from "./IpcHandlers"; 11 | 12 | export let session: LlamaChatSession | null = null; 13 | export let context: LlamaContext | null = null; 14 | 15 | export const modelInit = async () => { 16 | const { LlamaModel, LlamaContext, LlamaChatSession } = await nodeLlamaCpp; 17 | 18 | try { 19 | const modelFilePath = getModelPath(); 20 | if (!modelFilePath) { 21 | sendMessageToOutputWindow( 22 | "ia-output", 23 | "Before you can interact with the AI, you need to first select a model in the settings." 24 | ); 25 | return; 26 | } 27 | const model = new LlamaModel({ modelPath: modelFilePath }); 28 | context = new LlamaContext({ model }); 29 | session = new LlamaChatSession({ context }); 30 | } catch (error) { 31 | console.error(error); 32 | } 33 | }; 34 | 35 | export async function findDownloadedModels() { 36 | const homeDirectory = homedir(); 37 | const modelsPath = path.join(homeDirectory, ".orac", "models"); 38 | 39 | if (!fs.existsSync(modelsPath)) { 40 | console.log("Models directory does not exist."); 41 | return { 42 | openchat: false, 43 | capybarahermes: false, 44 | }; 45 | } 46 | 47 | try { 48 | const files = await fs.readdirSync(modelsPath); 49 | return { 50 | openchat: files.includes("openchat_3.5.Q6_K.gguf"), 51 | capybarahermes: files.includes( 52 | "capybarahermes-2.5-mistral-7b.Q5_K_M.gguf" 53 | ), 54 | }; 55 | } catch (error) { 56 | console.error("Error reading the folder:", error); 57 | throw error; 58 | } 59 | } 60 | 61 | export function calculateRecommandedModel() { 62 | const totalMemBytes = os.totalmem(); 63 | const totalMemGB = totalMemBytes / Math.pow(1024, 3); 64 | const formattedMemGB = parseInt(totalMemGB.toFixed(2)); 65 | 66 | if (formattedMemGB <= 16) { 67 | return "capybarahermes"; 68 | } else if (formattedMemGB > 16) { 69 | return "openchat"; 70 | } 71 | } 72 | 73 | ipcMain.on("download-model", async (event: any, modelName: string) => { 74 | let fileUrl: string; 75 | let fileName: string; 76 | if (modelName === "openchat") { 77 | fileUrl = 78 | "https://huggingface.co/TheBloke/openchat_3.5-GGUF/resolve/main/openchat_3.5.Q6_K.gguf"; 79 | fileName = "openchat_3.5.Q6_K.gguf"; 80 | } else if (modelName === "capybarahermes") { 81 | fileUrl = 82 | "https://huggingface.co/TheBloke/CapybaraHermes-2.5-Mistral-7B-GGUF/resolve/main/capybarahermes-2.5-mistral-7b.Q5_K_M.gguf"; 83 | fileName = "capybarahermes-2.5-mistral-7b.Q5_K_M.gguf"; 84 | } 85 | 86 | const homeDirectory = homedir(); 87 | const modelsDirectory = path.join(homeDirectory, ".orac", "models"); 88 | 89 | // Check if the `.orac/models` directory exists, create it if not 90 | if (!fs.existsSync(modelsDirectory)) { 91 | fs.mkdirSync(modelsDirectory, { recursive: true }); 92 | } 93 | 94 | const downloader = Downloader.getInstance(windowManager); 95 | downloader.downloadFile( 96 | fileUrl, 97 | path.join(modelsDirectory, fileName), 98 | modelName 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /src/main/WindowManager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, screen } from "electron"; 2 | import Downloader from "./Downloader"; 3 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string; 4 | declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; 5 | declare const OUTPUT_WINDOW_WEBPACK_ENTRY: string; 6 | declare const SETTINGS_WINDOW_WEBPACK_ENTRY: string; 7 | 8 | class WindowManager { 9 | searchWindow: BrowserWindow | null = null; 10 | outputWindow: BrowserWindow | null = null; 11 | settingsWindow: BrowserWindow | null = null; 12 | 13 | private createWindow( 14 | options: Electron.BrowserWindowConstructorOptions, 15 | url: string, 16 | windowProperty: keyof WindowManager 17 | ) { 18 | let window = new BrowserWindow(options); 19 | window.loadURL(url); 20 | window.on("closed", () => { 21 | if (this[windowProperty] === this.settingsWindow) { 22 | Downloader.getInstance(this).stopDownload(); 23 | } 24 | this[windowProperty] = null; 25 | }); 26 | return window; 27 | } 28 | 29 | createSearchWindow() { 30 | this.searchWindow = this.createWindow( 31 | { 32 | width: 560, 33 | height: 60, 34 | transparent: true, 35 | frame: false, 36 | type: "panel", 37 | resizable: true, 38 | webPreferences: { 39 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 40 | contextIsolation: true, 41 | nodeIntegration: false, 42 | }, 43 | }, 44 | MAIN_WINDOW_WEBPACK_ENTRY, 45 | "searchWindow" 46 | ); 47 | } 48 | 49 | createOutputWindow() { 50 | const { width, height } = screen.getPrimaryDisplay().workAreaSize; 51 | this.outputWindow = this.createWindow( 52 | { 53 | width: 420, 54 | height: 420, 55 | x: width - 432, 56 | y: 0, 57 | type: "panel", 58 | frame: false, 59 | alwaysOnTop: true, 60 | transparent: true, 61 | resizable: true, 62 | movable: true, 63 | webPreferences: { 64 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 65 | contextIsolation: true, 66 | nodeIntegration: false, 67 | }, 68 | }, 69 | OUTPUT_WINDOW_WEBPACK_ENTRY, 70 | "outputWindow" 71 | ); 72 | } 73 | 74 | createSettingsWindow() { 75 | this.settingsWindow = this.createWindow( 76 | { 77 | width: 620, 78 | height: 480, 79 | titleBarStyle: "hidden", 80 | movable: true, 81 | resizable: false, 82 | webPreferences: { 83 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 84 | contextIsolation: true, 85 | nodeIntegration: false, 86 | }, 87 | }, 88 | SETTINGS_WINDOW_WEBPACK_ENTRY, 89 | "settingsWindow" 90 | ); 91 | } 92 | 93 | closeSearchWindow() { 94 | if (this.searchWindow) { 95 | this.searchWindow.close(); 96 | this.searchWindow = null; 97 | } 98 | } 99 | 100 | minimizeSearchWindow() { 101 | if (this.searchWindow) { 102 | this.searchWindow.minimize(); 103 | } 104 | } 105 | 106 | closeOutputWindow() { 107 | if (this.outputWindow) { 108 | this.outputWindow.close(); 109 | this.outputWindow = null; 110 | } 111 | } 112 | 113 | closeSettingsWindow() { 114 | if (this.settingsWindow) { 115 | this.settingsWindow.close(); 116 | this.settingsWindow = null; 117 | } 118 | } 119 | } 120 | 121 | export const windowManager = new WindowManager(); 122 | -------------------------------------------------------------------------------- /src/components/UserInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useRef, 4 | useEffect, 5 | TextareaHTMLAttributes, 6 | } from "react"; 7 | import grid from "../assets/grid.svg"; 8 | 9 | interface InputParams { 10 | isOriginExtanded?: boolean; 11 | } 12 | 13 | function UserInput({ isOriginExtanded }: InputParams) { 14 | const [inputValue, setInputValue] = useState(""); 15 | const inputRef = useRef(null); 16 | const formRef = useRef(null); 17 | const [isinputExpanded, setIsInputExpanded] = useState(isOriginExtanded); 18 | 19 | const expandWindow = () => { 20 | setIsInputExpanded(true); 21 | window.api.send("extend-input-window"); 22 | }; 23 | 24 | useEffect(() => { 25 | if (inputRef.current) { 26 | inputRef.current.focus(); 27 | } 28 | 29 | const handleKeyDown = (event: any) => { 30 | if (event.key === "Escape") { 31 | window.api.send("minimize-search-window", inputValue); 32 | } 33 | if (event.shiftKey && event.key === "Enter") { 34 | event.preventDefault(); 35 | expandWindow(); 36 | } 37 | }; 38 | 39 | const handleClickOutside = (event: any) => { 40 | if (formRef.current && !formRef.current.contains(event.target)) { 41 | window.api.send("minimize-search-window", inputValue); 42 | } 43 | }; 44 | 45 | const handleClipboardPaste = (data: string) => { 46 | setInputValue(data + "\n"); 47 | expandWindow(); 48 | }; 49 | 50 | document.addEventListener("mousedown", handleClickOutside); 51 | document.addEventListener("keydown", handleKeyDown); 52 | window.api.receive("send-clipboard-content", handleClipboardPaste); 53 | return () => { 54 | document.removeEventListener("keydown", handleKeyDown); 55 | window.api.removeListener("send-clipboard-content", handleClipboardPaste); 56 | document.removeEventListener("mousedown", handleClickOutside); 57 | }; 58 | }, []); 59 | 60 | const handleChange = ( 61 | event: 62 | | React.ChangeEvent 63 | | React.ChangeEvent 64 | ) => { 65 | const { value } = event.target; 66 | setInputValue(value); 67 | }; 68 | 69 | const handlePaste = (event: React.ClipboardEvent) => { 70 | const paste = event.clipboardData.getData("text"); 71 | setInputValue(paste); 72 | if (paste.includes("\n")) { 73 | expandWindow(); 74 | } 75 | }; 76 | 77 | const handleTextAreaKeyDown = (event: any) => { 78 | if (event.key === "Enter" && !event.shiftKey) { 79 | event.preventDefault(); 80 | handleSubmit(event); 81 | } else if (event.key === "Enter" && event.shiftKey) { 82 | setInputValue(inputValue + "\n"); 83 | } 84 | }; 85 | 86 | const handleSubmit = (e: any) => { 87 | e.preventDefault(); 88 | if (inputValue.trim() !== "") { 89 | window.api.send("user-input", inputValue); 90 | setInputValue(""); 91 | } 92 | }; 93 | 94 | return ( 95 |
100 | {isinputExpanded ? ( 101 |
102 |
103 | Logo 104 |
105 | 112 |
113 | 114 |
115 |
116 | ) : ( 117 |
118 | Logo 119 | 128 | 131 |
132 | )} 133 |
134 | ); 135 | } 136 | 137 | export default UserInput; 138 | -------------------------------------------------------------------------------- /src/main/IpcHandlers.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, dialog } from "electron"; 2 | import { windowManager } from "./WindowManager"; 3 | import { 4 | calculateRecommandedModel, 5 | context, 6 | findDownloadedModels, 7 | modelInit, 8 | session, 9 | } from "./Models"; 10 | import { Token } from "node-llama-cpp"; 11 | 12 | const Store = require("electron-store"); 13 | const store = new Store(); 14 | 15 | export const sendMessageToOutputWindow = ( 16 | messageType: string, 17 | messageContent: string 18 | ) => { 19 | windowManager.outputWindow?.webContents.send(messageType, messageContent); 20 | }; 21 | 22 | const sendMessages = async (input: string) => { 23 | if (!session) { 24 | sendMessageToOutputWindow("ia-input", input); 25 | sendMessageToOutputWindow( 26 | "ia-output", 27 | "Before you can interact with the AI, you need to first select a model in the settings." 28 | ); 29 | return; 30 | } 31 | 32 | try { 33 | sendMessageToOutputWindow("ia-input", input); 34 | session.prompt(input, { 35 | onToken: (chunk: Token[]) => { 36 | const decoded = context?.decode(chunk); 37 | if (decoded) sendMessageToOutputWindow("ia-output", decoded); 38 | }, 39 | }); 40 | } catch (error) { 41 | console.error(error); 42 | } 43 | }; 44 | 45 | const handleUserInput = async (input: string) => { 46 | if (windowManager?.searchWindow) { 47 | windowManager.minimizeSearchWindow(); 48 | } 49 | if (!windowManager.outputWindow) { 50 | windowManager.createOutputWindow(); 51 | windowManager.outputWindow.webContents.once("dom-ready", () => { 52 | sendMessages(input); 53 | }); 54 | } else { 55 | sendMessages(input); 56 | } 57 | }; 58 | 59 | export const sendClipboardContent = async (clipboardContent: string) => { 60 | if (windowManager.outputWindow) { 61 | windowManager.outputWindow?.webContents.send( 62 | "send-clipboard-content", 63 | clipboardContent 64 | ); 65 | } else { 66 | if (!windowManager.searchWindow) { 67 | windowManager.searchWindow.webContents.once("dom-ready", () => { 68 | windowManager.searchWindow?.webContents.send( 69 | "send-clipboard-content", 70 | clipboardContent 71 | ); 72 | }); 73 | } else { 74 | windowManager.searchWindow.on("show", () => { 75 | windowManager.searchWindow?.webContents.send( 76 | "send-clipboard-content", 77 | clipboardContent 78 | ); 79 | }); 80 | } 81 | } 82 | }; 83 | 84 | export function setupIpcHandlers() { 85 | ipcMain.on("user-input", async (event, input: string) => { 86 | await handleUserInput(input); 87 | }); 88 | 89 | ipcMain.on("settings-button-clicked", async (event, input: string) => { 90 | windowManager.createSettingsWindow(); 91 | windowManager.settingsWindow.webContents.once("dom-ready", async () => { 92 | const modelName = store.get("modelName"); 93 | const selectedModel = store.get("selectedModel"); 94 | const isUsingCustomModel = store.get("isUsingCustomModel"); 95 | const customModelPath = store.get("customModelPath"); 96 | const downloadedModels = await findDownloadedModels(); 97 | const recommandedModel = calculateRecommandedModel(); 98 | windowManager.settingsWindow?.webContents.send("init-model-name", { 99 | isUsingCustomModel, 100 | modelName, 101 | downloadedModels, 102 | selectedModel, 103 | recommandedModel, 104 | customModelPath, 105 | }); 106 | }); 107 | windowManager.settingsWindow.focus(); 108 | }); 109 | 110 | ipcMain.on("update-model", async (event, input: string) => { 111 | try { 112 | store.set("modelName", input); 113 | } catch (e) { 114 | console.log(e); 115 | } 116 | }); 117 | 118 | ipcMain.on("extend-input-window", async () => { 119 | windowManager.searchWindow.setSize(560, 420, true); 120 | }); 121 | 122 | ipcMain.on("close-output-window", async () => { 123 | windowManager.closeOutputWindow(); 124 | }); 125 | 126 | ipcMain.on("close-setting-window", async () => { 127 | windowManager.closeSettingsWindow(); 128 | }); 129 | 130 | ipcMain.on("minimize-search-window", async () => { 131 | windowManager.minimizeSearchWindow(); 132 | }); 133 | 134 | ipcMain.on("select-model", async (event: any, modelName: string) => { 135 | store.set("selectedModel", modelName); 136 | modelInit(); 137 | }); 138 | 139 | ipcMain.on( 140 | "switch-model-type", 141 | async (event: any, isUsingCustomModel: boolean) => { 142 | store.set("isUsingCustomModel", isUsingCustomModel); 143 | modelInit(); 144 | } 145 | ); 146 | 147 | ipcMain.on("close-input-window", async () => { 148 | windowManager.closeSearchWindow(); 149 | }); 150 | } 151 | 152 | ipcMain.on("set-custom-model-path", async (event, modelPath: string) => { 153 | try { 154 | store.set("customModelPath", modelPath); 155 | } catch (e) { 156 | console.log(e); 157 | } 158 | }); 159 | 160 | ipcMain.on("open-file-dialog", (event) => { 161 | dialog 162 | .showOpenDialog({ 163 | properties: ["openFile"], 164 | filters: [{ name: "GGUF Files", extensions: ["gguf"] }], 165 | }) 166 | .then((result) => { 167 | if (!result.canceled && result.filePaths.length > 0) { 168 | store.set("customModelPath", result.filePaths[0]); 169 | modelInit(); 170 | windowManager.settingsWindow?.webContents.send( 171 | "get-custom-model-path", 172 | result.filePaths[0] 173 | ); 174 | } 175 | }) 176 | .catch((err) => { 177 | console.log(err); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/components/OutputRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import gridIconFixed from "../assets/grid.png"; // Adjust the path as necessary 4 | import { CopyBlock, irBlack } from "react-code-blocks"; 5 | import UserInput from "./UserInput"; 6 | import hljs from "highlight.js"; 7 | 8 | interface Segment { 9 | text: string; 10 | isCode: boolean; 11 | language: string; 12 | } 13 | 14 | function OutputRenderer() { 15 | // Use an array to store blocks of messages 16 | const [messages, setMessages] = useState([]); 17 | const [inputValue, setInputValue] = useState(""); 18 | const scrollableContentRef = useRef(null); 19 | const [icon, setIcon] = useState(gridIconFixed); 20 | 21 | const parseMessage = (text: string) => { 22 | const segments = []; 23 | const regex = /```(.*?)```/gs; 24 | 25 | let lastIndex = 0; 26 | text.replace(regex, (match: string, ...args: any[]): string => { 27 | if (args[1] > lastIndex) { 28 | segments.push({ text: text.slice(lastIndex, args[1]), isCode: false }); 29 | } 30 | const detectedLanguage = hljs.highlightAuto(args[0]).language; 31 | segments.push({ 32 | text: args[0], 33 | isCode: true, 34 | language: detectedLanguage || "plaintext", 35 | }); 36 | lastIndex = args[1] + match.length; 37 | return "hehe"; 38 | }); 39 | 40 | if (lastIndex < text.length) { 41 | segments.push({ text: text.slice(lastIndex), isCode: false }); 42 | } 43 | 44 | return segments; 45 | }; 46 | 47 | const openSettingsWindow = () => { 48 | window.api.send("settings-button-clicked"); 49 | }; 50 | 51 | const closeWindow = () => { 52 | window.api.send("close-output-window", inputValue); 53 | }; 54 | 55 | useEffect(() => { 56 | if (scrollableContentRef.current) { 57 | scrollableContentRef.current.scrollTop = 58 | scrollableContentRef.current.scrollHeight; 59 | } 60 | }, [messages]); 61 | 62 | useEffect(() => { 63 | const messageListener = (data: string) => { 64 | setMessages((prevMessages) => { 65 | if ( 66 | prevMessages.length > 0 && 67 | prevMessages[prevMessages.length - 1].type === "output" 68 | ) { 69 | const updatedMessages = [...prevMessages]; 70 | updatedMessages[updatedMessages.length - 1] = { 71 | ...updatedMessages[updatedMessages.length - 1], 72 | text: updatedMessages[updatedMessages.length - 1].text + data, 73 | }; 74 | return updatedMessages; 75 | } else { 76 | return [...prevMessages, { text: data, type: "output" }]; 77 | } 78 | }); 79 | }; 80 | 81 | const messageEnd = () => { 82 | setMessages((prevMessages) => [ 83 | ...prevMessages, 84 | { text: "", type: "output" }, // Adjust according to your actual structure 85 | ]); 86 | setIcon(gridIconFixed); 87 | }; 88 | 89 | const inputListener = (data: string) => { 90 | setMessages((prevMessages) => [ 91 | ...prevMessages, 92 | { text: data, type: "input" }, // Specify message type as "input" 93 | ]); 94 | }; 95 | 96 | const displayError = (data: string) => { 97 | setMessages((prevMessages) => { 98 | return [...prevMessages, { text: data, type: "output" }]; 99 | }); 100 | }; 101 | 102 | window.api.receive("ia-error", displayError); 103 | window.api.receive("ia-output", messageListener); 104 | window.api.receive("ia-output-end", messageEnd); 105 | window.api.receive("ia-input", inputListener); 106 | 107 | return () => { 108 | window.api.removeListener("ia-output", messageListener); 109 | window.api.removeListener("ia-output-end", messageEnd); 110 | window.api.removeListener("ia-input", inputListener); 111 | }; 112 | }, []); 113 | 114 | return ( 115 |
116 |
117 | 124 | 125 | 132 |
133 |
137 | {messages.map( 138 | (message, index) => 139 | message.text.trim() !== "" && ( 140 |
146 | {parseMessage(message.text).map( 147 | (segment: Segment, segmentIndex) => 148 | segment.isCode ? ( 149 | 150 | 156 | 157 | ) : ( 158 | 159 | {segment.text} 160 | 161 | ) 162 | )} 163 |
164 | ) 165 | )} 166 |
167 | 168 |
169 | 170 |
171 |
172 | ); 173 | } 174 | 175 | export default OutputRenderer; 176 | -------------------------------------------------------------------------------- /src/components/ModelSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import logo from "../assets/logo.png"; // Adjust the path as necessary 3 | import ModelDownloadButton from "./ModelButton"; 4 | import { CircleStackIcon, StopCircleIcon } from "@heroicons/react/24/outline"; // Adjust the import path as necessary 5 | 6 | interface DownloadInformation { 7 | progress: number; 8 | remainingTime: string; 9 | speed: number; 10 | } 11 | 12 | declare global { 13 | interface Window { 14 | api: { 15 | send: (channel: string, data?: any) => void; 16 | receive: (channel: string, func: (data: any) => void) => void; 17 | removeListener: (channel: string, func?: (data: any) => void) => void; 18 | }; 19 | } 20 | } 21 | 22 | const ModelSettings: React.FC = () => { 23 | const [downloadInformations, setDownloadInformations] = 24 | useState({ 25 | progress: 0, 26 | remainingTime: "0s", 27 | speed: 0, 28 | }); 29 | const [downloadingModelName, setDownloadingModelName] = useState(""); 30 | const [isUsingCustomModel, setIsUsingCustomModel] = useState(false); 31 | const [modelName, setModelName] = useState(""); 32 | const [models, setModels] = useState({ 33 | capybarahermes: { 34 | isDownloaded: false, 35 | isSelected: false, 36 | isRecommanded: false, 37 | }, 38 | openchat: { 39 | isDownloaded: false, 40 | isSelected: false, 41 | isRecommanded: false, 42 | }, 43 | }); 44 | const [isDownloading, setIsDownloading] = useState(false); 45 | const [customModelPath, setCustomModelPath] = useState(""); 46 | 47 | const selectFile = () => { 48 | window.api.send("open-file-dialog"); 49 | }; 50 | 51 | const handleChange = (event: React.ChangeEvent) => { 52 | setModelName(event.target.value); 53 | }; 54 | 55 | const switchModelType = (newValue: boolean) => { 56 | setIsUsingCustomModel(newValue); 57 | window.api.send("switch-model-type", newValue); 58 | }; 59 | 60 | const downloadModel = (model: string) => { 61 | window.api.send("download-model", model); 62 | setIsDownloading(true); 63 | console.log(model); 64 | setDownloadingModelName(model); 65 | }; 66 | 67 | const selectModel = (model: string) => { 68 | console.log(model); 69 | window.api.send("select-model", model); 70 | 71 | type ModelKeys = keyof typeof models; 72 | const updatedModels = Object.keys(models).reduce((acc, key) => { 73 | const modelName = key as ModelKeys; 74 | 75 | acc[modelName] = { 76 | ...models[modelName], 77 | isSelected: modelName === model, 78 | }; 79 | return acc; 80 | }, {} as typeof models); 81 | 82 | setModels(updatedModels); 83 | }; 84 | 85 | const updateModel = () => { 86 | window.api.send("update-model", modelName); 87 | closeWindow(); 88 | }; 89 | 90 | const stopDownload = () => { 91 | window.api.send("stop-download"); 92 | setIsDownloading(false); 93 | }; 94 | 95 | const closeWindow = () => { 96 | window.api.send("close-setting-window"); 97 | }; 98 | 99 | const completeDownload = (modelName: string) => { 100 | const downloadedModelName = modelName as keyof typeof models; 101 | setIsDownloading(false); 102 | setDownloadInformations({ 103 | progress: 0, 104 | remainingTime: "0s", 105 | speed: 0, 106 | }); 107 | console.log(downloadedModelName); 108 | setModels((prevModels) => ({ 109 | ...prevModels, 110 | [downloadedModelName]: { 111 | ...prevModels[downloadedModelName], 112 | isDownloaded: true, 113 | }, 114 | })); 115 | setDownloadingModelName(""); 116 | }; 117 | 118 | useEffect(() => { 119 | const initModelName = (data: any) => { 120 | console.log(data); 121 | const models = { 122 | capybarahermes: { 123 | isDownloaded: data.downloadedModels.capybarahermes, 124 | isSelected: data.selectedModel === "capybarahermes", 125 | isRecommanded: data.recommandedModel === "capybarahermes", 126 | }, 127 | openchat: { 128 | isDownloaded: data.downloadedModels.openchat, 129 | isSelected: data.selectedModel === "openchat", 130 | isRecommanded: data.recommandedModel === "openchat", 131 | }, 132 | }; 133 | setModels(models); 134 | setModelName(data.modelName); 135 | setIsUsingCustomModel(data.isUsingCustomModel); 136 | setCustomModelPath(data.customModelPath); 137 | }; 138 | 139 | function sToTime(s: number): string { 140 | let seconds = s.toFixed(1); 141 | let minutes = (s / 60).toFixed(1); 142 | let hours = (s / (60 * 60)).toFixed(1); 143 | let days = (s / (60 * 60 * 24)).toFixed(1); 144 | 145 | if (parseFloat(seconds) < 60) return seconds + " Sec"; 146 | else if (parseFloat(minutes) < 60) return minutes + " Min"; 147 | else if (parseFloat(hours) < 24) return hours + " Hrs"; 148 | else return days + " Days"; 149 | } 150 | 151 | const updateProgress = (data: any) => { 152 | setDownloadInformations({ 153 | progress: data.percentage.toFixed(2), 154 | remainingTime: sToTime(data.eta), 155 | speed: data.speed, 156 | }); 157 | }; 158 | 159 | const getCustomModelPath = (modelPath: string) => { 160 | console.log(modelPath); 161 | setCustomModelPath(modelPath); 162 | }; 163 | 164 | window.api.receive("init-model-name", initModelName); 165 | window.api.receive("download-data", updateProgress); 166 | window.api.receive("download-completed", completeDownload); 167 | window.api.receive("get-custom-model-path", getCustomModelPath); 168 | 169 | return () => { 170 | window.api.removeListener("init-model-name", initModelName); 171 | window.api.removeListener("download-data", updateProgress); 172 | window.api.removeListener("download-completed", completeDownload); 173 | window.api.removeListener("get-custom-model-path", getCustomModelPath); 174 | }; 175 | }, []); 176 | 177 | return ( 178 |
179 |
180 |
181 |
188 |
189 |

190 | Quick Install 191 |

192 | switchModelType(false)} 195 | type="radio" 196 | name="selectedModelType" 197 | className="cursor-pointer" 198 | /> 199 |
200 |
201 | { 207 | selectModel("capybarahermes"); 208 | }} 209 | downloadModel={() => { 210 | downloadModel("capybarahermes"); 211 | }} 212 | isSelected={models.capybarahermes.isSelected} 213 | /> 214 | { 220 | selectModel("openchat"); 221 | }} 222 | downloadModel={() => { 223 | downloadModel("openchat"); 224 | }} 225 | isSelected={models.openchat.isSelected} 226 | /> 227 |
228 | {isDownloading ? ( 229 |
230 |
231 |

Downloading {downloadingModelName}:

232 |

233 | {" " + 234 | downloadInformations.remainingTime + 235 | " - " + 236 | downloadInformations.progress + 237 | "%"}{" "} 238 |

239 |
240 | 243 |
244 | ) : ( 245 | "" 246 | )} 247 |
248 |
255 |
256 |

257 | Custom Model 258 |

259 | switchModelType(true)} 264 | /> 265 |
266 | 272 |

{customModelPath}

273 |
274 |
275 |
276 |
277 | ); 278 | }; 279 | 280 | export default ModelSettings; 281 | --------------------------------------------------------------------------------