├── .env.development ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .setup ├── error-with-remedy.mjs ├── format.mjs ├── log.mjs ├── macos │ ├── python-path.mjs │ ├── validate-executable.mjs │ └── validate-setup.mjs └── yarn-preinstall-system-validation.mjs ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── entitlements.plist ├── env.d.ts ├── forge.config.ts ├── package.json ├── src ├── backend │ ├── ai-models.ts │ ├── handlers.ts │ ├── icon.png │ ├── index.html │ ├── index.ts │ ├── logo_white.ico │ ├── logo_white.png │ ├── preload.ts │ ├── renderer.ts │ ├── services │ │ ├── logger.ts │ │ ├── ollama.ts │ │ ├── prompts.ts │ │ └── system.ts │ ├── storage.ts │ └── types.ts ├── events.ts ├── executables │ ├── ollama-darwin │ ├── ollama-linux │ └── ollama.exe └── frontend │ ├── assets │ ├── fonts │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Light.ttf │ │ ├── Montserrat-LightItalic.ttf │ │ ├── Montserrat-Regular.ttf │ │ ├── Montserrat-SemiBold.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Light.ttf │ │ └── Roboto-Regular.ttf │ └── images │ │ ├── MOR_logo-sq.icns │ │ ├── MOR_logo-sq.iconset │ │ ├── icon_1024x1024.png │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ ├── icon_512x512@2x.png │ │ └── icon_64x64.png │ │ ├── MOR_logo_circle.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ │ ├── chat.svg │ │ ├── circle-mor-logo.icns │ │ ├── circle-mor-logo.ico │ │ ├── close.svg │ │ ├── copy-link.png │ │ ├── dmgbg.png │ │ ├── dmgbg.svg │ │ ├── home.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo_white.png │ │ ├── metamask_fox.png │ │ ├── metamask_fox.svg │ │ ├── metamask_fox_black_font.png │ │ ├── metamask_fox_black_font.svg │ │ ├── metamask_fox_white_font.png │ │ ├── metamask_fox_white_font.svg │ │ ├── minimize.svg │ │ ├── morph_square.icns │ │ ├── settings.svg │ │ └── wallet.png │ ├── components │ ├── buttons │ │ ├── connect-wallet-button.tsx │ │ ├── meta-mask-button.tsx │ │ ├── navigation-button.tsx │ │ └── round-button.tsx │ ├── layout │ │ ├── app-init.tsx │ │ ├── bottom-bar.tsx │ │ ├── main.tsx │ │ ├── navigation.tsx │ │ └── top-bar.tsx │ └── modals │ │ ├── backdrop.tsx │ │ ├── choose-directory-modal.tsx │ │ ├── metamask-modal.tsx │ │ └── qr-code-modal.tsx │ ├── constants.ts │ ├── contexts.tsx │ ├── fonts.d.ts │ ├── helpers.ts │ ├── hooks.tsx │ ├── images.d.ts │ ├── index.css │ ├── index.tsx │ ├── renderer.d.ts │ ├── router.tsx │ ├── theme.d.ts │ ├── theme │ ├── index.tsx │ ├── theme-provider.tsx │ └── theme.ts │ ├── types.ts │ ├── utils │ ├── chain.ts │ ├── transaction.ts │ ├── types.ts │ └── utils.ts │ └── views │ ├── chat.tsx │ ├── home.tsx │ └── settings.tsx ├── tsconfig.json ├── webpack.main.config.ts ├── webpack.plugins.ts ├── webpack.renderer.config.ts ├── webpack.rules.ts └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_COMM_SERVER_URL=https://metamask-sdk-socket.metafi.codefi.network/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/electron", 13 | "plugin:import/typescript" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "ignorePatterns": [".webpack/*", "out/*"] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [pull_request] 3 | 4 | jobs: 5 | build_on_linux: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-node@master 10 | with: 11 | node-version: 18 12 | - name: install dependencies 13 | run: yarn 14 | - name: build 15 | run: yarn make 16 | 17 | build_on_mac: 18 | runs-on: macos-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.10' 24 | - uses: actions/setup-node@master 25 | with: 26 | node-version: 18 27 | - name: install dependencies 28 | run: yarn 29 | - name: build 30 | run: yarn make 31 | 32 | build_on_win: 33 | runs-on: windows-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-node@master 37 | with: 38 | node-version: 18 39 | - name: install dependencies 40 | run: yarn 41 | - name: build 42 | run: yarn make 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | release: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [macos-latest, ubuntu-latest, windows-latest] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.10' 20 | - uses: actions/setup-node@master 21 | with: 22 | node-version: 18 23 | - name: install dependencies 24 | run: npm install 25 | - name: publish 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: npm run publish 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | yarn.lock 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # Webpack 87 | .webpack/ 88 | 89 | # Vite 90 | .vite/ 91 | 92 | # Electron-Forge 93 | out/ 94 | 95 | # Ollama 96 | blobs/ 97 | manifests/ 98 | 99 | package-lock.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.setup/error-with-remedy.mjs: -------------------------------------------------------------------------------- 1 | export class ErrorWithRemedy extends Error { 2 | constructor(errorMessage, remedyMessage) { 3 | super(errorMessage); 4 | this.name = this.constructor.name; 5 | this.remedy = remedyMessage; 6 | 7 | // Maintaining proper stack trace for where our error was thrown (only available on V8) 8 | if (Error.captureStackTrace) { 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.setup/format.mjs: -------------------------------------------------------------------------------- 1 | export function formatRed(message) { 2 | return `\x1b[31m${message}\x1b[0m`; 3 | } 4 | 5 | export function formatGray(message) { 6 | return `\x1b[37m${message}\x1b[0m`; 7 | } 8 | 9 | export function formatItalics(message) { 10 | return `\x1b[3m${message}\x1b[0m`; 11 | } 12 | 13 | export function formatBold(message) { 14 | return `\x1b[1m${message}\x1b[0m`; 15 | } 16 | 17 | export function formatError(message) { 18 | return formatBold(formatRed(message)); 19 | } 20 | 21 | export function formatExample(message) { 22 | return formatGray(formatItalics(message)); 23 | } -------------------------------------------------------------------------------- /.setup/log.mjs: -------------------------------------------------------------------------------- 1 | import { formatBold, formatError, formatGray } from './format.mjs'; 2 | 3 | export function logInfo(message) { 4 | console.log(formatGray(message)); 5 | } 6 | 7 | /** 8 | * Log an error to the console with an error `message` and optional `remedy` 9 | * @param error in the format { message: string, remedy?: string } 10 | * @returns void 11 | */ 12 | export function logError(error) { 13 | console.error(); 14 | console.error(formatError(error?.message || error)); 15 | if (error?.remedy) { 16 | console.error(); 17 | console.error(formatBold('Suggested remedy:')); 18 | console.error(error.remedy); 19 | } 20 | console.error(); 21 | } 22 | -------------------------------------------------------------------------------- /.setup/macos/python-path.mjs: -------------------------------------------------------------------------------- 1 | import { logInfo } from '../log.mjs'; 2 | import { execSync } from 'child_process'; 3 | 4 | export function getPythonPath() { 5 | const nodeGypPythonPath = process.env.NODE_GYP_FORCE_PYTHON; 6 | if (nodeGypPythonPath) { 7 | logInfo(`NODE_GYP_FORCE_PYTHON=${nodeGypPythonPath}`); 8 | return nodeGypPythonPath; 9 | } 10 | logInfo(`defaulting to system's python3`); 11 | return execSync(`which python3`).toString().trim(); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /.setup/macos/validate-executable.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export function validateExecutable(path) { 4 | existsOnSystem(path) 5 | isNotADirectory(path) 6 | isExecutable(path) 7 | } 8 | 9 | function existsOnSystem(path) { 10 | if (!fs.existsSync(path)) { 11 | throw new Error(`Path ${path} does not exist`); 12 | } 13 | } 14 | 15 | function isNotADirectory(path) { 16 | if (fs.statSync(path).isDirectory()) { 17 | throw new Error(`${path} is a directory. Please provide the path to an executable.`); 18 | } 19 | } 20 | 21 | function isExecutable(path) { 22 | try { 23 | fs.accessSync(path, fs.constants.X_OK); 24 | } catch (err) { 25 | throw new Error(`${path} is not executable`); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.setup/macos/validate-setup.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { ErrorWithRemedy } from '../error-with-remedy.mjs'; 3 | import { formatExample } from '../format.mjs'; 4 | import { getPythonPath } from './python-path.mjs'; 5 | import { logInfo } from '../log.mjs'; 6 | import { validateExecutable } from './validate-executable.mjs'; 7 | 8 | /** 9 | * On macOS, this script checks if Python 3.10 is installed and accessible to node-gyp. 10 | * 11 | * I ran into a problem trying to `yarn` install, with a system Python version of `3.12.2`, 12 | * but ran into the error `ModuleNotFoundError: No module named 'distutils'`. 13 | * Since node-gyp relies on `distutils`, which is removed in Python `3.12`, 14 | * you need to use a Python version that still includes `distutils`. 15 | */ 16 | export function validateMacSetup() { 17 | logInfo('Installing on macOS'); 18 | const pythonPath = getPythonPath(); 19 | validateExecutable(pythonPath); 20 | 21 | let error; 22 | try { 23 | const pythonVersionOutput = execSync(`${pythonPath} --version`).toString().trim(); 24 | logInfo(`${pythonPath} == (${pythonVersionOutput})`); 25 | 26 | const pythonVersion = pythonVersionOutput.split(' ')[1].trim(); 27 | const majorVersion = parseInt(pythonVersion.split('.')[0]); 28 | const minorVersion = parseInt(pythonVersion.split('.')[1]); 29 | const noCompatiblePythonVersionFound = !(majorVersion === 3 && (minorVersion >= 10 && minorVersion < 12)); 30 | 31 | if (noCompatiblePythonVersionFound) { 32 | error = `Incompatible Python version ${pythonVersion} found. Python 3.10 is required.`; 33 | } 34 | 35 | } catch (caughtError) { 36 | error = `Python 3.10 was not found with error: ${caughtError?.message || caughtError}`; 37 | } 38 | if (error) { 39 | const checkForPythonInstall = 'Check for versions of python installed on your system. For example, if you use brew:'; 40 | const displayBrewPythonVersionsExample = formatExample('brew list --versions | grep python'); 41 | 42 | const pleaseInstallPython = 'If python 3.10 was not found, install it. For example:'; 43 | const installPythonExample = formatExample('brew install python@3.10'); 44 | 45 | const configureNodeGypPython = 'Ensure you have an environment variable for NODE_GYP_FORCE_PYTHON pointing to your python 3.10 path.\n For example, assuming you installed python@3.10 with brew:'; 46 | const exportNodeGypPythonEnvVariable = formatExample('export NODE_GYP_FORCE_PYTHON=$(brew --prefix python@3.10)/bin/python3.10'); 47 | 48 | throw new ErrorWithRemedy(error, ` STEP 1: ${checkForPythonInstall} ${displayBrewPythonVersionsExample} 49 | \n STEP 2: ${pleaseInstallPython} ${installPythonExample} 50 | \n STEP 3: ${configureNodeGypPython} ${exportNodeGypPythonEnvVariable}` 51 | ); 52 | } 53 | } -------------------------------------------------------------------------------- /.setup/yarn-preinstall-system-validation.mjs: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import { validateMacSetup } from './macos/validate-setup.mjs'; 3 | import { logError } from './log.mjs'; 4 | 5 | /** 6 | * Validate the system setup at the beginning of `yarn` install. 7 | */ 8 | try { 9 | const platform = os.platform(); 10 | switch (platform) { 11 | case 'darwin': 12 | validateMacSetup(); 13 | break; 14 | default: 15 | console.log(`No setup validation required for platform ${platform}`); 16 | } 17 | } catch(setupError) { 18 | logError(setupError); 19 | process.exit(1); 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "asar", 4 | "codefi", 5 | "metafi", 6 | "notarytool", 7 | "ollama", 8 | "qrcode", 9 | "relocator", 10 | "Roboto", 11 | "submod", 12 | "svgr" 13 | ], 14 | "[typescript]": { 15 | "editor.tabSize": 2 16 | } 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Morpheus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Morhpheus Node Setup 3 | 4 | 5 | 6 | Windows OS Systems 7 | 8 | Prerequisites 9 | 10 | Node.js for Windows 11 | Follow the instructions listed here for Node.js setup: 12 | • https://phoenixnap.com/kb/install-node-js-npm-on-windows 13 | 14 | Yarn for Windows 15 | Follow the instructions listed here for Yarn Package Manager setup: 16 | • https://phoenixnap.com/kb/yarn-windows 17 | 18 | 1. Create Project Directory 19 | Open a Windows command prompt and create a project folder and put your command line at that directory 20 | 21 | I.e. Presume the starting directory is: C:\projects 22 | 23 | mkdir c:\projects 24 | 25 | cd /d c:\projects 26 | 27 | 2. Morpheus client Workspace Setup 28 | From a command line at the project directory (created in #1), type: 29 | 30 | git clone https://github.com/MorpheusAIs/Lite-Client.git 31 | 32 | 3. Project Environment Setup 33 | Set up the modules and components using the Yarn Package Manager 34 | 35 | Navigate to the command line of the Morpheus client local repository, then type the following command: 36 | 37 | yarn 38 | 39 | This will set up all node modules needed. 40 | 41 | 4. Build the Morpheus Client App 42 | From a command line at the root directory of the local Morpheus Client repo, build the client executable, type: 43 | 44 | yarn make 45 | 46 | 47 | 6. Application Runtime and Testing 48 | Upon successful build, run the Morpheus client app by clicking the following executable or start from the command line: 49 | 50 | \Lite-Client-main\out\morpheus-win32-x64.exe 51 | 52 | 53 | 54 | NOTES 55 | 56 | • Additional Run, Test, and Build scripts are located in the package.json configuration file in the root directory of the Morpheus client repo 57 | 58 | • Please visit https://mor.software/ to sign up as a developer to be rewarded for your merged contributions. Submit an MRC to get support for feature and improvement ideas. 59 | 60 | • https://mor.software/ is also the place to build, submit, deploy, and manage all of your Smart Agents. 61 | 62 | • Be sure to complete these steps from an account with administrative access to the host machine 63 | 64 | • The initial start of the application may take extended time to start due to the initial configuration and run of the application 65 | 66 | 67 | 68 | ### Windows installer instructions (Doc format) 69 | [Google Doc](https://docs.google.com/document/d/1YjGAlTzglct8aNEqZAUeYD7SAmOETtmv/edit?usp=sharing&ouid=118042204753952761929&rtpof=true&sd=true) 70 | 71 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | APPLE_ID: string; 4 | APPLE_ID_PASSWORD: string; 5 | APPLE_TEAM_ID: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | import { PublisherGithub } from '@electron-forge/publisher-github'; 10 | import fs from 'fs'; 11 | import { exec } from 'child_process'; 12 | 13 | import { mainConfig, mainDevConfig } from './webpack.main.config'; 14 | import { rendererConfig } from './webpack.renderer.config'; 15 | 16 | const config: ForgeConfig = { 17 | packagerConfig: { 18 | asar: true, 19 | name: 'morpheus', 20 | extraResource: ['./executables/'], 21 | icon: 'src/frontend/assets/images/circle-mor-logo', 22 | osxSign: { 23 | identity: process.env.APPLE_DEVELOPER_ID, 24 | optionsForFile: () => { 25 | return { 26 | entitlements: './entitlements.plist', 27 | }; 28 | }, 29 | }, 30 | ...(process.env.APPLE_ID && 31 | process.env.APPLE_ID_PASSWORD && 32 | process.env.APPLE_TEAM_ID && { 33 | osxNotarize: { 34 | appleId: process.env.APPLE_ID, 35 | appleIdPassword: process.env.APPLE_ID_PASSWORD, 36 | teamId: process.env.APPLE_TEAM_ID, 37 | }, 38 | }), 39 | }, 40 | hooks: { 41 | prePackage: async (_, platform) => { 42 | const platformFile = 43 | platform === 'darwin' 44 | ? 'ollama-darwin' 45 | : platform === 'win32' 46 | ? 'ollama.exe' 47 | : 'ollama-linux'; 48 | 49 | const filePath = `src/executables/${platformFile}`; 50 | 51 | platform !== 'win32' 52 | ? exec(`chmod +x ${filePath}`) 53 | : fs.chmodSync(filePath, 755); 54 | 55 | fs.mkdirSync('executables'); 56 | fs.copyFileSync(filePath, `executables/${platformFile}`); 57 | }, 58 | postPackage: async () => { 59 | fs.rmSync('executables', { recursive: true, force: true }); 60 | } 61 | }, 62 | rebuildConfig: {}, 63 | makers: [ 64 | new MakerSquirrel({ 65 | setupIcon: 'src/frontend/assets/images/circle-mor-logo.ico', 66 | }), 67 | new MakerZIP({}, ['darwin']), 68 | new MakerRpm({}), 69 | new MakerDeb({ 70 | options: { 71 | icon: 'src/frontend/assets/images/MOR_logo_circle.iconset/icon_512x512.png', 72 | maintainer: 'Morpheus', 73 | homepage: 'https://www.mor.org', 74 | categories: ['Utility'], 75 | }, 76 | }), 77 | new MakerDMG({ 78 | icon: 'src/frontend/assets/images/circle-mor-logo.icns', 79 | format: 'ULFO', 80 | background: 'src/frontend/assets/images/dmgbg.svg', 81 | overwrite: true, 82 | additionalDMGOptions: { 83 | window: { 84 | size: { 85 | width: 600, 86 | height: 600, 87 | }, 88 | }, 89 | }, 90 | }), 91 | ], 92 | publishers: [ 93 | new PublisherGithub({ 94 | repository: { 95 | owner: 'MorpheusAIs', 96 | name: 'Node', 97 | }, 98 | draft: true, 99 | }), 100 | ], 101 | plugins: [ 102 | new AutoUnpackNativesPlugin({}), 103 | new WebpackPlugin({ 104 | mainConfig: process.env.NODE_ENV === 'development' ? mainDevConfig : mainConfig, 105 | devContentSecurityPolicy: 106 | "connect-src 'self' unsafe-inline ws://localhost:* https://metamask-sdk-socket.metafi.codefi.network wss://metamask-sdk-socket.metafi.codefi.network data:", 107 | renderer: { 108 | config: rendererConfig, 109 | entryPoints: [ 110 | { 111 | html: './src/backend/index.html', 112 | js: './src/backend/renderer.ts', 113 | name: 'main_window', 114 | preload: { 115 | js: './src/backend/preload.ts', 116 | }, 117 | }, 118 | ], 119 | }, 120 | }), 121 | ], 122 | }; 123 | 124 | export default config; 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morpheus", 3 | "productName": "Morpheus", 4 | "version": "0.0.6", 5 | "description": "Morpheus is private, sovereign, AI", 6 | "main": ".webpack/main", 7 | "scripts": { 8 | "preinstall": "node .setup/yarn-preinstall-system-validation.mjs", 9 | "start": "cross-env NODE_ENV=development DEBUG=electron-packager electron-forge start", 10 | "package": "set DEBUG=electron-packager && electron-forge package", 11 | "make": "electron-forge make", 12 | "publish": "electron-forge publish", 13 | "publish-github-minor": "yarn version --minor && git push --follow-tags", 14 | "publish-github-major": "yarn version --major && git push --follow-tags", 15 | "publish-github-patch": "yarn version --patch && git push --follow-tags", 16 | "lint": "eslint --ext .ts,.tsx .", 17 | "pretty": "npx prettier --no-error-on-unmatched-pattern src/**/*.{js,jsx,ts,tsx,json,css} --write", 18 | "prepare": "husky" 19 | }, 20 | "prettier": { 21 | "tabWidth": 2, 22 | "semi": true, 23 | "singleQuote": true, 24 | "printWidth": 100 25 | }, 26 | "lint-staged": { 27 | "src/**/*.{js,jsx,ts,tsx,json,css}": [ 28 | "prettier --write" 29 | ] 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "yarn run pretty" 34 | } 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "keywords": [], 40 | "author": { 41 | "name": "Morpheus", 42 | "email": "morpheusai@proton.me" 43 | }, 44 | "license": "MIT", 45 | "devDependencies": { 46 | "@electron-forge/cli": "^7.2.0", 47 | "@electron-forge/maker-deb": "^7.2.0", 48 | "@electron-forge/maker-dmg": "^7.2.0", 49 | "@electron-forge/maker-rpm": "^7.2.0", 50 | "@electron-forge/maker-squirrel": "^7.2.0", 51 | "@electron-forge/maker-zip": "^7.2.0", 52 | "@electron-forge/plugin-auto-unpack-natives": "^7.2.0", 53 | "@electron-forge/plugin-webpack": "^7.2.0", 54 | "@electron-forge/publisher-github": "^7.2.0", 55 | "@svgr/webpack": "^8.1.0", 56 | "@types/ajv": "^1.0.0", 57 | "@types/qrcode": "^1.5.5", 58 | "@types/react-dom": "^18.2.19", 59 | "@types/react-router-dom": "^5.3.3", 60 | "@types/react-transition-group": "^4.4.10", 61 | "@types/styled-components": "^5.1.34", 62 | "@typescript-eslint/eslint-plugin": "^5.0.0", 63 | "@typescript-eslint/parser": "^5.0.0", 64 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 65 | "copy-webpack-plugin": "^12.0.2", 66 | "cross-env": "^7.0.3", 67 | "css-loader": "^6.0.0", 68 | "electron": "28.1.4", 69 | "eslint": "^8.0.1", 70 | "eslint-plugin-import": "^2.25.0", 71 | "fork-ts-checker-webpack-plugin": "^7.2.13", 72 | "husky": "^9.0.5", 73 | "lint-staged": "^15.2.2", 74 | "node-loader": "^2.0.0", 75 | "prettier": "^3.2.4", 76 | "style-loader": "^3.0.0", 77 | "ts-loader": "^9.2.2", 78 | "ts-node": "^10.0.0", 79 | "typescript": "~4.5.4", 80 | "webpack-permissions-plugin": "^1.0.9" 81 | }, 82 | "dependencies": { 83 | "@metamask/sdk": "^0.14.2", 84 | "@metamask/sdk-react": "^0.14.2", 85 | "@metamask/sdk-react-ui": "^0.14.3", 86 | "axios": "^1.6.7", 87 | "check-disk-space": "^3.4.0", 88 | "electron-squirrel-startup": "^1.0.0", 89 | "electron-store": "^8.1.0", 90 | "ethers": "^6.11.1", 91 | "ollama": "^0.4.3", 92 | "qrcode": "^1.5.3", 93 | "react-loader-spinner": "^6.1.6", 94 | "react-router-dom": "^6.21.3", 95 | "react-transition-group": "^4.4.5", 96 | "styled-components": "^6.1.8", 97 | "sudo-prompt": "^9.2.1", 98 | "winston": "^3.11.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/backend/ai-models.ts: -------------------------------------------------------------------------------- 1 | export const models = { 2 | mistral: { 3 | tags: ['text', '7b', '7b-instruct', '7b-text'], 4 | }, 5 | llama2: { 6 | tags: ['chat', '7b', 'text'], 7 | }, 8 | orcaMini: { 9 | tags: ['latest', '3b'], 10 | }, 11 | nousHermes: { 12 | tags: ['latest', '7b'], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/backend/handlers.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from 'electron'; 2 | 3 | import { IpcChannel } from './../events'; 4 | import { 5 | loadOllama, 6 | stopOllama, 7 | getAllLocalModels, 8 | askOllama, 9 | getOrPullModel, 10 | } from './services/ollama'; 11 | import { OllamaQuestion } from './types'; 12 | import { saveModelPathToStorage, getModelPathFromStorage } from './storage'; 13 | 14 | export const initOllama = async (_: Electron.IpcMainEvent) => { 15 | try { 16 | const ollamaLoaded = await loadOllama(); 17 | 18 | return ollamaLoaded; 19 | } catch (err) { 20 | handleError(err); 21 | 22 | return false; 23 | } 24 | }; 25 | 26 | export const stopOllamaServe = async () => { 27 | await stopOllama(); 28 | }; 29 | 30 | export const getAllModels = async (_: Electron.IpcMainEvent) => { 31 | try { 32 | const models = await getAllLocalModels(); 33 | 34 | return models; 35 | // event.reply(OllamaChannel.OllamaGetAllModels, models); 36 | } catch (err) { 37 | handleError(err); 38 | } 39 | }; 40 | 41 | export const getModel = async (_: Electron.IpcMainEvent, model: string) => { 42 | try { 43 | const response = await getOrPullModel(model); 44 | 45 | return response; 46 | } catch (err) { 47 | handleError(err); 48 | } 49 | }; 50 | 51 | export const askOlama = async (_: Electron.IpcMainEvent, { model, query }: OllamaQuestion) => { 52 | try { 53 | const response = await askOllama(model, query); 54 | 55 | return response; 56 | } catch (err) { 57 | handleError(err); 58 | } 59 | }; 60 | 61 | export const getModelsFolderPath = async (_: Electron.IpcMainEvent) => { 62 | return getModelPathFromStorage(); 63 | }; 64 | 65 | export const setModelFolderPath = async (_: Electron.IpcMainEvent) => { 66 | const result = await dialog.showOpenDialog({ 67 | properties: ['openDirectory', 'createDirectory'], 68 | }); 69 | 70 | if (result.filePaths) { 71 | saveModelPathToStorage(result.filePaths[0]); 72 | } 73 | 74 | return true; 75 | }; 76 | 77 | const handleError = (err: Error) => { 78 | console.error(err); 79 | 80 | // log with winston here 81 | }; 82 | -------------------------------------------------------------------------------- /src/backend/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/backend/icon.png -------------------------------------------------------------------------------- /src/backend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 11 | MorpheusAI 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/backend/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron'; 2 | import path from 'path'; 3 | import { IpcChannel, IpcMainChannel, OllamaChannel } from '../events'; 4 | import { 5 | initOllama, 6 | getAllModels, 7 | askOlama, 8 | getModel, 9 | setModelFolderPath, 10 | getModelsFolderPath, 11 | stopOllamaServe, 12 | } from './handlers'; 13 | 14 | // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack 15 | // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on 16 | // whether you're running in development or production). 17 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string; 18 | declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; 19 | 20 | let mainWindow: BrowserWindow; 21 | 22 | // Handle creating/removing shortcuts on Windows when installing/uninstalling. 23 | if (require('electron-squirrel-startup')) { 24 | app.quit(); 25 | } 26 | 27 | const createWindow = async (): Promise => { 28 | mainWindow = new BrowserWindow({ 29 | icon: 'icon.png', 30 | height: 800, 31 | width: 1200, 32 | autoHideMenuBar: true, 33 | frame: false, 34 | resizable: isDev, 35 | fullscreenable: false, 36 | show: true, 37 | movable: true, 38 | webPreferences: { 39 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 40 | }, 41 | }); 42 | 43 | // and load the index.html of the app. 44 | await mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); 45 | 46 | // Open the DevTools. 47 | if (isDev) { 48 | mainWindow.webContents.openDevTools(); 49 | } 50 | }; 51 | 52 | app.on('ready', async () => { 53 | await createWindow(); 54 | 55 | ipcMain.on(IpcMainChannel.CommandOuput, (_, output: string) => { 56 | console.log(output); 57 | }); 58 | 59 | ipcMain.handle(OllamaChannel.OllamaInit, initOllama); 60 | ipcMain.handle(OllamaChannel.OllamaGetAllModels, getAllModels); 61 | ipcMain.handle(OllamaChannel.OllamaQuestion, askOlama); 62 | ipcMain.handle(OllamaChannel.OllamaGetModel, getModel); 63 | 64 | ipcMain.on(IpcChannel.Close, () => app.quit()); 65 | ipcMain.on(IpcChannel.Minimize, () => mainWindow.minimize()); 66 | ipcMain.handle(IpcChannel.GetModelsPath, getModelsFolderPath); 67 | ipcMain.handle(IpcChannel.SetFolderPath, setModelFolderPath); 68 | }); 69 | 70 | app.on('window-all-closed', async () => { 71 | if (process.platform !== 'darwin') { 72 | app.quit(); 73 | } 74 | }); 75 | 76 | app.on('before-quit', async () => { 77 | // shutdown ollama 78 | await stopOllamaServe(); 79 | }); 80 | 81 | app.on('activate', () => { 82 | // On OS X it's common to re-create a window in the app when the 83 | // dock icon is clicked and there are no other windows open. 84 | if (BrowserWindow.getAllWindows().length === 0) { 85 | createWindow(); 86 | } 87 | }); 88 | 89 | export const sendOllamaStatusToRenderer = async (status: string) => { 90 | mainWindow.webContents.send(OllamaChannel.OllamaStatusUpdate, status); 91 | }; 92 | 93 | export const isDev = !app.isPackaged; 94 | export const appPath = path.parse(app.getPath('exe')).dir; 95 | -------------------------------------------------------------------------------- /src/backend/logo_white.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/backend/logo_white.ico -------------------------------------------------------------------------------- /src/backend/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/backend/logo_white.png -------------------------------------------------------------------------------- /src/backend/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | import { ChatResponse, GenerateResponse, ListResponse, ModelResponse } from 'ollama'; 3 | import { IpcChannel, OllamaChannel } from '../events'; 4 | import { OllamaQuestion } from './types'; 5 | 6 | contextBridge.exposeInMainWorld('backendBridge', { 7 | main: { 8 | init: () => invokeNoParam(IpcChannel.AppInit), 9 | onInit: (callback: (result: boolean) => void) => 10 | ipcRenderer.on(IpcChannel.AppInit, (_, value: boolean) => callback(value)), 11 | sendInit: () => ipcRenderer.send(IpcChannel.AppInit), 12 | getFolderPath: () => invokeNoParam(IpcChannel.GetModelsPath), 13 | setFolderPath: () => invokeNoParam(IpcChannel.SetFolderPath), 14 | close: () => ipcRenderer.send(IpcChannel.Close), 15 | minimize: () => ipcRenderer.send(IpcChannel.Minimize), 16 | }, 17 | ollama: { 18 | init: (result: boolean) => invokeNoParam(OllamaChannel.OllamaInit, result), 19 | onStatusUpdate: (callback: (status: string) => void) => 20 | ipcRenderer.on(OllamaChannel.OllamaStatusUpdate, (_, status) => callback(status)), 21 | question: ({ model, query }: OllamaQuestion) => 22 | ipcRenderer.invoke(OllamaChannel.OllamaQuestion, { 23 | model, 24 | query, 25 | }) as Promise, 26 | onAnswer: (callback: (response: ChatResponse) => void) => 27 | ipcRenderer.on(OllamaChannel.OllamaAnswer, (_, response) => callback(response)), 28 | getAllModels: () => invokeNoParam(OllamaChannel.OllamaGetAllModels), 29 | getModel: (model: string) => 30 | invoke(OllamaChannel.OllamaGetModel, model), 31 | }, 32 | removeAllListeners(channel: string) { 33 | ipcRenderer.removeAllListeners(channel); 34 | }, 35 | }); 36 | 37 | function invoke

