├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── ci.yaml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── LICENSE ├── README-CN.md ├── README.md ├── assets ├── .gitkeep ├── banner │ ├── banner.01.png │ ├── bar.png │ ├── brand01.png │ ├── brand02.png │ ├── brand_nobg.png │ └── demo.gif ├── icon │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icon128.png │ ├── icon16.png │ ├── icon24.png │ ├── icon32.png │ └── icon64.png └── screenshot │ ├── account.png │ ├── chat.png │ ├── code.png │ ├── console.png │ ├── list.png │ ├── plugin.png │ ├── prompts.png │ ├── setting.png │ └── video.png ├── babel.config.js ├── commitlint.config.js ├── forge.config.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── public └── index.html ├── renovate.json ├── src ├── @types │ ├── bridge.d.ts │ ├── deps.d.ts │ ├── i18next.d.ts │ ├── image.d.ts │ └── index.ts ├── App.tsx ├── app │ ├── api.ts │ ├── constants.ts │ ├── hooks.ts │ ├── images.ts │ └── store.ts ├── assets │ └── icon128.png ├── components │ ├── Button │ │ ├── Button.spec.tsx │ │ ├── index.tsx │ │ └── styles.ts │ ├── ChatPanel │ │ ├── OperatePanel.tsx │ │ └── index.tsx │ ├── Logo │ │ ├── index.tsx │ │ └── styles.ts │ ├── Modal │ │ └── prompt.tsx │ ├── Preset │ │ ├── detail.tsx │ │ └── index.tsx │ ├── Search │ │ └── index.tsx │ └── Setting │ │ ├── Account │ │ └── index.tsx │ │ ├── Basic │ │ └── index.tsx │ │ ├── Prompt │ │ └── index.tsx │ │ └── index.tsx ├── electron │ ├── apis │ │ ├── account.ts │ │ ├── apply.ts │ │ ├── crawl.ts │ │ ├── prompt.ts │ │ └── test.ts │ ├── client │ │ ├── bridge.ts │ │ ├── clipboard.ts │ │ ├── index.ts │ │ ├── link.ts │ │ ├── store.ts │ │ └── window.ts │ ├── constants │ │ ├── common.ts │ │ ├── event.ts │ │ └── index.ts │ ├── global.d.ts │ ├── main.ts │ ├── os │ │ ├── applescript.ts │ │ ├── index.ts │ │ ├── shortcuts.ts │ │ └── tray.ts │ ├── prompt │ │ ├── prompts-zh.json │ │ └── prompts.json │ ├── server.ts │ ├── sound │ │ └── index.ts │ ├── types.ts │ └── utils │ │ ├── global.ts │ │ ├── log.ts │ │ ├── util.ts │ │ └── window.ts ├── features │ ├── chat │ │ └── chatSlice.ts │ ├── clipboard │ │ └── clipboardSlice.ts │ ├── history │ │ └── historySlice.ts │ ├── preset │ │ └── presetSlice.ts │ └── setting │ │ └── settingSlice.ts ├── i18n.ts ├── index.tsx ├── locales │ ├── en.json │ └── zh.json ├── styles │ ├── GlobalStyle.ts │ └── Main.ts └── utils │ ├── fetch.ts │ └── index.ts ├── tests └── setupTests.ts ├── tsconfig.json ├── webpack ├── main.webpack.js ├── renderer.webpack.js └── rules.webpack.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "plugins": ["unused-imports"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/recommended", 13 | "plugin:import/electron", 14 | "plugin:import/typescript" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/ban-ts-comment": "off", 18 | "@typescript-eslint/no-empty-function": "off", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "@typescript-eslint/no-non-null-assertion": "off", 21 | "@typescript-eslint/no-unused-vars": "off", 22 | "@typescript-eslint/no-var-requires": "off", 23 | "import/no-named-as-default": "off", 24 | "no-constant-condition": "off", 25 | "unused-imports/no-unused-imports": "error", 26 | "unused-imports/no-unused-vars": [ 27 | "warn", 28 | { 29 | "vars": "all", 30 | "varsIgnorePattern": "^_", 31 | "args": "after-used", 32 | "argsIgnorePattern": "^_" 33 | } 34 | ] 35 | }, 36 | "parser": "@typescript-eslint/parser" 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | createrelease: 10 | name: Create Release 11 | permissions: write-all 12 | runs-on: [ubuntu-latest] 13 | steps: 14 | - name: Create Release 15 | id: create_release 16 | uses: actions/create-release@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: Release ${{ github.ref }} 22 | draft: false 23 | prerelease: false 24 | 25 | - name: Output Release URL File 26 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 27 | 28 | - name: Save Release URL File for publish 29 | uses: actions/upload-artifact@v1 30 | with: 31 | name: release_url 32 | path: release_url.txt 33 | 34 | build: 35 | permissions: write-all 36 | runs-on: ${{ matrix.os }} 37 | 38 | strategy: 39 | matrix: 40 | include: 41 | - os: macos-latest 42 | TARGET: macos 43 | PLATFORM: darwin 44 | TYPE: x64 45 | FORMAT: zip 46 | ASSET_MIME: application/zip 47 | - os: windows-latest 48 | TARGET: windows 49 | PLATFORM: squirrel.windows 50 | TYPE: x64 51 | FORMAT: exe 52 | ASSET_MIME: application/vnd.microsoft.portable-executable 53 | 54 | steps: 55 | - name: Checkout Code 56 | uses: actions/checkout@v2 57 | 58 | - name: Setup Node.js 59 | uses: actions/setup-node@v2 60 | with: 61 | node-version: 18 62 | 63 | - name: Install Dependencies 64 | run: | 65 | npm install yarn -g 66 | yarn 67 | 68 | - name: Build Release Files 69 | run: yarn release 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | # - name: Show Build Output 74 | # run: ls -al out/make/zip/darwin/x64 75 | 76 | # - name: Upload Artifact 77 | # uses: actions/upload-artifact@v3 78 | # with: 79 | # name: release_on_${{ matrix. os }} 80 | # path: release/ 81 | # retention-days: 5 82 | 83 | - name: Load Release URL File from release job 84 | uses: actions/download-artifact@v1 85 | with: 86 | name: release_url 87 | 88 | - name: Get Release File Name & Upload URL 89 | id: get_release_info 90 | shell: bash 91 | run: | 92 | value=`cat release_url/release_url.txt` 93 | echo ::set-output name=upload_url::$value 94 | 95 | - name: Set env 96 | shell: bash 97 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 98 | 99 | - name: Upload Release Asset ${{ matrix.PLATFORM }} 100 | if: runner.os != 'windows' 101 | id: upload-release-asset 102 | uses: actions/upload-release-asset@v1 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | with: 106 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 107 | asset_path: ./out/make/zip/${{ matrix.PLATFORM }}/${{ matrix.TYPE }}/onepoint-${{ matrix.PLATFORM }}-${{ matrix.TYPE }}-${{ env.RELEASE_VERSION }}.zip 108 | asset_name: onepoint-${{ matrix.PLATFORM }}-${{ matrix.TYPE }}-${{ env.RELEASE_VERSION }}.zip 109 | asset_content_type: ${{ matrix.ASSET_MIME}} 110 | 111 | - name: Upload Release Asset win 112 | if: runner.os == 'windows' 113 | id: upload-release-asset-win 114 | uses: actions/upload-release-asset@v1 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | with: 118 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 119 | asset_path: ./out/make/${{ matrix.PLATFORM }}/${{ matrix.TYPE }}/onepoint-${{ env.RELEASE_VERSION }} Boot.exe 120 | asset_name: onepoint-${{ env.RELEASE_VERSION }} Boot.exe 121 | asset_content_type: ${{ matrix.ASSET_MIME}} 122 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | all: 17 | name: All 18 | 19 | strategy: 20 | matrix: 21 | os: 22 | - macos-latest 23 | # - windows-latest 24 | 25 | runs-on: ${{matrix.os}} 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | # - name: Setup macOS/linux 31 | # if: ${{ matrix.os != 'windows-latest' }} 32 | # run: ./setup.sh 33 | 34 | # - name: Setup windows 35 | # if: ${{ matrix.os == 'windows-latest' }} 36 | # shell: pwsh 37 | # run: ./setup.ps1 38 | 39 | - name: Install dependencies 40 | run: yarn 41 | 42 | - name: Check formatting 43 | if: ${{ matrix.os != 'windows-latest' }} 44 | run: yarn format-check 45 | 46 | # - name: Package 47 | # run: npm run package 48 | # timeout-minutes: 30 49 | 50 | # - name: Upload artifacts 51 | # uses: actions/upload-artifact@v3 52 | # with: 53 | # name: ${{ matrix.os }}-binary 54 | # path: out 55 | -------------------------------------------------------------------------------- /.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 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .webpack/ 106 | data/ 107 | .DS_Store 108 | config.json 109 | 110 | /*.py 111 | 112 | out/ 113 | build/ 114 | /lsp/ 115 | settings.json 116 | resources/** 117 | lsp/** 118 | *.zip 119 | scripts/** 120 | 121 | run_todesktop.sh 122 | 123 | # hidden files 124 | .key -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.webpack 2 | /out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Box Tsang 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-CN.md: -------------------------------------------------------------------------------- 1 | # onepoint 2 | 3 |

4 | English | 中文 5 |

6 | 7 |
8 | 9 |

10 |

11 | 不仅仅是聊天 12 |

13 |
14 | 15 |
16 |
17 |
18 | 19 | macOS 20 | 21 | 22 | Windows 23 | 24 | 25 | Linux 26 | 27 |
28 |
29 | GitHub Actions 30 | PR Welcome 31 | 32 |
33 |
34 |
35 | 36 | Onepoint 是一款基于 Electron 的开源 AI 助手,旨在打造极致的桌面端效能工具,最初的目标是实现一个类似苹果的智能辅助悬浮窗,在使用时不占用桌面空间和系统性能,并通过快捷键全局呼起,方便用户随时使用。 37 | 38 | 借助 ChatGPT 技术,用户可以通过对 Onepoint 不断调教,使其生成和重构的内容更加精确到位(onpoint),从而帮助用户提高效率。Onepoint 目前可以在各种编辑场景(如 VSCode、Pages、Microsoft Word 和 Email 等)下使用,同时也覆盖了 Safari 和 Chrome 等阅读场景,真正实现了全场景智能覆盖。 39 | 40 |
41 | 42 |
43 |
44 | 45 | ## 01 功能 46 | 47 |
48 | 49 |
50 |
51 | 52 | **基础** 53 | 54 | - 提供快捷、简约的功能入口,并作用全局,即用即走 55 | - 支持多种 IDE 的代码一键编写与重构能力 56 | - 翻译与文稿写手,支持多种文本编辑场景下的内容总结与输出能力 57 | 58 | **高阶** 59 | 60 | - 伴读助手,支持 Safari 与 Chrome 等浏览器内容总结与输出 61 | - 支持第三方设备(如小爱同学)语音输出 62 | - 个性化 Prompt 与自定义角色预设 63 | - 高阶提问请求参数设定 64 | 65 | **更多** 66 | 67 | - 插件市场支持 68 | - 本地数据存储与导出 69 | - 账号余额查询 70 | - 多语言支持 71 | 72 | ## 02 截图 73 | 74 |
75 | 详情 76 | 77 | #### 极简风 78 | 79 | 80 | 81 | #### 历史模式 82 | 83 | 84 | 85 | #### Code 辅助 86 | 87 | 88 | #### 插件列表 89 | 90 | 91 | #### 设置页 92 | 93 | 94 | #### 账户页 95 | 96 | 97 | #### 自定义 Prompts 98 | 99 | 100 | 更多功能持续推进开发中 101 | 102 |
103 | 104 |
105 | 106 |
107 | 108 | 109 | 110 |
111 | 112 |
113 | 114 | ## 03 开始 115 | 116 | 前往 [官网](https://www.1ptai.com/) 下载并试用该工具。 117 | 118 | 如果你遇到相关 Bug,或者是有其他的功能需求,欢迎提 issue 或相关 PR,除了能得到我们大大的赞 👍 以外,还有机会获得个人定制的 avatar 形象,并通过 [NFT](https://opensea.io/zh-CN/collection/onepointai-collection) 的形式免费赠与到你的钱包(详情参见[贡献者须知 - 开发者激励](https://github.com/onepointAI/onepoint/issues/4) )。 119 | 120 | ## 04 开发 121 | 122 | 欢迎为我们提交 PR,或具有建设性的意见,一起做点有意思的事情 123 | 124 | ``` 125 | > git clone git@github.com:onepointAI/onepoint.git 126 | > cd onepoint 127 | > yarn 128 | > yarn start 129 | ``` 130 | 131 | ## 05 愿景与路线图 132 | 133 | 长远地看,我们希望把 onepoint 打造成个性化的智能辅助工具,以作为各个编辑与阅读软件的能力延伸,同时借助可扩展的插件机制丰富更多样的玩法,它既是工具,也是入口,希望对屏幕前的你有所帮助或启发。 134 | 135 | - 🚗 高可用性:快速便捷的入口,包括良好的用户体验(尽可能少的干扰、优雅的界面与交互和高性能) 136 | - 🔧 高效输出:不是为了替代某某,而是作为原有编辑器的能力补充与增强 137 | - 📖 阅读护航:总结归纳阅读场景,提高获取信息的能力与速度 138 | - 🎈 创意玩法:作为入口提供插件机制满足各类场景,提供 NFT 生态与和谐友好的技术社区氛围 139 | - 🤖 模型训练:提供自定义数据集的模型训练能力(LLMs) 140 | 141 | ## 06 QA 142 | 143 |
144 | 145 | Q1: onepoint 不能用在 windows 平台? 146 | 147 | 聊天、角色切换等基础能力可以正常使用,但 IDE 代码选择与应用、浏览器内容获取等需要调用到原生能力(macOS 通过 applescript 实现),Windows 暂不支持这样的原生调用,但以后会考虑 vbscript 来实现类似的能力。 148 | 149 |
150 | 151 |
152 | 153 | Q2: 怎么使用代码辅助或者网页抓取工具? 154 | 155 | 首先需要点击左侧的图标选择并切换到对应的模式(如代码重构、总结等),然后在 IDE 中选择一段代码或者鼠标聚焦到当前浏览器,通过`command + k` 全局呼起 onepoint,此时会显示是否对应用修改,选择 `yes`。 156 | 157 |
158 | 159 |
160 | 161 | Q3: 网页总结有什么限制吗? 162 | 163 | 目前对抓取网页的字符限制数为 4000(已经提出换行、回车和 html 标签等)以获得更快的速度,后续会通过开关已经上下文分段的能力处理长网页的内容总结 164 | 165 |
166 | 167 |
168 | 169 | ## 贡献者 170 | 171 | 172 | 173 | 174 | 175 | ## License 176 | 177 | [MIT License](./LICENSE) 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # onepoint 2 | 3 |

4 | English | 中文 5 |

6 | 7 |
8 | 9 |

10 |

11 | more than just chat 12 |