(channel: string, ...args: P) { 38 | return ipcRenderer.invoke(channel, ...args) as Promise; 39 | } 40 | 41 | function invokeNoParam(channel: string, ...args: any[]) { 42 | return ipcRenderer.invoke(channel, ...args) as Promise; 43 | } 44 | -------------------------------------------------------------------------------- /src/backend/renderer.ts: -------------------------------------------------------------------------------- 1 | import '../frontend'; 2 | -------------------------------------------------------------------------------- /src/backend/services/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import path from 'path'; 3 | import { isDev } from '..'; 4 | import { app } from 'electron'; 5 | 6 | const logFilePath = isDev ? '.' : app.getPath('userData'); 7 | 8 | export const logger = winston.createLogger({ 9 | format: winston.format.simple(), 10 | transports: [ 11 | new winston.transports.Console(), 12 | new winston.transports.File({ 13 | filename: path.join(logFilePath, 'error.log'), 14 | maxFiles: 1, 15 | level: 'error', 16 | }), 17 | new winston.transports.File({ 18 | filename: path.join(logFilePath, 'app.log'), 19 | maxFiles: 1, 20 | level: 'info', 21 | }), 22 | ], 23 | exceptionHandlers: [ 24 | new winston.transports.File({ 25 | filename: path.join(logFilePath, 'exceptions.log'), 26 | }), 27 | ], 28 | exitOnError: false, 29 | }); 30 | -------------------------------------------------------------------------------- /src/backend/services/ollama.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import { Ollama } from 'ollama'; 3 | import { execFile, ChildProcess } from 'child_process'; 4 | import fs from 'fs'; 5 | import { sendOllamaStatusToRenderer } from '..'; 6 | import { MOR_PROMPT } from './prompts'; 7 | 8 | // events 9 | import { IpcMainChannel } from '../../events'; 10 | import { 11 | createDirectoryElevated, 12 | executeCommandElevated, 13 | getExecutablePathByPlatform, 14 | killProcess, 15 | runDelayed, 16 | } from './system'; 17 | 18 | // storage 19 | import { getModelPathFromStorage } from '../storage'; 20 | import { logger } from './logger'; 21 | 22 | // constants 23 | const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434/'; 24 | 25 | // commands 26 | export const SERVE_OLLAMA_CMD = 'ollama serve'; 27 | export const WSL_SERVE_OLLAMA_CMD = 'wsl ollama serve'; 28 | 29 | // ollama instance 30 | let ollama: Ollama; 31 | let ollamaProcess: ChildProcess | null; 32 | 33 | export const loadOllama = async () => { 34 | let runningInstance = await isOllamaInstanceRunning(); 35 | 36 | if (runningInstance) { 37 | // connect to local instance 38 | ollama = new Ollama({ 39 | host: DEFAULT_OLLAMA_URL, 40 | }); 41 | 42 | await sendOllamaStatusToRenderer( 43 | `local instance of ollama is running and connected at ${DEFAULT_OLLAMA_URL}`, 44 | ); 45 | 46 | return true; 47 | } 48 | 49 | const customAppData = getModelPathFromStorage(); 50 | runningInstance = await packedExecutableOllamaSpawn(customAppData); 51 | 52 | if (runningInstance) { 53 | // connect to local instance 54 | ollama = new Ollama({ 55 | host: DEFAULT_OLLAMA_URL, 56 | }); 57 | 58 | await sendOllamaStatusToRenderer( 59 | `local instance of ollama is running and connected at ${DEFAULT_OLLAMA_URL}`, 60 | ); 61 | 62 | return true; 63 | } 64 | 65 | ipcMain.emit(IpcMainChannel.Error, `Couldn't start Ollama locally.`); 66 | 67 | return false; 68 | }; 69 | 70 | export const isOllamaInstanceRunning = async (url?: string): Promise => { 71 | try { 72 | const usedUrl = url ?? DEFAULT_OLLAMA_URL; 73 | 74 | await sendOllamaStatusToRenderer(`checking if ollama instance is running at ${usedUrl}`); 75 | 76 | const ping = await fetch(usedUrl); 77 | 78 | return ping.status === 200; 79 | } catch (err) { 80 | return false; 81 | } 82 | }; 83 | 84 | export const packedExecutableOllamaSpawn = async (customDataPath?: string) => { 85 | await sendOllamaStatusToRenderer(`trying to spawn locally installed ollama`); 86 | 87 | try { 88 | spawnLocalExecutable(customDataPath); 89 | } catch (err) { 90 | console.error(err); 91 | } 92 | 93 | return await runDelayed(isOllamaInstanceRunning, 10000); 94 | }; 95 | 96 | export const devRunLocalWSLOllama = (customDataPath?: string) => { 97 | executeCommandElevated( 98 | WSL_SERVE_OLLAMA_CMD, 99 | customDataPath ? { OLLAMA_MODELS: customDataPath } : undefined, 100 | ); 101 | }; 102 | 103 | export const spawnLocalExecutable = async (customDataPath?: string) => { 104 | try { 105 | const { executablePath, appDataPath } = getOllamaExecutableAndAppDataPath(customDataPath); 106 | 107 | if (!fs.existsSync(appDataPath)) { 108 | createDirectoryElevated(appDataPath); 109 | } 110 | 111 | const env = { 112 | ...process.env, 113 | OLLAMA_MODELS: appDataPath, 114 | }; 115 | 116 | ollamaProcess = execFile(executablePath, ['serve'], { env }, (err, stdout, stderr) => { 117 | if (err) { 118 | throw new Error(`exec error: ${err.message}`); 119 | } 120 | 121 | if (stderr) { 122 | throw new Error(`stderr: ${stderr}`); 123 | } 124 | }); 125 | } catch (err) { 126 | logger.error(err); 127 | } 128 | }; 129 | 130 | export const getOllamaExecutableAndAppDataPath = ( 131 | customDataPath?: string, 132 | ): { 133 | executablePath: string; 134 | appDataPath: string; 135 | } => { 136 | const appDataPath = customDataPath || app.getPath('userData'); 137 | const executablePath = getExecutablePathByPlatform(); 138 | 139 | return { 140 | executablePath, 141 | appDataPath, 142 | }; 143 | }; 144 | 145 | export const askOllama = async (model: string, message: string) => { 146 | return await ollama.chat({ 147 | model, 148 | messages: [ 149 | { 150 | role: 'system', 151 | content: MOR_PROMPT, 152 | }, 153 | { 154 | role: 'user', 155 | content: `Answer the following query in a valid formatted JSON object without comments with both the response and action fields deduced from the user's question. Adhere strictly to JSON syntax without comments. Query: ${message}. Response: { "response":`, 156 | }, 157 | ], 158 | }); 159 | }; 160 | 161 | export const getOrPullModel = async (model: string) => { 162 | await installModelWithStatus(model); 163 | 164 | // init the model on pull to load into memory 165 | await ollama.chat({ model }); 166 | 167 | return findModel(model); 168 | }; 169 | 170 | export const installModelWithStatus = async (model: string) => { 171 | const stream = await ollama.pull({ 172 | model, 173 | stream: true, 174 | }); 175 | 176 | for await (const part of stream) { 177 | if (part.digest) { 178 | let percent = 0; 179 | 180 | if (part.completed && part.total) { 181 | percent = Math.round((part.completed / part.total) * 100); 182 | 183 | await sendOllamaStatusToRenderer(`${part.status} ${percent}%`); 184 | } 185 | } else { 186 | await sendOllamaStatusToRenderer(`${part.status}`); 187 | } 188 | } 189 | }; 190 | 191 | export const findModel = async (model: string) => { 192 | const allModels = await ollama.list(); 193 | 194 | return allModels.models.find((m) => m.name.toLowerCase().includes(model)); 195 | }; 196 | 197 | export const getAllLocalModels = async () => { 198 | return await ollama.list(); 199 | }; 200 | 201 | export const stopOllama = async () => { 202 | if (!ollamaProcess) { 203 | return; 204 | } 205 | 206 | killProcess(ollamaProcess); 207 | 208 | ollamaProcess.removeAllListeners(); 209 | ollamaProcess = null; 210 | }; 211 | -------------------------------------------------------------------------------- /src/backend/services/prompts.ts: -------------------------------------------------------------------------------- 1 | export const MOR_PROMPT = `###System: 2 | You are MORPHEUS, but you prefer to be called a SmartAgent. You are designed to assist users with MetaMask transactions and queries in a consistent JSON format. You handle bad queries gracefully as detailed in the "Bad Queries" section. Your responses should always contain a "response" field for textual feedback 3 | and an "action" field for transaction details. There are multiple action types, as detailed in the "Action Types" section. 4 | 5 | ###Response Format: 6 | All responses must follow this JSON structure: 7 | { 8 | "response": "Textual feedback here.", 9 | "action": { 10 | // Action details or an empty object 11 | } 12 | } 13 | Respond only in valid JSON without any comments. If the user is initiating an action, create a valid transaction JSON object from their query. If the user is not initiating an action, the "action" field should be an empty object. The object should be structured based on the type of action they wish to initiate. Keep the "response" field short, using 3 sentences maximum. 14 | 15 | ###Action Types: 16 | 1. **Transfer**: For users wanting to transfer ETH. The user's input should provide the target address and ETH amount. 17 | - **Format**: 18 | { 19 | "response": "Textual feedback here.", 20 | "action": { 21 | "type": "Transfer", 22 | "targetAddress": "address", 23 | "ethAmount": "amount" 24 | } 25 | } 26 | 27 | 2. **Balance Inquiry**: For users inquiring about their ETH balance. For all Balance inquiries, the "action" field should contain only the "type" key with the value "Balance". The "response" field should be set to empty. 28 | - **Format**: 29 | { 30 | "response": "", 31 | "action": { 32 | "type": "Balance" 33 | } 34 | } 35 | 36 | 37 | 3. **Address Inquiry**: For users inquiring about their wallet address. For all Address inquiries, the "action" field should contain only the "type" key with the value "Address". The "response" field should be set to empty. 38 | - **Format**: 39 | { 40 | "response": "", 41 | "action": { 42 | "type": "Address" 43 | } 44 | } 45 | 46 | ###Error Handling: 47 | For actions requiring more information (e.g., missing ETH amount for transfers), respond with a request for the necessary details: 48 | 49 | { 50 | "response": "Request for more information goes here", 51 | "action": {} 52 | } 53 | 54 | ###Examples: 55 | 56 | // Transfer Action 57 | - **Transfer actions**: 58 | - Question: "transfer 2 eth to 0x123..." 59 | - Response: 60 | { 61 | "response": "Transfer prepared. Please confirm the details in MetaMask.", 62 | "action": {"type": "Transfer", "targetAddress": "0x123...", "ethAmount": "2"} 63 | } 64 | 65 | // Balance Inquiries 66 | - **Balance inquiry**: 67 | - Questions: "What's my balance?", "Could you tell me my current balance, please?", "how much eth I got?", "Hey Morpheus, can you show me my balance now?", "I need to see my ETH balance, can you help?", "balance?" 68 | - Response for all: 69 | { 70 | "response": "", 71 | "action": {"type": "Balance"} 72 | } 73 | 74 | // Address Inquiries 75 | - **Address inquiry**: 76 | - Question: "What is my wallet address?", "What is my public Eth address?", "Can you show me my wallet address?", "Hey Morpheus, can you tell me my wallet address?" 77 | - Response for all: 78 | { 79 | "response": "", 80 | "action": {"type": "Address"} 81 | } 82 | 83 | // Insufficient Information for Transfer 84 | - **Insufficient info for transfer**: 85 | - Question: "I want to transfer ETH." 86 | 87 | - Response: 88 | { 89 | "response": "Please provide the ETH amount and the target address for the transfer.", 90 | "action": {} 91 | } 92 | 93 | - **Bad Query**: 94 | - Questions: "please explain", "why does", "who is" 95 | - Response: 96 | { 97 | "response": "Sorry! I dont think I understand, what would you like me to explain?", 98 | "action": {} 99 | } 100 | 101 | // Non-action Queries 102 | - **Non-action query (e.g., general question)**: 103 | - Question: "What is stETH?" 104 | - Response: 105 | { 106 | "response": "stETH stands for staked Ether...", 107 | "action": {} 108 | } 109 | `; 110 | 111 | export const errorHandling = `###Error Handling: 112 | - For buy or transfer actions without a specified ETH amount, request the missing details. 113 | - For sell actions without a specified token amount, request the missing details. 114 | - Never include comments within the JSON objects returned. 115 | - Plan for detailed error messages for unsupported or incomplete action requests to guide users effectively.`; 116 | 117 | //TODO: allow for staking MOR and swap tokens 118 | //TODO: use RAG to include a database to tokenAddresses and symbols 119 | //TODO: include chat history 120 | //TODO: include error handling in prompt 121 | -------------------------------------------------------------------------------- /src/backend/services/system.ts: -------------------------------------------------------------------------------- 1 | import checkDiskSpace from 'check-disk-space'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import sudo from 'sudo-prompt'; 5 | import { ChildProcess } from 'child_process'; 6 | import { isDev, appPath } from './../index'; 7 | import { logger } from './logger'; 8 | 9 | export const getDiskSpaceInformation = async (url: string) => { 10 | return await checkDiskSpace(url); 11 | }; 12 | 13 | export const hasEnoughSpace = async (url: string, sizeInBytes: number) => { 14 | const diskSpace = await checkDiskSpace(url); 15 | 16 | return diskSpace.free > sizeInBytes; 17 | }; 18 | 19 | export const executeCommandElevated = (command: string, envOptions?: { OLLAMA_MODELS: string }) => { 20 | const options = { 21 | name: 'MorpheusAI SubMod', 22 | icns: './../logo_white.ico', 23 | ...(envOptions && { ...envOptions }), 24 | }; 25 | 26 | sudo.exec(command, options, (error, stdout, stderr) => { 27 | if (error) { 28 | throw error; 29 | } 30 | 31 | if (stderr) { 32 | throw stderr; 33 | } 34 | }); 35 | }; 36 | 37 | export const createDirectoryElevated = (path: string) => { 38 | const options = { 39 | name: 'MorpheusAI SubMod', 40 | icns: './../logo_white.ico', 41 | }; 42 | 43 | const command = `mkdir ${path}${process.platform !== 'win32' ? ' -p' : ''}`; 44 | 45 | logger.log({ 46 | level: 'info', 47 | message: `Creating directory with command: ${command}`, 48 | }); 49 | 50 | sudo.exec(command, options, (error, stdout, stderr) => { 51 | if (error) { 52 | throw error; 53 | } 54 | 55 | if (stderr) { 56 | throw stderr; 57 | } 58 | }); 59 | }; 60 | 61 | export const killProcess = (process: ChildProcess) => { 62 | if (os.platform() === 'win32') { 63 | const options = { 64 | name: 'MorpheusAI SubMod', 65 | icns: './../logo_white.ico', 66 | }; 67 | 68 | sudo.exec(`taskkill /pid ${process.pid} /f /t`, options, (err) => { 69 | logger.error(err); 70 | }); 71 | } else { 72 | process.kill(); 73 | } 74 | }; 75 | 76 | export const runDelayed = async (handler: () => Promise, delayInMs = 3000) => { 77 | return new Promise((resolve) => setTimeout(resolve, delayInMs)).then(async () => await handler()); 78 | }; 79 | 80 | export const getDefaultAppDataPathByPlatform = () => { 81 | switch (process.platform) { 82 | case 'win32': 83 | return path.join(os.homedir(), 'AppData', 'Local', 'MorpheusAI', 'SubMod'); 84 | case 'darwin': 85 | return path.join(os.homedir(), 'Library', 'Application Support', 'MorpheusAI', 'SubMod'); 86 | case 'linux': 87 | return path.join(os.homedir(), '.config', 'MorpheusAI', 'SubMod'); 88 | default: 89 | throw new Error(`Unsupported platform detected: ${process.platform}`); 90 | } 91 | }; 92 | 93 | export const getExecutablePathByPlatform = () => { 94 | switch (process.platform) { 95 | case 'win32': 96 | return isDev 97 | ? path.join(__dirname, '..', 'executables', 'ollama.exe') 98 | : path.join(appPath, 'resources', 'executables', 'ollama.exe'); 99 | case 'darwin': 100 | return isDev 101 | ? path.join(__dirname, '..', 'executables', 'ollama-darwin') 102 | : path.join(appPath, '..', 'Resources', 'executables', 'ollama-darwin'); 103 | case 'linux': 104 | return isDev 105 | ? path.join(__dirname, '..', 'executables', 'ollama-linux') 106 | : path.join(appPath, 'resources', 'executables', 'ollama-linux'); 107 | default: 108 | throw new Error(`Unsupported platform detected: ${process.platform}`); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/backend/storage.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store'; 2 | 3 | export type SchemaType = { 4 | modelsPath: string; 5 | }; 6 | 7 | const store = new Store({ 8 | defaults: { 9 | modelsPath: '', 10 | }, 11 | }); 12 | 13 | export const saveModelPathToStorage = (path: string) => { 14 | store.set('modelsPath', path); 15 | }; 16 | 17 | export const getModelPathFromStorage = () => { 18 | return store.get('modelsPath'); 19 | }; 20 | 21 | export const clearStore = () => { 22 | store.clear(); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/types.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from 'electron'; 2 | 3 | export type OllamaQuestion = { 4 | model: string; 5 | query: string; 6 | }; 7 | 8 | export interface IpcMainEventExtended extends IpcMainEvent { 9 | status: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | export enum IpcChannel { 2 | AppInit = 'app:init', 3 | GetModelsPath = 'app:getfolder', 4 | SetFolderPath = 'app:setfolder', 5 | Close = 'app:close', 6 | Minimize = 'app:minimize', 7 | } 8 | 9 | export enum OllamaChannel { 10 | OllamaInit = 'ollama:init', 11 | OllamaStatusUpdate = 'ollama:status', 12 | OllamaGetAllModels = 'ollama:getallmodels', 13 | OllamaQuestion = 'ollama:question', 14 | OllamaAnswer = 'ollama:answer', 15 | OllamaGetModel = 'ollama:getmodel', 16 | } 17 | 18 | export enum IpcMainChannel { 19 | Error = 'main:error', 20 | CommandOuput = 'command:output', 21 | } 22 | -------------------------------------------------------------------------------- /src/executables/ollama-darwin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/executables/ollama-darwin -------------------------------------------------------------------------------- /src/executables/ollama-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/executables/ollama-linux -------------------------------------------------------------------------------- /src/executables/ollama.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/executables/ollama.exe -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Montserrat-Light.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Montserrat-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Montserrat-LightItalic.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/frontend/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.icns -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_1024x1024.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo-sq.iconset/icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo-sq.iconset/icon_64x64.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src/frontend/assets/images/MOR_logo_circle.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/MOR_logo_circle.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src/frontend/assets/images/circle-mor-logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/circle-mor-logo.icns -------------------------------------------------------------------------------- /src/frontend/assets/images/circle-mor-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/circle-mor-logo.ico -------------------------------------------------------------------------------- /src/frontend/assets/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/frontend/assets/images/copy-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/copy-link.png -------------------------------------------------------------------------------- /src/frontend/assets/images/dmgbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/dmgbg.png -------------------------------------------------------------------------------- /src/frontend/assets/images/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/logo.png -------------------------------------------------------------------------------- /src/frontend/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/assets/images/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/logo_white.png -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/metamask_fox.png -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 54 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox_black_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/metamask_fox_black_font.png -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox_black_font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 33 | 37 | 41 | 43 | 47 | 51 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 87 | 89 | 91 | 92 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox_white_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/metamask_fox_white_font.png -------------------------------------------------------------------------------- /src/frontend/assets/images/metamask_fox_white_font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 33 | 37 | 41 | 43 | 47 | 51 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 87 | 89 | 91 | 92 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/frontend/assets/images/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/frontend/assets/images/morph_square.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/morph_square.icns -------------------------------------------------------------------------------- /src/frontend/assets/images/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorpheusAIs/Lite-Client/9761bf900628ec4b51dacd309ccc421ab6106219/src/frontend/assets/images/wallet.png -------------------------------------------------------------------------------- /src/frontend/components/buttons/connect-wallet-button.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { forwardRef } from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // img 6 | import wallet from './../../assets/images/wallet.png'; 7 | 8 | export interface Props { 9 | connected: boolean; 10 | connecting: boolean; 11 | onClick: () => void; 12 | } 13 | 14 | type BadgeProps = { 15 | $connected: boolean; 16 | $connecting: boolean; 17 | }; 18 | 19 | export default forwardRef((props: Props, ref) => { 20 | const { connected, connecting, onClick } = props; 21 | 22 | return ( 23 | 24 | 25 | {connected ? 'connected' : 'connect'} 26 | 27 | ); 28 | }); 29 | 30 | const ConnectWalletButton = { 31 | Wrapper: Styled.div` 32 | display: flex; 33 | position: relative; 34 | flex-direction: row; 35 | justify-content: space-between; 36 | align-items: center; 37 | height: 40px; 38 | border-radius: 30px; 39 | margin: 0; 40 | background: ${(props) => props.theme.colors.core}; 41 | cursor: pointer; 42 | transition: all 0.5s; 43 | border: 2px solid ${(props) => props.theme.colors.hunter}; 44 | z-index: 1; 45 | padding: 0 15px; 46 | 47 | &:hover { 48 | border: 2px solid ${(props) => props.theme.colors.emerald}; 49 | } 50 | `, 51 | Logo: Styled.img` 52 | display: flex; 53 | width: 20px; 54 | height: 20px; 55 | margin-right: 10px; 56 | `, 57 | Text: Styled.span` 58 | display: flex; 59 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 60 | font-size: ${(props) => props.theme.fonts.size.small}; 61 | color: ${(props) => props.theme.colors.notice}; 62 | margin-right: 10px; 63 | `, 64 | Badge: Styled.div` 65 | display: flex; 66 | width: 10px; 67 | height: 10px; 68 | border-radius: 5px; 69 | background-color: ${(props) => (props.$connecting ? 'orange' : props.$connected ? 'green' : 'red')}; 70 | `, 71 | }; 72 | -------------------------------------------------------------------------------- /src/frontend/components/buttons/meta-mask-button.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { forwardRef } from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // img 6 | import metamaskLogo from './../../assets/images/metamask_fox_white_font.png'; 7 | 8 | export interface Props { 9 | connected: boolean; 10 | connecting: boolean; 11 | onClick: () => void; 12 | } 13 | 14 | type BadgeProps = { 15 | $connected: boolean; 16 | $connecting: boolean; 17 | }; 18 | 19 | export default forwardRef((props: Props, ref) => { 20 | const { connected, connecting, onClick } = props; 21 | 22 | return ( 23 | 24 | 25 | {!connecting && } 26 | 27 | ); 28 | }); 29 | 30 | const MetaMaskButton = { 31 | Wrapper: Styled.div` 32 | display: flex; 33 | position: relative; 34 | flex-direction: row; 35 | justify-content: center; 36 | align-items: center; 37 | height: 50px; 38 | border-radius: 15px; 39 | margin: 0; 40 | background: ${(props) => props.theme.colors.core}; 41 | cursor: pointer; 42 | transition: all 0.5s; 43 | border: none; 44 | z-index: 1; 45 | 46 | &:hover { 47 | background: ${(props) => props.theme.colors.emerald}; 48 | } 49 | `, 50 | Logo: Styled.img` 51 | display: flex; 52 | width: 150px; 53 | height: 50px; 54 | `, 55 | Badge: Styled.div` 56 | display: flex; 57 | width: 10px; 58 | height: 10px; 59 | border-radius: 5px; 60 | background-color: ${(props) => (props.$connecting ? 'orange' : props.$connected ? 'green' : 'red')}; 61 | margin-right: 10px; 62 | `, 63 | }; 64 | -------------------------------------------------------------------------------- /src/frontend/components/buttons/navigation-button.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { ReactNode } from 'react'; 3 | import { Link, useLocation } from 'react-router-dom'; 4 | import Styled from 'styled-components'; 5 | 6 | interface ButtonProps { 7 | active: 'active' | ''; 8 | } 9 | 10 | interface MainNavButtonProps { 11 | text: string; 12 | icon: ReactNode; 13 | href: string; 14 | exact: boolean; 15 | } 16 | 17 | const MainNavigationButton = ({ text, icon, href, exact }: MainNavButtonProps) => { 18 | const path = useLocation(); 19 | const active = exact ? path.pathname === href : path.pathname.includes(href); 20 | 21 | return ( 22 | 23 | {icon} 24 | {text} 25 | 26 | ); 27 | }; 28 | 29 | const MainNavButton = { 30 | Wrapper: Styled(Link)` 31 | display: flex; 32 | flex-direction: column; 33 | height: 100%; 34 | margin: 0 15px; 35 | align-items: center; 36 | color: ${(props) => props.theme.colors.balance}; 37 | opacity: ${(props) => (props.active === 'active' ? 1 : 0.5)}; 38 | font-family: ${(props) => props.theme.fonts.family.primary.bold}; 39 | font-size: ${(props) => props.theme.fonts.size.medium}; 40 | text-decoration: none; 41 | transition: border 0.25s, color 0.25s; 42 | 43 | &:hover span { 44 | opacity: 0.75; 45 | } 46 | `, 47 | Icon: Styled.img` 48 | display: flex; 49 | width: 30px; 50 | height: 30px; 51 | `, 52 | ButtonText: Styled.span` 53 | display: flex; 54 | height: 100%; 55 | align-items: center; 56 | justify-content: center; 57 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 58 | font-size: ${(props) => props.theme.fonts.size.small}; 59 | color: ${(props) => props.theme.colors.hunter}; 60 | padding: 0 12px; 61 | `, 62 | }; 63 | 64 | export default MainNavigationButton; 65 | -------------------------------------------------------------------------------- /src/frontend/components/buttons/round-button.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled, { useTheme } from 'styled-components'; 4 | import { TailSpin } from 'react-loader-spinner'; 5 | 6 | export interface Props { 7 | value: string; 8 | logo?: string; 9 | inProgress?: boolean; 10 | onClick: () => void; 11 | } 12 | 13 | const RoundButton = ({ value, inProgress, onClick }: Props) => { 14 | const theme = useTheme(); 15 | 16 | return ( 17 | 18 | {!inProgress && value} 19 | {inProgress && ( 20 | 21 | )} 22 | 23 | ); 24 | }; 25 | 26 | const Button = { 27 | Wrapper: Styled.div` 28 | display: flex; 29 | position: relative; 30 | flex-direction: row; 31 | justify-content: space-between; 32 | align-items: center; 33 | height: 40px; 34 | border-radius: 30px; 35 | margin: 0; 36 | background: ${(props) => props.theme.colors.core}; 37 | cursor: pointer; 38 | transition: all 0.5s; 39 | border: 2px solid ${(props) => props.theme.colors.hunter}; 40 | z-index: 1; 41 | padding: 0 15px; 42 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 43 | font-size: ${(props) => props.theme.fonts.size.small}; 44 | color: ${(props) => props.theme.colors.notice}; 45 | 46 | &:hover { 47 | border: 2px solid ${(props) => props.theme.colors.emerald}; 48 | } 49 | `, 50 | Loader: Styled(TailSpin)` 51 | display: flex; 52 | width: 100%; 53 | position: fixed; 54 | top: 50%; 55 | left: 50%; 56 | transform: translate(-50%, -50%); 57 | `, 58 | }; 59 | 60 | export default RoundButton; 61 | -------------------------------------------------------------------------------- /src/frontend/components/layout/app-init.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { useEffect, useState } from 'react'; 3 | import Styled, { useTheme } from 'styled-components'; 4 | import { InfinitySpin } from 'react-loader-spinner'; 5 | import { OllamaChannel } from '../../../events'; 6 | 7 | const AppInit = () => { 8 | const [currentStatus, setCurrentStatus] = useState(''); 9 | const theme = useTheme(); 10 | 11 | useEffect(() => { 12 | window.backendBridge.ollama.onStatusUpdate((status: string) => { 13 | setCurrentStatus(status); 14 | }); 15 | 16 | return () => { 17 | window.backendBridge.removeAllListeners(OllamaChannel.OllamaStatusUpdate); 18 | }; 19 | }); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | Initializing... 27 | 28 | {currentStatus && {currentStatus}} 29 | 30 | ); 31 | }; 32 | 33 | const Main = { 34 | Layout: Styled.div` 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | align-items: center; 39 | width: 100%; 40 | height: 100%; 41 | background: ${(props) => props.theme.colors.core}; 42 | position: relative; 43 | `, 44 | Draggable: Styled.div` 45 | display: flex; 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | height: 35px; 51 | z-index: 0; 52 | -webkit-app-region: drag; 53 | `, 54 | LoaderWrapper: Styled.div` 55 | display: flex; 56 | width: 250px; 57 | height: 250px; 58 | flex-direction: column; 59 | align-items: center; 60 | justify-content: center; 61 | `, 62 | Loader: Styled(InfinitySpin)` 63 | display: flex; 64 | width: 100%; 65 | position: fixed; 66 | top: 50%; 67 | left: 50%; 68 | transform: translate(-50%, -50%); 69 | `, 70 | LoaderText: Styled.span` 71 | display: flex; 72 | font-size: 22px; 73 | width: 100%; 74 | align-self: center; 75 | align-items: center; 76 | justify-content: center; 77 | font-family: ${(props) => props.theme.fonts.family.primary.bold}; 78 | color: ${(props) => props.theme.colors.emerald}; 79 | `, 80 | StatusText: Styled.div` 81 | display: flex; 82 | color: ${(props) => props.theme.colors.notice}; 83 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 84 | font-size: ${(props) => props.theme.fonts.size.smallest}; 85 | position: absolute; 86 | bottom: 10px; 87 | `, 88 | }; 89 | 90 | export default AppInit; 91 | -------------------------------------------------------------------------------- /src/frontend/components/layout/bottom-bar.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled from 'styled-components'; 4 | import MainNavigation from './navigation'; 5 | 6 | export default () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | const LeftBar = { 17 | Layout: Styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | width: 100%; 21 | height: 100%; 22 | background: #ddd; 23 | padding-top: 20px; 24 | background: ${(props) => props.theme.colors.core}; 25 | justify-content: center; 26 | align-items: center; 27 | `, 28 | HeaderWrapper: Styled.div` 29 | display: flex; 30 | flex-direction: column; 31 | `, 32 | Header: Styled.h2` 33 | font-size: ${(props) => props.theme.fonts.size.medium}; 34 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 35 | `, 36 | MainNav: Styled(MainNavigation)` 37 | display: flex; 38 | flex-direction: column; 39 | `, 40 | }; 41 | -------------------------------------------------------------------------------- /src/frontend/components/layout/main.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // layout components 6 | import TopBar from './top-bar'; 7 | import BottomBar from './bottom-bar'; 8 | 9 | // router 10 | import { MainRouter } from '../../router'; 11 | 12 | export default () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | const Main = { 29 | Layout: Styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | width: 100%; 34 | height: 100%; 35 | background: ${(props) => props.theme.colors.core}; 36 | `, 37 | TopWrapper: Styled.div` 38 | display: flex; 39 | width: 100%; 40 | height: ${(props) => props.theme.layout.topBarHeight}px; 41 | flex-shrink: 0; 42 | `, 43 | MainWrapper: Styled.div` 44 | display: flex; 45 | width: 80%; 46 | max-width: 850px; 47 | flex-grow: 1; 48 | border-radius: 30px; 49 | border: 5px solid ${(props) => props.theme.colors.hunter}; 50 | padding: 10px; 51 | overflow: hidden; 52 | `, 53 | BottomWrapper: Styled.div` 54 | display: flex; 55 | width: 100%; 56 | height: ${(props) => props.theme.layout.bottomBarHeight}px; 57 | flex-shrink: 0; 58 | `, 59 | }; 60 | -------------------------------------------------------------------------------- /src/frontend/components/layout/navigation.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // components 6 | import MainNavigationButton from '../buttons/navigation-button'; 7 | 8 | // images 9 | import ChatImg from './../../assets/images/chat.svg'; 10 | import HomeImg from './../../assets/images/home.svg'; 11 | import SettingsImg from './../../assets/images/settings.svg'; 12 | 13 | const MainNavigation = () => { 14 | return ( 15 | 16 | } text="chat" href="/chat" exact={true} /> 17 | {/* } text="home" href="/" exact={true} /> */} 18 | } 20 | text="settings" 21 | href="/settings" 22 | exact={true} 23 | /> 24 | 25 | ); 26 | }; 27 | 28 | const TopNav = { 29 | Layout: Styled.div` 30 | display: flex; 31 | flex-direction: row; 32 | align-items: flex-start; 33 | height: 100%; 34 | width: 100%; 35 | `, 36 | MainNavButton: Styled(MainNavigationButton)` 37 | display: flex; 38 | margin: 0 20px; 39 | `, 40 | Icon: Styled.img` 41 | display: flex; 42 | width: 50px; 43 | height: 50px; 44 | `, 45 | ChatIcon: Styled(ChatImg)` 46 | display: flex; 47 | width: 50px; 48 | height: 50px; 49 | `, 50 | HomeIcon: Styled(HomeImg)` 51 | display: flex; 52 | width: 50px; 53 | height: 50px; 54 | `, 55 | SettingsIcon: Styled(SettingsImg)` 56 | display: flex; 57 | width: 50px; 58 | height: 50px; 59 | `, 60 | }; 61 | 62 | export default MainNavigation; 63 | -------------------------------------------------------------------------------- /src/frontend/components/layout/top-bar.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { useRef, useState } from 'react'; 3 | import Styled from 'styled-components'; 4 | import { useSDK } from '@metamask/sdk-react'; 5 | 6 | // components 7 | import MetaMaskModal from '../modals/metamask-modal'; 8 | import ConnectWalletButton from '../buttons/connect-wallet-button'; 9 | 10 | // custonm hooks 11 | import { useClickOutside } from '../../hooks'; 12 | 13 | // img 14 | import logo from './../../assets/images/logo_white.png'; 15 | import close from './../../assets/images/close.svg'; 16 | import minimize from './../../assets/images/minimize.svg'; 17 | 18 | export default () => { 19 | const { ready, sdk, connected, connecting, provider, chainId, account, balance } = useSDK(); 20 | const [metamaskVisible, setMetamaskVisible] = useState(false); 21 | 22 | const metamaskButtonRef = useRef(null); 23 | const ref = useClickOutside((event) => { 24 | // eslint-disable-next-line 25 | // @ts-ignore 26 | if (metamaskButtonRef.current && !metamaskButtonRef.current.contains(event.target)) { 27 | setMetamaskVisible(false); 28 | } 29 | }); 30 | 31 | const connect = async () => { 32 | try { 33 | const connectResult = await sdk?.connect(); 34 | } catch (err) { 35 | console.error(`failed to connect...`, err); 36 | } 37 | }; 38 | 39 | const onConnectClicked = async () => { 40 | if (connected) { 41 | if (metamaskVisible) { 42 | setMetamaskVisible(false); 43 | 44 | return; 45 | } 46 | 47 | setMetamaskVisible(true); 48 | 49 | return; 50 | } 51 | 52 | await connect(); 53 | }; 54 | 55 | const onCloseClicked = () => { 56 | window.backendBridge.main.close(); 57 | }; 58 | 59 | const onMinimizeClicked = () => { 60 | window.backendBridge.main.minimize(); 61 | }; 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | {/* 69 | */} 70 | 71 | 72 | 73 | Morpheus 74 | 75 | 76 | 80 | {metamaskVisible && } 81 | 82 | 83 | 84 | ); 85 | }; 86 | 87 | const TopBar = { 88 | Layout: Styled.div` 89 | display: flex; 90 | flex-direction: row; 91 | justify-content: center; 92 | width: 100%; 93 | height: 100%; 94 | margin: 0; 95 | background: ${(props) => props.theme.colors.core}; 96 | `, 97 | Draggable: Styled.div` 98 | display: flex; 99 | position: absolute; 100 | top: 0; 101 | left: 0; 102 | width: 80%; 103 | height: 40px; 104 | z-index: 0; 105 | -webkit-app-region: drag; 106 | `, 107 | HeaderWrapper: Styled.div` 108 | display: flex; 109 | flex-direction: row; 110 | width: 100%; 111 | margin: 0 20px; 112 | align-items: center; 113 | justify-content: space-between; 114 | user-select: none; 115 | `, 116 | Left: Styled.div` 117 | display: flex; 118 | flex: 1; 119 | `, 120 | Middle: Styled.div` 121 | display: flex; 122 | flex: 1; 123 | flex-direction: column; 124 | width: 250px; 125 | align-items: center; 126 | justify-content: center; 127 | z-index: 1; 128 | position: relative; 129 | height: 100%; 130 | `, 131 | Right: Styled.div` 132 | display: flex; 133 | flex: 1; 134 | flex-direction: row; 135 | align-items: center; 136 | justify-content: flex-end; 137 | z-index: 1; 138 | position: relative; 139 | `, 140 | CloseButton: Styled(close)` 141 | display: flex; 142 | width: 25px; 143 | height: 25px; 144 | cursor: pointer; 145 | margin-right: 10px; 146 | `, 147 | MinimizeButton: Styled(minimize)` 148 | display: flex; 149 | width: 25px; 150 | height: 25px; 151 | cursor: pointer; 152 | `, 153 | Logo: Styled.img` 154 | display: flex; 155 | height: 100px; 156 | width: 100px; 157 | `, 158 | Header: Styled.h2` 159 | font-size: ${(props) => props.theme.fonts.size.medium}; 160 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 161 | font-weight: normal; 162 | color: ${(props) => props.theme.colors.balance}; 163 | position: absolute; 164 | bottom: 10px; 165 | `, 166 | }; 167 | -------------------------------------------------------------------------------- /src/frontend/components/modals/backdrop.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | export interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | const BackDropComponent = ({ children }: Props) => { 10 | return {children}; 11 | }; 12 | 13 | const BackDrop = { 14 | Layout: Styled.div` 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | background-color: rgba(0, 0, 0, 0.7); 21 | backdrop-filter: blur(5px); 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | z-index: 1000; 26 | animation: fadeIn 0.3s; 27 | `, 28 | }; 29 | 30 | export default BackDropComponent; 31 | -------------------------------------------------------------------------------- /src/frontend/components/modals/choose-directory-modal.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // custom 6 | import BackDropComponent from './backdrop'; 7 | import RoundButton from '../buttons/round-button'; 8 | 9 | export interface Props { 10 | onClick: () => void; 11 | } 12 | 13 | const ChooseDirectoryModalComponent = ({ onClick }: Props) => { 14 | return ( 15 | 16 | 17 | Welcome to MorpheusAI 18 | 19 | Welcome to Morpheus client, this tool utilizes AI LLM models, which can be big in file 20 | size. Please select a folder you would like them to reside in. 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | const ChooseDirectoryModal = { 31 | Layout: Styled.div` 32 | display: flex; 33 | position: relative; 34 | align-items: center; 35 | justify-content: center; 36 | flex-direction: column; 37 | background: ${(props) => props.theme.colors.core}; 38 | width: 450px; 39 | height: 300px; 40 | border-radius: 30px; 41 | border: 5px solid ${(props) => props.theme.colors.hunter}; 42 | padding: 20px; 43 | `, 44 | Title: Styled.h2` 45 | display: flex; 46 | color: ${(props) => props.theme.colors.emerald}; 47 | font-size: ${(props) => props.theme.fonts.size.medium}; 48 | font-family: ${(props) => props.theme.fonts.family.primary.bold}; 49 | margin-bottom: 10px; 50 | `, 51 | Description: Styled.p` 52 | display: flex; 53 | color: ${(props) => props.theme.colors.emerald}; 54 | font-size: ${(props) => props.theme.fonts.size.small}; 55 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 56 | text-align: center; 57 | `, 58 | ButtonRow: Styled.div` 59 | display: flex; 60 | flex-direction: row; 61 | position: absolute; 62 | bottom: 20px; 63 | `, 64 | }; 65 | 66 | export default ChooseDirectoryModalComponent; 67 | -------------------------------------------------------------------------------- /src/frontend/components/modals/metamask-modal.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { forwardRef } from 'react'; 3 | import Styled from 'styled-components'; 4 | 5 | // helpers 6 | import { truncateString } from './../../helpers'; 7 | 8 | // img 9 | import copyIcon from './../../assets/images/copy-link.png'; 10 | 11 | export interface Props { 12 | account: string; 13 | } 14 | 15 | const MetaMaskModal = forwardRef((props: Props, ref) => { 16 | const { account } = props; 17 | 18 | const isClipboardAvailable = navigator.clipboard && navigator.clipboard.writeText !== undefined; 19 | 20 | return ( 21 | 22 | 23 | Connected Account 24 | {isClipboardAvailable ? ( 25 | 26 | {truncateString(account)} 27 | navigator.clipboard.writeText(account)} 30 | /> 31 | 32 | ) : ( 33 | {truncateString(account)} 34 | )} 35 | 36 | 37 | 38 | ); 39 | }); 40 | 41 | const MetaMaskRoot = { 42 | Layout: Styled.div` 43 | display: flex; 44 | flex-direction: column; 45 | width: 250px; 46 | height: 70px; 47 | border-radius: 10px; 48 | background: ${(props) => props.theme.colors.core}; 49 | border: 5px solid ${(props) => props.theme.colors.hunter}; 50 | position: absolute; 51 | right: -10px; 52 | top: 75px; 53 | padding: 10px; 54 | `, 55 | Group: Styled.div` 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | width: 100%; 60 | height: 30px; 61 | `, 62 | Row: Styled.div` 63 | display: flex; 64 | flex-direction: row; 65 | align-items: center; 66 | `, 67 | Label: Styled.label` 68 | display: flex; 69 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 70 | font-size: ${(props) => props.theme.fonts.size.small}; 71 | color: ${(props) => props.theme.colors.balance}; 72 | `, 73 | Value: Styled.span` 74 | display: flex; 75 | font-family: ${(props) => props.theme.fonts.family.secondary.bold}; 76 | font-size: ${(props) => props.theme.fonts.size.small}; 77 | color: ${(props) => props.theme.colors.notice}; 78 | `, 79 | CopyButton: Styled.img` 80 | display: flex; 81 | width: 15px; 82 | height: 15px; 83 | margin-left: 5px; 84 | cursor: pointer; 85 | `, 86 | }; 87 | 88 | export default MetaMaskModal; 89 | -------------------------------------------------------------------------------- /src/frontend/components/modals/qr-code-modal.tsx: -------------------------------------------------------------------------------- 1 | export const QrCodeModal = ({ onClose }: { onClose: () => void }) => { 2 | return ( 3 |