13 |
14 | 15 |
16 |
17 |
18 | 19 | macOS 20 | 21 | 22 | Windows 23 | 24 | 25 | Linux 26 | 27 |
28 |
29 | GitHub Actions 30 | PR Welcome 31 | 32 |
33 |
34 |
35 | 36 | Onepoint is an open-source AI assistant based on Electron, designed to create the ultimate desktop productivity tool. Its initial goal was to develop a smart floating window similar to Apple's intelligent assistant that does not take up desktop space or system performance and can be quickly accessed through global hotkeys for user convenience. 37 | 38 | With ChatGPT technology, users can continuously train onepoint to generate and reconstruct content with greater accuracy (onpoint), thereby improving efficiency. Onepoint currently supports various editing scenarios such as VSCode, Pages, Microsoft Word, Email etc, as well as reading scenarios like Safari and Chrome, achieving true full-scene intelligent coverage. 39 | 40 |
41 | 42 |
43 |
44 | 45 | ## 01 Features 46 | 47 |
48 | 49 |
50 |
51 | 52 | **Basical** 53 | 54 | - Provide quick and concise functional access points that act globally and allow for immediate use. 55 | - Support one-click code writing and refactoring capabilities for multiple IDEs. 56 | - Translation and document writing assistant, supporting content summarization and output in various text editing scenarios. 57 | 58 | **Advanced** 59 | 60 | - Reading assistant supporting content summarization and output on browsers such as Safari and Chrome. 61 | - Support for third-party device (such as Xiao Ai) voice output. 62 | - Personalized prompts and custom character presets. 63 | - Advanced question requesting parameter settings. 64 | 65 | **More** 66 | 67 | - Plugin market support. 68 | - Local data storage and export. 69 | - Account balance inquiry. 70 | - Multi-language support. 71 | 72 |
73 | 74 | ## 02 Screenshots 75 | 76 |
77 | Detail 78 | 79 | #### Minimal Mode 80 | 81 | 82 | 83 | #### History Mode 84 | 85 | 86 | 87 | #### Code Assistant 88 | 89 | 90 | #### Plugin List 91 | 92 | 93 | #### Setting Page 94 | 95 | 96 | #### Account Page 97 | 98 | 99 | #### Custom Prompts 100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 | 108 | 109 |
110 | 111 |
112 | 113 | ## 03 Getting Started 114 | 115 | Please go to the [official website](https://www.1ptai.com/) to download and try out the tool. 116 | 117 | If you encounter any bugs or have other feature requests, please feel free to submit an issue or related PR. You will not only receive our appreciation 👍 but also have the chance to receive a personalized avatar image, which will be gifted to your wallet for free in the form of an [NFT](https://opensea.io/zh-CN/collection/onepointai-collection) (Detail in [README of contributors](https://github.com/onepointAI/onepoint/issues/5)). 118 | 119 |
120 | 121 | ## 04 Development 122 | 123 | Welcome to submit a Pull Request (PR) or provide constructive feedback for us. Let's do something interesting together. 124 | 125 | ``` 126 | > git clone git@github.com:onepointAI/onepoint.git 127 | > cd onepoint 128 | > yarn 129 | > yarn start 130 | ``` 131 | 132 | ## 05 Vision & Roadmap 133 | 134 | In the long term, we hope to develop onepoint into a personalized intelligent assistant tool that extends the capabilities of various editing and reading software. At the same time, we aim to enrich its functionality through scalable plugin mechanisms, making it not only a tool but also an entry point that can help or inspire you in front of your screen. 135 | 136 | - 🚗 High availability, fast access with good user experience, elegant interface and interaction, and high performance. 137 | - 💻 Personalized service, providing users with tuning mechanisms to customize their personal intelligent assistants. 138 | - 🔧 Efficient output, not to replace certain tools but to complement and enhance the capabilities of existing editors. 139 | - 📖 Reading assistance, summarizing and organizing reading scenarios to improve the speed of information acquisition. 140 | - 🎈 Creative play, providing plugin mechanisms as an entry point to meet various scenarios and providing an NFT ecosystem with a harmonious technical community atmosphere. 141 | - 🤖 Model Training, providing ability to train models with custom datasets(LLMs). 142 | 143 | ## 06 QA 144 | 145 |
146 | 147 | Q1: Can onepoint be used on the Windows platform? 148 | 149 | Basic abilities such as chatting and switching roles can be used normally, but others such as IDE code selection and application, and browser content acquisition require native capabilities (applescript is used on the Mac platform), which is not yet supported on Windows. In the future, vbscript will be considered to implement similar capabilities. 150 | 151 |
152 | 153 |
154 | 155 | Q2: How to use code helpers or web scraping tools? 156 | 157 | First, you need to click on the icon on the left to select and switch to the corresponding mode (such as code refactoring, summarization, etc.), and then select a piece of code in the IDE or focus the mouse on the current browser. Use `command + k` to globally call up onepoint. At this time, it will display whether to make changes to the application, choose `yes`. 158 | 159 |
160 | 161 |
162 | 163 | Q3: What are the limitations of web scraping? 164 | 165 | Currently, there is a character limit of 4000 for web page crawling (excluding line breaks, carriage returns, and HTML tags) to achieve faster speed. In the future, the ability to segment long web pages with context will be used to summarize their contents. 166 | 167 |
168 | 169 |
170 | 171 | ## Contributors 172 | 173 | 174 | 175 | 176 | 177 | ## License 178 | 179 | [MIT License](./LICENSE) 180 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/.gitkeep -------------------------------------------------------------------------------- /assets/banner/banner.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/banner.01.png -------------------------------------------------------------------------------- /assets/banner/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/bar.png -------------------------------------------------------------------------------- /assets/banner/brand01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/brand01.png -------------------------------------------------------------------------------- /assets/banner/brand02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/brand02.png -------------------------------------------------------------------------------- /assets/banner/brand_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/brand_nobg.png -------------------------------------------------------------------------------- /assets/banner/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/banner/demo.gif -------------------------------------------------------------------------------- /assets/icon/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon.icns -------------------------------------------------------------------------------- /assets/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon.ico -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon.png -------------------------------------------------------------------------------- /assets/icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon128.png -------------------------------------------------------------------------------- /assets/icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon16.png -------------------------------------------------------------------------------- /assets/icon/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon24.png -------------------------------------------------------------------------------- /assets/icon/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon32.png -------------------------------------------------------------------------------- /assets/icon/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/icon/icon64.png -------------------------------------------------------------------------------- /assets/screenshot/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/account.png -------------------------------------------------------------------------------- /assets/screenshot/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/chat.png -------------------------------------------------------------------------------- /assets/screenshot/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/code.png -------------------------------------------------------------------------------- /assets/screenshot/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/console.png -------------------------------------------------------------------------------- /assets/screenshot/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/list.png -------------------------------------------------------------------------------- /assets/screenshot/plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/plugin.png -------------------------------------------------------------------------------- /assets/screenshot/prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/prompts.png -------------------------------------------------------------------------------- /assets/screenshot/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/setting.png -------------------------------------------------------------------------------- /assets/screenshot/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/assets/screenshot/video.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-typescript', 5 | [ 6 | '@babel/preset-react', 7 | { 8 | runtime: 'automatic', 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | [ 14 | '@babel/plugin-transform-runtime', 15 | { 16 | regenerator: true, 17 | }, 18 | ], 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | name: 'onepoint', 4 | executableName: 'onepoint', 5 | icon: 'assets/icon/icon.icns', 6 | extraResource: ['assets'], 7 | }, 8 | plugins: [ 9 | { 10 | name: '@electron-forge/plugin-webpack', 11 | config: { 12 | devContentSecurityPolicy: 13 | "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: file: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';", 14 | mainConfig: './webpack/main.webpack.js', 15 | renderer: { 16 | config: './webpack/renderer.webpack.js', 17 | entryPoints: [ 18 | { 19 | html: './public/index.html', 20 | js: './src/index.tsx', 21 | name: 'main_window', 22 | preload: { 23 | js: './src/electron/client/bridge.ts', 24 | }, 25 | }, 26 | ], 27 | }, 28 | }, 29 | }, 30 | ], 31 | makers: [ 32 | { 33 | name: '@electron-forge/maker-squirrel', 34 | config: {}, 35 | }, 36 | { 37 | name: '@electron-forge/maker-zip', 38 | platforms: ['darwin'], 39 | }, 40 | { 41 | name: '@electron-forge/maker-deb', 42 | config: { 43 | options: { 44 | icon: 'assets/icon/icon.png', 45 | }, 46 | }, 47 | }, 48 | { 49 | name: '@electron-forge/maker-rpm', 50 | config: {}, 51 | }, 52 | ], 53 | } 54 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/tmp/jest_rs", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: true, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | // coverageDirectory: undefined, 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | // coverageProvider: "babel", 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | preset: 'ts-jest', 95 | 96 | // Run tests from one or more projects 97 | // projects: undefined, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: undefined, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: undefined, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | setupFilesAfterEnv: ['./tests/setupTests.ts'], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: 'jsdom', 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: undefined, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: undefined, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "/node_modules/" 175 | // ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | // verbose: undefined, 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onepoint", 3 | "author": "Edwiin Tsang ", 4 | "version": "0.1.6", 5 | "homepage": "https://www.1ptai.com/", 6 | "description": "An Electron boilerplate including TypeScript, React, Jest and ESLint.", 7 | "main": ".webpack/main", 8 | "scripts": { 9 | "start": "electron-forge start", 10 | "package": "electron-forge package", 11 | "make": "electron-forge make", 12 | "release": "electron-forge publish", 13 | "lint": "eslint . --ext js,ts", 14 | "lint-fix": "eslint --ext .ts,.tsx --fix .", 15 | "lint-staged": "lint-staged", 16 | "format": "prettier --write .", 17 | "format-check": "prettier --check .", 18 | "test": "jest", 19 | "prepare": "husky install", 20 | "rebuild": "./node_modules/.bin/electron-rebuild" 21 | }, 22 | "lint-staged": { 23 | "*.{js,ts,jsx,tsx}": [ 24 | "yarn format", 25 | "yarn lint" 26 | ] 27 | }, 28 | "keywords": [], 29 | "license": "MIT", 30 | "dependencies": { 31 | "applescript": "^1.0.0", 32 | "chatgpt": "^5.1.3", 33 | "electron-clipboard-watcher": "^1.0.1", 34 | "electron-log": "^5.0.0-beta.16", 35 | "electron-store": "^8.1.0", 36 | "i18next": "^22.4.15", 37 | "i18next-http-backend": "^2.2.0", 38 | "node-abi": "^3.33.0", 39 | "openai": "^3.2.1", 40 | "phantomjscloud": "^3.5.5", 41 | "pubsub-js": "^1.9.4", 42 | "react": "17.0.2", 43 | "react-dom": "17.0.2", 44 | "react-hot-loader": "4.13.0", 45 | "react-i18next": "^12.2.0", 46 | "react-markdown": "^8.0.6", 47 | "react-syntax-highlighter": "^15.5.0", 48 | "socks-proxy-agent": "^7.0.0", 49 | "styled-components": "5.3.0" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "7.14.6", 53 | "@babel/plugin-transform-runtime": "7.14.5", 54 | "@babel/preset-env": "7.14.5", 55 | "@babel/preset-react": "7.14.5", 56 | "@babel/preset-typescript": "7.14.5", 57 | "@commitlint/cli": "^17.5.1", 58 | "@commitlint/config-conventional": "^17.4.4", 59 | "@electron-forge/cli": "^6.0.4", 60 | "@electron-forge/maker-deb": "^6.0.4", 61 | "@electron-forge/maker-rpm": "^6.0.4", 62 | "@electron-forge/maker-squirrel": "^6.0.4", 63 | "@electron-forge/maker-zip": "^6.0.4", 64 | "@electron-forge/plugin-webpack": "^6.1.0", 65 | "@marshallofsound/webpack-asset-relocator-loader": "0.5.0", 66 | "@reduxjs/toolkit": "^1.9.3", 67 | "@testing-library/jest-dom": "5.14.1", 68 | "@testing-library/react": "11.2.7", 69 | "@types/electron-devtools-installer": "2.2.0", 70 | "@types/jest": "26.0.23", 71 | "@types/react": "17.0.11", 72 | "@types/react-dom": "17.0.8", 73 | "@types/react-syntax-highlighter": "^15.5.6", 74 | "@types/styled-components": "5.1.10", 75 | "@typescript-eslint/eslint-plugin": "^5.57.1", 76 | "@typescript-eslint/parser": "^5.57.1", 77 | "@vercel/webpack-asset-relocator-loader": "^1.7.3", 78 | "@zeit/webpack-asset-relocator-loader": "^0.8.0", 79 | "ajv": "^7", 80 | "ajv-formats": "^2.1.1", 81 | "antd": "^5.3.3", 82 | "babel-loader": "8.2.2", 83 | "commitizen": "^4.3.0", 84 | "cross-env": "7.0.3", 85 | "cz-conventional-changelog": "^3.3.0", 86 | "electron": "^24.0.0", 87 | "electron-rebuild": "^3.2.9", 88 | "eslint": "^8.37.0", 89 | "eslint-config-airbnb": "^19.0.4", 90 | "eslint-config-prettier": "8.3.0", 91 | "eslint-config-standard": "16.0.3", 92 | "eslint-plugin-import": "^2.27.5", 93 | "eslint-plugin-jsx-a11y": "^6.7.1", 94 | "eslint-plugin-node": "11.1.0", 95 | "eslint-plugin-prettier": "3.4.0", 96 | "eslint-plugin-promise": "5.1.0", 97 | "eslint-plugin-react": "^7.32.2", 98 | "eslint-plugin-react-hooks": "^4.6.0", 99 | "eslint-plugin-standard": "5.0.0", 100 | "eslint-plugin-unused-imports": "^2.0.0", 101 | "file-loader": "^6.2.0", 102 | "html2plaintext": "^2.1.4", 103 | "husky": "^8.0.3", 104 | "jest": "27.0.4", 105 | "lint-staged": "^13.2.0", 106 | "npm-run-all": "4.1.5", 107 | "prettier": "2.3.1", 108 | "react-redux": "^8.0.5", 109 | "ts-jest": "27.0.3", 110 | "typescript": "4.3.4", 111 | "wait-on": "5.3.0", 112 | "watcher": "^2.2.2" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | arrowParens: 'avoid', 5 | } 6 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Electron starter 9 | 10 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "semanticCommits": true, 4 | "stabilityDays": 3, 5 | "prCreation": "not-pending", 6 | "labels": ["dependencies"] 7 | } 8 | -------------------------------------------------------------------------------- /src/@types/bridge.d.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../electron/client/bridge' 2 | 3 | declare global { 4 | // eslint-disable-next-line 5 | interface Window { 6 | Main: typeof api 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/@types/deps.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pubsub-js' 2 | -------------------------------------------------------------------------------- /src/@types/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import 'i18next' 2 | 3 | declare module 'i18next' { 4 | interface CustomTypeOptions { 5 | returnNull: false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/@types/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpeg' 3 | declare module '*.jpg' 4 | declare module '*.gif' 5 | -------------------------------------------------------------------------------- /src/@types/index.ts: -------------------------------------------------------------------------------- 1 | export interface PluginType { 2 | logo: string 3 | id: PresetType 4 | title: PresetType 5 | desc?: string 6 | loading: boolean 7 | inputDisable?: boolean 8 | nostore?: boolean 9 | monitorClipboard?: boolean 10 | monitorBrowser?: boolean 11 | } 12 | export interface PresetModule { 13 | listVisible: boolean 14 | builtInPlugins: PluginType[] 15 | currentPreset: PresetType 16 | } 17 | 18 | export enum PresetType { 19 | Casual = 'Casual', 20 | Translator = 'Translator', 21 | Summarizer = 'Summarizer', 22 | Programmer = 'Programmer', 23 | Analyst = 'Analyst', 24 | } 25 | 26 | export interface PanelVisible { 27 | plugin?: boolean 28 | setting?: boolean 29 | chatPanel?: boolean 30 | } 31 | 32 | export interface DataType { 33 | key: string 34 | character: string 35 | prompt: string 36 | } 37 | 38 | export interface PosType { 39 | posX: number 40 | posY: number 41 | } 42 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState, useLayoutEffect } from 'react' 2 | import { Image, message } from 'antd' 3 | import common from './electron/constants/common' 4 | import PubSub from 'pubsub-js' 5 | import { MoreOutlined } from '@ant-design/icons' 6 | import { GlobalStyle } from './styles/GlobalStyle' 7 | import { ChatPanel } from './components/ChatPanel' 8 | import { Setting } from './components/Setting' 9 | import { Preset } from './components/Preset' 10 | import { Logo } from './components/Logo' 11 | import Search from './components/Search' 12 | import { Prompt as PromptModal } from './components/Modal/prompt' 13 | 14 | import { init as initI18n } from './i18n' 15 | import { useAppDispatch, useAppSelector } from './app/hooks' 16 | import { StoreKey } from './app/constants' 17 | import { draggableStyle } from './utils' 18 | import { setVisible as setChatVisible } from './features/chat/chatSlice' 19 | import { 20 | setListVisible as setPresetListVisible, 21 | setPreset, 22 | } from './features/preset/presetSlice' 23 | import { 24 | setVisible as setSettingVisible, 25 | setMinimal, 26 | setLng, 27 | setContexual, 28 | setStore as setStoreSet, 29 | defaultVals, 30 | } from './features/setting/settingSlice' 31 | import { setUrl, setSelection } from './features/clipboard/clipboardSlice' 32 | 33 | import { 34 | selection_change, 35 | url_change, 36 | setting_show, 37 | } from './electron/constants/event' 38 | import { PresetType, PanelVisible } from './@types' 39 | interface Tips { 40 | type: 'success' | 'error' | 'warning' 41 | message: string 42 | } 43 | 44 | export function App() { 45 | const [show, setShow] = useState(false) 46 | const presetState = useAppSelector(state => state.preset) 47 | const [messageApi, contextHolder] = message.useMessage() 48 | const dispatch = useAppDispatch() 49 | useRef(null) 50 | const preset = presetState.builtInPlugins.filter( 51 | p => p.title === presetState.currentPreset 52 | ) 53 | const presetIcon = preset.length > 0 ? preset[0].logo : null 54 | 55 | // TODO: need to perf 56 | const getSettings = async () => { 57 | const getter = window.Main.getSettings 58 | const lng = await getter(StoreKey.Set_Lng) 59 | dispatch(setLng(lng || defaultVals.lng)) 60 | initI18n(lng) 61 | setShow(true) 62 | const storeSet = await getter(StoreKey.Set_StoreChat) 63 | dispatch(setStoreSet(storeSet || defaultVals.store)) 64 | const contextual = await getter(StoreKey.Set_Contexual) 65 | dispatch(setContexual(contextual || defaultVals.contexual)) 66 | const simpleMode = await getter(StoreKey.Set_SimpleMode) 67 | dispatch(setMinimal(simpleMode || false)) 68 | } 69 | 70 | useLayoutEffect(() => { 71 | getSettings() 72 | }, []) 73 | 74 | useEffect(() => { 75 | if (common.production()) { 76 | window.addEventListener('mousemove', event => { 77 | const flag = event.target === document.documentElement 78 | window.Main.ignoreWinMouse(flag) 79 | }) 80 | } 81 | window.Main.on( 82 | selection_change, 83 | (selection: { txt: string; app: string }) => { 84 | const { txt, app } = selection 85 | dispatch(setSelection({ txt, app })) 86 | dispatch(setChatVisible(!!txt && !!app)) 87 | } 88 | ) 89 | window.Main.on(url_change, (selection: { url: string }) => { 90 | const { url } = selection 91 | dispatch(setUrl({ url })) 92 | dispatch(setChatVisible(true)) 93 | }) 94 | window.Main.on(setting_show, () => 95 | showPanel({ 96 | setting: true, 97 | }) 98 | ) 99 | PubSub.subscribe('tips', (name: string, data: Tips) => { 100 | const { type, message } = data 101 | messageApi.open({ 102 | type, 103 | content: message, 104 | }) 105 | }) 106 | PubSub.subscribe('showPanel', (name: string, data: PanelVisible) => { 107 | showPanel(data) 108 | }) 109 | }, []) 110 | 111 | const showPanel = (options: PanelVisible) => { 112 | const { plugin, setting, chatPanel } = options 113 | dispatch(setPresetListVisible(!!plugin && !setting && !chatPanel)) 114 | dispatch(setSettingVisible(!plugin && !!setting && !chatPanel)) 115 | dispatch(setChatVisible(!plugin && !setting && !!chatPanel)) 116 | } 117 | 118 | const onPresetChange = (preset: PresetType) => { 119 | dispatch(setPreset(preset)) 120 | window.Main.setUsePreset(preset) 121 | } 122 | 123 | return show ? ( 124 | <> 125 | 126 | {contextHolder} 127 |
128 |
129 | {presetIcon ? ( 130 | 136 | showPanel({ 137 | plugin: true, 138 | }) 139 | } 140 | /> 141 | ) : null} 142 | 143 | { 146 | PubSub.publish('showPromptModal') 147 | }} 148 | /> 149 | 150 |
151 | 152 | 153 | 154 | 155 |
156 | 157 | ) : null 158 | } 159 | 160 | const padding = 15 161 | const styles = { 162 | container: { 163 | backgroundColor: '#FFF', 164 | border: 'none', 165 | borderRadius: 15, 166 | borderWidth: 1, 167 | borderColor: '#FFF', 168 | overflow: 'hidden', 169 | }, 170 | inputWrap: { 171 | position: 'relative', 172 | display: 'flex', 173 | flexDirection: 'row', 174 | border: 'none', 175 | borderWidth: 0, 176 | borderColor: '#FFF', 177 | justifyContent: 'space-between', 178 | alignItems: 'center', 179 | padding, 180 | ...draggableStyle(true), 181 | } as React.CSSProperties, 182 | nonDragable: { 183 | ...draggableStyle(false), 184 | } as React.CSSProperties, 185 | moreIcon: { 186 | fontSize: 20, 187 | margin: '0 10px', 188 | ...draggableStyle(false), 189 | }, 190 | } 191 | -------------------------------------------------------------------------------- /src/app/api.ts: -------------------------------------------------------------------------------- 1 | export const baseApiHost = 'http://127.0.0.1:4000' 2 | -------------------------------------------------------------------------------- /src/app/constants.ts: -------------------------------------------------------------------------------- 1 | import { chat, translate, code, post } from './images' 2 | import { PluginType } from '../@types' 3 | 4 | export const Casual = 'Casual' 5 | export const Translator = 'Translator' 6 | export const Summarizer = 'Summarizer' 7 | export const Programmer = 'Programmer' 8 | export const Analyst = 'Analyst' 9 | export const BuiltInPlugins = [ 10 | { 11 | logo: chat, 12 | id: Casual, 13 | title: Casual, 14 | loading: false, 15 | desc: 'Chat mode, feel free to ask any questions you want.', 16 | }, 17 | { 18 | logo: code, 19 | id: Programmer, 20 | title: Programmer, 21 | loading: false, 22 | inputDisable: true, 23 | desc: 'Code Master, generate or refactor the code you want.', 24 | nostore: true, 25 | }, 26 | { 27 | logo: post, 28 | id: Summarizer, 29 | title: Summarizer, 30 | loading: false, 31 | inputDisable: true, 32 | desc: 'Content analysis summary assistant, helps you read and browse web pages more effectively.', 33 | nostore: true, 34 | monitorBrowser: true, 35 | }, 36 | { 37 | logo: translate, 38 | id: Translator, 39 | title: Translator, 40 | loading: false, 41 | inputDisable: false, 42 | desc: 'Language expert, proficient in various languages from different countries.', 43 | nostore: true, 44 | }, 45 | ] as PluginType[] 46 | 47 | export const Prompts_Link = 48 | 'https://github.com/onepointAI/awesome-chatgpt-prompts/blob/main/prompts.csv' 49 | export const Prompts_ZH_Link = 50 | 'https://github.com/PlexPt/awesome-chatgpt-prompts-zh/blob/main/prompts-zh.json' 51 | 52 | export const Models = ['gpt-3.5-turbo-0301'] 53 | export const StoreKey = { 54 | Set_Model: 'KEY_MODEL', 55 | Set_BasePath: 'BASE_PATH', 56 | Set_ApiKey: 'APIKEY_GPT', 57 | Set_Lng: 'LNG', 58 | Set_StoreChat: 'STORE_CHAT', 59 | Set_SimpleMode: 'SIMPLE_MODE', 60 | Set_Contexual: 'CONTEXUAL', 61 | History_Chat: 'CHAT_HISTORY', 62 | List_Prompt: 'PROMPT_LIST', 63 | Map_Pluginprompt: 'PLUGIN_PROMPT_MAP', 64 | } 65 | -------------------------------------------------------------------------------- /src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | import type { RootState, AppDispatch } from './store' 3 | 4 | export const useAppDispatch: () => AppDispatch = useDispatch 5 | export const useAppSelector: TypedUseSelectorHook = useSelector 6 | -------------------------------------------------------------------------------- /src/app/images.ts: -------------------------------------------------------------------------------- 1 | // https://imgloc.com/ 2 | export const brand = 'https://i.imgur.com/T5ELmVC.png' 3 | export const logo = 'https://i.postimg.cc/tTJ3yHM9/pointer.png' 4 | export const robot = 'https://www.1ptai.com/logo_v2.png' 5 | 6 | export const chat = 'https://i.328888.xyz/2023/04/05/i8Skpc.png' 7 | export const post = 'https://i.328888.xyz/2023/04/05/i8S8Tz.png' 8 | export const code = 'https://i.328888.xyz/2023/04/05/i8SjEq.png' 9 | export const translate = 'https://i.328888.xyz/2023/04/05/i8SNkw.png' 10 | export const loadingGif = 11 | 'https://superstorefinder.net/support/wp-content/uploads/2018/01/elastic.gif' 12 | export const searchLogo = 'https://i.328888.xyz/2023/04/06/iI8ySQ.png' 13 | export const searchLogov2 = 'https://i.328888.xyz/2023/04/09/ic2Y8N.png' 14 | export const logoLoading = 'http://superstorefinder.net/img/ripple-loader.svg' 15 | export const logoSpin = 'https://i.328888.xyz/2023/04/09/icbeld.gif' 16 | -------------------------------------------------------------------------------- /src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, combineReducers } from '@reduxjs/toolkit' 2 | import chatReducer, { 3 | initialState as chatInitState, 4 | } from '../features/chat/chatSlice' 5 | import presetReducer, { 6 | initialState as presetInitState, 7 | } from '../features/preset/presetSlice' 8 | import settingReducer, { 9 | initialState as settingInitState, 10 | } from '../features/setting/settingSlice' 11 | import clipboardReducer, { 12 | initialState as clipboardInitState, 13 | } from '../features/clipboard/clipboardSlice' 14 | 15 | export type StateType = { 16 | chat: typeof chatInitState 17 | preset: typeof presetInitState 18 | setting: typeof settingInitState 19 | clipboard: typeof clipboardInitState 20 | } 21 | 22 | export const initialState: StateType = { 23 | chat: chatInitState, 24 | preset: presetInitState, 25 | setting: settingInitState, 26 | clipboard: clipboardInitState, 27 | } 28 | 29 | const store = configureStore({ 30 | reducer: combineReducers({ 31 | chat: chatReducer, 32 | preset: presetReducer, 33 | setting: settingReducer, 34 | clipboard: clipboardReducer, 35 | }), 36 | preloadedState: initialState, 37 | }) 38 | 39 | export type RootState = ReturnType 40 | export type AppDispatch = typeof store.dispatch 41 | export default store 42 | -------------------------------------------------------------------------------- /src/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/src/assets/icon128.png -------------------------------------------------------------------------------- /src/components/Button/Button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { Button } from './index' 3 | 4 | test('button should renders', () => { 5 | const { getByText } = render() 6 | 7 | expect(getByText('ButtonContent')).toBeTruthy() 8 | expect(getByText('ButtonContent')).toHaveAttribute('type', 'button') 9 | }) 10 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, ButtonHTMLAttributes } from 'react' 2 | 3 | import { Container } from './styles' 4 | 5 | type ButtonProps = { 6 | children: ReactNode 7 | } & ButtonHTMLAttributes 8 | 9 | export function Button(props: ButtonProps) { 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Container = styled.button` 4 | height: 42px; 5 | padding: 0 24px; 6 | 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | background: #8257e6; 12 | border-radius: 8px; 13 | border: 0; 14 | 15 | color: #fff; 16 | font-size: 16px; 17 | font-weight: bold; 18 | 19 | cursor: pointer; 20 | 21 | &:hover { 22 | filter: brightness(0.9); 23 | } 24 | 25 | &:active { 26 | filter: brightness(0.7); 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /src/components/ChatPanel/OperatePanel.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd' 2 | 3 | interface Props { 4 | tips: string 5 | app?: string 6 | confirmFn: () => void 7 | cancelFn: () => void 8 | } 9 | 10 | export function OperatePanel(props: Props) { 11 | const { tips, app, confirmFn, cancelFn } = props 12 | return ( 13 |
14 | 15 | {tips} 16 | {app ? {app} : null} 17 | 18 | 21 | 24 |
25 | ) 26 | } 27 | 28 | const padding = 15 29 | const styles = { 30 | selectWrap: { 31 | backgroundColor: 'rgb(240 240 240)', 32 | fontSize: 13, 33 | padding, 34 | }, 35 | selection: { 36 | color: 'rgb(74 74 74)', 37 | marginRight: 20, 38 | }, 39 | selectApp: { 40 | fontSize: 15, 41 | fontWeight: 'bold', 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ChatPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | import { Divider, Button, Alert, ConfigProvider } from 'antd' 3 | import { useTranslation } from 'react-i18next' 4 | import PubSub from 'pubsub-js' 5 | 6 | import ReactMarkdown from 'react-markdown' 7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 8 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism' 9 | import { CopyOutlined, ClearOutlined, SoundOutlined } from '@ant-design/icons' 10 | 11 | import { useAppSelector, useAppDispatch } from '../../app/hooks' 12 | import { BuiltInPlugins, StoreKey } from '../../app/constants' 13 | import { 14 | fetchChatResp, 15 | fetchWebCrawlResp, 16 | setCurPrompt, 17 | saveResp, 18 | } from '../../features/chat/chatSlice' 19 | import { setSelection, setUrl } from '../../features/clipboard/clipboardSlice' 20 | import { PluginType, PresetType } from '../../@types' 21 | import { ChatContent } from '../../electron/types' 22 | import { OperatePanel } from './OperatePanel' 23 | 24 | export function ChatPanel() { 25 | const { t } = useTranslation() 26 | const dispatch = useAppDispatch() 27 | const chatState = useAppSelector(state => state.chat) 28 | const presetState = useAppSelector(state => state.preset) 29 | const settingState = useAppSelector(state => state.setting) 30 | const clipboardState = useAppSelector(state => state.clipboard) 31 | const [minimal, setMinimal] = useState(true) 32 | const [chatList, setChatList] = useState([]) 33 | const [showSelection, setShowSelection] = useState(false) 34 | const [showUrl, setShowUrl] = useState('') 35 | const [usePlugin, setUsePlugin] = useState() 36 | const bottomLineRef = useRef(null) 37 | 38 | const fetchChatList = async () => { 39 | const list = await window.Main.getChatList(presetState.currentPreset) 40 | setChatList(list) 41 | } 42 | 43 | const fetchMinimal = async () => { 44 | const minimal = await window.Main.getSettings(StoreKey.Set_SimpleMode) 45 | setMinimal(minimal || false) 46 | } 47 | 48 | useEffect(() => { 49 | if (!chatState.isGenerating && bottomLineRef) { 50 | setTimeout(() => { 51 | bottomLineRef.current?.scrollIntoView({ behavior: 'smooth' }) 52 | }, 100) 53 | if (!usePlugin?.nostore && !minimal && settingState.store) { 54 | dispatch( 55 | setCurPrompt({ 56 | preset: presetState.currentPreset, 57 | content: '', 58 | }) 59 | ) 60 | dispatch( 61 | saveResp({ 62 | preset: presetState.currentPreset, 63 | content: '', 64 | }) 65 | ) 66 | fetchChatList() 67 | } 68 | } 69 | }, [chatState.isGenerating]) 70 | 71 | useEffect(() => { 72 | fetchMinimal() 73 | }, [settingState.minimal]) 74 | 75 | useEffect(() => { 76 | // TODO: should use id 77 | const plugin = BuiltInPlugins.filter( 78 | item => presetState.currentPreset === item.title 79 | )[0] 80 | setUsePlugin(plugin) 81 | fetchChatList() 82 | }, [ 83 | presetState.currentPreset, 84 | chatState.curPrompt[presetState.currentPreset], 85 | ]) 86 | 87 | useEffect(() => { 88 | setShowSelection( 89 | !!clipboardState.selectTxt && 90 | !!clipboardState.selectApp && 91 | !!usePlugin?.inputDisable 92 | ) 93 | }, [ 94 | clipboardState.selectTxt, 95 | clipboardState.selectApp, 96 | usePlugin?.inputDisable, 97 | ]) 98 | 99 | useEffect(() => { 100 | setShowUrl(clipboardState.url) 101 | }, [clipboardState.url, usePlugin?.inputDisable]) 102 | 103 | const speakRsp = (resp: string) => { 104 | window.Main.speakText(resp) 105 | } 106 | 107 | const copyRsp = (resp: string) => { 108 | window.Main.copyText(resp) 109 | PubSub.publish('tips', { 110 | type: 'success', 111 | message: 'Copyed Successfully', 112 | }) 113 | } 114 | 115 | const delRecord = async (index?: number) => { 116 | if (typeof index === 'undefined') return 117 | const list = await window.Main.removeChat(presetState.currentPreset, index) 118 | PubSub.publish('tips', { 119 | type: 'success', 120 | message: 'Deleted successfully', 121 | }) 122 | dispatch( 123 | setCurPrompt({ 124 | preset: presetState.currentPreset, 125 | content: '', 126 | }) 127 | ) 128 | dispatch( 129 | saveResp({ 130 | preset: presetState.currentPreset, 131 | content: '', 132 | }) 133 | ) 134 | setChatList(list) 135 | } 136 | 137 | const atemptChange = (resp: string) => { 138 | window.Main.attemptChange(resp.replace(/^`{3}[^\n]+|`{3}$/g, '')) 139 | } 140 | 141 | const doRequest = (txt: string) => { 142 | const qa = txt 143 | dispatch(setSelection({ txt: '', app: '' })) 144 | dispatch( 145 | fetchChatResp({ 146 | prompt: qa, 147 | preset: presetState.currentPreset, 148 | }) 149 | ) 150 | } 151 | 152 | const doSummaryWebsite = (url: string) => { 153 | dispatch(setUrl({ url: '' })) 154 | dispatch( 155 | fetchWebCrawlResp({ 156 | url, 157 | preset: presetState.currentPreset, 158 | }) 159 | ) 160 | } 161 | 162 | const cancelRequest = () => { 163 | dispatch( 164 | setSelection({ 165 | txt: '', 166 | app: '', 167 | }) 168 | ) 169 | } 170 | 171 | const showCopyFromEditor = () => { 172 | return showSelection && usePlugin?.id === PresetType.Programmer ? ( 173 | doRequest(clipboardState.selectTxt)} 177 | cancelFn={() => cancelRequest()} 178 | /> 179 | ) : null 180 | } 181 | 182 | const showSelectUrl = () => { 183 | return clipboardState.url && usePlugin?.monitorBrowser ? ( 184 | doSummaryWebsite(clipboardState.url)} 188 | cancelFn={() => cancelRequest()} 189 | /> 190 | ) : null 191 | } 192 | 193 | const showPrompt = (prompt: string, minimal?: boolean) => { 194 | return !minimal && !usePlugin?.nostore ? ( 195 |
➜ {prompt}
196 | ) : null 197 | } 198 | 199 | const showReply = (response: string, minimal?: boolean, index?: number) => { 200 | return ( 201 |
202 |
203 | 218 | ) : ( 219 | 220 | {children} 221 | 222 | ) 223 | }, 224 | }} 225 | /> 226 |
227 | {/* TODO: ban in windows & linux */} 228 | {response ? ( 229 | <> 230 | 231 |
232 | 240 | speakRsp(response)} 243 | /> 244 | copyRsp(response)} 247 | /> 248 | delRecord(index)} 251 | /> 252 |
253 | 254 | ) : null} 255 |
256 | ) 257 | } 258 | 259 | const respContent = chatState.resp[presetState.currentPreset] 260 | const showContent = 261 | showSelection || showUrl || respContent || chatState.respErr 262 | const showChat = 263 | ((chatState.visible && showContent) || !minimal) && 264 | !settingState.visible && 265 | !presetState.listVisible 266 | 267 | const curPrompt = chatState.curPrompt[presetState.currentPreset] 268 | return showChat ? ( 269 | 276 | 277 | {chatState.respErr ? ( 278 | 279 | ) : null} 280 |
281 | {showCopyFromEditor()} 282 | {showSelectUrl()} 283 | {!minimal 284 | ? chatList.map((chat, index) => ( 285 |
286 | {showPrompt(chat.prompt, minimal)} 287 | {showReply(chat.response, minimal, index)} 288 |
289 | )) 290 | : null} 291 | {/* need to separate prompt and resp */} 292 | {curPrompt ? showPrompt(curPrompt, minimal) : null} 293 | {respContent ? showReply(respContent) : null} 294 | {chatState.webCrawlResp ? showReply(chatState.webCrawlResp) : null} 295 |
296 |
297 |
298 | ) : null 299 | } 300 | 301 | const padding = 15 302 | const styles = { 303 | requestWrap: { 304 | backgroundColor: 'rgb(241 241 241)', 305 | fontSize: 15, 306 | lineHeight: '20px', 307 | fontWeight: 'bold', 308 | padding: '7px 45px 7px 45px', 309 | }, 310 | replyWrap: { 311 | position: 'relative', 312 | backgroundColor: '#FFF', 313 | fontSize: 14, 314 | lineHeight: 2.5, 315 | padding, 316 | }, 317 | mdWrap: { 318 | marginRight: 30, 319 | marginLeft: 30, 320 | overflow: 'auto', 321 | }, 322 | history: { 323 | maxHeight: 400, 324 | overflow: 'auto', 325 | }, 326 | bottomRspWrap: { 327 | position: 'relative', 328 | display: 'flex', 329 | flexDirection: 'row', 330 | justifyContent: 'center', 331 | alignItems: 'center', 332 | } as React.CSSProperties, 333 | speakIcon: { 334 | position: 'absolute', 335 | marginRight: 30, 336 | right: 65, 337 | top: 10, 338 | }, 339 | copyIcon: { 340 | position: 'absolute', 341 | marginRight: 15, 342 | right: 45, 343 | top: 10, 344 | }, 345 | clearIcon: { 346 | position: 'absolute', 347 | right: 25, 348 | top: 10, 349 | }, 350 | attemptBtn: { 351 | width: 300, 352 | fontSize: 12, 353 | fontWeight: 'bold', 354 | }, 355 | } 356 | -------------------------------------------------------------------------------- /src/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { Image } from 'antd' 3 | import PubSub from 'pubsub-js' 4 | import { searchLogov2, logoSpin } from '../../app/images' 5 | import { useAppSelector } from '../../app/hooks' 6 | import { draggableStyle } from '../../utils' 7 | interface Props { 8 | guardian?: boolean 9 | } 10 | 11 | export function Logo(props: Props) { 12 | const { guardian } = props 13 | const chatState = useAppSelector(state => state.chat) 14 | 15 | useEffect(() => { 16 | if (guardian) { 17 | // remote.getCurrentWindow().setPosition(10, 10) 18 | } 19 | }, [guardian]) 20 | 21 | return ( 22 |
23 | { 29 | PubSub.publish('showPanel', { 30 | setting: true, 31 | }) 32 | }} 33 | /> 34 | {chatState.inputDiabled ? ( 35 | 39 | ) : null} 40 |
41 | ) 42 | } 43 | 44 | const styles = { 45 | container: { 46 | position: 'relative', 47 | alignItems: 'center', 48 | justifyContent: 'center', 49 | ...draggableStyle(false), 50 | } as React.CSSProperties, 51 | guardian: { 52 | position: 'absolute', 53 | right: 30, 54 | bottom: 100, 55 | width: 200, 56 | height: 300, 57 | overflow: 'auto', 58 | flexDirection: 'column', 59 | // alignItems: 'flex-end', 60 | // justifyContent: 'flex-end', 61 | padding: 20, 62 | textAlign: 'right', 63 | } as React.CSSProperties, 64 | guardLoad: { 65 | position: 'absolute', 66 | right: 14, 67 | top: 15, 68 | width: 50, 69 | height: 50, 70 | } as React.CSSProperties, 71 | loading: { 72 | position: 'absolute', 73 | right: -5, 74 | top: -5, 75 | width: 50, 76 | height: 50, 77 | } as React.CSSProperties, 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Logo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Container = styled.button` 4 | height: 42px; 5 | padding: 0 24px; 6 | 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | background: #8257e6; 12 | border-radius: 8px; 13 | border: 0; 14 | 15 | color: #fff; 16 | font-size: 16px; 17 | font-weight: bold; 18 | 19 | cursor: pointer; 20 | 21 | &:hover { 22 | filter: brightness(0.9); 23 | } 24 | 25 | &:active { 26 | filter: brightness(0.7); 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /src/components/Modal/prompt.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Modal, Select, ConfigProvider } from 'antd' 3 | import { useTranslation } from 'react-i18next' 4 | import PubSub from 'pubsub-js' 5 | import { useAppSelector } from '../../app/hooks' 6 | interface SelectOption { 7 | value: string 8 | label: string 9 | } 10 | 11 | export function Prompt() { 12 | const { t } = useTranslation() 13 | const presetState = useAppSelector(state => state.preset) 14 | const [isModalOpen, setIsModalOpen] = useState(false) 15 | const [selectOptions, setSelectOptions] = useState([]) 16 | const [curval, setCurval] = useState('') 17 | 18 | const getPromptList = async () => { 19 | const list = await window.Main.getPromptList() 20 | const options = list.map((item: { character: string }) => { 21 | return { 22 | label: item.character, 23 | value: item.character, 24 | } 25 | }) 26 | setSelectOptions(options) 27 | } 28 | 29 | const getUseCharacter = async () => { 30 | const prompt = await window.Main.getPluginPrompt(presetState.currentPreset) 31 | setCurval(prompt.character) 32 | } 33 | 34 | useEffect(() => { 35 | getUseCharacter() 36 | }, [presetState.currentPreset]) 37 | 38 | useEffect(() => { 39 | getPromptList() 40 | PubSub.subscribe('showPromptModal', () => { 41 | showModal() 42 | }) 43 | }, []) 44 | 45 | const showModal = () => { 46 | setIsModalOpen(true) 47 | getPromptList() 48 | } 49 | const handleOk = () => { 50 | setIsModalOpen(false) 51 | } 52 | const handleCancel = () => { 53 | setIsModalOpen(false) 54 | } 55 | const onChange = (value: string) => { 56 | console.log(`selected ${value}`) 57 | setCurval(value) 58 | window.Main.setPluginPrompt(presetState.currentPreset, value) 59 | } 60 | const onSearch = (value: string) => { 61 | console.log('search:', value) 62 | } 63 | 64 | return ( 65 | 72 | 78 | search()} 80 | disabled={chatState.inputDiabled} 81 | onFocus={() => 82 | PubSub.publish('showPanel', { 83 | chatPanel: true, 84 | }) 85 | } 86 | /> 87 | ) 88 | } 89 | 90 | const styles = { 91 | search: { 92 | height: 40, 93 | resize: 'none', 94 | ...draggableStyle(false), 95 | } as React.CSSProperties, 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Setting/Account/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Progress, Button, Form, Input, Select, Alert, Spin } from 'antd' 3 | import { useTranslation } from 'react-i18next' 4 | import { useAppDispatch, useAppSelector } from '../../../app/hooks' 5 | import { Models, StoreKey } from '../../../app/constants' 6 | import { fetchAccountDetail } from '../../../features/setting/settingSlice' 7 | 8 | const { Option } = Select 9 | 10 | export default function () { 11 | const { t } = useTranslation() 12 | const [form] = Form.useForm() 13 | const [, forceUpdate] = useState({}) 14 | const dispatch = useAppDispatch() 15 | const settingState = useAppSelector(state => state.setting) 16 | const [saveSuc, setSaveSuc] = useState(false) 17 | 18 | const formatDate = (num: number) => { 19 | return num.toString().padStart(2, '0') 20 | } 21 | 22 | const formatDateStr = (date: Date) => { 23 | return `${date.getFullYear()}-${formatDate( 24 | date.getMonth() + 1 25 | )}-${formatDate(date.getDate())}` 26 | } 27 | 28 | const refreshPage = () => { 29 | const date = new Date() 30 | const prevDate = new Date(date.valueOf() - 1000 * 60 * 60 * 24 * 99) 31 | dispatch( 32 | fetchAccountDetail({ 33 | startDate: formatDateStr(prevDate), 34 | endDate: formatDateStr(date), 35 | }) 36 | ) 37 | } 38 | 39 | // To disable submit button at the beginning. 40 | useEffect(() => { 41 | forceUpdate({}) 42 | refreshPage() 43 | }, []) 44 | 45 | useEffect(() => { 46 | form.resetFields() 47 | }, [settingState]) 48 | 49 | const onFinish = (values: any) => { 50 | setSaveSuc(true) 51 | window.Main.setStore(StoreKey.Set_BasePath, values.basePath) 52 | window.Main.setStore(StoreKey.Set_ApiKey, values.apikey) 53 | window.Main.setStore(StoreKey.Set_Model, values.model) 54 | refreshPage() 55 | } 56 | 57 | return ( 58 |
59 | 60 |
61 |
{t('Token Usage')}
62 | 68 | 69 |
81 | 82 | 83 | 84 | 85 | 88 | 89 | 94 | 101 | 102 | 103 | 106 | 107 | 110 | 111 |
112 |
113 | 114 | {saveSuc ? ( 115 | 116 | ) : null} 117 |
118 |
119 | ) 120 | } 121 | 122 | const styles = { 123 | wrap: { 124 | paddingTop: 10, 125 | }, 126 | inner: { 127 | marginBottom: 10, 128 | paddingLeft: 20, 129 | paddingRight: 20, 130 | }, 131 | title: { 132 | fontSize: 14, 133 | color: 'rgb(10, 11, 60)', 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Setting/Basic/index.tsx: -------------------------------------------------------------------------------- 1 | import { Select, Spin, Switch, Space } from 'antd' 2 | import { useTranslation } from 'react-i18next' 3 | import { useAppDispatch, useAppSelector } from '../../../app/hooks' 4 | import { StoreKey } from '../../../app/constants' 5 | import { 6 | setMinimal, 7 | setLng, 8 | setContexual, 9 | setStore as setStoreSet, 10 | // defaultVals, 11 | } from '../../../features/setting/settingSlice' 12 | import { Languages, localeOptions } from '../../../i18n' 13 | 14 | export default function () { 15 | const { t, i18n } = useTranslation() 16 | const dispatch = useAppDispatch() 17 | const settingState = useAppSelector(state => state.setting) 18 | const setStore = (key: string, value: string | boolean | number) => { 19 | window.Main.setStore(key, value) 20 | } 21 | 22 | return ( 23 |
24 | 25 |
26 | 27 |
28 |
{t('Language')}
29 | { 58 | dispatch(setStoreSet(val)) 59 | setStore(StoreKey.Set_StoreChat, val) 60 | }} 61 | value={settingState.store} 62 | options={[ 63 | { 64 | value: 1, 65 | label: 'YES', 66 | }, 67 | { 68 | value: 0, 69 | label: 'NO', 70 | }, 71 | ]} 72 | /> 73 |
74 | 75 |
76 |
{t('Quantity Of Context')}
77 | 144 | 145 | 146 | 147 | 148 | {/* */} 149 | jumpReference()}> 150 | {t('Prompt Reference')} 151 | 152 | {/* */} 153 | 154 | 155 |
156 | ) 157 | } 158 | 159 | const styles = { 160 | wrap: { 161 | paddingTop: 10, 162 | height: 400, 163 | overflow: 'auto', 164 | }, 165 | addBtn: { 166 | margin: '0px 0px 10px 10px', 167 | }, 168 | } 169 | -------------------------------------------------------------------------------- /src/components/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, Divider, ConfigProvider } from 'antd' 2 | import { useTranslation } from 'react-i18next' 3 | import { 4 | SettingFilled, 5 | UserOutlined, 6 | UsbOutlined, 7 | MacCommandOutlined, 8 | } from '@ant-design/icons' 9 | import Account from './Account' 10 | import Basic from './Basic' 11 | import Prompt from './Prompt' 12 | import { useAppSelector } from '../../app/hooks' 13 | 14 | export function Setting() { 15 | const settingState = useAppSelector(state => state.setting) 16 | const { t } = useTranslation() 17 | 18 | return settingState.visible ? ( 19 | 26 | 27 |
28 | 34 | 35 | {t('Setting')} 36 | 37 | ), 38 | key: '1', 39 | children: , 40 | }, 41 | { 42 | label: ( 43 | 44 | 45 | {t('Account')} 46 | 47 | ), 48 | key: '2', 49 | children: , 50 | }, 51 | { 52 | label: ( 53 | 54 | 55 | {t('Prompts')} 56 | 57 | ), 58 | key: '3', 59 | children: , 60 | // disabled: true, 61 | }, 62 | { 63 | label: ( 64 | 65 | 66 | {t('Advanced')} 67 | 68 | ), 69 | key: '4', 70 | children: , 71 | disabled: true, 72 | }, 73 | { 74 | label: ( 75 | 76 | 77 | {t('Plugins')} 78 | 79 | ), 80 | key: '5', 81 | children: 'Tab 3', 82 | disabled: true, 83 | }, 84 | ]} 85 | /> 86 |
87 |
88 | ) : null 89 | } 90 | 91 | const styles = { 92 | wrap: { 93 | backgroundColor: '#F8F8F8', 94 | padding: 15, 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /src/electron/apis/account.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store' 2 | import { StoreKey } from '../../app/constants' 3 | import { BalanceResponse } from '../types' 4 | 5 | const store = new Store() 6 | 7 | export default async (req: any, res: any) => { 8 | const { start_date, end_date } = req.body 9 | const basePath = store.get(StoreKey.Set_BasePath) as string 10 | const apiHost = basePath || `https://closeai.deno.dev` 11 | const apiKey = store.get(StoreKey.Set_ApiKey) as string 12 | const usemodel = store.get(StoreKey.Set_Model) as string 13 | const basic = { 14 | apiHost, 15 | usemodel, 16 | apiKey, 17 | } 18 | 19 | try { 20 | const response = await fetch( 21 | `${apiHost}/v1/dashboard/billing/usage?start_date=${start_date}&end_date=${end_date}`, 22 | { 23 | method: 'GET', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | Authorization: `Bearer ${apiKey}`, 27 | }, 28 | } 29 | ) 30 | 31 | const usageData = (await response.json()) as BalanceResponse 32 | res.send({ 33 | code: 0, 34 | result: { 35 | usageData, 36 | basic, 37 | }, 38 | }) 39 | } catch (e: any) { 40 | res.send({ 41 | code: -1, 42 | result: { 43 | message: e.message, 44 | basic, 45 | }, 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/electron/apis/apply.ts: -------------------------------------------------------------------------------- 1 | import { activeApp, applySelection } from '../os/applescript' 2 | import { Singleton } from '../utils/global' 3 | 4 | export default async (req: any, res: any) => { 5 | await activeApp(Singleton.getInstance().getRecentApp()) 6 | const result = await applySelection() 7 | res.send({ 8 | code: 0, 9 | result, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/electron/apis/crawl.ts: -------------------------------------------------------------------------------- 1 | import { ERR_CODES } from '../types' 2 | import { getAiInstance } from '../server' 3 | import { Logger } from '../utils/util' 4 | import { getPluginPrompt } from '../client/store' 5 | import { getBrowserContnet } from '../os/applescript' 6 | import { PresetType } from '../../@types' 7 | 8 | interface UserMsg { 9 | role: string 10 | content: string 11 | } 12 | 13 | function getUserContent(content: string): UserMsg { 14 | return { 15 | role: 'user', 16 | content, 17 | } 18 | } 19 | 20 | function generatePayload(contents: UserMsg[], preset: PresetType) { 21 | return { 22 | model: 'gpt-3.5-turbo-0301', 23 | messages: [ 24 | { 25 | role: 'system', 26 | content: getPluginPrompt(preset).prompt, 27 | }, 28 | ...contents, 29 | ], 30 | } 31 | } 32 | 33 | export default async (req: any, res: any) => { 34 | const { preset } = req.body 35 | try { 36 | const resp = (await getBrowserContnet()) as string 37 | const text = resp.replace(/[\r\n\t ]/g, '') 38 | if (text.length > 4000) { 39 | res.send({ 40 | code: ERR_CODES.TOKEN_TOO_LONG, 41 | result: null, 42 | message: 43 | 'The webpage content is too long(Exceeds 4000 characters.), which will affect the speed and experience of summarizing(Long article summary support is coming soon, please stay tuned)', 44 | }) 45 | return 46 | } 47 | 48 | // let chunkLen = 250; 49 | // let total = text.length; 50 | // let o = [] 51 | // Logger.log('length ===>', total) 52 | // for(let i = 0; i < total; i = i+chunkLen) { 53 | // o.push(text.slice(i, i+chunkLen)) 54 | // } 55 | // console.log('o ===>', o) 56 | // const contents = [] as UserMsg[]; 57 | // [...o, 'summarize this website:'].map((content: string) => { 58 | // contents.push(getUserContent(content)) 59 | // }) 60 | 61 | const contents = [getUserContent(text)] 62 | const completion = await getAiInstance().createChatCompletion( 63 | generatePayload(contents, preset) 64 | ) 65 | 66 | console.log(completion.data.choices) 67 | const result = completion.data.choices 68 | const respContent = result[0].message.content 69 | 70 | res.send({ 71 | code: 0, 72 | result: respContent, 73 | }) 74 | } catch (e) { 75 | Logger.error(e) 76 | res.send({ 77 | code: ERR_CODES.NETWORK_CONGESTION, 78 | result: e, 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/electron/apis/prompt.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store' 2 | import { ERR_CODES } from '../types' 3 | import { generatePayload, getAiInstance } from '../server' 4 | import { Logger } from '../utils/util' 5 | import { setChat } from '../client/store' 6 | import { StoreKey, BuiltInPlugins } from '../../app/constants' 7 | 8 | const store = new Store() 9 | 10 | export default async (req: any, res: any) => { 11 | const { prompt, preset } = req.body 12 | Logger.log('ask question', prompt) 13 | res.setHeader('Content-type', 'application/octet-stream') 14 | if (!getAiInstance()) { 15 | res.write(String(ERR_CODES.NOT_SET_APIKEY)) 16 | res.end() 17 | return 18 | } 19 | let result = '' 20 | try { 21 | const response = await getAiInstance().createChatCompletion( 22 | generatePayload(prompt, preset), 23 | { responseType: 'stream' } 24 | ) 25 | const stream = response.data 26 | stream.on('data', (chunk: Buffer) => { 27 | const payloads = chunk.toString().split('\n\n') 28 | for (const payload of payloads) { 29 | if (payload.includes('[DONE]')) return 30 | if (payload.startsWith('data:')) { 31 | const data = payload.replace(/(\n)?^data:\s*/g, '') 32 | try { 33 | const delta = JSON.parse(data.trim()) 34 | const resp = delta.choices[0].delta?.content 35 | result += resp || '' 36 | res.write(resp || '') 37 | Logger.log('chunk resp', resp) 38 | } catch (error) { 39 | const errmsg = `Error with JSON.parse and ${payload}.\n${error}` 40 | Logger.log(errmsg) 41 | res.write(errmsg) 42 | res.end() 43 | } 44 | } 45 | } 46 | }) 47 | stream.on('end', () => { 48 | Logger.log('Stream done') 49 | const usePlugin = BuiltInPlugins.filter( 50 | plugin => plugin.title === preset 51 | )[0] 52 | if (store.get(StoreKey.Set_StoreChat) && !usePlugin.nostore) { 53 | setChat({ 54 | prompt, 55 | response: result, 56 | preset, 57 | }) 58 | } 59 | res.end() 60 | }) 61 | stream.on('error', (e: Error) => { 62 | Logger.error(e) 63 | res.write(e.message) 64 | res.end() 65 | }) 66 | } catch (e) { 67 | Logger.error(e) 68 | if (e instanceof Error) { 69 | res.write(String(ERR_CODES.NETWORK_CONGESTION)) 70 | res.end() 71 | } else { 72 | res.write(JSON.stringify(e)) 73 | res.end() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/electron/apis/test.ts: -------------------------------------------------------------------------------- 1 | import { clipboard } from 'electron' 2 | import { getBrowserContnet } from '../os/applescript' 3 | import { Singleton } from '../utils/global' 4 | import { Logger } from '../utils/util' 5 | import { getAiInstance, generatePayload } from '../server' 6 | const h2p = require('html2plaintext') 7 | 8 | export default async (req: any, res: any) => { 9 | try { 10 | if (!getAiInstance()) { 11 | res.send({ 12 | code: -1, 13 | result: 'openkey not set!', 14 | }) 15 | return 16 | } 17 | clipboard.writeText('test resp text') 18 | try { 19 | // await activeApp(Singleton.getInstance().getRecentApp()) 20 | const content = await getBrowserContnet() 21 | const plainText = h2p(content) 22 | Logger.log('getBrowserContnet:', plainText) 23 | 24 | const completion = await getAiInstance().createChatCompletion( 25 | generatePayload(`${plainText}`, 'Summarizer') 26 | ) 27 | Logger.log(completion.data.choices) 28 | const result = completion.data.choices 29 | const respContent = result[0].message.content 30 | clipboard.writeText(respContent) 31 | Singleton.getInstance().setCopyStateSource(true) 32 | res.send({ 33 | code: 0, 34 | result, 35 | }) 36 | } catch (e) { 37 | Logger.error('getBrowserContnet:', e) 38 | } 39 | } catch (e) { 40 | res.send({ 41 | code: -1, 42 | result: e, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/electron/client/bridge.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | import { PresetType, PosType } from '../../@types' 3 | import { Languages } from '../../i18n' 4 | import { 5 | winIgnoreMouse, 6 | winMouseMove, 7 | attemptChange, 8 | usePreset, 9 | jumpLink, 10 | copyText, 11 | speakText, 12 | setStore, 13 | getStore, 14 | removeChat, 15 | getChatList, 16 | addPrompt, 17 | removePrompt, 18 | getPromptList, 19 | editPrompt, 20 | setPluginPrompt, 21 | getPluginPrompt, 22 | changeLanguage, 23 | runScript, 24 | setWinSize, 25 | } from '../constants/event' 26 | 27 | export const api = { 28 | /** 29 | * Emit events 30 | */ 31 | ignoreWinMouse: (ignore: boolean) => ipcRenderer.send(winIgnoreMouse, ignore), 32 | moveWindowPos: (pos: PosType) => ipcRenderer.send(winMouseMove, pos), 33 | setWinSize: (w: number, h: number) => ipcRenderer.send(setWinSize, { w, h }), 34 | setUsePreset: (preset: PresetType) => ipcRenderer.send(usePreset, preset), 35 | jumpLink: (link: string) => ipcRenderer.send(jumpLink, link), 36 | changeLanguage: (lng: Languages) => ipcRenderer.send(changeLanguage, lng), 37 | setStore: (key: string, blob: any) => 38 | ipcRenderer.invoke(setStore, { key, blob }), 39 | getSettings: (key: string) => ipcRenderer.invoke(getStore, key), 40 | attemptChange: (changes: string) => 41 | ipcRenderer.invoke(attemptChange, changes), 42 | copyText: (changes: string) => ipcRenderer.invoke(copyText, changes), 43 | speakText: (changes: string) => ipcRenderer.invoke(speakText, changes), 44 | getChatList: (preset: PresetType) => ipcRenderer.invoke(getChatList, preset), 45 | removeChat: (preset: PresetType, index: number) => 46 | ipcRenderer.invoke(removeChat, { preset, index }), 47 | addPrompt: (character: string, prompt: string) => 48 | ipcRenderer.invoke(addPrompt, { character, prompt }), 49 | editPrompt: (former: string, character: string, prompt: string) => 50 | ipcRenderer.invoke(editPrompt, { former, character, prompt }), 51 | removePrompt: (character: string) => 52 | ipcRenderer.invoke(removePrompt, { character }), 53 | getPromptList: () => ipcRenderer.invoke(getPromptList), 54 | setPluginPrompt: (plugin: string, character: string) => 55 | ipcRenderer.invoke(setPluginPrompt, { plugin, character }), 56 | getPluginPrompt: (plugin: string) => 57 | ipcRenderer.invoke(getPluginPrompt, { plugin }), 58 | runScript: (script: string) => ipcRenderer.invoke(runScript, script), 59 | 60 | /** 61 | * Provide an easier way to listen to events 62 | */ 63 | on: (channel: string, callback: (data: any) => void) => { 64 | ipcRenderer.on(channel, (_, data) => callback(data)) 65 | }, 66 | } 67 | 68 | contextBridge?.exposeInMainWorld('Main', api) 69 | -------------------------------------------------------------------------------- /src/electron/client/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainInvokeEvent, ipcMain, BrowserWindow, clipboard } from 'electron' 2 | import { Logger } from '../utils/util' 3 | import { Singleton } from '../utils/global' 4 | import { activeApp, applySelection } from '../os/applescript' 5 | import { copyText, attemptChange } from '../constants/event' 6 | 7 | const clipboardWatcher = require('electron-clipboard-watcher') 8 | 9 | export function listen(_win: BrowserWindow | null) { 10 | clipboardWatcher({ 11 | watchDelay: 200, 12 | onImageChange() {}, 13 | onTextChange(_text: string) { 14 | if (Singleton.getInstance().getCopyFromElectron()) { 15 | Singleton.getInstance().setCopyStateSource(false) 16 | } 17 | // TODO: Pop-up notifications for clipboard content changes are unnecessary and can have a significant impact on user experience. 18 | // setWindowVisile(true) 19 | // win?.webContents.send(clipboard_change, text) 20 | }, 21 | }) 22 | 23 | ipcMain.handle( 24 | copyText, 25 | async (event: IpcMainInvokeEvent, changes: string) => { 26 | try { 27 | clipboard.writeText(changes) 28 | return true 29 | } catch (e) { 30 | Logger.error(e) 31 | return false 32 | } 33 | } 34 | ) 35 | 36 | ipcMain.handle( 37 | attemptChange, 38 | async (event: IpcMainInvokeEvent, changes: string) => { 39 | try { 40 | clipboard.writeText(changes) 41 | await activeApp(Singleton.getInstance().getRecentApp()) 42 | await applySelection() 43 | } catch (e) { 44 | Logger.error(e) 45 | } 46 | } 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/electron/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bridge' 2 | export * from './clipboard' 3 | export * from './link' 4 | export * from './store' 5 | export * from './window' 6 | -------------------------------------------------------------------------------- /src/electron/client/link.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainInvokeEvent, ipcMain, shell } from 'electron' 2 | import { jumpLink } from '../constants/event' 3 | 4 | export function setupLinkHandlers() { 5 | ipcMain.on(jumpLink, async (event: IpcMainInvokeEvent, link: string) => { 6 | shell.openExternal(link) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/electron/client/store.ts: -------------------------------------------------------------------------------- 1 | import { t } from 'i18next' 2 | import { IpcMainInvokeEvent, ipcMain } from 'electron' 3 | import Store from 'electron-store' 4 | import { ChatContent, PromptSet } from '../types' 5 | import { 6 | setStore, 7 | getStore, 8 | removeChat, 9 | addPrompt, 10 | editPrompt, 11 | removePrompt, 12 | getChatList as getChatListEvt, 13 | getPromptList as getPromptListEvt, 14 | getPluginPrompt as getPluginPromptEvt, 15 | setPluginPrompt as setPluginPromptEvt, 16 | } from '../constants/event' 17 | import { PresetType } from '../../@types' 18 | import { 19 | Casual, 20 | Translator, 21 | Summarizer, 22 | Programmer, 23 | Analyst, 24 | StoreKey, 25 | } from '../../app/constants' 26 | import { Languages } from '../../i18n' 27 | import * as prompts from '../prompt/prompts.json' 28 | 29 | const store = new Store() 30 | 31 | // TODO: need to refactor to schema 32 | export function init() { 33 | const promptTemplates = store.get(StoreKey.List_Prompt) as string | undefined 34 | const pluginPrompts = store.get(StoreKey.Map_Pluginprompt) as 35 | | string 36 | | undefined 37 | let lng = store.get(StoreKey.Set_Lng) as Languages | undefined 38 | if (typeof lng === 'undefined') { 39 | store.set(StoreKey.Set_Lng, 'English') 40 | lng = 'English' 41 | } 42 | if (typeof promptTemplates === 'undefined') { 43 | const _prompts = prompts.map(item => { 44 | return { 45 | character: item.act, 46 | prompt: item.prompt, 47 | } 48 | }) 49 | store.set(StoreKey.List_Prompt, JSON.stringify(_prompts)) 50 | } 51 | if (typeof pluginPrompts === 'undefined') { 52 | setPluginPrompt({ plugin: Casual, character: t('Casual') }) 53 | setPluginPrompt({ plugin: Programmer, character: t('Programmer') }) 54 | setPluginPrompt({ plugin: Summarizer, character: t('Summarizer') }) 55 | setPluginPrompt({ plugin: Analyst, character: t('Analyst') }) 56 | setPluginPrompt({ plugin: Translator, character: t('Translator') }) 57 | } 58 | } 59 | 60 | export function setupStoreHandlers() { 61 | ipcMain.handle( 62 | setStore, 63 | async ( 64 | event: IpcMainInvokeEvent, 65 | { key, blob }: { key: string; blob: any } 66 | ) => { 67 | console.log('== store ==>', key, blob) 68 | store.set(key, blob) 69 | } 70 | ) 71 | 72 | ipcMain.handle(getStore, async (event: IpcMainInvokeEvent, key: string) => { 73 | return store.get(key) 74 | }) 75 | 76 | ipcMain.handle( 77 | getChatListEvt, 78 | async (event: IpcMainInvokeEvent, preset: PresetType) => { 79 | return getChatList(preset) 80 | } 81 | ) 82 | 83 | ipcMain.handle( 84 | removeChat, 85 | async ( 86 | event: IpcMainInvokeEvent, 87 | { preset, index }: { preset: PresetType; index: number } 88 | ) => { 89 | const list = getChatList(preset) 90 | list.splice(index, 1) 91 | const mapStr = store.get(StoreKey.History_Chat) as string | undefined 92 | 93 | if (typeof mapStr !== 'undefined') { 94 | const chatMap = JSON.parse(mapStr) 95 | chatMap[preset] = list 96 | store.set(StoreKey.History_Chat, JSON.stringify(chatMap)) 97 | return list 98 | } else { 99 | return [] 100 | } 101 | } 102 | ) 103 | 104 | ipcMain.handle( 105 | addPrompt, 106 | async ( 107 | event: IpcMainInvokeEvent, 108 | { character, prompt }: { character: string; prompt: string } 109 | ) => { 110 | const list = getPromptList() 111 | const index = list.findIndex(item => item.character === character) 112 | if (index !== -1) { 113 | return false 114 | } 115 | list.push({ 116 | character, 117 | prompt, 118 | }) 119 | store.set(StoreKey.List_Prompt, JSON.stringify(list)) 120 | return list 121 | } 122 | ) 123 | 124 | ipcMain.handle( 125 | editPrompt, 126 | async ( 127 | event: IpcMainInvokeEvent, 128 | { 129 | former, 130 | character, 131 | prompt, 132 | }: { former: string; character: string; prompt: string } 133 | ) => { 134 | const list = getPromptList() 135 | const index = list.findIndex(item => item.character === former) 136 | if (index !== -1) { 137 | list.splice(index, 1) 138 | list.splice(index, 1, { 139 | character, 140 | prompt, 141 | }) 142 | store.set(StoreKey.List_Prompt, JSON.stringify(list)) 143 | return list 144 | } 145 | return false 146 | } 147 | ) 148 | 149 | ipcMain.handle( 150 | removePrompt, 151 | async (event: IpcMainInvokeEvent, { character }: { character: string }) => { 152 | const list = getPromptList() 153 | const index = list.findIndex(item => item.character === character) 154 | if (index !== -1) { 155 | list.splice(index, 1) 156 | store.set(StoreKey.List_Prompt, JSON.stringify(list)) 157 | return list 158 | } 159 | return false 160 | } 161 | ) 162 | 163 | ipcMain.handle(getPromptListEvt, async () => { 164 | return getPromptList() 165 | }) 166 | 167 | ipcMain.handle( 168 | getPluginPromptEvt, 169 | async (event: IpcMainInvokeEvent, { plugin }: { plugin: PresetType }) => { 170 | return getPluginPrompt(plugin) 171 | } 172 | ) 173 | 174 | ipcMain.handle( 175 | setPluginPromptEvt, 176 | async ( 177 | event: IpcMainInvokeEvent, 178 | { plugin, character }: { plugin: string; character: string } 179 | ) => { 180 | return setPluginPrompt({ plugin, character }) 181 | } 182 | ) 183 | } 184 | 185 | export function setChat({ 186 | prompt, 187 | response, 188 | preset, 189 | }: { 190 | prompt: string 191 | response: string 192 | preset: PresetType 193 | }) { 194 | const mapStr = store.get(StoreKey.History_Chat) as string | undefined 195 | if (typeof mapStr !== 'undefined') { 196 | const chatMap = JSON.parse(mapStr) 197 | const list = chatMap[preset] 198 | if (Array.isArray(list)) { 199 | list.push({ 200 | prompt, 201 | response, 202 | }) 203 | } 204 | store.set(StoreKey.History_Chat, JSON.stringify(chatMap)) 205 | } else { 206 | const map = { 207 | [preset]: [ 208 | { 209 | prompt, 210 | response, 211 | }, 212 | ], 213 | } 214 | store.set(StoreKey.History_Chat, JSON.stringify(map)) 215 | } 216 | } 217 | 218 | export function getChatList(type: PresetType): ChatContent[] { 219 | const mapStr = store.get(StoreKey.History_Chat) as string | undefined 220 | if (typeof mapStr !== 'undefined') { 221 | const chatMap = JSON.parse(mapStr) 222 | const list = chatMap[type] || [] 223 | return list 224 | } else { 225 | return [] 226 | } 227 | } 228 | 229 | export function getPromptList(): PromptSet[] { 230 | const mapStr = store.get(StoreKey.List_Prompt) as string | undefined 231 | if (typeof mapStr !== 'undefined') { 232 | const promptList = JSON.parse(mapStr) 233 | return promptList 234 | } else { 235 | return [] 236 | } 237 | } 238 | 239 | export function getPromptByCharacter(character: string): string { 240 | const list = getPromptList() 241 | const selectedItems = list.filter(item => item.character === character) 242 | if (selectedItems.length > 0) { 243 | return selectedItems[0].prompt 244 | } 245 | return '' 246 | } 247 | 248 | export function setPluginPrompt({ 249 | plugin, 250 | character, 251 | }: { 252 | plugin: string 253 | character: string 254 | }) { 255 | const mapStr = store.get(StoreKey.Map_Pluginprompt) as string | undefined 256 | if (typeof mapStr !== 'undefined') { 257 | const map = JSON.parse(mapStr) 258 | map[plugin] = character 259 | store.set(StoreKey.Map_Pluginprompt, JSON.stringify(map)) 260 | } else { 261 | const map = { 262 | [plugin]: character, 263 | } 264 | store.set(StoreKey.Map_Pluginprompt, JSON.stringify(map)) 265 | } 266 | } 267 | 268 | export function getPluginPrompt(plugin: PresetType): PromptSet { 269 | const mapStr = store.get(StoreKey.Map_Pluginprompt) as string | undefined 270 | if (typeof mapStr !== 'undefined') { 271 | const map = JSON.parse(mapStr) 272 | const character = map[plugin] || '' 273 | return { 274 | character, 275 | prompt: getPromptByCharacter(character), 276 | } 277 | } 278 | return { 279 | character: '', 280 | prompt: '', 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/electron/client/window.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron' 2 | import { winIgnoreMouse, winMouseMove, setWinSize } from '../constants/event' 3 | 4 | export function setupWindowHandlers(win: BrowserWindow | null) { 5 | ipcMain.on(winIgnoreMouse, (_, ignore) => { 6 | win?.setIgnoreMouseEvents(ignore, { forward: true }) 7 | }) 8 | 9 | ipcMain.on(winMouseMove, (_, pos) => { 10 | win?.setPosition(pos.posX, pos.posY) 11 | }) 12 | 13 | ipcMain.on(setWinSize, (_, size) => { 14 | console.log('???win', size) 15 | win?.setSize(size.w, size.h) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/electron/constants/common.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | linux(): boolean { 3 | return process.platform === 'linux' 4 | }, 5 | macOS(): boolean { 6 | return process.platform === 'darwin' 7 | }, 8 | windows(): boolean { 9 | return process.platform === 'win32' 10 | }, 11 | production(): boolean { 12 | return process.env.NODE_ENV !== 'development' 13 | }, 14 | dev(): boolean { 15 | return process.env.NODE_ENV === 'development' 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/electron/constants/event.ts: -------------------------------------------------------------------------------- 1 | // receiver 2 | export const winIgnoreMouse = 'winIgnoreMouse' 3 | export const winMouseMove = 'winMouseMove' 4 | export const attemptChange = 'attemptChange' 5 | export const usePreset = 'usePreset' 6 | export const jumpLink = 'jumpLink' 7 | export const copyText = 'copyText' 8 | export const speakText = 'speakText' 9 | export const setStore = 'setStore' 10 | export const getStore = 'getStore' 11 | export const removeChat = 'removeChat' 12 | export const getChatList = 'getChatList' 13 | export const addPrompt = 'addPrompt' 14 | export const editPrompt = 'editPrompt' 15 | export const removePrompt = 'removePrompt' 16 | export const getPromptList = 'getPromptList' 17 | export const setPluginPrompt = 'setPluginPrompt' 18 | export const getPluginPrompt = 'getPluginPrompt' 19 | export const changeLanguage = 'changeLanguage' 20 | export const runScript = 'runScript' 21 | export const setWinSize = 'setWinSize' 22 | 23 | // sender 24 | export const clipboard_change = 'clipboard_change' 25 | export const selection_change = 'selection_change' 26 | export const url_change = 'url_change' 27 | export const setting_show = 'setting_show' 28 | -------------------------------------------------------------------------------- /src/electron/constants/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/src/electron/constants/index.ts -------------------------------------------------------------------------------- /src/electron/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'compression' 2 | -------------------------------------------------------------------------------- /src/electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron' 2 | import Store from 'electron-store' 3 | import { PresetType } from '../@types' 4 | 5 | import initLog from './utils/log' 6 | import { Singleton } from './utils/global' 7 | import { setWindowVisile } from './utils/window' 8 | 9 | import { setupSoundHandlers } from './sound' 10 | import { 11 | listen as setupClipboardHandlers, 12 | setupStoreHandlers, 13 | init as initStore, 14 | setupLinkHandlers, 15 | setupWindowHandlers, 16 | } from './client' 17 | import { 18 | setupScriptHandlers, 19 | listen as setupShortcutHandlers, 20 | initTray, 21 | } from './os' 22 | import { StoreKey } from '../app/constants' 23 | import { init as initI18n, Languages } from '../i18n' 24 | require('./server') 25 | 26 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string 27 | declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string 28 | const userLog = initLog() 29 | const store = new Store() 30 | let win: BrowserWindow | null 31 | 32 | function initWindow() { 33 | win = new BrowserWindow({ 34 | resizable: false, 35 | width: 800, 36 | height: 600, 37 | frame: false, 38 | show: true, 39 | transparent: true, 40 | backgroundColor: '#00000000', 41 | skipTaskbar: true, 42 | webPreferences: { 43 | webSecurity: false, 44 | backgroundThrottling: false, 45 | contextIsolation: true, 46 | webviewTag: true, 47 | nodeIntegration: true, 48 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 49 | }, 50 | }) 51 | 52 | if (!app.isPackaged) { 53 | win?.webContents.openDevTools({ 54 | mode: 'bottom', 55 | }) 56 | } 57 | win.loadURL(MAIN_WINDOW_WEBPACK_ENTRY) 58 | win.on('closed', () => { 59 | win = null 60 | }) 61 | win.on('blur', () => { 62 | setWindowVisile({ 63 | win, 64 | visible: false, 65 | }) 66 | }) 67 | app.dock?.hide() 68 | 69 | initI18n(store.get(StoreKey.Set_Lng) as Languages) 70 | initStore() 71 | registerListeners() 72 | } 73 | 74 | async function registerListeners() { 75 | ipcMain.on('usePreset', (_, preset: PresetType) => { 76 | Singleton.getInstance().setCurPreset(preset) 77 | }) 78 | setupWindowHandlers(win) 79 | setupClipboardHandlers(win) 80 | setupShortcutHandlers(win) 81 | setupSoundHandlers() 82 | setupStoreHandlers() 83 | setupLinkHandlers() 84 | setupScriptHandlers() 85 | } 86 | 87 | app 88 | .on('ready', initWindow) 89 | .whenReady() 90 | .then(() => win && initTray(win, app)) 91 | .catch(e => { 92 | console.error(e) 93 | userLog.error(e) 94 | }) 95 | 96 | app.on('window-all-closed', () => { 97 | // if (process.platform !== 'darwin') { 98 | app.quit() 99 | // } 100 | }) 101 | 102 | app.on('activate', () => { 103 | if (BrowserWindow.getAllWindows().length === 0) { 104 | initWindow() 105 | } 106 | }) 107 | -------------------------------------------------------------------------------- /src/electron/os/applescript.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainInvokeEvent, ipcMain } from 'electron' 2 | import { Logger } from '../utils/util' 3 | import { runScript } from '../constants/event' 4 | const applescript = require('applescript') 5 | 6 | export function setupScriptHandlers() { 7 | ipcMain.handle( 8 | runScript, 9 | async (event: IpcMainInvokeEvent, script: string) => { 10 | runAppleScript(script) 11 | } 12 | ) 13 | } 14 | 15 | function runAppleScript(script: string) { 16 | const date = Date.now() 17 | return new Promise((resolve, reject) => { 18 | applescript.execString(script, (err: any, rtn: any) => { 19 | Logger.log(`runscript: ${script}`, Date.now() - date) 20 | if (err) { 21 | reject(err) 22 | return 23 | } 24 | resolve(rtn) 25 | }) 26 | }) 27 | } 28 | 29 | export function printer(words: string) { 30 | return ` 31 | set textBuffer to "${words}" 32 | repeat with i from 1 to count characters of textBuffer 33 | set the clipboard to (character i of textBuffer) 34 | delay 0.05 35 | keystroke "v" using command down 36 | end repeat 37 | ` 38 | } 39 | export function getSelection() { 40 | const script = ` 41 | tell application "System Events" to keystroke "c" using {command down} 42 | delay 0.5 43 | set selectedText to the clipboard 44 | ` 45 | return runAppleScript(script) 46 | } 47 | 48 | export function applySelection() { 49 | const script = ` 50 | tell application "System Events" to keystroke "v" using {command down} 51 | delay 1 52 | set selectedText to the clipboard 53 | ` 54 | return runAppleScript(script) 55 | } 56 | 57 | export function getRecentApp() { 58 | const script = ` 59 | tell application "System Events" 60 | set frontmostAppName to displayed name of first application process whose frontmost is true 61 | end tell` 62 | return runAppleScript(script) 63 | } 64 | 65 | export function activeApp(app: string) { 66 | const script = ` 67 | tell application "${app}" 68 | activate 69 | end tell 70 | ` 71 | return runAppleScript(script) 72 | } 73 | 74 | export function getBrowserContnet() { 75 | const script = ` 76 | tell application "Google Chrome" 77 | tell window 1 78 | tell active tab 79 | execute javascript "document.documentElement.innerText" 80 | end tell 81 | end tell 82 | end tell 83 | ` 84 | return runAppleScript(script) 85 | } 86 | 87 | export function getBrowserUrl(browser: string) { 88 | const safariScript = `tell application "Safari" to get the URL of the current tab of window 1 89 | ` 90 | const chromeScript = ` 91 | tell application "Google Chrome" to get the URL of the active tab of window 1 92 | ` 93 | if (browser.includes('Chrome')) return runAppleScript(chromeScript) 94 | if (browser.includes('Safari')) return runAppleScript(safariScript) 95 | return '' 96 | } 97 | 98 | export function speakTxt(txt: string, rate: number) { 99 | // using "${voice}" 100 | const script = ` 101 | tell application "Finder" to say "${txt}" speaking rate ${rate} 102 | ` 103 | return runAppleScript(script) 104 | } 105 | -------------------------------------------------------------------------------- /src/electron/os/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tray' 2 | export * from './applescript' 3 | export * from './shortcuts' 4 | -------------------------------------------------------------------------------- /src/electron/os/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import { BrowserWindow, globalShortcut, clipboard } from 'electron' 3 | import { getRecentApp, getSelection, getBrowserUrl } from './applescript' 4 | import { BuiltInPlugins } from '../../app/constants' 5 | import { Logger } from '../utils/util' 6 | import { setWindowVisile } from '../utils/window' 7 | import { Singleton } from '../utils/global' 8 | import { selection_change, url_change } from '../constants/event' 9 | 10 | export const config = { 11 | shortCut: { 12 | showAndHidden: 'CommandOrControl+k', 13 | }, 14 | } 15 | 16 | function setApp() { 17 | /* eslint-disable no-async-promise-executor */ 18 | return new Promise(async (resolve, reject) => { 19 | try { 20 | const app = (await getRecentApp()) as string 21 | Logger.log('appName', app) 22 | Singleton.getInstance().setRecentApp(app) 23 | resolve(app) 24 | } catch (e) { 25 | reject(e) 26 | } 27 | }) 28 | } 29 | 30 | export function listen(win: BrowserWindow | null) { 31 | globalShortcut.register(config.shortCut.showAndHidden, async () => { 32 | const visible = win?.isVisible() && win.isFocused() 33 | if (!visible) { 34 | try { 35 | if (os.platform() !== 'darwin') { 36 | throw new Error('Only support macOS') 37 | } 38 | const preset = Singleton.getInstance().getCurPreset() 39 | const plugin = BuiltInPlugins.filter(plugin => plugin.title === preset) 40 | const app = await setApp() 41 | if (plugin.length > 0) { 42 | const usePlugin = plugin[0] 43 | if (usePlugin.monitorClipboard) { 44 | const clipboardContent = clipboard.readText() 45 | const selection = await getSelection() 46 | Logger.log('selectionTxt =>', selection) 47 | win?.webContents.send(selection_change, { 48 | txt: selection, 49 | app, 50 | }) 51 | clipboard.writeText(clipboardContent) 52 | } else if (usePlugin.monitorBrowser) { 53 | const url = await getBrowserUrl(app) 54 | win?.webContents.send(url_change, { 55 | url, 56 | }) 57 | } 58 | } 59 | 60 | setWindowVisile({ 61 | win, 62 | visible: true, 63 | }) 64 | } catch (e) { 65 | Logger.error(e) 66 | setWindowVisile({ 67 | win, 68 | visible: true, 69 | }) 70 | } 71 | return 72 | } 73 | setWindowVisile({ 74 | win, 75 | visible: false, 76 | }) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /src/electron/os/tray.ts: -------------------------------------------------------------------------------- 1 | import { Tray, Menu, shell, dialog, BrowserWindow, ipcMain } from 'electron' 2 | import i18n, { t } from 'i18next' 3 | import path from 'path' 4 | import { config } from './shortcuts' 5 | import { setWindowVisile } from '../utils/window' 6 | import common from '../constants/common' 7 | import { setting_show, changeLanguage } from '../constants/event' 8 | import { Languages, localeOptions } from '../../i18n' 9 | import pkg from '../../../package.json' 10 | 11 | function getTrayImagePath() { 12 | if (common.production()) { 13 | return path.join(process.resourcesPath, 'assets/icon/icon24.png') 14 | } 15 | return 'assets/icon/icon24.png' 16 | } 17 | 18 | export function initTray(win: BrowserWindow, app: Electron.App) { 19 | const getMenuTemplate = () => { 20 | return Menu.buildFromTemplate([ 21 | { 22 | label: t('Feedback & Help'), 23 | click: () => { 24 | process.nextTick(() => { 25 | shell.openExternal('https://github.com/onepointAI/onepoint/issues') 26 | }) 27 | }, 28 | }, 29 | { type: 'separator' }, 30 | { 31 | label: t('Display Window'), 32 | accelerator: config.shortCut.showAndHidden, 33 | click: () => { 34 | setWindowVisile({ 35 | win, 36 | visible: true, 37 | }) 38 | }, 39 | }, 40 | { 41 | label: t('Settings'), 42 | click: () => { 43 | win?.webContents.send(setting_show) 44 | setWindowVisile({ 45 | win, 46 | visible: true, 47 | }) 48 | }, 49 | }, 50 | { type: 'separator' }, 51 | { 52 | role: 'quit', 53 | label: t('exit'), 54 | }, 55 | { 56 | label: t('reload'), 57 | click() { 58 | app.relaunch() 59 | app.quit() 60 | }, 61 | }, 62 | { type: 'separator' }, 63 | { 64 | label: t('About'), 65 | click() { 66 | dialog.showMessageBox({ 67 | title: t('onepoint'), 68 | message: t( 69 | 'More than just chat. Write, read, and code with powerful AI anywhere.' 70 | ), 71 | detail: t('Version:') + pkg.version, 72 | }) 73 | }, 74 | }, 75 | ]) 76 | } 77 | 78 | const tray = new Tray(getTrayImagePath()) 79 | const buildMenu = () => { 80 | const contextMenu = getMenuTemplate() 81 | tray.setToolTip(t('onepoint | more than just chat')) 82 | tray.setContextMenu(contextMenu) 83 | } 84 | 85 | buildMenu() 86 | ipcMain.on(changeLanguage, (_, lng: Languages) => { 87 | i18n.changeLanguage(localeOptions[lng]) 88 | buildMenu() 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/electron/prompt/prompts-zh.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "act": "随便侃侃", 4 | "prompt": "" 5 | }, 6 | { 7 | "act": "编程大师", 8 | "prompt": "希望你能像大师一样精通各种编程语言。您需要将我的请求翻译成代码或重构我提交的代码。不需要解释和注释,直接返回可以直接使用的markdown格式即可。再次强调,只需要代码,不需要解释。" 9 | }, 10 | { 11 | "act": "翻译专家", 12 | "prompt": "我希望你担任英文或中文的翻译、拼写证明和修辞改进的角色。我会用中文或英文与你交流,你会认得语言,翻译并用更优美精致的语言回答我,如果我说中文,请用英语回答我,否则如果我说英语,请用中文翻译" 13 | }, 14 | { 15 | "act": "网页大纲", 16 | "prompt": "希望你是一个总结内容的专家。我会发给你一篇或多篇文字,你需要先用简洁的语言概括要点,然后尝试挖掘更深层次的核心要点,最后用编号的标题组织起来。" 17 | }, 18 | { 19 | "act": "文本分析", 20 | "prompt": "希望你是一个很擅长挖掘内容重点的人,我会发给你一段文本,请告诉我他字面上的意思,并尝试探索更深层次的意义" 21 | }, 22 | { 23 | "act": "IT 架构师", 24 | "prompt": "我希望你担任 IT 架构师。我将提供有关应用程序或其他数字产品功能的一些详细信息,而您的工作是想出将其集成到 IT 环境中的方法。这可能涉及分析业务需求、执行差距分析以及将新系统的功能映射到现有 IT 环境。接下来的步骤是创建解决方案设计、物理网络蓝图、系统集成接口定义和部署环境蓝图。我的第一个请求是“我需要帮助来集成 CMS 系统”。\n" 25 | }, 26 | { 27 | "act": "表情符号翻译", 28 | "prompt": "我要你把我写的句子翻译成表情符号。我会写句子,你会用表情符号表达它。我只是想让你用表情符号来表达它。除了表情符号,我不希望你回复任何内容。当我需要用英语告诉你一些事情时,我会用 {like this} 这样的大括号括起来。我的第一句话是“你好,请问你的职业是什么?”\n" 29 | }, 30 | { 31 | "act": "脱口秀喜剧演员", 32 | "prompt": "我想让你扮演一个脱口秀喜剧演员。我将为您提供一些与时事相关的话题,您将运用您的智慧、创造力和观察能力,根据这些话题创建一个例程。您还应该确保将个人轶事或经历融入日常活动中,以使其对观众更具相关性和吸引力。我的第一个请求是“我想要幽默地看待政治”。\n" 33 | }, 34 | { 35 | "act": "AI写作导师", 36 | "prompt": "我想让你做一个 AI 写作导师。我将为您提供一名需要帮助改进其写作的学生,您的任务是使用人工智能工具(例如自然语言处理)向学生提供有关如何改进其作文的反馈。您还应该利用您在有效写作技巧方面的修辞知识和经验来建议学生可以更好地以书面形式表达他们的想法和想法的方法。我的第一个请求是“我需要有人帮我修改我的硕士论文”。\n" 37 | }, 38 | { 39 | "act": "医生", 40 | "prompt": "我想让你扮演医生的角色,想出创造性的治疗方法来治疗疾病。您应该能够推荐常规药物、草药和其他天然替代品。在提供建议时,您还需要考虑患者的年龄、生活方式和病史。我的第一个建议请求是“为患有关节炎的老年患者提出一个侧重于整体治疗方法的治疗计划”。\n" 41 | }, 42 | { 43 | "act": "会计师", 44 | "prompt": "我希望你担任会计师,并想出创造性的方法来管理财务。在为客户制定财务计划时,您需要考虑预算、投资策略和风险管理。在某些情况下,您可能还需要提供有关税收法律法规的建议,以帮助他们实现利润最大化。我的第一个建议请求是“为小型企业制定一个专注于成本节约和长期投资的财务计划”。\n" 45 | }, 46 | { 47 | "act": "厨师", 48 | "prompt": "我需要有人可以推荐美味的食谱,这些食谱包括营养有益但又简单又不费时的食物,因此适合像我们这样忙碌的人以及成本效益等其他因素,因此整体菜肴最终既健康又经济!我的第一个要求——“一些清淡而充实的东西,可以在午休时间快速煮熟”\n" 49 | }, 50 | { 51 | "act": "金融分析师", 52 | "prompt": "需要具有使用技术分析工具理解图表的经验的合格人员提供的帮助,同时解释世界各地普遍存在的宏观经济环境,从而帮助客户获得长期优势需要明确的判断,因此需要通过准确写下的明智预测来寻求相同的判断!第一条陈述包含以下内容——“你能告诉我们根据当前情况未来的股市会是什么样子吗?”。\n" 53 | }, 54 | { 55 | "act": "投资经理", 56 | "prompt": "从具有金融市场专业知识的经验丰富的员工那里寻求指导,结合通货膨胀率或回报估计等因素以及长期跟踪股票价格,最终帮助客户了解行业,然后建议最安全的选择,他/她可以根据他们的要求分配资金和兴趣!开始查询 - “目前投资短期前景的最佳方式是什么?”\n" 57 | }, 58 | { 59 | "act": "心理学家", 60 | "prompt": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}\n" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /src/electron/prompt/prompts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "act": "Casual", 4 | "prompt": "" 5 | }, 6 | { 7 | "act": "Programming Master", 8 | "prompt": "I hope you can master various programming languages like a master. Please translate my request into code or refactor the code I submitted. No need for explanations or comments, just return the markdown format that can be directly used. Again, only code is needed, no explanation required." 9 | }, 10 | { 11 | "act": "Translator", 12 | "prompt": "I hope you can serve as a Chinese-English translator. When I input in Chinese, please translate it into English; when I input in English, please translate it into Chinese. You don't need to understand what my question is about, just translate what I say. If my text is a question, you don't need to try to answer it, just translate my question. Once again, do not answer the text I input, just translate it." 13 | }, 14 | { 15 | "act": "Summarizer", 16 | "prompt": "I hope you are an expert at summarizing content. I will send you one or more texts, and you need to summarize the main points in concise language first, then try to dig deeper core points, and finally organize them with numbered headings." 17 | }, 18 | { 19 | "act": "Analyst", 20 | "prompt": "I hope you are a person who is very good at digging out the key points of the content, I will send you a text, please tell me what it means literally, and try to explore the deeper meaning." 21 | }, 22 | { 23 | "act": "随便侃侃", 24 | "prompt": "" 25 | }, 26 | { 27 | "act": "编程大师", 28 | "prompt": "希望你能像大师一样精通各种编程语言。您需要将我的请求翻译成代码或重构我提交的代码。不需要解释和注释,直接返回可以直接使用的markdown格式即可。再次强调,只需要代码,不需要解释。" 29 | }, 30 | { 31 | "act": "翻译专家", 32 | "prompt": "我希望你担任中英翻译,当我输入的是中文,你用英语翻译,当我输入的是英文,你用中文翻译,你不喜欢理解我的问题是什么,只要翻译我所说的话即可,当我输入的文字是一个疑问句是,你不用尝试去回答他,直接翻译我的疑问就好,再次重申,不要回答我输入的文字,只要翻译它就好了" 33 | }, 34 | { 35 | "act": "网页大纲", 36 | "prompt": "希望你是一个总结内容的专家。我会发给你一篇或多篇文字,你需要先用简洁的语言概括要点,然后尝试挖掘更深层次的核心要点,最后用编号的标题组织起来。" 37 | }, 38 | { 39 | "act": "文本分析", 40 | "prompt": "希望你是一个很擅长挖掘内容重点的人,我会发给你一段文本,请告诉我他字面上的意思,并尝试探索更深层次的意义" 41 | }, 42 | { 43 | "act": "IT 架构师", 44 | "prompt": "我希望你担任 IT 架构师。我将提供有关应用程序或其他数字产品功能的一些详细信息,而您的工作是想出将其集成到 IT 环境中的方法。这可能涉及分析业务需求、执行差距分析以及将新系统的功能映射到现有 IT 环境。接下来的步骤是创建解决方案设计、物理网络蓝图、系统集成接口定义和部署环境蓝图。我的第一个请求是“我需要帮助来集成 CMS 系统”。\n" 45 | }, 46 | { 47 | "act": "表情符号翻译", 48 | "prompt": "我要你把我写的句子翻译成表情符号。我会写句子,你会用表情符号表达它。我只是想让你用表情符号来表达它。除了表情符号,我不希望你回复任何内容。当我需要用英语告诉你一些事情时,我会用 {like this} 这样的大括号括起来。我的第一句话是“你好,请问你的职业是什么?”\n" 49 | }, 50 | { 51 | "act": "脱口秀喜剧演员", 52 | "prompt": "我想让你扮演一个脱口秀喜剧演员。我将为您提供一些与时事相关的话题,您将运用您的智慧、创造力和观察能力,根据这些话题创建一个例程。您还应该确保将个人轶事或经历融入日常活动中,以使其对观众更具相关性和吸引力。我的第一个请求是“我想要幽默地看待政治”。\n" 53 | }, 54 | { 55 | "act": "AI写作导师", 56 | "prompt": "我想让你做一个 AI 写作导师。我将为您提供一名需要帮助改进其写作的学生,您的任务是使用人工智能工具(例如自然语言处理)向学生提供有关如何改进其作文的反馈。您还应该利用您在有效写作技巧方面的修辞知识和经验来建议学生可以更好地以书面形式表达他们的想法和想法的方法。我的第一个请求是“我需要有人帮我修改我的硕士论文”。\n" 57 | }, 58 | { 59 | "act": "医生", 60 | "prompt": "我想让你扮演医生的角色,想出创造性的治疗方法来治疗疾病。您应该能够推荐常规药物、草药和其他天然替代品。在提供建议时,您还需要考虑患者的年龄、生活方式和病史。我的第一个建议请求是“为患有关节炎的老年患者提出一个侧重于整体治疗方法的治疗计划”。\n" 61 | }, 62 | { 63 | "act": "会计师", 64 | "prompt": "我希望你担任会计师,并想出创造性的方法来管理财务。在为客户制定财务计划时,您需要考虑预算、投资策略和风险管理。在某些情况下,您可能还需要提供有关税收法律法规的建议,以帮助他们实现利润最大化。我的第一个建议请求是“为小型企业制定一个专注于成本节约和长期投资的财务计划”。\n" 65 | }, 66 | { 67 | "act": "厨师", 68 | "prompt": "我需要有人可以推荐美味的食谱,这些食谱包括营养有益但又简单又不费时的食物,因此适合像我们这样忙碌的人以及成本效益等其他因素,因此整体菜肴最终既健康又经济!我的第一个要求——“一些清淡而充实的东西,可以在午休时间快速煮熟”\n" 69 | }, 70 | { 71 | "act": "金融分析师", 72 | "prompt": "需要具有使用技术分析工具理解图表的经验的合格人员提供的帮助,同时解释世界各地普遍存在的宏观经济环境,从而帮助客户获得长期优势需要明确的判断,因此需要通过准确写下的明智预测来寻求相同的判断!第一条陈述包含以下内容——“你能告诉我们根据当前情况未来的股市会是什么样子吗?”。\n" 73 | }, 74 | { 75 | "act": "投资经理", 76 | "prompt": "从具有金融市场专业知识的经验丰富的员工那里寻求指导,结合通货膨胀率或回报估计等因素以及长期跟踪股票价格,最终帮助客户了解行业,然后建议最安全的选择,他/她可以根据他们的要求分配资金和兴趣!开始查询 - “目前投资短期前景的最佳方式是什么?”\n" 77 | }, 78 | { 79 | "act": "心理学家", 80 | "prompt": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}\n" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /src/electron/server.ts: -------------------------------------------------------------------------------- 1 | import compression from 'compression' 2 | import Store from 'electron-store' 3 | import express from 'express' 4 | import accountApi from './apis/account' 5 | import promptApi from './apis/prompt' 6 | import applyApi from './apis/apply' 7 | import testApi from './apis/test' 8 | import crawlApi from './apis/crawl' 9 | import { PresetType } from '../@types' 10 | import { StoreKey } from '../app/constants' 11 | import { Logger } from './utils/util' 12 | import { getChatList, getPluginPrompt } from './client/store' 13 | 14 | const { Configuration, OpenAIApi } = require('openai') 15 | const store = new Store() 16 | let openai = null as any 17 | let prevBasePath: string 18 | let prevApiKey: string 19 | 20 | function getContextual(prompt: PresetType) { 21 | const num = (store.get(StoreKey.Set_Contexual) as number) || 0 22 | const sPreset = [ 23 | { 24 | role: 'system', 25 | content: getPluginPrompt(prompt).prompt, 26 | }, 27 | ] 28 | 29 | if (prompt && num) { 30 | const list = getChatList(prompt).slice(-num) 31 | const contexual = list.map(item => { 32 | return [ 33 | { 34 | role: 'user', 35 | content: item.prompt, 36 | }, 37 | { 38 | role: 'assistant', 39 | content: item.response, 40 | }, 41 | ] 42 | }) 43 | return [...contexual.flat(), ...sPreset] 44 | } 45 | return [...sPreset] 46 | } 47 | 48 | export function generatePayload(content: string, prompt: PresetType) { 49 | return { 50 | model: 'gpt-3.5-turbo-0301', 51 | messages: [ 52 | ...getContextual(prompt), 53 | { 54 | role: 'user', 55 | content, 56 | }, 57 | ], 58 | temperature: 0, 59 | top_p: 1, 60 | frequency_penalty: 1, 61 | presence_penalty: 1, 62 | stream: true, 63 | } 64 | } 65 | 66 | export function getAiInstance() { 67 | const basePath = store.get(StoreKey.Set_BasePath) as string 68 | const apiKey = store.get(StoreKey.Set_ApiKey) as string 69 | 70 | if (openai && prevApiKey === apiKey && prevBasePath === basePath) { 71 | return openai 72 | } 73 | 74 | if (apiKey) { 75 | const _basePath = basePath || 'https://closeai.deno.dev' 76 | openai = new OpenAIApi( 77 | new Configuration({ 78 | apiKey, 79 | basePath: _basePath + '/v1', 80 | }) 81 | ) 82 | prevApiKey = apiKey 83 | prevBasePath = _basePath 84 | return openai 85 | } 86 | return null 87 | } 88 | 89 | const app = express() 90 | const port = 4000 91 | app.use(compression()) 92 | app.use(express.json()) 93 | app.use(express.urlencoded({ extended: true })) 94 | app.post('/prompt', promptApi) 95 | app.post('/apply', applyApi) 96 | app.post('/test', testApi) 97 | app.post('/account', accountApi) 98 | app.post('/crawl', crawlApi) 99 | app.listen(port, async () => { 100 | Logger.log(`onepoint listening on port ${port}!`) 101 | }) 102 | -------------------------------------------------------------------------------- /src/electron/sound/index.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainInvokeEvent, ipcMain } from 'electron' 2 | import { speakText as speakTextEvt } from '../constants/event' 3 | import { speakTxt } from '../os/applescript' 4 | import { Logger } from '../utils/util' 5 | 6 | export function setupSoundHandlers() { 7 | ipcMain.handle( 8 | speakTextEvt, 9 | async (event: IpcMainInvokeEvent, resp: string) => { 10 | try { 11 | // https://gist.github.com/mculp/4b95752e25c456d425c6 12 | speakTxt(resp, 200) 13 | return true 14 | } catch (e) { 15 | Logger.error(e) 16 | return false 17 | } 18 | } 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/electron/types.ts: -------------------------------------------------------------------------------- 1 | export interface BalanceResponse { 2 | total_usage: number 3 | } 4 | export interface ChatContent { 5 | prompt: string 6 | response: string 7 | } 8 | 9 | export interface PromptSet { 10 | character: string 11 | prompt: string 12 | } 13 | 14 | export enum ERR_CODES { 15 | NETWORK_CONGESTION = -1000, 16 | TIMEOUT = -999, 17 | NOT_SET_APIKEY = -998, 18 | TOKEN_TOO_LONG = -997, 19 | } 20 | 21 | export const CODE_SPLIT = '&^.>' 22 | -------------------------------------------------------------------------------- /src/electron/utils/global.ts: -------------------------------------------------------------------------------- 1 | import { PresetType } from '../../@types' 2 | export class Singleton { 3 | private static instance: Singleton 4 | 5 | private static copyFromElectron = false 6 | 7 | private static recentApp: string 8 | 9 | private static preset: PresetType = PresetType.Casual 10 | 11 | public static getInstance(): Singleton { 12 | if (!Singleton.instance) { 13 | Singleton.instance = new Singleton() 14 | } 15 | return Singleton.instance 16 | } 17 | 18 | public setRecentApp(app: string) { 19 | Singleton.recentApp = app 20 | } 21 | 22 | public setCopyStateSource(fromElectron: boolean) { 23 | Singleton.copyFromElectron = fromElectron 24 | } 25 | 26 | public setCurPreset(preset: PresetType) { 27 | Singleton.preset = preset 28 | } 29 | 30 | public getCopyFromElectron() { 31 | return Singleton.copyFromElectron 32 | } 33 | 34 | public getRecentApp() { 35 | return Singleton.recentApp 36 | } 37 | 38 | public getCurPreset() { 39 | return Singleton.preset 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/electron/utils/log.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log' 2 | 3 | export default (): typeof log => { 4 | log.transports.file.level = 'debug' 5 | log.transports.file.fileName = 'user.log' 6 | log.transports.file.format = 7 | '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}' 8 | log.transports.file.maxSize = 1048576 9 | return log 10 | } 11 | -------------------------------------------------------------------------------- /src/electron/utils/util.ts: -------------------------------------------------------------------------------- 1 | export const Logger = console 2 | -------------------------------------------------------------------------------- /src/electron/utils/window.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | 3 | export const setWindowVisile = (opt: { 4 | win: BrowserWindow | null 5 | visible?: boolean 6 | }) => { 7 | const { win, visible } = opt 8 | if (!visible) { 9 | win?.hide() 10 | win?.blur() 11 | return 12 | } 13 | 14 | // win?.setAlwaysOnTop(true) 15 | win?.setVisibleOnAllWorkspaces(true, { 16 | visibleOnFullScreen: true, 17 | }) 18 | win?.focus() 19 | win?.show() 20 | } 21 | -------------------------------------------------------------------------------- /src/features/chat/chatSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' 2 | import { PresetType } from '../../@types' 3 | import { baseApiHost } from '../../app/api' 4 | import { timeoutPromise } from '../../utils/fetch' 5 | import { ERR_CODES } from '../../electron/types' 6 | 7 | interface ChatModule { 8 | resp: Record 9 | visible: boolean 10 | inputDiabled: boolean 11 | respErr: boolean 12 | respErrMsg: string 13 | curPrompt: Record 14 | isGenerating: boolean 15 | webCrawlResp: string 16 | } 17 | 18 | export const initialState: ChatModule = { 19 | resp: {}, 20 | visible: false, 21 | inputDiabled: false, 22 | respErr: false, 23 | respErrMsg: '', 24 | curPrompt: {}, 25 | isGenerating: false, 26 | webCrawlResp: '', // Distinguishing proprietary err & errmsg 27 | } 28 | 29 | interface PresetContent { 30 | preset: PresetType 31 | content: string 32 | } 33 | 34 | export const chatSlice = createSlice({ 35 | name: 'chat', 36 | initialState, 37 | reducers: { 38 | saveResp: (state, action: PayloadAction) => { 39 | const { 40 | payload: { preset, content }, 41 | } = action 42 | const resp = state.resp 43 | resp[preset] = content 44 | state.resp = resp 45 | }, 46 | setVisible: (state, action: PayloadAction) => { 47 | const { payload } = action 48 | state.visible = payload 49 | }, 50 | setInputDisabled: (state, action: PayloadAction) => { 51 | const { payload } = action 52 | state.inputDiabled = payload 53 | }, 54 | setRespErr: (state, action: PayloadAction) => { 55 | const { payload } = action 56 | state.respErr = payload 57 | }, 58 | setRespErrMsg: (state, action: PayloadAction) => { 59 | const { payload } = action 60 | state.respErrMsg = payload 61 | }, 62 | setCurPrompt: (state, action: PayloadAction) => { 63 | const { 64 | payload: { preset, content }, 65 | } = action 66 | const curPrompt = state.curPrompt 67 | curPrompt[preset] = content 68 | state.curPrompt = curPrompt 69 | }, 70 | setGenerating: (state, action: PayloadAction) => { 71 | const { payload } = action 72 | state.isGenerating = payload 73 | }, 74 | saveWebCrawlResp: (state, action: PayloadAction) => { 75 | const { payload } = action 76 | state.webCrawlResp = payload 77 | }, 78 | }, 79 | }) 80 | 81 | export const { 82 | saveResp, 83 | setVisible, 84 | setInputDisabled, 85 | setRespErr, 86 | setRespErrMsg, 87 | setCurPrompt, 88 | setGenerating, 89 | saveWebCrawlResp, 90 | } = chatSlice.actions 91 | 92 | export const fetchChatResp = createAsyncThunk( 93 | 'chat/fetchChatResp', 94 | async ( 95 | args: { 96 | prompt: string 97 | preset: PresetType 98 | }, 99 | { dispatch } 100 | ) => { 101 | const { prompt, preset } = args 102 | dispatch(setInputDisabled(true)) 103 | dispatch(setRespErr(false)) 104 | dispatch(setGenerating(true)) 105 | 106 | const request = () => { 107 | /* eslint-disable no-async-promise-executor */ 108 | return new Promise(async (resolve, reject) => { 109 | const response = await fetch(`${baseApiHost}/prompt`, { 110 | method: 'POST', 111 | headers: { 112 | 'Content-Type': 'application/json', 113 | }, 114 | body: JSON.stringify({ 115 | prompt, 116 | preset, 117 | }), 118 | }) 119 | 120 | const reader = response.body 121 | ?.pipeThrough(new TextDecoderStream()) 122 | .getReader() 123 | 124 | let str = '' 125 | let shown = false 126 | while (true) { 127 | if (!reader) break 128 | const { value, done } = await reader.read() 129 | if (done) break 130 | if (!shown) { 131 | dispatch(setVisible(true)) 132 | shown = true 133 | } 134 | if (Number(value) === ERR_CODES.NOT_SET_APIKEY) { 135 | reject(new Error('please set your apikey first.')) 136 | break 137 | } 138 | 139 | if (Number(value) === ERR_CODES.NETWORK_CONGESTION) { 140 | reject( 141 | new Error('Network error. Check whether you have set up a proxy') 142 | ) 143 | break 144 | } 145 | str += value 146 | dispatch( 147 | saveResp({ 148 | preset, 149 | content: str, 150 | }) 151 | ) 152 | } 153 | resolve(true) 154 | }) 155 | } 156 | 157 | Promise.race([ 158 | timeoutPromise( 159 | 20000, 160 | 'High network latency. Check whether you have set up a proxy' 161 | ), 162 | request(), 163 | ]) 164 | .then() 165 | .catch(e => { 166 | dispatch(setVisible(true)) 167 | dispatch(setRespErr(true)) 168 | dispatch(setRespErrMsg(e.message)) 169 | }) 170 | .finally(() => { 171 | dispatch(setGenerating(false)) 172 | dispatch(setInputDisabled(false)) 173 | }) 174 | } 175 | ) 176 | 177 | export const fetchWebCrawlResp = createAsyncThunk( 178 | 'chat/fetchWebCrawlResp', 179 | async ( 180 | args: { 181 | url: string 182 | preset: PresetType 183 | }, 184 | { dispatch } 185 | ) => { 186 | const { url, preset } = args 187 | dispatch(setInputDisabled(true)) 188 | dispatch(setRespErr(false)) 189 | dispatch(setGenerating(true)) 190 | 191 | const request = async () => { 192 | const response = await fetch(`${baseApiHost}/crawl`, { 193 | method: 'POST', 194 | headers: { 195 | 'Content-Type': 'application/json', 196 | }, 197 | body: JSON.stringify({ 198 | url, 199 | preset, 200 | }), 201 | }) 202 | return response.json() 203 | } 204 | 205 | Promise.race([ 206 | timeoutPromise( 207 | 20000, 208 | 'High network latency. Check whether you have set up a proxy' 209 | ), 210 | request(), 211 | ]) 212 | .then(resp => { 213 | const { result, code, message } = resp 214 | if (code === 0) { 215 | dispatch(setVisible(true)) 216 | dispatch(saveWebCrawlResp(result)) 217 | } else { 218 | dispatch(setRespErr(true)) 219 | dispatch(setRespErrMsg(message)) 220 | } 221 | }) 222 | .catch(e => { 223 | dispatch(setVisible(true)) 224 | dispatch(setRespErr(true)) 225 | dispatch(setRespErrMsg(e.message)) 226 | }) 227 | .finally(() => { 228 | dispatch(setGenerating(false)) 229 | dispatch(setInputDisabled(false)) 230 | }) 231 | } 232 | ) 233 | 234 | export default chatSlice.reducer 235 | -------------------------------------------------------------------------------- /src/features/clipboard/clipboardSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | 3 | interface ClipboardModule { 4 | selectTxt: string 5 | selectApp: string 6 | url: string 7 | } 8 | 9 | interface Selection { 10 | txt: string 11 | app: string 12 | } 13 | 14 | interface Url { 15 | url: string 16 | } 17 | 18 | export const initialState: ClipboardModule = { 19 | selectTxt: '', 20 | selectApp: '', 21 | url: '', 22 | } 23 | 24 | export const clipboardSlice = createSlice({ 25 | name: 'clipboard', 26 | initialState, 27 | reducers: { 28 | setSelection: (state, action: PayloadAction) => { 29 | const { payload } = action 30 | state.selectTxt = payload.txt 31 | state.selectApp = payload.app 32 | }, 33 | setUrl: (state, action: PayloadAction) => { 34 | const { payload } = action 35 | state.url = payload.url 36 | }, 37 | }, 38 | }) 39 | 40 | export const { setSelection, setUrl } = clipboardSlice.actions 41 | export default clipboardSlice.reducer 42 | -------------------------------------------------------------------------------- /src/features/history/historySlice.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onepointAI/onepoint/7d99f7e9a91dd54dc317c85f3891af8fc215d46a/src/features/history/historySlice.ts -------------------------------------------------------------------------------- /src/features/preset/presetSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | import { BuiltInPlugins } from '../../app/constants' 3 | import { PresetModule, PresetType } from '../../@types' 4 | 5 | export const initialState: PresetModule = { 6 | listVisible: false, 7 | builtInPlugins: BuiltInPlugins, 8 | currentPreset: PresetType.Casual, 9 | } 10 | 11 | export const presetSlice = createSlice({ 12 | name: 'preset', 13 | initialState, 14 | reducers: { 15 | setListVisible: (state, action: PayloadAction) => { 16 | const { payload } = action 17 | state.listVisible = payload 18 | }, 19 | setPreset: (state, action: PayloadAction) => { 20 | const { payload } = action 21 | state.currentPreset = payload 22 | }, 23 | }, 24 | }) 25 | 26 | export const { setListVisible, setPreset } = presetSlice.actions 27 | export default presetSlice.reducer 28 | -------------------------------------------------------------------------------- /src/features/setting/settingSlice.ts: -------------------------------------------------------------------------------- 1 | // import { init as initI18n } from '../../i18n' 2 | import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' 3 | import { timeoutPromise } from '../../utils/fetch' 4 | import { baseApiHost } from '../../app/api' 5 | import { Languages } from '../../i18n' 6 | interface SettingModule { 7 | visible: boolean 8 | billUsage: number 9 | basePath: string 10 | apikey: string 11 | loadAccount: boolean 12 | usemodel: string 13 | minimal: boolean 14 | contextual: number 15 | store: number 16 | lng: Languages 17 | } 18 | 19 | export const defaultVals = { 20 | lng: 'English', 21 | store: 0, 22 | contexual: 0, 23 | minimal: false, 24 | } 25 | 26 | export const initialState: SettingModule = { 27 | visible: false, 28 | loadAccount: true, 29 | billUsage: 0, 30 | basePath: '', 31 | apikey: '', 32 | usemodel: '', 33 | minimal: defaultVals.minimal, 34 | contextual: defaultVals.contexual, 35 | store: defaultVals.store, 36 | lng: defaultVals.lng, 37 | } 38 | 39 | export const settingSlice = createSlice({ 40 | name: 'setting', 41 | initialState, 42 | reducers: { 43 | setVisible: (state, action: PayloadAction) => { 44 | const { payload } = action 45 | state.visible = payload 46 | }, 47 | setUsage: (state, action: PayloadAction) => { 48 | const { payload } = action 49 | state.billUsage = payload 50 | }, 51 | setBasePath: (state, action: PayloadAction) => { 52 | const { payload } = action 53 | state.basePath = payload 54 | }, 55 | setApikey: (state, action: PayloadAction) => { 56 | const { payload } = action 57 | state.apikey = payload 58 | }, 59 | setUsemodel: (state, action: PayloadAction) => { 60 | const { payload } = action 61 | state.usemodel = payload 62 | }, 63 | setLoadAccount: (state, action: PayloadAction) => { 64 | const { payload } = action 65 | state.loadAccount = payload 66 | }, 67 | setMinimal: (state, action: PayloadAction) => { 68 | const { payload } = action 69 | state.minimal = payload 70 | }, 71 | setContexual: (state, action: PayloadAction) => { 72 | const { payload } = action 73 | state.contextual = payload 74 | }, 75 | setStore: (state, action: PayloadAction) => { 76 | const { payload } = action 77 | state.store = payload 78 | }, 79 | setLng: (state, action: PayloadAction) => { 80 | const { payload } = action 81 | state.lng = payload 82 | }, 83 | }, 84 | }) 85 | 86 | export const { 87 | setVisible, 88 | setUsage, 89 | setBasePath, 90 | setApikey, 91 | setUsemodel, 92 | setLoadAccount, 93 | setMinimal, 94 | setContexual, 95 | setStore, 96 | setLng, 97 | } = settingSlice.actions 98 | 99 | export const initState = createAsyncThunk( 100 | 'setting/initState', 101 | async (args: null, { dispatch }) => { 102 | // const config = await connector.getProject() 103 | // connector.refreshTokens() 104 | } 105 | ) 106 | 107 | export const fetchAccountDetail = createAsyncThunk( 108 | 'setting/fetchAccountDetail', 109 | async ( 110 | args: { 111 | startDate: string 112 | endDate: string 113 | }, 114 | { dispatch } 115 | ) => { 116 | const { startDate, endDate } = args 117 | dispatch(setLoadAccount(true)) 118 | const request = async () => { 119 | const response = await fetch(`${baseApiHost}/account`, { 120 | method: 'POST', 121 | headers: { 122 | 'Content-Type': 'application/json', 123 | }, 124 | body: JSON.stringify({ 125 | start_date: startDate, 126 | end_date: endDate, 127 | }), 128 | }) 129 | return response.json() 130 | } 131 | 132 | Promise.race([ 133 | timeoutPromise( 134 | 5000, 135 | 'High network latency. Check whether you have set up a proxy' 136 | ), 137 | request(), 138 | ]) 139 | .then(resp => { 140 | console.log(resp) 141 | const { 142 | basic: { apiHost, apiKey, usemodel }, 143 | usageData, 144 | } = resp.result 145 | dispatch(setBasePath(apiHost)) 146 | dispatch(setApikey(apiKey)) 147 | dispatch(setUsemodel(usemodel)) 148 | if (resp.code === 0) { 149 | dispatch(setUsage(Math.round(usageData.total_usage))) 150 | } 151 | }) 152 | .catch(e => { 153 | console.log(e) 154 | }) 155 | .finally(() => { 156 | dispatch(setLoadAccount(false)) 157 | }) 158 | } 159 | ) 160 | 161 | export default settingSlice.reducer 162 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { use } from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import HttpBackend from 'i18next-http-backend' 4 | 5 | import en from './locales/en.json' 6 | import zh from './locales/zh.json' 7 | 8 | export const localeOptions = { 9 | English: 'en', 10 | 中文: 'zh', 11 | } 12 | 13 | export type Languages = keyof typeof localeOptions 14 | 15 | export const init = (locale: Languages = 'English') => { 16 | const lng = localeOptions[locale] 17 | use(initReactI18next) 18 | .use(HttpBackend) 19 | .init({ 20 | lng, 21 | fallbackLng: lng, 22 | interpolation: { 23 | escapeValue: false, 24 | }, 25 | resources: { 26 | en: { translation: en }, 27 | zh: { translation: zh }, 28 | }, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import { Provider } from 'react-redux' 3 | import store from './app/store' 4 | import { App } from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Casual": "Casual", 3 | "Programmer": "Programmer", 4 | "Translator": "Translator", 5 | "Summarizer": "Summarizer", 6 | "Analyst": "Analyst", 7 | "Feedback & Help": "Feedback & Help", 8 | "Display Window": "Display Window", 9 | "Settings": "Settings", 10 | "About": "About", 11 | "More than just chat. Write, read, and code with powerful AI anywhere.": "More than just chat. Write, read, and code with powerful AI anywhere.", 12 | "Version:": "Version:", 13 | "onepoint | more than just chat": "onepoint | more than just chat", 14 | "exit": "exit", 15 | "Type '/' to bring up the plugin list, or enter your question directly in the input box.": "Type '/' to bring up the plugin list, or enter your question directly in the input box.", 16 | "Select a character": "Select a character", 17 | "Token Usage": "Token Usage", 18 | "Please enter your API key first.": "Please enter your API key first.", 19 | "Submit": "Submit", 20 | "Refresh": "Refresh", 21 | "Loading...": "Loading...", 22 | "Language": "Language", 23 | "Select A Language": "Select A Language", 24 | "Save Chat History": "Save Chat History", 25 | "Save Chat": "Save Chat", 26 | "Quantity Of Context": "Quantity Of Context", 27 | "Minimalist Mode(shrink panel)": "Minimalist Mode(shrink panel)", 28 | "Saved successfully": "Saved successfully", 29 | "Duplicated Character": "Duplicated Character", 30 | "Removed successfully": "Removed successfully", 31 | "Remove Error": "Remove Error", 32 | "Character": "Character", 33 | "Prompt": "Prompt", 34 | "Action": "Action", 35 | "Please enter the role.": "Please enter the role.", 36 | "Please input your prompt.": "Please input your prompt.", 37 | "Prompt Reference": "Prompt Reference", 38 | "Setting": "Setting", 39 | "Account": "Account", 40 | "Prompts": "Prompts", 41 | "Advanced": "Advanced", 42 | "Plugins": "Plugins", 43 | "please set your apikey first.": "please set your apikey first.", 44 | "High network latency. Check whether you have set up a proxy": "High network latency. Check whether you have set up a proxy", 45 | "Network error. Check whether you have set up a proxy": "Network error. Check whether you have set up a proxy", 46 | "Prompt Setting": "Prompt Setting", 47 | "Chat mode, feel free to ask any questions you want.": "Chat mode, feel free to ask any questions you want.", 48 | "Code Master, generate or refactor the code you want.": "Code Master, generate or refactor the code you want.", 49 | "Content analysis summary assistant, helps you read and browse web pages more effectively.": "Content analysis summary assistant, helps you read and browse web pages more effectively.", 50 | "Language expert, proficient in various languages from different countries.": "Language expert, proficient in various languages from different countries.", 51 | "Base Path": "Base Path", 52 | "Model": "Model", 53 | "reload": "reload", 54 | "Sure operate the selection in ": "Sure operate the selection in ", 55 | "Summarize this page?": "Summarize this page?", 56 | "Attempt Change": "Attempt Change", 57 | "The webpage content is too long(Exceeds 4000 characters.), which will affect the speed and experience of summarizing(Long article summary support is coming soon, please stay tuned)": "The webpage content is too long(Exceeds 4000 characters.), which will affect the speed and experience of summarizing(Long article summary support is coming soon, please stay tuned)" 58 | } 59 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Casual": "随便侃侃", 3 | "Programmer": "编程模式", 4 | "Translator": "翻译专家", 5 | "Summarizer": "网页大纲", 6 | "Analyst": "文本分析", 7 | "Feedback & Help": "反馈与帮助", 8 | "Display Window": "显示窗口", 9 | "Settings": "设置", 10 | "About": "关于", 11 | "More than just chat. Write, read, and code with powerful AI anywhere.": "不仅仅是聊天,利用AI辅助你阅读、写作与编程,用完即走", 12 | "Version:": "版本号:", 13 | "onepoint | more than just chat": "onepoint | 不仅仅是聊天", 14 | "exit": "退出", 15 | "Type '/' to bring up the plugin list, or enter your question directly in the input box.": "输入“/”呼出插件列表,或直接输入框中提出问题。", 16 | "Select a character": "选择一位角色", 17 | "Token Usage": "Token 使用量", 18 | "Please enter your API key first.": "请先输入你的apikey", 19 | "Submit": "提交", 20 | "Refresh": "刷新", 21 | "Loading...": "加载中...", 22 | "Language": "语言", 23 | "Select A Language": "选择一种语言", 24 | "Save Chat History": "保存聊天记录", 25 | "Save Chat": "保存聊天", 26 | "Quantity Of Context": "上下文数量", 27 | "Minimalist Mode(shrink panel)": "极简模式(折叠面板)", 28 | "Saved successfully": "保存成功", 29 | "Duplicated Character": "重复的角色", 30 | "Removed successfully": "删除成功", 31 | "Remove Error": "删除失败", 32 | "Character": "角色", 33 | "Prompt": "Prompt", 34 | "Action": "操作", 35 | "Please enter the role.": "请输入角色", 36 | "Please input your prompt.": "请输入相关Prompt", 37 | "Prompt Reference": "Prompt 参考", 38 | "Setting": "设置", 39 | "Account": "账户", 40 | "Prompts": "Prompts", 41 | "Advanced": "高级", 42 | "Plugins": "插件", 43 | "please set your apikey first.": "请先设置你的apikey", 44 | "High network latency. Check whether you have set up a proxy": "网络拥堵,请检查是否设置了代理。", 45 | "Network error. Check whether you have set up a proxy": "网络错误,请检查是否设置了代理。", 46 | "Prompt Setting": "Prompt设置", 47 | "Chat mode, feel free to ask any questions you want.": "聊天模式,随意提出任何你想问的问题。", 48 | "Code Master, generate or refactor the code you want.": "编程模式,生成或重构你想要的代码。", 49 | "Content analysis summary assistant, helps you read and browse web pages more effectively.": "内容分析摘要助手,帮助您更有效地阅读和浏览网页。", 50 | "Language expert, proficient in various languages from different countries.": "语言专家,精通来自不同国家的各种语言。", 51 | "Base Path": "API 请求域名", 52 | "Model": "模型", 53 | "reload": "重启", 54 | "Sure operate the selection in ": "确定在以下应用中格式化代码吗:", 55 | "Summarize this page?": "确定要总结当前的网页内容吗?", 56 | "Attempt Change": "应用修改", 57 | "The webpage content is too long(Exceeds 4000 characters.), which will affect the speed and experience of summarizing(Long article summary support is coming soon, please stay tuned)": "所选网页内容太长(超出4000字),将影响总结的速度与体验(PS: 长文内容支持中,敬请期待)" 58 | } 59 | -------------------------------------------------------------------------------- /src/styles/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | font-family: Arial, Helvetica, sans-serif; 12 | font-size: 16px; 13 | background-color: rgba(0,0,0,0); 14 | overflow: hidden; 15 | } 16 | 17 | .ant-list-item:hover { 18 | background-color: #F3F3F3; 19 | cursor: pointer; 20 | } 21 | .anticon:hover { 22 | background-color: #F3F3F3; 23 | cursor: pointer; 24 | } 25 | ol li { 26 | list-style-type:decimal !important; 27 | list-style-position:inside !important; 28 | } 29 | ` 30 | 31 | // highlightColor: rgb(10, 11, 60) 32 | -------------------------------------------------------------------------------- /src/styles/Main.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components' 2 | 3 | const rotate = keyframes` 4 | from { 5 | transform: rotate(0deg); 6 | } 7 | to { 8 | transform: rotate(360deg); 9 | } 10 | ` 11 | 12 | const bounceInDown = keyframes` 13 | from, 14 | 60%, 15 | 75%, 16 | 90%, 17 | to { 18 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 19 | } 20 | 21 | 0% { 22 | opacity: 0; 23 | transform: translate3d(0, -3000px, 0) scaleY(3); 24 | } 25 | 26 | 60% { 27 | opacity: 1; 28 | transform: translate3d(0, 25px, 0) scaleY(0.9); 29 | } 30 | 31 | 75% { 32 | transform: translate3d(0, -10px, 0) scaleY(0.95); 33 | } 34 | 35 | 90% { 36 | transform: translate3d(0, 5px, 0) scaleY(0.985); 37 | } 38 | 39 | to { 40 | transform: translate3d(0, 0, 0); 41 | } 42 | ` 43 | 44 | export const Container = styled.div` 45 | background-color: "#FFF", 46 | width: 800px; 47 | height: 100vh; 48 | flex-direction: "column", 49 | justify-content: "center", 50 | button { 51 | margin-top: 24px; 52 | } 53 | ` 54 | 55 | export const Image = styled.img` 56 | width: 240px; 57 | animation: ${rotate} 15s linear infinite; 58 | ` 59 | 60 | export const Text = styled.p` 61 | margin-top: 24px; 62 | font-size: 18px; 63 | ` 64 | 65 | export const Logo = styled.img` 66 | width: 100px; 67 | animation: ${bounceInDown} 2s linear infinite; 68 | ` 69 | -------------------------------------------------------------------------------- /src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | export const timeoutPromise = (timeout: number, timeoutTips: string) => { 2 | return new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | reject(new Error(timeoutTips)) 5 | }, timeout) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const draggableStyle = (draggle: boolean) => { 2 | return { 3 | WebkitAppRegion: draggle ? 'drag' : 'no-drag', 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["dom", "es2015", "es2016", "es2017"], 6 | "allowJs": true, 7 | "jsx": "react-jsx", 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "node": { 17 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack/main.webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolve: { 3 | extensions: ['.ts', '.js'], 4 | }, 5 | entry: './src/electron/main.ts', 6 | module: { 7 | rules: [ 8 | ...require('./rules.webpack'), 9 | { 10 | test: /\.(m?js|node)$/, 11 | parser: { amd: false }, 12 | use: { 13 | // loader: '@marshallofsound/webpack-asset-relocator-loader', 14 | loader: '@vercel/webpack-asset-relocator-loader', 15 | options: { 16 | outputAssetBase: 'native_modules', 17 | }, 18 | }, 19 | }, 20 | ], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /webpack/renderer.webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolve: { 3 | extensions: ['.ts', '.tsx', '.js'], 4 | }, 5 | module: { 6 | rules: require('./rules.webpack'), 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /webpack/rules.webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | test: /\.node$/, 4 | use: 'node-loader', 5 | }, 6 | { 7 | test: /\.(js|ts|tsx)$/, 8 | exclude: /node_modules/, 9 | use: { 10 | loader: 'babel-loader', 11 | }, 12 | }, 13 | { 14 | test: /\.(png|jpe?g|gif)$/i, 15 | loader: 'file-loader', 16 | options: { 17 | name: '[path][name].[ext]', 18 | }, 19 | }, 20 | ] 21 | --------------------------------------------------------------------------------