onClose()} 19 | > 20 |
30 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/frontend/constants.ts: -------------------------------------------------------------------------------- 1 | export const LOGO_METAMASK_BASE64 = `data:image/png;base64,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`; 2 | -------------------------------------------------------------------------------- /src/frontend/contexts.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { createContext, PropsWithChildren, useContext, useState } from 'react'; 4 | import { AIMessage } from './types'; 5 | 6 | export type AIMessagesContextType = [Array, (messages: Array) => void]; 7 | 8 | export const AIMessagesContext = createContext([ 9 | [], 10 | (messages: Array) => {}, 11 | ]); 12 | 13 | export const AIMessagesProvider = ({ children }: PropsWithChildren) => { 14 | const [messages, setMessages] = useState>([]); 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export const useAIMessagesContext = () => { 24 | const context = useContext(AIMessagesContext); 25 | 26 | if (!context) { 27 | throw new Error(`useAIMessagesContext must be used within AIMessagesProvider`); 28 | } 29 | 30 | return context; 31 | }; 32 | -------------------------------------------------------------------------------- /src/frontend/fonts.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.ttf'; 2 | -------------------------------------------------------------------------------- /src/frontend/helpers.ts: -------------------------------------------------------------------------------- 1 | import { lightTheme } from './theme/theme'; 2 | 3 | export const updateQrCode = (link: string) => { 4 | const qrCodeNode = document.getElementById('sdk-qrcode-container'); 5 | 6 | const LOGO = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAMAAADbzpy9AAAC/VBMVEUAAAAA/wAAgIAAqlUAgIAAmWYrqlUkkm0gn2AcqnEamWYXol0VlWoUnWISpG0RmWYQn2AelmkcnGMboWsamWYYnmEXl2gWm2QVn2oUmWYUnWIToWgbm2QanmEamWYZnGMYn2gXm2QXnmIWmWYVnGMVn2cUmmUanWIamWYZm2QYnmcYmmUXnGMXn2YWm2QWnWcVmmUVnGManmYZm2QZnWcYmmUYnGMXnmYXm2QWnWcWmmUWnGMVnWYVm2QZnGcYnmUYm2QYnWYXnGcXnmUWm2QWnWYWmmUVnGcYnWUYm2QYnGYXmmUXnGMXnWUXm2QWnGYWnWUWm2QWnWUYm2QYnGYYnWUXm2QXnGUXm2QXnGYWnWUWm2QWnGUWnWQYnGYYnWUYm2QXnGUXnWQXnGYXnWUXm2QWnGYWnWUWm2YYnGUYm2QYnGYXnWUXm2YXnGUXnWQXnGYWnWUWm2YWnGUYnWQYnGYYnGUXm2YXnGUXnWQXm2YXnGUWm2YWnGUWnWQYm2YYnGUYnWQXnGUXnGQXm2YXnGUXnWQXnGUWnGUWm2YYnGUYnWQYnGUXnGUXm2YXnGUXnWQXnGUWnWYWnGUYnGQYm2UXnGUXnWYXnGUXnGQXm2UXnGUXnWYWnGUWnGQYm2UYnGUXnGYXm2UXnGQXnWUXnGUXnGYXm2UXnGUWnWUYnGUYnGYXm2UXnGUXnGUXnGUXnGQXnWUXnGUXnGUWnGUYnGQYnWUXnGUXnGUXm2UXnGQXnWUXnGUXnGUXm2UWnGQWnGUYnGUXnGUXnWUXnGQXnGUXnGUXnGUXnWUXnGQXnGUWm2UYnGYXnGUXnGUXnGUXm2UXnGYXnGUXnGUXnGUXnWUWnGYYnGUXnGUXnGUXnGUXnGYXnGUXnGUXnGUXnGUXnGYWnGUXm2UXnGUXnGUXnGQXnGUXnWUXnGUXnGUXnGQXnGUXnGUXnGUXnGUXnGUXnGUXnGUXnGUXnGUXm2UXnGUXnGUXnGUXnGUXnGUXnGUXnGUXnGUXnGX////NnhjiAAAA/XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+Ajbw8wAAAAFiS0dE/tIAwlMAACn6SURBVHja7d2Jv11VeTDgfRMyAJlAzQgihCHIDHUA0WLR1gFxStWqadXPW1u1qUO9altNRe2tU5tW7dUq7dU6RBAEUWuoQwuillk0DEGmEEGGhATJAPfu3+8Lg5DhDuectfbea+/zPH/AvWevtd73nL33Wu+bZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCI3/mjyQYB4pr8R7/T8CucdPNt/U800RDP3L5b1jT++/idef7AuSf3mG2I4rjBrXn+jsZf5vR1+Tarlu5pxiHU1CWXPxhP98xs/qX+ff6QewYONe0Q4oD+Ox+Opg93wcXO2fTwteZDKxdPNPfQmZ6TVzzwSChtntcNF/yZ/FGr+/a2AKB9M3p//lgcDXTFJR889NgV55sGj7QIoD2HLN+4XRANdcld/5n5Di5eMslKgFZNOGXl8A4RdEaXXPhT8p2s7V9gOUArZvfduHP8HN8t1/6Dna8837LiZEsCxnPcwH27BM/3u+bqn5+P4OLePawLGN3kxReMFDnP654RuGyk68/XLX+SxQEjm7fs1yOGzZVdtA37tfnIhlaeYjM67OrEFfePEjSv6aJRmHRjPppr+mZZJbC9ab1Xjhowt3TVO8m/zEe3YeAwSwV+68D+u8YIl6VdNRZ73pmP5YLFu1kvsG3TxsnnDo8VKndN667xOC0f25plj7do6HYzl/5ynED5QJeNyOz7xhmQfPPg0RYO3ezogXvHi5JNc7ttUD6Vj89mdLrWxFNWthAin+y6cdn//haGJb+tf19LiO4zp+/mVuLjgQO7b2i+mrdkq83odJvjBja1Fh1f6cLBOWo4b9GlveoM0jWmLL6o1cjIj+vGATq/5eHJ1y/f34KiGyxYdkfrcbGyK4fouXkbbEanC4y+sXxEz+nOUbokb8t16gzSZNN7r2ovIi7v0u/SV+Vt2jhwhOVFMx3cv67deHhllw7VxNV522xGp4HG21g+ol92bSi8Je/ArcueYKHRJLOW3tBJJLy5awdsj193Ml755hUnWGw0xbEDv+koDO7s4k0K7887dHHv7lYc9Td58cpOY+B9XTxse2/sdNTy2zW9p+7m9a3pOAB+09XHx5fnnRvS9J46e6jNfMf+qavHbt+QocvzqzW9p56mLrkiaOnfv193j98X8zD3DDzZIqRuFo5ZDbAVX+jyETxyOHAANb2nZiY81ma+c8d0+yh+Ow+3uu9xliP1MKP3FxGW/Le6fhyfncewafAoS5L0HbL83igL/iRDeVEehzqDpH6PsnOb+Y791GBmL89j+VX/PoaTVM3uuynaUn+Z4cwmXBttODW9J1UjtZnv2PVeB2zzp3lMl2h6T2qmLL4w6iLvNaQPjuraqIOq6T1pmb/s13FX+O0Oaj3kvXlk6gySjDarAbbiPUb1ITPX59Fd27eXgaVq03t/Fn9tb5hlYB/28bwAGwYON7JU6aD+u4tY2R8zso9YsCUvhDqDVKajaoAttShTT+JR/54X5FZN76nk9nvcNvMdO93oPurQoaJGeVudweONL+U6psNqgK0Ydge+nXPzAl3cO9UIU5aAaoCtOMcIb+eZeaE0vackc/tuKXYtn2iMt3dhsaOdP3CuzegULqwaYCt+YpB38OK8cKvUGaRIU5ZcVvwqPtU476Dn58WPeb5+YJGRphgH9N9ZwhK+eoKR3tEb8jKoM0gh33sxqgG24vXGeuffebfm5dD0nshm9P68pMX7K+8Hd/GuvCybBo803MRy8PKNpS3dvzLcu6btdXl5Ll5iMzoRxKsG2FIzkJlGfFf/kJdprab3hJrdd2Opi7bfkI9gzqZSJ2FbncFnGHQ6F7UaYEvHJ+Yb9JH8W142Te/p0OTFF5S+XD9r2Ed0yFDpU7Gt6f1+Bp52zYtdDbCl422HGviRnZVXQNN72r1HGby/ipX6dSM/iqfm1bhm6TSDT2tC28x3Tn2IUf1PRVOi6T2tOTC4zXzHfmj0R/XCvDLDNqMzjgllbSwf0QtMwKh6fpZX6HpN7xndzN5VVa7OXzjeNoY/ziu1efBoc8BIFkVqM9+xJeZgDJNuyium6T27mHjKyqrX5S2TTcNY3p5XTtN7djCn7+bqV+XbzMOY9ryz+jnS9J7HHDewKYElefd0MzG2D+ZJuFTTex5sM/+jNNbjaeZiHLPvS2Om8vXL9zcb3W3+sjsSWYyb5pqN8Xw6T4Wm912tgDbzHfuU6RjXgVvzdKx6ywwz0o1mvPXqhJbh1gPNyPjm9a1JaM40ve9CBbWZd4K7YAU30WuXpvddpbA28x1uLFIzph2lV1ca063qDHaLWUtvSGjhqVLXyQzemNAMbl5xgilpvmMLbDPfPnVx/WbEHbI75PIc3L8uoam8vf+JpqSpCm8z35aNA0eYkiDTe69KaD7VGWzqM7XBlHYB6DYYw7a+nPcnNKlXa3rfNFOXXJ7Sl5OSUtEc0H9HQjN7z4By0w2ysP/OhBbX+oFFpiSiKUsu86VAdNVWA9x1p7Kfs02/DV3tNrT+ymsz34oHzlXNoRhzU6io8tiJRU3v6+2QqqsB7uC2/n1NSWESe9WuzmB971FKbTPfwiahqeakWMektbmvf4EpqZ/ZfTcltIg2r9BrqQwzl/4yqeME7kzr9rQsqYNQa5Y93pSU9Tsztc3o6gzWxpTFF9pY3sUOTKpkwrrlTzIldTC/ijbzo9owcJgpKd203iuT2tqhzmDyTkxqD/I1fbNMiYVgIfii8UXjp6efnm5t3dp62OVhVzdL7GH6JR6mp8HrNcbg9T2jSW1Dz9NNSSrS2jCoMXFyv0ZtIWYXjigwPoeW2EFqhyKPMiWJSuuY9FZ3s5U++UqsDMPjTEnKdyxpFWa5tFdhlkoo/ES7EisFp+l9+Q5IagkoNVkXqRWftUewTD1p/ehU3NoNbseUuy/xMZd2GoTQYKcLHbx8Y0KTroFXPWnp1120DCUWTYS7hiblxF1PNyS1np5hSgp5ppXUcaVbfUMkIHRz/8SXnJ/SL9gfv2aKOY1rymt/ktAED5//ktBNG/PNaQTvDt/hf1BSxRhu79/PrMYzr29NQpO7Ifgp+LZTV39tViOYsX7bmcLQcx/Te3/mLV0j71GSeu9+bd9egdczu+/G/F6706P4aB6lisGJiTW9n2ZmQ01dckWzdvo9/KjmE2Y2igVbHjn3EVo3af6ytJreP9nchjiw/65GnS2YvPiChw9HupON5PPR5mbK4h85+9QIyZ1mDP1Wm/do9dz/MLuRHDIU9dfgJqet625m7y8SmsS499HDR5vfWL6xw7mP0OdPc9R3qbdFSVVsCn9yv2Pjhm+a4GieFvncx8RTVJSrq4mJ1YgMnrudH9U8yxzHc0H0cx+pfWupYdvir8WkqlIPht5W7Hq+5qcmOaIXFbCrd2bvqmbdJzdfWs+nrg9+PjVS44aXmOaIeq4q4hxRYk/m9ekZU1pvxIbD34gdPTDCj95rJpjpmF5X0MnltPYC6Aw4qtT24IT2+pw0Ss2H/2eqo5p002jnPkJrpTRu92ETJdZdPHjX76gv9m7TjieydxZ47uO4wZSWZfh5h4Zp3DmjMR7V9Jnu2Ktn3VjnPkLrwc5LrOn94Sb8tw5KrM186H7wKYsvGmPmZ5nw2D5c7LmP3x4USIQ6g488vm5YNcAFYz+q+Ygpj27OfUWf+0itilTXN71Pq3pbCQe0t+oaWoCB4s99PFgMIaHtRSuO7+b5TqxebP+C4Ec14zZu+JwoL8DC8fdchJ/70PQ+DY1rM39w/7rx94aos1CIM0qa4qR6c9zWjb050uqJs2nwyHIe1ZwtxgvxlLLOfaTV9P6Bbqsz2LQufC0/qtFRoSDfL+0xVmL9R1d1T//R1Pr+Rnjk3uqjmv8V4QV5fpnnPtLqeL6+OzqeL2zYoLf1qOYUEV6Uy0o99zFlyWXN+vJL3ISm/cxrr3HDKsfbCvOa9h4OnPV7oQ8HnvnVpIrvL53Z3Lmd+bbrEhrqrV99ZugFnXRGewcZXie+CzOp3W0W4ec+0nrAv2F5Q7cIzV22LqFhvq2Co5NrJovv4vxlBec+0tpSsHWwgedYjkiqQE/45pmFHRRreIfoLtC0DqpnDH/3RaF3j8d+LqGFPXzmIc2a1EPPTmjP3abPHRv6qOYF3x5q//+umy66i/SBis59jFTkrbrnpIMN6kq8YCChmgbhC2VGh40bPiS2CzW7w3No9wV/kUw89bvpfDHee1pDGkjO+Pt0DqQMf/fU0PdWR32mw8vZNFdsF+uTFd66plQa4taXN2E2T0mnw034w7CQxg3/KrILtn/AD9vbGtX0/tzaH2KZf0Yyg3lthDZfAY0bhg4S2UX7StA7ia+cGPjve04+K5VdShveWuszLBPelkpnmwfOCj4NdPyXtoR8gjPEdeGOCnzSEL4hcH4ydQbPr/FD0jnfTmQQI2wxDm7c8HRxXbzgXRXrBxYFr5QL01j0v67t+YZTEzmUEn6oKULjhu+J6hI8J8L7zHN/P/i36X9uSWHdDy+vZaWf3T+VRNbY8p+hNdZ6nnN2hDvX54nqMlwSY82El1mYnUYb00tquAl9/k9SGLnwwi2RnpVfqZVOKV4ZaY9geGGnJOoM3vq0uk3g8WsTSBvhpeKivZ1/tZguxcTV8RZPaBOCQxJoer/5T+o1f6+uvph8hOK08Ro33DxJTJfjzfFW0M3vDW16v9fbV1ceBx+s0eT1fKTy4Vr99tBNG4+PWQ5/qYguyR4xX4duWfGM4C+fygvQfLo2RWAmfqbioYpQEClu44a7ponosrwv8u1ucIOuhVU3vf9yTX7uTl5R7TjFaPkXucrC34nn0uwdu4HB7f2hLUGrbnp/3h51mLg9vlPpIF0d3GY+fl2n3zxBPJdnefQ1df8Zzw79UCd9rcoD4itrsKFj9x9UOED3f+2k0M//rBXxK0l+UjSXaN8iSoFeE/x91F512si+kfzdyqRvVjc6MX5TFtG44YGForlMX+yWO+A2nJF4FfSJX65u00aEp1jF7JH/slgu1REF7bwaTu2ZeztOT3oHYk9V71M2p/ze7FixXK7izlZe/47Qt/xPeE9Fm9Hfn/KMfbCaMbnpPaFPH2cV2LhhpUgu2bOL/IoaPDr4K+rcKjajD78y3Ql7eSW78y9YnPbe4JNFctkuKvamOLzpfX8FnULue2qq03VsBfdvGweOCP0CKPg00uWOt5X/DVbwqotxdvKq0mNl7T5pzta88tta1eH88yvEcfnP6K8teuVt+dIJoc8Dn3vOUMnh8uMpKU7WlJ+WPAxD5zw39Mv8aV/cXPSnvH6iOC7fn5aw/i4Nrg+1ILw+VHs+neJcfbbcMVi/fP/QTFdKjbc/F8VVfIutrcsavKjUqHldelP1mlIH4LLemlSVvXNPUVyF95b0q3flKaG/eo8rszfqpuS2BhxV4oPRrSuCX1ScuKKkowN/K4YrMXN9WasxRteN8loP3fi4tOZpr+tLu/TwzjnTeq8s68Pe+3gxXI2PlfdFFt7na1J5m9HPTao6x4Tzyrru8F59B5bZq+8fRXBFFpRaaDx8L9ExAyXVGfyblGZpWTnXvHlFaHOSknftbd1PBFfl9HIf1t+6LHT3cklN74f+IJ05OrmU6mhrgtvMlzQ1jxkUv5VZVPYuic0rQrd2lPO1dtf+qUzRfnfU5Mdg6Ttbjxa/1Tmn9P2IEU5oH1hCncHLdk9jgqZeXMLjp8MCP2QltRDOE70VemZegdv7Q/vEl/DofiCNCfq3oq/zmr5ZgR9xbokvvLZzkuitUjU9XIfODW5vXvhmgdenMD1vTH+LzeDWSpbQT8VupV6cV+Tq1Jvebzqu+tk5utDWSxHazC+5rKr18zKxW6men1c189vqDB4aum4XX1DkPrDK9xftVeR7ivA28wcUVA2wFasdb6vYG/LqROjtc9xAcV/K3614cU74VmGXtiV4Y3lPtV20ekVuxSbdnFdpdYSiDzcW9eHeV+3UfKCo61rbvyDwo82ooFTK9m7bXeRW7V15tRJuej/0vCon5gUFbbKJUJxt+caKF817xG3lZqzLqxa+lA8pZinffUB187LfnV2WptvZejJL3FavP69ejB/PRTzmvbyyn8SF7PxK+sawDR8VtQmYsymBpRDhcV0hzTsGq5qVz3Xbo+g2bH2iqE3BZ/M0hL8gLKBd2BuqmZPodR3DX35PLvTldzs+L2aTcMhQIgsiwpak6A1KN/1OFVMSe+dX+Ha7Stv67mj4MDGbhq/nyUhvE/RNFewD2zvqzq8IG/yPG7w/nTXyDRGbiIO//kA6yyL/+Z+HNr0/M+rn+Xbp9cAm/FfUCzgz8ONMe9NVCS2PoXMWidhklFSWutW3bYEHveMmjnxZ2bMRuUtsWOIoo4hB68KL5hNXOY0wWhZUWiZy4hh6frlT8cKhZBLHhGo3lu/s0l4NERKUyMu2RwQUs4ucOPK7F5Y5DU+K/Wao48Qxs3dVQgsiQuMGClJ8s892dFw+N3biyK/Yo7w5mHpJnkbiWLT83oQWQ3jjBgp9LpfAhuLtdFawP3riyL9Q3gx8Pk8hcUw8ZWWe0joIPpRA4Q6p/TdN/MRR3jHuP8sTSBxz+m5OaAmEN26gHMWc+yjv3raAxLH5KeUM/VM3V584Sm24WeSzLsq/Y0nraXqbbZALSBz5zU8oY9z3viGvOHFMWfyjxrxdowoLKywMF/j+vojEkZ9fQj2wCd/Jq00cC5bdkdCkhzduoArRz32E7aZofTN6IYkjP634Ef9wXmniKK3NfEvCGzdQmaqK34/suhbLSRSTOIZfUvRonzJcYeKY3ntVPb8mSNLcvlsSWk8bB46oLHHk6wreB3ZgQbXYWkkcB/evS2iaw09JU71KGvwFPS4rKHEUvA9s90vzihJHyW3mx9+8s4eoa4Zjy28pPIa14zW9Lypx5F8scpBPz6tJHLOW3pDQ5G6xsbxRUltdz6gkceRvKm6E35xXkjgS+04Irj1LapL7Pbt7BYlj6zOKGt6nba4gcSR2F2pjeUMd1H93Qsvs9v79Sk8che0Dm13gI+jREse8pJ57hzduIF3Te3+W0ju7UWriFZg48v8uZB9Y5JpfrSSOtN60hzduIHFp7RK6eum0chNH/qEiBrXQ3jYjJI6pS65IatPGYj2ku8D8pPYl3zPw5FITx/BL44/oi4ZLTRwLk6oGGN64gbpI6yTU8M5fWIUmjnxD9GK5B63Py0sciZ1fXLVUNcCuktbZ69V9jystceRXRl7qhe38GiFxzOj9RULT9kB44wZqJ61qL5sGjyorceRfijuQ/56XlThSq9GklWN3SrW+XNGJI39zzFF8a15O4kiuKuTuIqh7pVXR9lf9+5SSOLaeGG8En76llMSRWh3qE8ROl0urhv5DRx0KTxz52nmxhm928Zuwzkyt88WtqgHy4G/gF34nqd/Arzuv+H8Sax/Ybt8r/rOe97qLk3oHdqpNGzwirT6BZeiPM3Af6bJh2zBwuGhhO2ntRSzhi/NlMUbt1OGuGrRr+/YSKezsuMH7u+m7M8J+x4J3fqVFNUBGM69vTfcEwqrpocO151XdM1rrVQNkDJMXX9A1sfDl0MEa7JqhukQ1QMa9Y0nq1V+R3ho2UEu7ZJhUA6Q1s/tu7IqI2PrMkFE6fktXDNIjm/LoArt/PLQY024v+143BMWaOZ0P0Zxbu2GEfviHodUAD/uYu5za+HiE8o8HL9/Y/Lj4fsedTXf7QfNHZ/uDh5158HzNP4rH2liwJcZPzLSa3hfjI50OzscaPzTX71DqoONb3q37icf6OD3OQ62etIrGFGB4cWcj0/SdX8Ph1QAfecg+KBprZNFQrNdoByTV9D6+jU/u6D7unkYPygjlHNv0WOOGo0VjnZwTb+POlCWXNTlIrp7R/pBMa/TOrxELSLdlu42E54nFWjnxsXUQYatwWqX4I/tK++Px1eaOxlB4NcAdVsvvisV6uXD71RB+OGluUnUG41ra7mC8rbFDMXqTrFbtdFjypyKxZk7dcUWEH4dOrN1gRPc/q72RaOzOr/BqgLuUZ3ipSKyZnl1epV6weLfAv3lMUg2O47ltfjuj0NCdX+M0Am/BCI0bVqv5Uzuv33VthJd8m7n0l00Mmgsntz4Eu/2wiSOwdlloU92ZIzVueKM4rJ1JIz2U2Lzi+OCvlXMbuIXh460PwCcamDbCf4yOXPT6tqnisH7+arQb2dDZTKvpfRx/2OrFv7hxaXPjwBGBC2LiaI0b3i0Ka2j6utFu6YMb6UzvvbJp0XNYa1d+SNN2fl0X3GZ+zqiNGzbMEoV1NHoL9Qit+9Jqeh/umpb2gU1r1umdGFt8xmgl+lExWEtzxuoOG94seP6yXzcpiM5qIYR6VjTpitcv3z9wCYzdvHzrvmKwnj479roZODR43VzYoDh6+/gX/M4GXe5lveHfHHeM+R8+LwJr6uCh8X6pxjoC2Yh9YOPujj6hMRvvt92rhi6uce9Vhw8TgXX19RKejaXV1DRoH9iCsa90blN2ft3WH3oTMb33Z+P+l7PFX209pYVVtGkwtM5gYm3UO/ejMfeBTfqfZqSNst7HP0P81VdruxzD6wweklTT+46NWeVueROucPOKp4d+TbS4A/B/RV+NvaDF9bS2f0Hgf5ox0o7j2nnt6Bf4igZc3prgMwezlt7Q4v86RfTVWE/LNXjC6wxOaECdwXtHfaDXgJ1f4RvLj239lOOqCaKvzpa0dfMbeqp6Ye3rDF4zc+Qrm1b331MbBkJfcrRXV+FPxF6tTWqrtdK65eF1XC6vd4CdPeI+sJ6v1Twf9oXu/p7bd0tbN0WTxV69tVmtKnbluPp550jX9K46X1EVtSPfIfJqbs+2bx6uCa5V2+a3U2J7o56z6wWdVONzOeuCq1V38Cvynpkir+5Oa3+t3VPy/XBabt/lBdPctbW9mIr6Y3xY3NXe7E72hEfox3NsbesMXrTT/Xltd35V1pFr8zxxV3+f6mzVhXcAbP2df2KW73gd/1zPq4ixN6fDKgIDoq4B9u/0Bn3zYGgXrrrWGVyy/UW8spZpI8Zu4E67jg8dJOqa4KtVLr+D+9fVL+ruO+axCzi8hpvpKz5/dIaYa4SjQr71fxXhJGX9GiZe++hbgen12/m1OsKJ5xtDPsDTxVwznF/xI7Ya1hn8xiM7H3rOqN2mjcprrHxfxDXEc0NX46XBL/UWjFMtKjl9D3/ud9frU98TXNVt8uILQj/EH4i4prgkeEUWXZ8yvW/u5z74oZ9dq19K4XVk50WoI3tlj4BrildF+Q1caEXs5Ny+z7Y4qtHOr2Qq179avDXGxNVxHhkGN72fU6Om9z+eMumC2nzY8F450yL1yrl5knhrjrdEWp8bYnT9qksw/ssna7NpI36b+Y79hWhrkD2iPZocPv/FoU/tj/5sM5veV7Vp4/PHBU7IhFP+K9o+vbumibYmeX/ElXprhM7mqwR8rMkIrQY4c+kvI36evxNrjbL3xpirdfOKE0K/5BpQZzAB4dUAjx6IujP2N08Qa80Su0Z3SrfVXWrDwOHJPXD6F5HWME+KviPhttNCT19O+7MCNqP/JMUYL+BDrXrLjMDhn/f+6G+bHzhApDXNFwvYJJVi0/tF16eXN25aFHvkE91U82Vx1jhHFnLA/eokNitub85RyXWz3fyUx0f9ezG28V5UyJUeK86a59vFREUSxyO2TxzZa1NLHG/MYiaOhA8OfVeUNdCzi4qLtJrez8myz6aVN76QxUscaR9VPlmUNdFFxcVG5SUgdkgcU/4vpbxx+R7REsev+vcJHOZCi6Nc7nhbI728yPBIpun9tsSRPTGhM/x3P/iiIUriSL4c2yvEWCNNuLbYEImwsJdvjJI4st9LZnfZ0POzKIlj8+BRodNfdAHYX+4mxprpT4uOkgoLa++YOLL3pZI4lmUxEkctSs7/uQhrqCnFl5eorJXHTomj56w08sbKieGJoyZNbm7fXYQ11XvLiJX/++OpgR/z6+GJI9sriX1gNzzyzDgocXwj9PviNT8u41r/Vnw11sz1pYRLaLvSMyMkjuzIBM7ub/rtgfegxHF20GDO7VtTyrXe+zjx1VwfK+uRYNCW6CiJI3t19YnjDVnViaPtNvMd+0fR1WALtpQWNNf0zao2cWT/WnXe+ExWbeLooM18x7buJ7qa7PQS42ZDp03vIyWOSRdWmzcu273SxLGwkzbzHRsUW422aKjU2Oms0EykxJHt++sq88Zd251EKz1xlF4o6Wix1WznlBw+azoobRcrcVS6D2zoeVlliWNGb9mNK88TWQ13YukRtHnF06tKHNlfV5c4dng7WWriOGR5+X2yf1dkNV0VN/4X906tJnH0nFlV3vjmhGoSR6QTP236qbhqvFMriaPb2ml6Hy9xZLOuqyZv3LjjroayEsfsvpsqudyXiqvG6/l5NaG0rUVhBYkjO6KSfWCbdiqFVU7iiFjVpM1X7xPEVfO9vrr3k717lp44sj+q4kpfl5WeOKYsru7t8xtFVReYVGH71taqZUZNHNmnyr/MT2dlJ475yyp89XzbVFHVDf6qyt0NrWxGj5s4ym8d/ZMpJSeOAqsBtuLdYqorTF+XV+q68eoMxk0c2T4lfxnfteuvqiITx/RIbeY7tmGWmOoO/XnFNo7d9D5y4sieXer38dDvZyUmjoP67656Nj8iorrEnE155cbajB47cWTvLvPK3puVljgKrwbYiq37iqhukUT/gLWjNr2Pnjh6zijvss7tKStxxG0z37HPiaeucfBQCituW53BZ5STOLLppZ3duG7EG/4CEscxA79JYhKHnyyeusfX80SM2PQ+fuLIDi/p+MamY7IyEsfkxStTmcGzRVMXeUqejNv79yshcWSvKudq/iQrIXHM7bs5nfk7QTR1k2+ls/Ly+792UvGJI/vnMi7lk1nxiePEr2xNaPK0i+0yFe8Z2snVS6cVnTgm/W/xl/HjKUUnjilLLkto2sJKy1JPle5S3sU9A08uNnFkcwvvKnP7qJ1dIyWOA0qtBjie9QOLRFFXqvJc1AiP5x/tO1RM4shOKvg31tBzsyITR0/Z1QDHtmrpniKoe1V2EntEqx/udFhQ4sjeVeyn78sKTBwzimwz37Y2aiTQUFXVfhnldeaDvZWLShw9Xyvyo3+jp7jEEaMbdzxtVWWisXZb/IOEVmX+w8XnFJQ4shlXF/exr5mZFZQ4vvnS76U0Pxe+arKY4WFV1LctyJwxr/Oeov7tvYdnRSWOlGxecbxoYfsv49Ir6leROLJXFPVvl2RdkDhu7aDXBU03Ia2n9sUkjuyfivmvy7PmJ47OumvRBRb239X0xLHb/xTxTy+a3PTEsWHgcPHBqKYuuaLZiSObe2sBrxkWZM1OHNf27SU2GNtxg1ubnDiyE6Jf3gPPyZqcOGwspzXz+tY0OHFk74j9L9+ZNThxrFv+JBFBixKq9xA/cfR8Ne5/PLunuYnjkt49RAPtODaRClPxE0c2LWozuzF3ftU6cWxZYWM57Zu19IZmJo6o+8A2HpY1M3H8qn8fMUBHkqiiXUDiyF4c77JemzUycVy8ZJL1T+cO7l/XwMSRfSLWv/tE1sDE8dBRQwgyPakD3ZESx26RzvX9aHLzEscjxQ0gVFp1BmMkjmxOlDfO4+78ql3iGHq0nBKEW7DsjmYljuz4LeH/6/5nZc1KHNsXcIQY0iqTG544sr8M/19vyxqVOHYsGQ2R1GIzesuJIxsM/Vdn9TQocQyde7KN5RQkqVZAoYljWuBD36tnZI1JHCO0xYKIkt+M3nriyA5eH7Tzq43HAYknjhEbcUJcxyS9Gb2NxJGdGrAPbHhx1ozEMVrrb4ht5tJfNiJxZB/t/P98NGtE4li77AnWM6VJdzN6W4lj4nc7/TcXTmpC4lANkNId2H937RNHNrvDfWC/mp/VPnFsHDjCKqYC03qvrHviyJ7e0T6wrc/M6p44ruvb2wqmKultRm8zcWR/0ck/+Yus3olDNUCqllbT+/YTR/Yf7f+PL2e1Thzrl+9v3VK5tJret504dr+03X+xanqdE4c28yQjoab3bSeO7KA294FtODSrbeLQZp60JNP0vv3Ekb2orTfLwy/P6po4tJknPRNOWTlcz8SR9bfzD/qzmiaOi3unWqWkKIWm950kjgn/1frf/95utUwc2syTsuqb3neSOLLZt7S8RXteVsPEsUabeVK/Y6m46X1HiSN72uYWd36dmNUvcdhYTi1U2vS+s8SRvbm1v/7mrG6JY8PAYVYkNVFh0/sOE0d2eit//EtZzRLHNX2zrEbqpKo6g50mjlb2gV25Z60Sh43l1FE1Te87TRzZgeN2ntqwKKtR4tBmnrqqos5gx4kje+E421CGX5rVJ3FoM0+tld70vvPEkX147L/8oawuiUObeeqv5Kb3AYljwnfG+sP/PbEmiUObeZqh1DqDAYkj23uMFHdzSG3OEhOHNvM0SHlN70MSR/bUUfeBbQ0qBV5W4tBmnqYpq+l9UOLI3jTan31Tln7i0GaeRiqlzmBY4sg+P/Jf/WKWeuLQZp7mKqHpfWDimHrJSH/0ij0STxzazNNshTe9D0wc2X53jrCXamGWdOK4WjVAmq/YzeihiSN74dAuO79ekiWcOLSZp1vMKbDpfXDiyE7b+U9+IEs3cdze/0Tria5R3Gb08MQx4Vs7/sXzJyabOLSZp+sU1PQ+PHFke+/QUvumCMWzCkkcm7WZpysV0vQ+QuLIjt6u2cPmp2RJJg5t5uleBWxGj5E4st7H/t4bsxQTh2qAdLnYTe+jJI7sc7/9c1/I0ksc2sxD7Kb3cRLH1Isf/muX75Fc4tBmHh4RcTN6nMSR7ffQFte7D8jSShyqAcL2ojW9j5Q4sudsa/Iw9PwsqcShzTzsLFLT+1iJI1uW58uylBLHpb02lsMIYjS9j5Y4Jnxr5cR0EsdW1QBhVHP+Zk0qiSPbO95DyNDEsXbZPGsDxvqiD2x6PyfFiwpLHNrMQwuOHLhX4vit+/7tGCsCWhLQ9L5ZiUObeWjrjqXTpvdNShw2lkPbFvbf2c2JQ5t56MzUJZd3a+LQZh4CtF1nsAmJw8ZyCDW375buShzazEMMbdUZrHvi0GYeomm96X2tE4c28xBXq03va5w41vYvMM8QWWt1BmubOLSZh4IcNH6dwXomjk2DR5pdKMz03p81L3GsVg0QijZ2ncHaJQ5t5qEc88doel+zxHHPwKHmE0oyZfGPmpA4tJmHkh03sKneiUObeajCiE3v65I4tJmHqkzctc5gPRKHNvNQqUXL761b4ti84gTzBhWbuUOdweQTx63azEMStq8zmHjiUA0QEnJg/13pJ44NA4ebKUjK1CVXpJ04ru3byyxBeh6sM5ho4lANENK174eS/Faf+Q9PMjcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQR/8fVl77ZtVg2u0AAAAASUVORK5CYII=`; 7 | 8 | if (qrCodeNode) { 9 | qrCodeNode.innerHTML = ''; 10 | // Prevent nextjs import issue: https://github.com/kozakdenys/qr-code-styling/issues/38 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const QRCodeStyling = require('qr-code-styling'); 13 | // Prevent nextjs import issue 14 | const qrCode = new QRCodeStyling({ 15 | width: 270, 16 | height: 270, 17 | type: 'svg', 18 | data: link, 19 | image: LOGO, 20 | dotsOptions: { 21 | color: 'black', 22 | type: 'rounded', 23 | }, 24 | imageOptions: { 25 | margin: 5, 26 | }, 27 | cornersDotOptions: { 28 | color: lightTheme.colors.emerald, 29 | }, 30 | qrOptions: { 31 | errorCorrectionLevel: 'M', 32 | }, 33 | }); 34 | 35 | qrCode.append(qrCodeNode); 36 | } 37 | }; 38 | 39 | export const updateOTPValue = (otpValue: string) => { 40 | const tryUpdate = () => { 41 | const otpNode = document.getElementById('sdk-mm-otp-value'); 42 | 43 | if (otpNode) { 44 | otpNode.textContent = otpValue; 45 | otpNode.style.display = 'block'; 46 | return true; 47 | } else { 48 | return false; 49 | } 50 | }; 51 | 52 | setTimeout(() => { 53 | tryUpdate(); 54 | }, 800); 55 | }; 56 | 57 | export const truncateString = ( 58 | value: string, 59 | length = 40, 60 | separator = '...', 61 | front = 8, 62 | back = 5, 63 | ) => { 64 | if (value.length < length) { 65 | return value; 66 | } 67 | 68 | return `${value.substring(0, front)}${separator}${value.substring(value.length - back)}`; 69 | }; 70 | -------------------------------------------------------------------------------- /src/frontend/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useClickOutside = (handleOnClickOutside: (event: MouseEvent | TouchEvent) => void) => { 4 | const ref = useRef(null); 5 | 6 | useEffect(() => { 7 | const listener = (event: MouseEvent | TouchEvent) => { 8 | if (!ref.current || ref.current.contains(event.target as Node)) { 9 | return; 10 | } 11 | 12 | handleOnClickOutside(event); 13 | }; 14 | document.addEventListener('mousedown', listener); 15 | document.addEventListener('touchstart', listener); 16 | 17 | return () => { 18 | document.removeEventListener('mousedown', listener); 19 | document.removeEventListener('touchstart', listener); 20 | }; 21 | }, [handleOnClickOutside]); 22 | 23 | return ref; 24 | }; 25 | -------------------------------------------------------------------------------- /src/frontend/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module '*.svg' { 7 | import React from 'react'; 8 | const SVG: React.FC>; 9 | export default SVG; 10 | } 11 | -------------------------------------------------------------------------------- /src/frontend/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body, 9 | #root { 10 | display: flex; 11 | width: 100%; 12 | height: 100%; 13 | margin: 0; 14 | padding: 0; 15 | flex-direction: column; 16 | } 17 | -------------------------------------------------------------------------------- /src/frontend/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createRoot } from 'react-dom/client'; 4 | import { MetaMaskProvider } from '@metamask/sdk-react'; 5 | import { HashRouter } from 'react-router-dom'; 6 | 7 | // custom components 8 | import { QrCodeModal } from './components/modals/qr-code-modal'; 9 | import AppInit from './components/layout/app-init'; 10 | import Main from './components/layout/main'; 11 | 12 | // helpers 13 | import { updateQrCode } from './helpers'; 14 | 15 | // theme 16 | import ThemeProvider from './theme/theme-provider'; 17 | import GlobalStyle from './theme/index'; 18 | import './index.css'; 19 | 20 | // context 21 | import { AIMessagesProvider } from './contexts'; 22 | 23 | // constants 24 | import { LOGO_METAMASK_BASE64 } from './constants'; 25 | 26 | // root 27 | const rootElement = document.querySelector('#root') as Element; 28 | const root = createRoot(rootElement); 29 | 30 | const AppRoot = () => { 31 | const [isModelsPathSet, setIsModelPathSet] = useState(false); 32 | const [modelsPathFetched, setIsModelPathFetched] = useState(false); 33 | const [modelsPath, setModelsPath] = useState(''); 34 | const [isInitialized, setIsInitialized] = useState(false); 35 | 36 | // get the status of the saved folder 37 | // useEffect(() => { 38 | // const getPath = async () => { 39 | // return await window.backendBridge.main.getFolderPath(); 40 | // }; 41 | 42 | // getPath() 43 | // .then((value) => { 44 | // setModelsPath(value); 45 | // setIsModelPathFetched(true); 46 | // }) 47 | // .catch((err) => console.error(err)); 48 | // }, []); 49 | 50 | // useEffect(() => { 51 | // if (modelsPath) { 52 | // setIsModelPathSet(true); 53 | 54 | // handleOllamaInit(); 55 | // } 56 | // }, [modelsPath]); 57 | 58 | // useEffect(() => { 59 | // window.backendBridge.main.onInit((result: boolean) => setIsInitialized(result)); 60 | 61 | // return () => { 62 | // window.backendBridge.removeAllListeners(IpcChannel.AppInit); 63 | // } 64 | // }); 65 | 66 | useEffect(() => { 67 | handleOllamaInit(); 68 | }, []); 69 | 70 | const handleOllamaInit = async () => { 71 | const ollamaInit = await window.backendBridge.ollama.init(); 72 | 73 | if (ollamaInit) { 74 | const model = await window.backendBridge.ollama.getModel('llama2'); 75 | 76 | if (model) { 77 | setIsInitialized(true); 78 | 79 | return; 80 | } else { 81 | console.error(`Something went wrong with pulling model ${'llama2'}`); 82 | } 83 | } 84 | 85 | console.error(`Couldn't initialize Ollama correctly.`); 86 | }; 87 | 88 | const handleSelectFolderClicked = async () => { 89 | const result = await window.backendBridge.main.setFolderPath(); 90 | 91 | if (result) { 92 | window.backendBridge.main.sendInit(); 93 | } 94 | }; 95 | 96 | return ( 97 | 98 | 99 | 100 | { 118 | let modalContainer: HTMLElement; 119 | 120 | return { 121 | mount: () => { 122 | if (modalContainer) return; 123 | 124 | modalContainer = document.createElement('div'); 125 | 126 | modalContainer.id = 'meta-mask-modal-container'; 127 | 128 | document.body.appendChild(modalContainer); 129 | 130 | ReactDOM.render( 131 | { 133 | ReactDOM.unmountComponentAtNode(modalContainer); 134 | modalContainer.remove(); 135 | }} 136 | />, 137 | modalContainer, 138 | ); 139 | 140 | setTimeout(() => { 141 | updateQrCode(link); 142 | }, 100); 143 | }, 144 | unmount: () => { 145 | if (modalContainer) { 146 | ReactDOM.unmountComponentAtNode(modalContainer); 147 | 148 | modalContainer.remove(); 149 | } 150 | }, 151 | }; 152 | }, 153 | }, 154 | }} 155 | > 156 | {!isInitialized && } 157 | {isInitialized &&
} 158 | {/* {modelsPathFetched && !isModelsPathSet && await handleSelectFolderClicked()} />} */} 159 | 160 | 161 | 162 | 163 | ); 164 | }; 165 | 166 | root.render( 167 | 168 | 169 | 170 | , 171 | ); 172 | -------------------------------------------------------------------------------- /src/frontend/renderer.d.ts: -------------------------------------------------------------------------------- 1 | import { ChatResponse, GenerateResponse, ListResponse, ModelResponse } from 'ollama'; 2 | 3 | export interface BackendBridge { 4 | main: { 5 | init: () => Promise; 6 | onInit: (callback: (result: boolean) => void) => Electron.IpcRenderer; 7 | sendInit: () => void; 8 | getFolderPath: () => Promise; 9 | setFolderPath: () => Promise; 10 | close: () => void; 11 | minimize: () => void; 12 | }; 13 | ollama: { 14 | init: () => Promise; 15 | onStatusUpdate: (callback: (status: string) => void) => Electron.IpcRenderer; 16 | question: ({ model, question }: OllamaQuestion) => Promise; 17 | onAnswer: (callback: (response: ChatResponse) => void) => Electron.IpcRenderer; 18 | getAllModels: () => Promise; 19 | getModel: (model: string) => Promise; 20 | }; 21 | removeAllListeners: (channel: string) => void; 22 | } 23 | 24 | declare global { 25 | interface Window { 26 | backendBridge: BackendBridge; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/frontend/router.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React from 'react'; 3 | import { Navigate, Route, Routes } from 'react-router-dom'; 4 | import Styled from 'styled-components'; 5 | 6 | // views 7 | // import HomeView from './views/home'; 8 | import SettingsView from './views/settings'; 9 | import ChatView from './views/chat'; 10 | 11 | export const RoutesWrapper = () => { 12 | return ( 13 | 14 | } /> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export const MainRouter = () => { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | const Router = Styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | height: 100%; 33 | width: 100%; 34 | `; 35 | -------------------------------------------------------------------------------- /src/frontend/theme.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components'; 2 | import { ITheme } from './theme/theme'; 3 | 4 | declare module 'styled-components' { 5 | // eslint-disable-next-line 6 | export interface DefaultTheme extends ITheme {} 7 | } 8 | -------------------------------------------------------------------------------- /src/frontend/theme/index.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import { createGlobalStyle } from 'styled-components'; 3 | 4 | // fonts 5 | import MontserratRegular from './../assets/fonts/Montserrat-Regular.ttf'; 6 | import MontserratLightItalic from './../assets/fonts/Montserrat-LightItalic.ttf'; 7 | import MontserratLight from './../assets/fonts/Montserrat-Light.ttf'; 8 | import MontserratSemiBold from './../assets/fonts/Montserrat-SemiBold.ttf'; 9 | import MontserratBold from './../assets/fonts/Montserrat-Bold.ttf'; 10 | import RobotoRegular from './../assets/fonts/Roboto-Regular.ttf'; 11 | import RobotoLight from './../assets/fonts/Roboto-Light.ttf'; 12 | import RobotoBold from './../assets/fonts/Roboto-Bold.ttf'; 13 | 14 | export default createGlobalStyle` 15 | @font-face { 16 | font-family: Montserrat Regular; 17 | src: url(${MontserratRegular}) format('truetype'); 18 | font-weight: normal; 19 | font-style: normal; 20 | } 21 | @font-face { 22 | font-family: Montserrat LightItalic; 23 | src: url(${MontserratLightItalic}) format('truetype'); 24 | font-weight: normal; 25 | font-style: normal; 26 | } 27 | @font-face { 28 | font-family: Montserrat Light; 29 | src: url(${MontserratLight}) format('truetype'); 30 | font-weight: normal; 31 | font-style: normal; 32 | } 33 | @font-face { 34 | font-family: Montserrat SemiBold; 35 | src: url(${MontserratSemiBold}) format('truetype'); 36 | font-weight: normal; 37 | font-style: normal; 38 | } 39 | @font-face { 40 | font-family: Montserrat Bold; 41 | src: url(${MontserratBold}) format('truetype'); 42 | font-weight: normal; 43 | font-style: normal; 44 | } 45 | @font-face { 46 | font-family: Roboto Regular; 47 | src: url(${RobotoRegular}) format('truetype'); 48 | font-weight: normal; 49 | font-style: normal; 50 | } 51 | @font-face { 52 | font-family: Roboto Light; 53 | src: url(${RobotoLight}) format('truetype'); 54 | font-weight: normal; 55 | font-style: normal; 56 | } 57 | @font-face { 58 | font-family: Roboto Bold; 59 | src: url(${RobotoBold}) format('truetype'); 60 | font-weight: normal; 61 | font-style: normal; 62 | } 63 | 64 | * { 65 | outline: none; 66 | list-style: none; 67 | } 68 | 69 | body { 70 | font-weight: normal; 71 | } 72 | 73 | ::-webkit-scrollbar { 74 | display: none; 75 | } 76 | `; 77 | -------------------------------------------------------------------------------- /src/frontend/theme/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext, useState } from 'react'; 2 | import { ThemeProvider } from 'styled-components'; 3 | 4 | import { darkTheme, lightTheme } from './theme'; 5 | 6 | export const ThemeContext = createContext({ 7 | isDarkTheme: true, 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | toggleTheme: () => {}, 10 | }); 11 | 12 | interface ThemeProviderProps { 13 | children?: ReactNode; 14 | } 15 | 16 | export default ({ children }: ThemeProviderProps) => { 17 | const [dark, setDark] = useState(false); 18 | 19 | const toggleTheme = () => { 20 | setDark(!dark); 21 | }; 22 | 23 | return ( 24 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/frontend/theme/theme.ts: -------------------------------------------------------------------------------- 1 | export interface ITheme { 2 | colors: { 3 | core: string; 4 | emerald: string; 5 | hunter: string; 6 | notice: string; 7 | balance: string; 8 | }; 9 | layout: { 10 | topBarHeight: number; 11 | bottomBarHeight: number; 12 | }; 13 | fonts: { 14 | family: { 15 | primary: { 16 | regular: string; 17 | bold: string; 18 | }; 19 | secondary: { 20 | regular: string; 21 | bold: string; 22 | }; 23 | }; 24 | size: { 25 | smallest: string; 26 | small: string; 27 | medium: string; 28 | large: string; 29 | }; 30 | }; 31 | } 32 | 33 | const common = { 34 | colors: { 35 | core: '#022C33', 36 | emerald: '#179C65', 37 | hunter: '#106F48', 38 | notice: '#FDB366', 39 | balance: '#FFFFFF', 40 | }, 41 | layout: { 42 | topBarHeight: 130, 43 | bottomBarHeight: 130, 44 | }, 45 | fonts: { 46 | size: { 47 | smallest: '12px', 48 | small: '14px', 49 | medium: '20px', 50 | large: '32px', 51 | }, 52 | }, 53 | }; 54 | 55 | export const lightTheme: ITheme = { 56 | layout: common.layout, 57 | fonts: { 58 | ...common.fonts, 59 | family: { 60 | primary: { 61 | regular: 'Roboto Regular', 62 | bold: 'Roboto Bold', 63 | }, 64 | secondary: { 65 | regular: 'Montserrat Regular', 66 | bold: 'Montserrat Bold', 67 | }, 68 | }, 69 | }, 70 | colors: { ...common.colors }, 71 | }; 72 | 73 | export const darkTheme: ITheme = { 74 | layout: common.layout, 75 | fonts: { 76 | ...common.fonts, 77 | family: { 78 | primary: { 79 | regular: 'Roboto Regular', 80 | bold: 'Roboto Bold', 81 | }, 82 | secondary: { 83 | regular: 'Montserrat Regular', 84 | bold: 'Montserrat Bold', 85 | }, 86 | }, 87 | }, 88 | colors: { ...common.colors }, 89 | }; 90 | -------------------------------------------------------------------------------- /src/frontend/types.ts: -------------------------------------------------------------------------------- 1 | export type AIMessage = { 2 | question: string; 3 | answer?: string; 4 | answered: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /src/frontend/utils/chain.ts: -------------------------------------------------------------------------------- 1 | type ChainInfo = { 2 | chainId: string; 3 | chainName: string; 4 | rpcUrls: string[]; 5 | iconUrls: string[]; 6 | nativeCurrency: { 7 | name: string; 8 | symbol: string; 9 | decimals: number; 10 | }; 11 | blockExplorerUrls: string[]; 12 | }; 13 | 14 | const params: ChainInfo[] = [ 15 | { 16 | chainId: '0x64', 17 | chainName: 'Gnosis', 18 | rpcUrls: ['https://rpc.ankr.com/gnosis'], 19 | iconUrls: [ 20 | 'https://xdaichain.com/fake/example/url/xdai.svg', 21 | 'https://xdaichain.com/fake/example/url/xdai.png', 22 | ], 23 | nativeCurrency: { 24 | name: 'xDAI', 25 | symbol: 'xDAI', 26 | decimals: 18, 27 | }, 28 | blockExplorerUrls: ['https://blockscout.com/poa/xdai/'], 29 | }, 30 | { 31 | chainId: '0xaa36a7', 32 | chainName: 'Sepolia', 33 | rpcUrls: ['https://rpc.notadegen.com/eth/sepolia'], 34 | iconUrls: [], 35 | nativeCurrency: { 36 | name: 'ETH', 37 | symbol: 'ETH', 38 | decimals: 18, 39 | }, 40 | blockExplorerUrls: ['https://sepolia.etherscan.io/'], 41 | }, 42 | { 43 | chainId: '0xa4b1', 44 | chainName: 'arbitrum', 45 | rpcUrls: ['https://arb1.arbitrum.io/rpc'], 46 | iconUrls: [], 47 | nativeCurrency: { 48 | name: 'ARB', 49 | symbol: 'ARB', 50 | decimals: 18, 51 | }, 52 | blockExplorerUrls: ['https://arbiscan.io/'], 53 | }, 54 | ]; 55 | 56 | export function getChainInfoByChainId(chainId: string): ChainInfo | undefined { 57 | return params.find((chain) => chain.chainId === chainId); 58 | } 59 | 60 | // const chainIdToSearch = "0x64"; 61 | // const chainInfo = getChainInfoByChainId(chainIdToSearch); 62 | 63 | // if (chainInfo) { 64 | // console.log("Found chain info:", chainInfo); 65 | // } else { 66 | // console.log("Chain info not found for chainId:", chainIdToSearch); 67 | // } 68 | -------------------------------------------------------------------------------- /src/frontend/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { SDKProvider } from '@metamask/sdk'; 3 | import { TransferAction, ActionParams } from './types'; 4 | 5 | export const isActionInitiated = (action: ActionParams) => { 6 | return !(Object.keys(action).length === 0); 7 | }; 8 | 9 | export const buildAction = (action: ActionParams, account: string, gasPrice: string) => { 10 | const transactionType = action.type.toLowerCase(); 11 | 12 | let tx: TransferAction; 13 | 14 | switch (transactionType) { 15 | case 'transfer': 16 | tx = buildTransferTransaction(action, account, gasPrice); 17 | break; 18 | default: 19 | throw Error(`Transaction of type ${transactionType} is not yet supported`); 20 | } 21 | 22 | // returned wrapped call with method for metamask with transaction params 23 | return { 24 | method: 'eth_sendTransaction', 25 | params: [tx], 26 | }; 27 | }; 28 | 29 | function extractEthereumAddress(text: string): string | null { 30 | const regex = /0x[a-fA-F0-9]{40}/; 31 | const match = text.match(regex); 32 | return match ? match[0] : null; 33 | } 34 | 35 | const buildTransferTransaction = ( 36 | action: ActionParams, 37 | account: string, 38 | gasPrice: any, 39 | ): TransferAction => { 40 | return { 41 | from: account, 42 | to: action.targetAddress, 43 | gas: '0x76c0', //for more complex tasks estimate this from metamast 44 | gasPrice: gasPrice, 45 | value: '0x' + ethers.parseEther(action.ethAmount).toString(16), 46 | data: '0x000000', 47 | }; 48 | }; 49 | 50 | //TODO: take chain ID to get arb balance or w/e chain 51 | const formatWalletBalance = (balanceWeiHex: string) => { 52 | const balanceBigInt = BigInt(balanceWeiHex); 53 | const balance = ethers.formatUnits(balanceBigInt, 'ether'); 54 | 55 | return `${parseFloat(balance).toFixed(2)} ETH`; 56 | }; 57 | 58 | export const handleBalanceRequest = async ( 59 | provider: SDKProvider | undefined, 60 | account: string | undefined, 61 | ) => { 62 | const blockNumber = await provider?.request({ 63 | method: 'eth_blockNumber', 64 | params: [], 65 | }); 66 | 67 | const balanceWeiHex = await provider?.request({ 68 | method: 'eth_getBalance', 69 | params: [account, blockNumber], 70 | }); 71 | 72 | if (typeof balanceWeiHex === 'string') { 73 | return `${formatWalletBalance(balanceWeiHex)}`; 74 | } else { 75 | console.error('Failed to retrieve a valid balance.'); 76 | 77 | throw Error('Invalid Balance Received from MetaMask.'); 78 | } 79 | }; 80 | 81 | const estimateGasWithOverHead = (estimatedGasMaybe: string) => { 82 | const estimatedGas = parseInt(estimatedGasMaybe, 16); 83 | const gasLimitWithOverhead = Math.ceil(estimatedGas * 2.5); 84 | 85 | return `0x${gasLimitWithOverhead.toString(16)}`; 86 | }; 87 | 88 | export const handleTransactionRequest = async ( 89 | provider: SDKProvider | undefined, 90 | transaction: ActionParams, 91 | account: string, 92 | question: string, 93 | ) => { 94 | const addressInQuestion = extractEthereumAddress(question); 95 | if (addressInQuestion?.toLowerCase() !== transaction.targetAddress.toLowerCase()) { 96 | console.error( 97 | `${addressInQuestion} !== ${transaction.targetAddress} target address did not match address in question`, 98 | ); 99 | throw new Error('Error, target address did not match address in question'); 100 | } 101 | 102 | const gasPrice = await provider?.request({ 103 | method: 'eth_gasPrice', 104 | params: [], 105 | }); 106 | 107 | // Sanity Check 108 | if (typeof gasPrice !== 'string') { 109 | console.error('Failed to retrieve a valid gasPrice'); 110 | 111 | throw new Error('Invalid gasPrice received'); 112 | } 113 | 114 | const builtTx = buildAction(transaction, account, gasPrice); 115 | 116 | const estimatedGas = await provider?.request({ 117 | method: 'eth_estimateGas', 118 | params: [builtTx], 119 | }); 120 | 121 | //Sanity Check 122 | if (typeof estimatedGas !== 'string') { 123 | console.error('Failed to estimate Gas with metamask'); 124 | 125 | throw new Error('Invalid gasPrice received'); 126 | } 127 | 128 | const gasLimitWithOverhead = estimateGasWithOverHead(estimatedGas); 129 | builtTx.params[0].gas = gasLimitWithOverhead; // Update the transaction with the new gas limit in hex 130 | 131 | return builtTx; 132 | }; 133 | -------------------------------------------------------------------------------- /src/frontend/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type ModelResponse = { 2 | response: string; 3 | action: ActionParams; 4 | }; 5 | 6 | export type ActionParams = { 7 | [key: string]: string; 8 | }; 9 | 10 | export type TransferAction = { 11 | from: string; 12 | to: string; 13 | gas: string; 14 | gasPrice: any; 15 | value: string; 16 | data: string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/frontend/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { json } from 'react-router-dom'; 2 | import { ModelResponse } from './types'; 3 | 4 | export const parseResponse = (jsonString: string) => { 5 | // Assert the type of the parsed object. 6 | console.log(jsonString); 7 | 8 | // uses regex to remove comments that llama sometimes includes in the JSON string 9 | // ranges from // to the end of the line or the end of the string 10 | // jsonString = jsonString.replace(/(? { 26 | return 'response' in object && 'action' in object; 27 | }; 28 | -------------------------------------------------------------------------------- /src/frontend/views/chat.tsx: -------------------------------------------------------------------------------- 1 | // libs 2 | import React, { FormEvent, useEffect, useState, useRef } from 'react'; 3 | import Styled from 'styled-components'; 4 | import { useSDK } from '@metamask/sdk-react'; 5 | import { ThreeDots } from 'react-loader-spinner'; 6 | 7 | // types and helpers 8 | import { AIMessage } from '../types'; 9 | import { OllamaChannel } from './../../events'; 10 | import { useAIMessagesContext } from '../contexts'; 11 | 12 | import { 13 | isActionInitiated, 14 | handleBalanceRequest, 15 | handleTransactionRequest, 16 | } from '../utils/transaction'; 17 | import { parseResponse } from '../utils/utils'; 18 | import { ActionParams } from '../utils/types'; 19 | import { getChainInfoByChainId } from '../utils/chain'; 20 | 21 | const ChatView = (): JSX.Element => { 22 | const [selectedModel, setSelectedModel] = useState('llama2'); 23 | const [dialogueEntries, setDialogueEntries] = useAIMessagesContext(); 24 | const [inputValue, setInputValue] = useState(''); 25 | const [currentQuestion, setCurrentQuestion] = useState(); 26 | const [isOllamaBeingPolled, setIsOllamaBeingPolled] = useState(false); 27 | const { ready, sdk, connected, connecting, provider, chainId, account, balance } = useSDK(); 28 | const ethInWei = '1000000000000000000'; 29 | const [selectedNetwork, setSelectedNetwork] = useState(''); 30 | 31 | const chatMainRef = useRef(null); 32 | const chatInputRef = useRef(null); 33 | 34 | useEffect(() => { 35 | window.backendBridge.ollama.onAnswer((response) => { 36 | setDialogueEntries([ 37 | ...dialogueEntries, 38 | { question: inputValue, answer: response.message.content, answered: true }, 39 | ]); 40 | 41 | setInputValue(''); 42 | }); 43 | 44 | return () => { 45 | window.backendBridge.removeAllListeners(OllamaChannel.OllamaAnswer); 46 | }; 47 | }); 48 | 49 | // Scroll to bottom of chat when user adds new dialogue 50 | useEffect(() => { 51 | const observer = new MutationObserver((mutations) => { 52 | for (const mutation of mutations) { 53 | if (chatMainRef.current && mutation.type === 'childList') { 54 | chatMainRef.current.scrollTop = chatMainRef.current.scrollHeight; 55 | } 56 | } 57 | }); 58 | 59 | if (chatMainRef.current) { 60 | observer.observe(chatMainRef?.current, { 61 | childList: true, // observe direct children 62 | }); 63 | } 64 | 65 | return () => observer.disconnect(); 66 | }, []); 67 | 68 | // Refocus onto input field once new dialogue entry is added 69 | useEffect(() => { 70 | if (chatInputRef.current) { 71 | chatInputRef.current.focus(); 72 | } 73 | }, [dialogueEntries]); 74 | 75 | //Function to update dialogue entries 76 | const updateDialogueEntries = (question: string, message: string) => { 77 | setCurrentQuestion(undefined); 78 | setDialogueEntries([ 79 | ...dialogueEntries, 80 | { question: question, answer: message, answered: true }, 81 | ]); 82 | }; 83 | 84 | const checkGasCost = (balance: string, transaction: ActionParams): boolean => { 85 | // calculate the max gas cost in Wei (gasPrice * gas) 86 | // User's balance in ETH as a float string 87 | const balanceInEth = parseFloat(balance); 88 | // Convert balance to Wei 89 | const balanceInWei = BigInt(balanceInEth * 1e18); // 1 ETH = 10^18 Wei 90 | const fivePercentOfBalanceInWei = balanceInWei / BigInt(20); // Equivalent to 5% 91 | const gasCostInWei = BigInt(transaction.gasPrice) * BigInt(transaction.gas); 92 | return gasCostInWei > fivePercentOfBalanceInWei; 93 | }; 94 | const processResponse = async ( 95 | question: string, 96 | response: string, 97 | action: ActionParams | undefined, 98 | ) => { 99 | if (action == undefined) { 100 | action = {}; 101 | } 102 | if (!isActionInitiated(action)) { 103 | updateDialogueEntries(question, response); //no additional logic in this case 104 | 105 | return; 106 | } 107 | 108 | // Sanity Checks: 109 | if (!account || !provider) { 110 | const errorMessage = `Error: Please connect to metamask`; 111 | updateDialogueEntries(question, errorMessage); 112 | 113 | return; 114 | } 115 | 116 | switch (action.type.toLowerCase()) { 117 | case 'balance': 118 | let message: string; 119 | try { 120 | message = await handleBalanceRequest(provider, account); 121 | } catch (error) { 122 | message = `Error: Failed to retrieve a valid balance from Metamask, try reconnecting.`; 123 | } 124 | updateDialogueEntries(question, message); 125 | break; 126 | 127 | case 'transfer': 128 | try { 129 | const builtTx = await handleTransactionRequest(provider, action, account, question); 130 | console.log('from: ' + builtTx.params[0].from); 131 | //if gas is more than 5% of balance - check with user 132 | const balance = await handleBalanceRequest(provider, account); 133 | const isGasCostMoreThan5Percent = checkGasCost(balance, builtTx.params[0]); 134 | if (isGasCostMoreThan5Percent) { 135 | updateDialogueEntries( 136 | question, 137 | `Important: The gas cost is expensive relative to your balance please proceed with caution\n\n${response}`, 138 | ); 139 | } else { 140 | updateDialogueEntries(question, response); 141 | } 142 | await provider?.request(builtTx); 143 | } catch (error) { 144 | const badTransactionMessage = 145 | 'Error: There was an error sending your transaction, if the transaction type is balance or transfer please reconnect to metamask'; 146 | updateDialogueEntries(question, badTransactionMessage); 147 | } 148 | break; 149 | 150 | case 'address': 151 | updateDialogueEntries(question, account); 152 | break; 153 | 154 | default: 155 | // If the transaction type is not recognized, we will not proceed with the transaction. 156 | const errorMessage = `Error: Invalid transaction type: ${action.type}`; 157 | updateDialogueEntries(question, errorMessage); 158 | } 159 | }; 160 | 161 | const handleQuestionAsked = async (question: string) => { 162 | if (isOllamaBeingPolled) { 163 | return; 164 | } 165 | 166 | const dialogueEntry = { 167 | question: question, 168 | answered: false, 169 | }; 170 | 171 | setCurrentQuestion(dialogueEntry); 172 | setInputValue(''); 173 | 174 | setIsOllamaBeingPolled(true); 175 | 176 | const inference = await window.backendBridge.ollama.question({ 177 | model: selectedModel, 178 | query: question, 179 | }); 180 | 181 | console.log(inference); 182 | if (inference) { 183 | const { response, action: action } = parseResponse(inference.message.content); 184 | 185 | if (response == 'error') { 186 | updateDialogueEntries(question, 'Sorry, I had a problem with your request.'); 187 | } else { 188 | await processResponse(question, response, action); 189 | } 190 | } 191 | 192 | setIsOllamaBeingPolled(false); 193 | }; 194 | 195 | const handleQuestionChange = (e: FormEvent) => { 196 | setInputValue(e.currentTarget.value); 197 | }; 198 | 199 | const handleNetworkChange = async (e: React.ChangeEvent) => { 200 | const selectedChain = e.target.value; 201 | 202 | // Check if the default option is selected 203 | if (!selectedChain) { 204 | console.log('No network selected.'); 205 | return; // Early return to avoid further execution 206 | } 207 | 208 | // Sanity Checks: 209 | if (!account || !provider) { 210 | const errorMessage = `Error: Please connect to MetaMask`; 211 | updateDialogueEntries('', errorMessage); 212 | return; 213 | } 214 | 215 | try { 216 | const response = await provider.request({ 217 | method: 'wallet_switchEthereumChain', 218 | params: [{ chainId: selectedChain }], 219 | }); 220 | console.log(response); 221 | } catch (error) { 222 | //if switch chain fails then add the chain 223 | try { 224 | const chainInfo = getChainInfoByChainId(selectedChain); 225 | const response = await provider.request({ 226 | method: 'wallet_addEthereumChain', 227 | params: [chainInfo], 228 | }); 229 | } catch (error) { 230 | console.error('Failed to switch networks:', error); 231 | } 232 | } 233 | }; 234 | 235 | return ( 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | {dialogueEntries.map((entry, index) => { 246 | return ( 247 | 251 | {entry.question && {`> ${entry.question}`}} 252 | {entry.answer && {entry.answer}} 253 | 254 | ); 255 | })} 256 | {currentQuestion && ( 257 | 258 | {`> ${currentQuestion.question}`} 259 | 260 | 261 | 262 | 263 | )} 264 | 265 | 266 | 267 | > 268 | ) => { 274 | if (e.key === 'Enter') { 275 | handleQuestionAsked(inputValue); 276 | } 277 | }} 278 | /> 279 | handleQuestionAsked(inputValue)} 282 | /> 283 | 284 | 285 | {/*
handleQuestionAsked('How much is 5 times 5?')}>Ask Olama
286 | 287 |
288 | {currentQuestion && {currentQuestion}} 289 |
*/} 290 |
291 | ); 292 | }; 293 | 294 | const Chat = { 295 | Layout: Styled.div` 296 | display: flex; 297 | flex-direction: column; 298 | width: 100%; 299 | height: 100%; 300 | background: ${(props) => props.theme.colors.core}; 301 | `, 302 | Main: Styled.div` 303 | display: flex; 304 | width: 100%; 305 | height: 80%; 306 | flex-direction: column; 307 | padding: 20px; 308 | margin-bottom: 20px; 309 | overflow: scroll; 310 | `, 311 | QuestionWrapper: Styled.div` 312 | display: flex; 313 | flex-direction: column; 314 | margin-bottom: 20px; 315 | `, 316 | Question: Styled.span` 317 | color: ${(props) => props.theme.colors.notice}; 318 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 319 | font-size: ${(props) => props.theme.fonts.size.small}; 320 | word-wrap: break-word; 321 | margin-bottom: 5px; 322 | `, 323 | Answer: Styled.span` 324 | display: flex; 325 | color: ${(props) => props.theme.colors.emerald}; 326 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 327 | font-size: ${(props) => props.theme.fonts.size.small}; 328 | margin-left: 20px; 329 | `, 330 | PollingIndicator: Styled(ThreeDots)` 331 | display: flex; 332 | `, 333 | Bottom: Styled.div` 334 | display: flex; 335 | width: 100%; 336 | height: 20%; 337 | background: ${(props) => props.theme.colors.core}; 338 | justify-content: center; 339 | `, 340 | InputWrapper: Styled.div` 341 | display: flex; 342 | width: 90%; 343 | height: 40px; 344 | position: relative; 345 | align-items: center; 346 | `, 347 | Input: Styled.input` 348 | display: flex; 349 | width: 100%; 350 | height: 40px; 351 | border-radius: 30px; 352 | padding: 0 40px 0 25px; 353 | background: ${(props) => props.theme.colors.core}; 354 | border: 2px solid ${(props) => props.theme.colors.hunter}; 355 | color: ${(props) => props.theme.colors.notice}; 356 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 357 | font-size: ${(props) => props.theme.fonts.size.small}; 358 | `, 359 | Arrow: Styled.span` 360 | display: flex; 361 | color: ${(props) => props.theme.colors.notice}; 362 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 363 | font-size: ${(props) => props.theme.fonts.size.small}; 364 | position: absolute; 365 | left: 10px; 366 | `, 367 | SubmitButton: Styled.button` 368 | display: flex; 369 | width: 30px; 370 | height: 30px; 371 | border-radius: 25px; 372 | background: ${(props) => props.theme.colors.hunter}; 373 | position: absolute; 374 | right: 5px; 375 | cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; 376 | border: none; 377 | 378 | &:hover { 379 | background: ${(props) => (props.disabled ? props.theme.colors.hunter : props.theme.colors.emerald)}; 380 | } 381 | `, 382 | Dropdown: Styled.select` 383 | position: absolute; 384 | top: 42px; 385 | left: 25px; 386 | padding: 8px 10px; 387 | border-radius: 10px; 388 | background-color: ${(props) => props.theme.colors.core}; 389 | color: ${(props) => props.theme.colors.notice}; 390 | border: 2px solid ${(props) => props.theme.colors.hunter}; 391 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 392 | font-size: ${(props) => props.theme.fonts.size.small}; 393 | cursor: pointer; 394 | 395 | &:hover { 396 | border: 2px solid ${(props) => props.theme.colors.emerald}; 397 | } 398 | 399 | option { 400 | background-color: ${(props) => props.theme.colors.core}; 401 | color: ${(props) => props.theme.colors.emerald}; 402 | } 403 | `, 404 | }; 405 | 406 | export default ChatView; 407 | -------------------------------------------------------------------------------- /src/frontend/views/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Styled from 'styled-components'; 3 | import { TailSpin } from 'react-loader-spinner'; 4 | 5 | const HomeView = (): JSX.Element => { 6 | return ( 7 | 8 | Home 9 | 10 | 11 | ); 12 | }; 13 | 14 | const Home = { 15 | Layout: Styled.div` 16 | display: flex; 17 | width: 100%; 18 | height: 100%; 19 | background: ${(props) => props.theme.colors.core}; 20 | `, 21 | Title: Styled.h2` 22 | color: #000; 23 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 24 | `, 25 | Loader: Styled(TailSpin)` 26 | display: flex; 27 | width: 100%; 28 | position: fixed; 29 | top: 50%; 30 | left: 50%; 31 | transform: translate(-50%, -50%); 32 | `, 33 | }; 34 | 35 | export default HomeView; 36 | -------------------------------------------------------------------------------- /src/frontend/views/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Styled from 'styled-components'; 3 | // import { useSDK } from '@metamask/sdk-react'; 4 | 5 | // helpers 6 | // import { truncateString } from '../helpers'; 7 | 8 | const SettingsView = (): JSX.Element => { 9 | // const { ready, sdk, connected, connecting, provider, chainId, account, balance } = 10 | // useSDK(); 11 | 12 | return ( 13 | 14 | Under Construction - Preview Alpha Build 15 | {/* 16 | Connected wallet: 17 | {connected && account ? truncateString(account) : 'not connected'} 18 | */} 19 | 20 | ); 21 | }; 22 | 23 | const Settings = { 24 | Layout: Styled.div` 25 | display: flex; 26 | width: 100%; 27 | height: 100%; 28 | align-items: center; 29 | justify-content: center; 30 | `, 31 | Title: Styled.h2` 32 | display: flex; 33 | font-family: ${(props) => props.theme.fonts.family.primary.bold}; 34 | font-size: ${(props) => props.theme.fonts.size.medium}; 35 | color: ${(props) => props.theme.colors.notice}; 36 | `, 37 | Row: Styled.div` 38 | display: flex; 39 | flex-direction: row; 40 | align-items: center; 41 | `, 42 | Label: Styled.label` 43 | display: flex; 44 | font-family: ${(props) => props.theme.fonts.family.primary.regular}; 45 | font-size: ${(props) => props.theme.fonts.size.small}; 46 | color: ${(props) => props.theme.colors.balance}; 47 | `, 48 | Value: Styled.span` 49 | display: flex; 50 | font-family: ${(props) => props.theme.fonts.family.secondary.bold}; 51 | font-size: ${(props) => props.theme.fonts.size.small}; 52 | color: ${(props) => props.theme.colors.notice}; 53 | `, 54 | }; 55 | 56 | export default SettingsView; 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "skipLibCheck": true, 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "sourceMap": true, 11 | "baseUrl": ".", 12 | "outDir": "dist", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "strictNullChecks": true, 16 | "paths": { 17 | "*": ["node_modules/*"], 18 | }, 19 | }, 20 | "include": ["src/**/*", "types.d.ts"], 21 | } 22 | -------------------------------------------------------------------------------- /webpack.main.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/default */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | import type { Configuration } from 'webpack'; 4 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 5 | const WebpackPermissionsPlugin = require('webpack-permissions-plugin'); 6 | import path from 'path'; 7 | 8 | import { rules } from './webpack.rules'; 9 | import { plugins } from './webpack.plugins'; 10 | 11 | const devPlugins = [ 12 | ...plugins, 13 | new CopyWebpackPlugin({ 14 | patterns: [ 15 | { 16 | from: path.join(__dirname, 'src/executables'), to: path.join(__dirname, '.webpack/executables'), 17 | force: true 18 | } 19 | ], 20 | }), 21 | new WebpackPermissionsPlugin({ 22 | buildFolders: [ 23 | { 24 | path: path.resolve(__dirname, '.webpack/executables'), 25 | fileMode: '755', 26 | dirMode: '644' 27 | } 28 | ], 29 | buildFiles: [ 30 | { path: path.resolve(__dirname, '.webpack/executables/ollama.exe'), fileMode: '755' }, 31 | { path: path.resolve(__dirname, '.webpack/executables/ollama-darwin'), fileMode: '755' }, 32 | { path: path.resolve(__dirname, '.webpack/executables/ollama-linux'), fileMode: '755' }, 33 | ] 34 | }) 35 | ]; 36 | 37 | export const mainConfig: Configuration = { 38 | entry: './src/backend/index.ts', 39 | module: { 40 | rules, 41 | }, 42 | plugins: [...plugins], 43 | resolve: { 44 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 45 | }, 46 | }; 47 | 48 | export const mainDevConfig: Configuration = { 49 | mode: 'development', 50 | entry: './src/backend/index.ts', 51 | module: { 52 | rules, 53 | }, 54 | plugins: [...devPlugins], 55 | resolve: { 56 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 9 | }); 10 | 11 | rules.push({ 12 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 13 | type: 'asset/resource', 14 | }); 15 | 16 | rules.push({ 17 | test: /\.svg/, 18 | issuer: /\.[jt]sx?$/, 19 | use: [{ loader: '@svgr/webpack' }], 20 | }); 21 | 22 | rules.push({ 23 | test: /\.png$/, 24 | type: 'asset/resource', 25 | }); 26 | 27 | export const rendererConfig: Configuration = { 28 | module: { 29 | rules, 30 | }, 31 | plugins, 32 | resolve: { 33 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], 34 | }, 35 | devtool: 'source-map', 36 | }; 37 | -------------------------------------------------------------------------------- /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: /\.tsx?$/, 23 | exclude: /(node_modules|\.webpack)/, 24 | use: { 25 | loader: 'ts-loader', 26 | options: { 27 | transpileOnly: true, 28 | }, 29 | }, 30 | }, 31 | ]; 32 | --------------------------------------------------------------------------------