├── .eslintrc.json
├── .gitattributes
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug-report---问题报告.md
│ └── feature-request---新功能请求.md
├── dependabot.yml
└── workflows
│ ├── pr-tests.yml
│ ├── pre-release-build.yml
│ ├── scripts
│ └── verify-search-engine-configs.mjs
│ ├── tagged-release.yml
│ └── verify-configs.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CURRENT_CHANGE.md
├── LICENSE
├── README.md
├── README_IN.md
├── README_JA.md
├── README_TR.md
├── README_ZH.md
├── build.mjs
├── package-lock.json
├── package.json
├── safari
├── appdmg.json
├── build.sh
├── export-options.plist
├── project.patch
├── project.pre.patch
└── project_developer.patch
├── screenshots
├── preview_github_rightclickmenu.jpg
├── preview_google_floatingwindow_conversationbranch.jpg
├── preview_independentpanel.jpg
├── preview_reddit_selectiontools.jpg
├── preview_settings.jpg
└── preview_youtube.jpg
└── src
├── _locales
├── de
│ └── main.json
├── en
│ └── main.json
├── es
│ └── main.json
├── fr
│ └── main.json
├── i18n-react.mjs
├── i18n.mjs
├── in
│ └── main.json
├── it
│ └── main.json
├── ja
│ └── main.json
├── ko
│ └── main.json
├── pt
│ └── main.json
├── resources.mjs
├── ru
│ └── main.json
├── tr
│ └── main.json
├── zh-hans
│ └── main.json
└── zh-hant
│ └── main.json
├── background
├── commands.mjs
├── index.mjs
└── menus.mjs
├── components
├── ConfirmButton
│ └── index.jsx
├── ConversationCard
│ └── index.jsx
├── ConversationItem
│ └── index.jsx
├── CopyButton
│ └── index.jsx
├── DecisionCard
│ └── index.jsx
├── DeleteButton
│ └── index.jsx
├── FeedbackForChatGPTWeb
│ └── index.jsx
├── FloatingToolbar
│ └── index.jsx
├── InputBox
│ └── index.jsx
├── MarkdownRender
│ ├── Hyperlink.jsx
│ ├── Pre.jsx
│ ├── markdown-without-katex.jsx
│ ├── markdown.jsx
│ └── mykatex.min.css
├── ReadButton
│ └── index.jsx
├── WebJumpBackNotification
│ └── index.jsx
└── index.mjs
├── config
├── index.mjs
└── language.mjs
├── content-script
├── index.jsx
├── menu-tools
│ └── index.mjs
├── selection-tools
│ └── index.mjs
├── site-adapters
│ ├── arxiv
│ │ └── index.mjs
│ ├── baidu
│ │ └── index.mjs
│ ├── bilibili
│ │ └── index.mjs
│ ├── brave
│ │ └── index.mjs
│ ├── duckduckgo
│ │ └── index.mjs
│ ├── followin
│ │ └── index.mjs
│ ├── github
│ │ └── index.mjs
│ ├── gitlab
│ │ └── index.mjs
│ ├── index.mjs
│ ├── juejin
│ │ └── index.mjs
│ ├── quora
│ │ └── index.mjs
│ ├── reddit
│ │ └── index.mjs
│ ├── stackoverflow
│ │ └── index.mjs
│ ├── weixin
│ │ └── index.mjs
│ ├── youtube
│ │ └── index.mjs
│ └── zhihu
│ │ └── index.mjs
└── styles.scss
├── fonts
├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2
├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2
├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2
└── styles.css
├── hooks
├── use-clamp-window-size.mjs
├── use-config.mjs
├── use-theme.mjs
├── use-window-size.mjs
└── use-window-theme.mjs
├── logo.png
├── manifest.json
├── manifest.v2.json
├── pages
├── IndependentPanel
│ ├── App.jsx
│ ├── index.html
│ ├── index.jsx
│ └── styles.scss
└── styles.scss
├── popup
├── Popup.jsx
├── index.html
├── index.jsx
├── sections
│ ├── AdvancedPart.jsx
│ ├── ApiModes.jsx
│ ├── FeaturePages.jsx
│ ├── GeneralPart.jsx
│ ├── ModulesPart.jsx
│ ├── SelectionTools.jsx
│ └── SiteAdapters.jsx
└── styles.scss
├── rules.json
├── services
├── apis
│ ├── azure-openai-api.mjs
│ ├── bard-web.mjs
│ ├── bing-web.mjs
│ ├── chatglm-api.mjs
│ ├── chatgpt-web.mjs
│ ├── claude-api.mjs
│ ├── claude-web.mjs
│ ├── custom-api.mjs
│ ├── moonshot-api.mjs
│ ├── moonshot-web.mjs
│ ├── ollama-api.mjs
│ ├── openai-api.mjs
│ ├── poe-web.mjs
│ ├── shared.mjs
│ └── waylaidwanderer-api.mjs
├── clients
│ ├── bard
│ │ └── index.mjs
│ ├── bing
│ │ ├── BingImageCreator.js
│ │ └── index.mjs
│ ├── claude
│ │ └── index.mjs
│ └── poe
│ │ ├── graphql
│ │ ├── AddHumanMessageMutation.graphql
│ │ ├── AddMessageBreakMutation.graphql
│ │ ├── AutoSubscriptionMutation.graphql
│ │ ├── BioFragment.graphql
│ │ ├── ChatAddedSubscription.graphql
│ │ ├── ChatFragment.graphql
│ │ ├── ChatPaginationQuery.graphql
│ │ ├── ChatViewQuery.graphql
│ │ ├── DeleteHumanMessagesMutation.graphql
│ │ ├── HandleFragment.graphql
│ │ ├── LoginWithVerificationCodeMutation.graphql
│ │ ├── MessageAddedSubscription.graphql
│ │ ├── MessageDeletedSubscription.graphql
│ │ ├── MessageFragment.graphql
│ │ ├── MessageRemoveVoteMutation.graphql
│ │ ├── MessageSetVoteMutation.graphql
│ │ ├── SendVerificationCodeForLoginMutation.graphql
│ │ ├── ShareMessagesMutation.graphql
│ │ ├── SignupWithVerificationCodeMutation.graphql
│ │ ├── StaleChatUpdateMutation.graphql
│ │ ├── SummarizePlainPostQuery.graphql
│ │ ├── SummarizeQuotePostQuery.graphql
│ │ ├── SummarizeSharePostQuery.graphql
│ │ ├── UserSnippetFragment.graphql
│ │ ├── ViewerInfoQuery.graphql
│ │ ├── ViewerStateFragment.graphql
│ │ └── ViewerStateUpdatedSubscription.graphql
│ │ ├── index.mjs
│ │ └── websocket.js
├── init-session.mjs
├── local-session.mjs
└── wrappers.mjs
└── utils
├── change-children-font-size.mjs
├── create-element-at-position.mjs
├── crop-text.mjs
├── ends-with-question-mark.mjs
├── eventsource-parser.mjs
├── fetch-bg.mjs
├── fetch-sse.mjs
├── get-client-position.mjs
├── get-conversation-pairs.mjs
├── get-core-content-text.mjs
├── get-possible-element-by-query-selector.mjs
├── index.mjs
├── is-edge.mjs
├── is-firefox.mjs
├── is-mobile.mjs
├── is-safari.mjs
├── jwt-token-generator.mjs
├── limited-fetch.mjs
├── model-name-convert.mjs
├── open-url.mjs
├── parse-float-with-clamp.mjs
├── parse-int-with-clamp.mjs
├── set-element-position-in-viewport.mjs
├── update-ref-height.mjs
└── wait-for-element-to-exist-and-select.mjs
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:react/recommended"],
7 | "overrides": [],
8 | "parserOptions": {
9 | "ecmaVersion": "latest",
10 | "sourceType": "module"
11 | },
12 | "rules": {
13 | "react/react-in-jsx-scope": "off"
14 | },
15 | "ignorePatterns": ["build/**", "build.mjs", "src/utils/is-mobile.mjs"],
16 | "settings": {
17 | "react": {
18 | "version": "detect"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/services/clients/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See https://github.com/josStorer/chatGPTBox/wiki/Development&Contributing
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report---问题报告.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report / 问题报告
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | **问题描述**
12 | A clear and concise description of what the bug is.
13 |
14 | **To Reproduce**
15 | **如何复现**
16 | Steps to reproduce the behavior:
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 | **期望行为**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | **截图说明**
28 | If applicable, add screenshots to help explain your problem.
29 |
30 | **Please complete the following information):**
31 | **请补全以下内容**
32 | - OS: [e.g. Windows]
33 | - Browser: [e.g. chrome, safari]
34 | - Extension Version: [e.g. v2.0.2]
35 |
36 | **Additional context**
37 | **其他**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request---新功能请求.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request / 新功能请求
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | **新功能是否与解决某个问题相关, 请描述**
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | **Describe the solution you'd like**
15 | **你期望的新功能实现方案**
16 | A clear and concise description of what you want to happen.
17 |
18 | **Additional context**
19 | **其他**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | commit-message:
8 | prefix: "chore"
9 | include: "scope"
--------------------------------------------------------------------------------
/.github/workflows/pr-tests.yml:
--------------------------------------------------------------------------------
1 | name: pr-tests
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - "opened"
7 | - "reopened"
8 | - "synchronize"
9 | paths:
10 | - "src/**"
11 | - "build.mjs"
12 |
13 | jobs:
14 | tests:
15 | runs-on: ubuntu-22.04
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: 20
22 | - run: npm ci
23 | - run: npm run lint
24 | - run: npm run build
--------------------------------------------------------------------------------
/.github/workflows/pre-release-build.yml:
--------------------------------------------------------------------------------
1 | name: pre-release
2 | on:
3 | workflow_dispatch:
4 | # push:
5 | # branches:
6 | # - master
7 | # paths:
8 | # - "src/**"
9 | # - "!src/**/*.json"
10 | # - "build.mjs"
11 | # tags-ignore:
12 | # - "v*"
13 |
14 | permissions:
15 | id-token: "write"
16 | contents: "write"
17 |
18 | jobs:
19 | build_and_release:
20 | runs-on: ubuntu-22.04
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: actions/setup-node@v4
25 | with:
26 | node-version: 20
27 | - run: npm ci
28 | - run: npm run build
29 |
30 | - uses: josStorer/get-current-time@v2
31 | id: current-time
32 | with:
33 | format: YYYY_MMDD_HHmm
34 |
35 | - uses: actions/upload-artifact@v4
36 | with:
37 | name: Chromium_Build_${{ steps.current-time.outputs.formattedTime }}
38 | path: build/chromium/*
39 |
40 | - uses: actions/upload-artifact@v4
41 | with:
42 | name: Firefox_Build_${{ steps.current-time.outputs.formattedTime }}
43 | path: build/firefox/*
44 |
45 | - uses: actions/upload-artifact@v4
46 | with:
47 | name: Chromium_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}
48 | path: build/chromium-without-katex-and-tiktoken/*
49 |
50 | - uses: actions/upload-artifact@v4
51 | with:
52 | name: Firefox_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}
53 | path: build/firefox-without-katex-and-tiktoken/*
54 |
55 | - uses: marvinpinto/action-automatic-releases@v1.2.1
56 | with:
57 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
58 | automatic_release_tag: "latest"
59 | prerelease: true
60 | title: "Development Build"
61 | files: |
62 | build/chromium.zip
63 | build/firefox.zip
64 | build/chromium-without-katex-and-tiktoken.zip
65 | build/firefox-without-katex-and-tiktoken.zip
66 |
--------------------------------------------------------------------------------
/.github/workflows/tagged-release.yml:
--------------------------------------------------------------------------------
1 | name: tagged-release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | permissions:
8 | id-token: "write"
9 | contents: "write"
10 | env:
11 | GH_TOKEN: ${{ github.token }}
12 |
13 | jobs:
14 | build_and_release:
15 | runs-on: macos-12
16 |
17 | steps:
18 | - run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
19 | - uses: actions/checkout@v4
20 | with:
21 | ref: master
22 |
23 | - name: Update manifest.json version
24 | uses: jossef/action-set-json-field@v2.2
25 | with:
26 | file: src/manifest.json
27 | field: version
28 | value: ${{ env.VERSION }}
29 |
30 | - name: Update manifest.v2.json version
31 | uses: jossef/action-set-json-field@v2.2
32 | with:
33 | file: src/manifest.v2.json
34 | field: version
35 | value: ${{ env.VERSION }}
36 |
37 | - name: Push files
38 | continue-on-error: true
39 | run: |
40 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
41 | git config --global user.name "github-actions[bot]"
42 | git commit -am "release v${{ env.VERSION }}"
43 | git push
44 |
45 | - run: |
46 | gh release create ${{github.ref_name}} -d -F CURRENT_CHANGE.md -t ${{github.ref_name}}
47 |
48 | - uses: actions/setup-node@v4
49 | with:
50 | node-version: 20
51 | - run: npm ci
52 |
53 | - uses: actions/setup-python@v5
54 | with:
55 | python-version: '3.10' # for appdmg
56 | - uses: maxim-lobanov/setup-xcode@v1
57 | with:
58 | xcode-version: 14.2
59 | - run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.pre.patch
60 | - run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.patch
61 | - run: npm run build:safari
62 |
63 | - run: |
64 | gh release upload ${{github.ref_name}} build/chromium.zip
65 | gh release upload ${{github.ref_name}} build/firefox.zip
66 | gh release upload ${{github.ref_name}} build/safari.dmg
67 | gh release upload ${{github.ref_name}} build/chromium-without-katex-and-tiktoken.zip
68 | gh release upload ${{github.ref_name}} build/firefox-without-katex-and-tiktoken.zip
69 |
70 | - run: |
71 | gh release edit ${{github.ref_name}} --draft=false
72 |
--------------------------------------------------------------------------------
/.github/workflows/verify-configs.yml:
--------------------------------------------------------------------------------
1 | name: verify-configs
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: "0 6 * * *"
6 |
7 | jobs:
8 | verify_configs:
9 | runs-on: ubuntu-22.04
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 | - run: npm ci
17 | - run: npm run verify
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | node_modules/
4 | build/
5 | .DS_Store
6 | *.zip
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | src/manifest.json
3 | src/manifest.v2.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "bracketSpacing": true,
8 | "overrides": [
9 | {
10 | "files": ".prettierrc",
11 | "options": {
12 | "parser": "json"
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/CURRENT_CHANGE.md:
--------------------------------------------------------------------------------
1 | # Changes
2 |
3 | this is a patch for v2.5.7, including some minor fixes and improvements
4 |
5 | ## Features
6 | - unlimited custom API Modes (#731, #717, #712, #659, #647)
7 |
8 |
9 |
10 |
11 |
12 | - Option to always display floating window, disable sidebar for all site adapters (#747, #753) f50249e7 226fb108 b96ba7c0
13 |
14 | 
15 |
16 | - Add Ollama native API to support keep alive parameters (#748) 6877a1b7 48817006
17 |
18 |
19 |
20 | - Option to allow ESC to close all floating windows (#750)
21 | - allow exporting and importing all data (#740)
22 |
23 |
24 |
25 | ## Improvements
26 | - for simplified chinese users, use Kimi.Moonshot Web for free by default, while other users default to using Claude.ai for free and a better user experience
27 | - improve chatglm support (#696, #464)
28 | - improve style conflicts (#724, #378)
29 | - improve user experience for claude.ai and kimi.moonshot.cn
30 |
31 | ## Fixes
32 | - fix firefox bilibili summary (#761)
33 | - fix Buffer is not defined when using tiny package (#691, https://github.com/josStorer/chatGPTBox/issues/752#issuecomment-2240977750)
34 |
35 | ## Chores
36 | - Added Claude 3.5 Sonnet API to available models e7cec334
37 | - Add gpt-4o-mini for both web and api access (#749)
38 | - update enforcement rule
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 josStorer
4 | Copyright (c) 2022 wong2
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ChatGPT Box
6 |
7 |
8 |
9 | 深い ChatGPT 統合をブラウザに、完全無料で。
10 |
11 | [![license][license-image]][license-url]
12 | [![release][release-image]][release-url]
13 | [][release-url]
14 | [![verfiy][verify-image]][verify-url]
15 |
16 | [English](README.md) | [Indonesia](README_IN.md) | [简体中文](README_ZH.md) | 日本語 | [Türkçe](README_TR.md)
17 |
18 | ### インストール
19 |
20 | [![Chrome][Chrome-image]][Chrome-url]
21 | [![Edge][Edge-image]][Edge-url]
22 | [![Firefox][Firefox-image]][Firefox-url]
23 | [![Safari][Safari-image]][Safari-url]
24 | [![Android][Android-image]][Android-url]
25 | [![Github][Github-image]][Github-url]
26 |
27 | [ガイド](https://github.com/josStorer/chatGPTBox/wiki/Guide) | [プレビュー](#プレビュー) | [開発 & コントリビュート][dev-url] | [ビデオデモ](https://www.youtube.com/watch?v=E1smDxJvTRs) | [クレジット](#クレジット)
28 |
29 | [dev-url]: https://github.com/josStorer/chatGPTBox/wiki/Development&Contributing
30 |
31 | [license-image]: http://img.shields.io/badge/license-MIT-blue.svg
32 |
33 | [license-url]: https://github.com/josStorer/chatGPTBox/blob/master/LICENSE
34 |
35 | [release-image]: https://img.shields.io/github/release/josStorer/chatGPTBox.svg
36 |
37 | [release-url]: https://github.com/josStorer/chatGPTBox/releases/latest
38 |
39 | [verify-image]: https://github.com/josStorer/chatGPTBox/workflows/verify-configs/badge.svg
40 |
41 | [verify-url]: https://github.com/josStorer/chatGPTBox/actions/workflows/verify-configs.yml
42 |
43 | [Chrome-image]: https://img.shields.io/badge/-Chrome-brightgreen?logo=google-chrome&logoColor=white
44 |
45 | [Chrome-url]: https://chrome.google.com/webstore/detail/chatgptbox/eobbhoofkanlmddnplfhnmkfbnlhpbbo
46 |
47 | [Edge-image]: https://img.shields.io/badge/-Edge-blue?logo=microsoft-edge&logoColor=white
48 |
49 | [Edge-url]: https://microsoftedge.microsoft.com/addons/detail/fission-chatbox-best/enjmfilpkbbabhgeoadmdpjjpnahkogf
50 |
51 | [Firefox-image]: https://img.shields.io/badge/-Firefox-orange?logo=firefox-browser&logoColor=white
52 |
53 | [Firefox-url]: https://addons.mozilla.org/firefox/addon/chatgptbox/
54 |
55 | [Safari-image]: https://img.shields.io/badge/-Safari-blue?logo=safari&logoColor=white
56 |
57 | [Safari-url]: https://apps.apple.com/app/fission-chatbox/id6446611121
58 |
59 | [Android-image]: https://img.shields.io/badge/-Android-brightgreen?logo=android&logoColor=white
60 |
61 | [Android-url]: https://github.com/josStorer/chatGPTBox/wiki/Install#install-to-android
62 |
63 | [Github-image]: https://img.shields.io/badge/-Github-black?logo=github&logoColor=white
64 |
65 | [Github-url]: https://github.com/josStorer/chatGPTBox/wiki/Install
66 |
67 |
68 |
69 | ## ニュース
70 |
71 | - この拡張機能はあなたのデータを収集しません。コード内の `fetch(` と `XMLHttpRequest(` をグローバル検索して、すべてのネットワークリクエストの呼び出しを見つけることで確認できます。コードの量はそれほど多くないので、簡単にできます。
72 |
73 | - このツールは、あなたが明示的に要求しない限り、ChatGPT にデータを送信しません。デフォルトでは、拡張機能は手動で有効にする必要があります ChatGPT へのリクエストは、"Ask ChatGPT" をクリックするか、選択フローティングツールをトリガーした場合にのみ送信されます。(issue #407)
74 |
75 | - https://github.com/BerriAI/litellm / https://github.com/songquanpeng/one-api のようなプロジェクトを使用して、LLM APIをOpenAI形式に変換し、それらをChatGPTBoxの `カスタムモデル` モードと組み合わせて使用することができます
76 |
77 | - もちろんです。ChatGPTBoxの `カスタムモデル` モードを使用する際には、[Ollama](https://github.com/josStorer/chatGPTBox/issues/616#issuecomment-1975186467) / https://openrouter.ai/docs#models もご利用いただけます
78 |
79 | ## ✨ 機能
80 |
81 | - 🌈 いつでもどのページでもチャットダイアログボックスを呼び出すことができます。 (Ctrl +B )
82 | - 📱 モバイル機器のサポート。
83 | - 📓 右クリックメニューで任意のページを要約。 (Alt +B )
84 | - 📖 独立した会話ページ。 (Ctrl +Shift +H )
85 | - 🔗 複数の API をサポート(無料および Plus ユーザー向け Web API、GPT-3.5、GPT-4、Claude、New Bing、Moonshot、セルフホスト、Azure など)。
86 | - 📦 よく使われる様々なウェブサイト(Reddit、Quora、YouTube、GitHub、GitLab、StackOverflow、Zhihu、Bilibili)の統合。 ([wimdenherder](https://github.com/wimdenherder) にインスパイアされました)
87 | - 🔍 すべての主要検索エンジンと統合し、追加のサイトをサポートするためのカスタムクエリ。
88 | - 🧰 選択ツールと右クリックメニューで、翻訳、要約、推敲、感情分析、段落分割、コード説明、クエリーなど、さまざまなタスクを実行できます。
89 | - 🗂️ 静的なカードは、複数の支店での会話のためのフローティングチャットボックスをサポートしています。
90 | - 🖨️ チャット記録を完全に保存することも、部分的にコピーすることも簡単です。
91 | - 🎨 コードのハイライトや複雑な数式など、強力なレンダリングをサポート。
92 | - 🌍 言語設定のサポート。
93 | - 📝 カスタム API アドレスのサポート
94 | - ⚙️ すべてのサイト適応と選択ツール(バブル)は、自由にオンまたはオフに切り替えることができ、不要なモジュールを無効にすることができます。
95 | - 💡 セレクションツールやサイトへの適応は簡単に開発・拡張できます。[開発 & コントリビュート][dev-url]のセクションを参照。
96 | - 😉 チャットして回答の質を高められます。
97 |
98 | ## プレビュー
99 |
100 |
101 |
102 | **検索エンジンの統合、フローティングウィンドウ、会話ブランチ**
103 |
104 | 
105 |
106 | **よく使われるウェブサイトや選択ツールとの統合**
107 |
108 | 
109 |
110 | **独立会話ページ**
111 |
112 | 
113 |
114 | **Git 分析、右クリックメニュー**
115 |
116 | 
117 |
118 | **ビデオ要約**
119 |
120 | 
121 |
122 | **モバイルサポート**
123 |
124 | 
125 |
126 | **設定**
127 |
128 | 
129 |
130 |
131 |
132 | ## クレジット
133 |
134 | このプロジェクトは、私の他のリポジトリ [josStorer/chatGPT-search-engine-extension](https://github.com/josStorer/chatGPT-search-engine-extension) に基づいています
135 |
136 | [josStorer/chatGPT-search-engine-extension](https://github.com/josStorer/chatGPT-search-engine-extension) は [wong2/chat-gpt-google-extension](https://github.com/wong2/chat-gpt-google-extension) (参考にしました)からフォークされ、2022年12月14日から切り離されています
137 |
138 | [wong2/chat-gpt-google-extension](https://github.com/wong2/chat-gpt-google-extension) は [ZohaibAhmed/ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) にインスパイアされています([upstream-c54528b](https://github.com/wong2/chatgpt-google-extension/commit/c54528b0e13058ab78bfb433c92603db017d1b6b))
139 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgptbox",
3 | "scripts": {
4 | "build": "node build.mjs --production",
5 | "build:safari": "bash ./safari/build.sh",
6 | "dev": "node build.mjs --development",
7 | "analyze": "node build.mjs --analyze",
8 | "lint": "eslint --ext .js,.mjs,.jsx .",
9 | "lint:fix": "eslint --ext .js,.mjs,.jsx . --fix",
10 | "pretty": "prettier --write ./**/*.{js,mjs,jsx,json,css,scss}",
11 | "stage": "run-script-os",
12 | "stage:default": "git add $(git diff --name-only --cached --diff-filter=d)",
13 | "stage:win32": "powershell git add $(git diff --name-only --cached --diff-filter=d)",
14 | "verify": "node .github/workflows/scripts/verify-search-engine-configs.mjs"
15 | },
16 | "pre-commit": [
17 | "pretty",
18 | "stage",
19 | "lint"
20 | ],
21 | "dependencies": {
22 | "@mozilla/readability": "^0.5.0",
23 | "@nem035/gpt-3-encoder": "^1.1.7",
24 | "@picocss/pico": "^1.5.13",
25 | "@primer/octicons-react": "^18.3.0",
26 | "buffer": "^6.0.3",
27 | "countries-list": "^2.6.1",
28 | "crypto-browserify": "^3.12.0",
29 | "diff": "^5.2.0",
30 | "file-saver": "^2.0.5",
31 | "github-markdown-css": "^5.6.1",
32 | "gpt-3-encoder": "^1.1.4",
33 | "graphql": "^16.9.0",
34 | "i18next": "^22.4.15",
35 | "js-sha3": "^0.9.3",
36 | "jsonwebtoken": "8.5.1",
37 | "katex": "^0.16.11",
38 | "lodash-es": "^4.17.21",
39 | "md5": "^2.3.0",
40 | "parse5": "^6.0.1",
41 | "preact": "^10.22.1",
42 | "process": "^0.11.10",
43 | "prop-types": "^15.8.1",
44 | "random-int": "^3.0.0",
45 | "react": "npm:@preact/compat@^17.1.2",
46 | "react-bootstrap-icons": "^1.11.4",
47 | "react-dom": "npm:@preact/compat@^17.1.2",
48 | "react-draggable": "^4.4.6",
49 | "react-i18next": "^12.2.0",
50 | "react-markdown": "^8.0.7",
51 | "react-tabs": "^4.3.0",
52 | "react-toastify": "^9.1.3",
53 | "rehype-highlight": "^6.0.0",
54 | "rehype-katex": "^6.0.3",
55 | "rehype-raw": "^6.1.1",
56 | "remark-breaks": "^3.0.3",
57 | "remark-gfm": "^3.0.1",
58 | "remark-math": "^5.1.1",
59 | "stream-browserify": "^3.0.0",
60 | "util": "^0.12.5",
61 | "uuid": "^9.0.1",
62 | "webextension-polyfill": "^0.12.0"
63 | },
64 | "devDependencies": {
65 | "@babel/core": "^7.24.7",
66 | "@babel/plugin-transform-react-jsx": "^7.24.7",
67 | "@babel/plugin-transform-runtime": "^7.24.7",
68 | "@babel/preset-env": "^7.24.7",
69 | "@types/archiver": "^5.3.4",
70 | "@types/fs-extra": "^11.0.4",
71 | "@types/jsdom": "^21.1.7",
72 | "@types/webextension-polyfill": "^0.10.7",
73 | "archiver": "^5.3.2",
74 | "babel-loader": "^9.1.3",
75 | "css-loader": "^6.11.0",
76 | "css-minimizer-webpack-plugin": "^5.0.1",
77 | "eslint": "^8.57.0",
78 | "eslint-plugin-react": "^7.34.3",
79 | "fs-extra": "^11.2.0",
80 | "graphql-tag": "^2.12.6",
81 | "jsdom": "^21.1.2",
82 | "less-loader": "^11.1.4",
83 | "mini-css-extract-plugin": "^2.9.0",
84 | "node-fetch": "^3.3.2",
85 | "pre-commit": "^1.2.2",
86 | "prettier": "^2.8.8",
87 | "progress-bar-webpack-plugin": "^2.1.0",
88 | "run-script-os": "^1.1.6",
89 | "sass": "^1.77.6",
90 | "sass-loader": "^13.3.3",
91 | "string-replace-loader": "^3.1.0",
92 | "terser-webpack-plugin": "^5.3.10",
93 | "webpack": "^5.92.1",
94 | "webpack-bundle-analyzer": "^4.10.2"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/safari/appdmg.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Fission - ChatBox",
3 | "icon": "../src/logo.png",
4 | "contents": [
5 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" },
6 | { "x": 192, "y": 344, "type": "file", "path": "../build/Fission - ChatBox.app" }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/safari/build.sh:
--------------------------------------------------------------------------------
1 | git apply safari/project.pre.patch
2 | npm run build
3 | xcrun safari-web-extension-converter ./build/firefox \
4 | --project-location ./build/safari --app-name "Fission - ChatBox" \
5 | --bundle-identifier dev.josStorer.chatGPTBox --force --no-prompt --no-open
6 | git apply safari/project.patch
7 | xcodebuild archive -project "./build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj" \
8 | -scheme "Fission - ChatBox (macOS)" -configuration Release -archivePath "./build/safari/Fission - ChatBox.xcarchive"
9 | xcodebuild -exportArchive -archivePath "./build/safari/Fission - ChatBox.xcarchive" \
10 | -exportOptionsPlist ./safari/export-options.plist -exportPath ./build
11 | npm install -D appdmg
12 | rm ./build/safari.dmg
13 | appdmg ./safari/appdmg.json ./build/safari.dmg
--------------------------------------------------------------------------------
/safari/export-options.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | method
6 | mac-application
7 |
8 |
--------------------------------------------------------------------------------
/safari/project.patch:
--------------------------------------------------------------------------------
1 | --- a/build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj/project.pbxproj
2 | +++ b/build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj/project.pbxproj
3 |
--------------------------------------------------------------------------------
/safari/project.pre.patch:
--------------------------------------------------------------------------------
1 | --- a/src/manifest.v2.json
2 | +++ b/src/manifest.v2.json
3 | @@ -1,5 +1,5 @@
4 | {
5 | - "name": "ChatGPTBox",
6 | + "name": "Fission - ChatBox",
7 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
8 | "version": "0.0.0",
9 | "manifest_version": 2,
10 | @@ -28,7 +28,7 @@
11 | "scripts": [
12 | "background.js"
13 | ],
14 | - "persistent": true
15 | + "persistent": true
16 | },
17 | "browser_action": {
18 | "default_popup": "popup.html?popup=true"
19 |
--------------------------------------------------------------------------------
/screenshots/preview_github_rightclickmenu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_github_rightclickmenu.jpg
--------------------------------------------------------------------------------
/screenshots/preview_google_floatingwindow_conversationbranch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_google_floatingwindow_conversationbranch.jpg
--------------------------------------------------------------------------------
/screenshots/preview_independentpanel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_independentpanel.jpg
--------------------------------------------------------------------------------
/screenshots/preview_reddit_selectiontools.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_reddit_selectiontools.jpg
--------------------------------------------------------------------------------
/screenshots/preview_settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_settings.jpg
--------------------------------------------------------------------------------
/screenshots/preview_youtube.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/screenshots/preview_youtube.jpg
--------------------------------------------------------------------------------
/src/_locales/i18n-react.mjs:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import { initReactI18next } from 'react-i18next'
3 | import { resources } from './resources'
4 |
5 | i18n.use(initReactI18next).init({
6 | resources,
7 | fallbackLng: 'en',
8 | interpolation: {
9 | escapeValue: false, // not needed for react as it escapes by default
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/src/_locales/i18n.mjs:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import { resources } from './resources'
3 |
4 | i18n.init({
5 | resources,
6 | fallbackLng: 'en',
7 | })
8 |
--------------------------------------------------------------------------------
/src/_locales/resources.mjs:
--------------------------------------------------------------------------------
1 | import de from './de/main.json'
2 | import en from './en/main.json'
3 | import es from './es/main.json'
4 | import fr from './fr/main.json'
5 | import inTrans from './in/main.json'
6 | import it from './it/main.json'
7 | import ja from './ja/main.json'
8 | import ko from './ko/main.json'
9 | import pt from './pt/main.json'
10 | import ru from './ru/main.json'
11 | import tr from './tr/main.json'
12 | import zhHans from './zh-hans/main.json'
13 | import zhHant from './zh-hant/main.json'
14 |
15 | export const resources = {
16 | de: {
17 | translation: de,
18 | },
19 | en: {
20 | translation: en,
21 | },
22 | es: {
23 | translation: es,
24 | },
25 | fr: {
26 | translation: fr,
27 | },
28 | in: {
29 | translation: inTrans,
30 | },
31 | it: {
32 | translation: it,
33 | },
34 | ja: {
35 | translation: ja,
36 | },
37 | ko: {
38 | translation: ko,
39 | },
40 | pt: {
41 | translation: pt,
42 | },
43 | ru: {
44 | translation: ru,
45 | },
46 | tr: {
47 | translation: tr,
48 | },
49 | zh: {
50 | translation: zhHans,
51 | },
52 | zhHant: {
53 | translation: zhHant,
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/src/background/commands.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
3 |
4 | export function registerCommands() {
5 | Browser.commands.onCommand.addListener(async (command, tab) => {
6 | const message = {
7 | itemId: command,
8 | selectionText: '',
9 | useMenuPosition: false,
10 | }
11 | console.debug('command triggered', message)
12 |
13 | if (command in menuConfig) {
14 | if (menuConfig[command].action) {
15 | menuConfig[command].action(true, tab)
16 | }
17 |
18 | if (menuConfig[command].genPrompt) {
19 | const currentTab = (await Browser.tabs.query({ active: true, currentWindow: true }))[0]
20 | Browser.tabs.sendMessage(currentTab.id, {
21 | type: 'CREATE_CHAT',
22 | data: message,
23 | })
24 | }
25 | }
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/src/background/menus.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { defaultConfig, getPreferredLanguageKey, getUserConfig } from '../config/index.mjs'
3 | import { changeLanguage, t } from 'i18next'
4 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
5 |
6 | const menuId = 'ChatGPTBox-Menu'
7 | const onClickMenu = (info, tab) => {
8 | Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
9 | const currentTab = tabs[0]
10 | const message = {
11 | itemId: info.menuItemId.replace(menuId, ''),
12 | selectionText: info.selectionText,
13 | useMenuPosition: tab.id === currentTab.id,
14 | }
15 | console.debug('menu clicked', message)
16 |
17 | if (defaultConfig.selectionTools.includes(message.itemId)) {
18 | Browser.tabs.sendMessage(currentTab.id, {
19 | type: 'CREATE_CHAT',
20 | data: message,
21 | })
22 | } else if (message.itemId in menuConfig) {
23 | if (menuConfig[message.itemId].action) {
24 | menuConfig[message.itemId].action(true, tab)
25 | }
26 |
27 | if (menuConfig[message.itemId].genPrompt) {
28 | Browser.tabs.sendMessage(currentTab.id, {
29 | type: 'CREATE_CHAT',
30 | data: message,
31 | })
32 | }
33 | }
34 | })
35 | }
36 | export function refreshMenu() {
37 | if (Browser.contextMenus.onClicked.hasListener(onClickMenu))
38 | Browser.contextMenus.onClicked.removeListener(onClickMenu)
39 | Browser.contextMenus.removeAll().then(async () => {
40 | if ((await getUserConfig()).hideContextMenu) return
41 |
42 | await getPreferredLanguageKey().then((lang) => {
43 | changeLanguage(lang)
44 | })
45 | Browser.contextMenus.create({
46 | id: menuId,
47 | title: 'ChatGPTBox',
48 | contexts: ['all'],
49 | })
50 |
51 | for (const [k, v] of Object.entries(menuConfig)) {
52 | Browser.contextMenus.create({
53 | id: menuId + k,
54 | parentId: menuId,
55 | title: t(v.label),
56 | contexts: ['all'],
57 | })
58 | }
59 | Browser.contextMenus.create({
60 | id: menuId + 'separator1',
61 | parentId: menuId,
62 | contexts: ['selection'],
63 | type: 'separator',
64 | })
65 | for (const index in defaultConfig.selectionTools) {
66 | const key = defaultConfig.selectionTools[index]
67 | const desc = defaultConfig.selectionToolsDesc[index]
68 | Browser.contextMenus.create({
69 | id: menuId + key,
70 | parentId: menuId,
71 | title: t(desc),
72 | contexts: ['selection'],
73 | })
74 | }
75 |
76 | Browser.contextMenus.onClicked.addListener(onClickMenu)
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/ConfirmButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { useEffect, useRef, useState } from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | ConfirmButton.propTypes = {
6 | onConfirm: PropTypes.func.isRequired,
7 | text: PropTypes.string.isRequired,
8 | }
9 |
10 | function ConfirmButton({ onConfirm, text }) {
11 | const { t } = useTranslation()
12 | const [waitConfirm, setWaitConfirm] = useState(false)
13 | const confirmRef = useRef(null)
14 |
15 | useEffect(() => {
16 | if (waitConfirm) confirmRef.current.focus()
17 | }, [waitConfirm])
18 |
19 | return (
20 |
21 | {
29 | e.preventDefault()
30 | e.stopPropagation()
31 | }}
32 | onBlur={() => {
33 | setWaitConfirm(false)
34 | }}
35 | onClick={() => {
36 | setWaitConfirm(false)
37 | onConfirm()
38 | }}
39 | >
40 | {t('Confirm')}
41 |
42 | {
49 | setWaitConfirm(true)
50 | }}
51 | >
52 | {text}
53 |
54 |
55 | )
56 | }
57 |
58 | export default ConfirmButton
59 |
--------------------------------------------------------------------------------
/src/components/ConversationItem/index.jsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from 'react'
2 | import { ChevronDownIcon, XCircleIcon, SyncIcon } from '@primer/octicons-react'
3 | import CopyButton from '../CopyButton'
4 | import ReadButton from '../ReadButton'
5 | import PropTypes from 'prop-types'
6 | import MarkdownRender from '../MarkdownRender/markdown.jsx'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | function AnswerTitle({ descName }) {
10 | const { t } = useTranslation()
11 |
12 | return {descName ? `${descName}:` : t('Loading...')}
13 | }
14 |
15 | AnswerTitle.propTypes = {
16 | descName: PropTypes.string,
17 | }
18 |
19 | export function ConversationItem({ type, content, descName, onRetry }) {
20 | const { t } = useTranslation()
21 | const [collapsed, setCollapsed] = useState(false)
22 |
23 | switch (type) {
24 | case 'question':
25 | return (
26 |
27 |
28 |
{t('You')}:
29 |
30 | content.replace(/\n $/, '')} size={14} />
31 | content} size={14} />
32 | {!collapsed ? (
33 | setCollapsed(true)}
37 | >
38 |
39 |
40 | ) : (
41 | setCollapsed(false)}
45 | >
46 |
47 |
48 | )}
49 |
50 |
51 | {!collapsed &&
{content} }
52 |
53 | )
54 | case 'answer':
55 | return (
56 |
57 |
58 |
59 |
60 | {onRetry && (
61 |
62 |
63 |
64 | )}
65 | {descName && (
66 | content.replace(/\n $/, '')} size={14} />
67 | )}
68 | {descName && content} size={14} />}
69 | {!collapsed ? (
70 | setCollapsed(true)}
74 | >
75 |
76 |
77 | ) : (
78 | setCollapsed(false)}
82 | >
83 |
84 |
85 | )}
86 |
87 |
88 | {!collapsed &&
{content} }
89 |
90 | )
91 | case 'error':
92 | return (
93 |
94 |
95 |
{t('Error')}:
96 |
97 | {onRetry && (
98 |
99 |
100 |
101 | )}
102 | content.replace(/\n $/, '')} size={14} />
103 | {!collapsed ? (
104 | setCollapsed(true)}
108 | >
109 |
110 |
111 | ) : (
112 | setCollapsed(false)}
116 | >
117 |
118 |
119 | )}
120 |
121 |
122 | {!collapsed &&
{content} }
123 |
124 | )
125 | }
126 | }
127 |
128 | ConversationItem.propTypes = {
129 | type: PropTypes.oneOf(['question', 'answer', 'error']).isRequired,
130 | content: PropTypes.string.isRequired,
131 | descName: PropTypes.string,
132 | onRetry: PropTypes.func,
133 | }
134 |
135 | export default memo(ConversationItem)
136 |
--------------------------------------------------------------------------------
/src/components/CopyButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { CheckIcon, CopyIcon } from '@primer/octicons-react'
3 | import PropTypes from 'prop-types'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | CopyButton.propTypes = {
7 | contentFn: PropTypes.func.isRequired,
8 | size: PropTypes.number.isRequired,
9 | className: PropTypes.string,
10 | }
11 |
12 | function CopyButton({ className, contentFn, size }) {
13 | const { t } = useTranslation()
14 | const [copied, setCopied] = useState(false)
15 |
16 | const onClick = () => {
17 | navigator.clipboard
18 | .writeText(contentFn())
19 | .then(() => setCopied(true))
20 | .then(() =>
21 | setTimeout(() => {
22 | setCopied(false)
23 | }, 600),
24 | )
25 | }
26 |
27 | return (
28 |
33 | {copied ? : }
34 |
35 | )
36 | }
37 |
38 | export default CopyButton
39 |
--------------------------------------------------------------------------------
/src/components/DecisionCard/index.jsx:
--------------------------------------------------------------------------------
1 | import { LightBulbIcon, SearchIcon } from '@primer/octicons-react'
2 | import { useState, useEffect } from 'react'
3 | import PropTypes from 'prop-types'
4 | import ConversationCard from '../ConversationCard'
5 | import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils'
6 | import { useTranslation } from 'react-i18next'
7 | import { useConfig } from '../../hooks/use-config.mjs'
8 |
9 | function DecisionCard(props) {
10 | const { t } = useTranslation()
11 | const [triggered, setTriggered] = useState(false)
12 | const [render, setRender] = useState(false)
13 | const config = useConfig(() => {
14 | setRender(true)
15 | })
16 |
17 | const question = props.question
18 |
19 | const updatePosition = () => {
20 | if (!render) return
21 |
22 | const container = props.container
23 | const siteConfig = props.siteConfig
24 | container.classList.remove('chatgptbox-sidebar-free')
25 |
26 | if (config.appendQuery) {
27 | const appendContainer = getPossibleElementByQuerySelector([config.appendQuery])
28 | if (appendContainer) {
29 | appendContainer.appendChild(container)
30 | return
31 | }
32 | }
33 |
34 | if (config.prependQuery) {
35 | const prependContainer = getPossibleElementByQuerySelector([config.prependQuery])
36 | if (prependContainer) {
37 | prependContainer.prepend(container)
38 | return
39 | }
40 | }
41 |
42 | if (!siteConfig) return
43 |
44 | if (config.insertAtTop) {
45 | const resultsContainerQuery = getPossibleElementByQuerySelector(
46 | siteConfig.resultsContainerQuery,
47 | )
48 | if (resultsContainerQuery) resultsContainerQuery.prepend(container)
49 | } else {
50 | const sidebarContainer = getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery)
51 | if (sidebarContainer) {
52 | sidebarContainer.prepend(container)
53 | } else {
54 | const appendContainer = getPossibleElementByQuerySelector(siteConfig.appendContainerQuery)
55 | if (appendContainer) {
56 | container.classList.add('chatgptbox-sidebar-free')
57 | appendContainer.appendChild(container)
58 | } else {
59 | const resultsContainerQuery = getPossibleElementByQuerySelector(
60 | siteConfig.resultsContainerQuery,
61 | )
62 | if (resultsContainerQuery) resultsContainerQuery.prepend(container)
63 | }
64 | }
65 | }
66 | }
67 |
68 | useEffect(() => updatePosition(), [config])
69 |
70 | return (
71 | render && (
72 |
73 | {(() => {
74 | if (question)
75 | switch (config.triggerMode) {
76 | case 'always':
77 | return
78 | case 'manually':
79 | if (triggered) {
80 | return
81 | }
82 | return (
83 |
setTriggered(true)}>
84 |
85 | {t('Ask ChatGPT')}
86 |
87 |
88 | )
89 | case 'questionMark':
90 | if (endsWithQuestionMark(question.trim())) {
91 | return
92 | }
93 | if (triggered) {
94 | return
95 | }
96 | return (
97 |
setTriggered(true)}>
98 |
99 | {t('Ask ChatGPT')}
100 |
101 |
102 | )
103 | }
104 | else
105 | return (
106 |
107 |
108 | {t('No Input Found')}
109 |
110 |
111 | )
112 | })()}
113 |
114 | )
115 | )
116 | }
117 |
118 | DecisionCard.propTypes = {
119 | session: PropTypes.object.isRequired,
120 | question: PropTypes.string.isRequired,
121 | siteConfig: PropTypes.object.isRequired,
122 | container: PropTypes.object.isRequired,
123 | }
124 |
125 | export default DecisionCard
126 |
--------------------------------------------------------------------------------
/src/components/DeleteButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useTranslation } from 'react-i18next'
4 | import { TrashIcon } from '@primer/octicons-react'
5 |
6 | DeleteButton.propTypes = {
7 | onConfirm: PropTypes.func.isRequired,
8 | size: PropTypes.number.isRequired,
9 | text: PropTypes.string.isRequired,
10 | }
11 |
12 | function DeleteButton({ onConfirm, size, text }) {
13 | const { t } = useTranslation()
14 | const [waitConfirm, setWaitConfirm] = useState(false)
15 | const confirmRef = useRef(null)
16 |
17 | useEffect(() => {
18 | if (waitConfirm) confirmRef.current.focus()
19 | }, [waitConfirm])
20 |
21 | return (
22 |
23 | {
32 | e.preventDefault()
33 | e.stopPropagation()
34 | }}
35 | onBlur={() => {
36 | setWaitConfirm(false)
37 | }}
38 | onClick={() => {
39 | setWaitConfirm(false)
40 | onConfirm()
41 | }}
42 | >
43 | {t('Confirm')}
44 |
45 | {
50 | setWaitConfirm(true)
51 | }}
52 | >
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default DeleteButton
60 |
--------------------------------------------------------------------------------
/src/components/FeedbackForChatGPTWeb/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { memo, useCallback, useState } from 'react'
3 | import { ThumbsupIcon, ThumbsdownIcon } from '@primer/octicons-react'
4 | import Browser from 'webextension-polyfill'
5 | import { useTranslation } from 'react-i18next'
6 |
7 | const FeedbackForChatGPTWeb = (props) => {
8 | const { t } = useTranslation()
9 | const [action, setAction] = useState(null)
10 |
11 | const clickThumbsUp = useCallback(async () => {
12 | if (action) {
13 | return
14 | }
15 | setAction('thumbsUp')
16 | await Browser.runtime.sendMessage({
17 | type: 'FEEDBACK',
18 | data: {
19 | conversation_id: props.conversationId,
20 | message_id: props.messageId,
21 | rating: 'thumbsUp',
22 | },
23 | })
24 | }, [props, action])
25 |
26 | const clickThumbsDown = useCallback(async () => {
27 | if (action) {
28 | return
29 | }
30 | setAction('thumbsDown')
31 | await Browser.runtime.sendMessage({
32 | type: 'FEEDBACK',
33 | data: {
34 | conversation_id: props.conversationId,
35 | message_id: props.messageId,
36 | rating: 'thumbsDown',
37 | text: '',
38 | tags: [],
39 | },
40 | })
41 | }, [props, action])
42 |
43 | return (
44 |
45 |
49 |
50 |
51 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | FeedbackForChatGPTWeb.propTypes = {
64 | messageId: PropTypes.string.isRequired,
65 | conversationId: PropTypes.string.isRequired,
66 | }
67 |
68 | export default memo(FeedbackForChatGPTWeb)
69 |
--------------------------------------------------------------------------------
/src/components/InputBox/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { isFirefox, isMobile, isSafari, updateRefHeight } from '../../utils'
4 | import { useTranslation } from 'react-i18next'
5 | import { getUserConfig } from '../../config/index.mjs'
6 |
7 | export function InputBox({ onSubmit, enabled, postMessage, reverseResizeDir }) {
8 | const { t } = useTranslation()
9 | const [value, setValue] = useState('')
10 | const reverseDivRef = useRef(null)
11 | const inputRef = useRef(null)
12 | const resizedRef = useRef(false)
13 | const [internalReverseResizeDir, setInternalReverseResizeDir] = useState(reverseResizeDir)
14 |
15 | useEffect(() => {
16 | setInternalReverseResizeDir(
17 | !isSafari() && !isFirefox() && !isMobile() ? internalReverseResizeDir : false,
18 | )
19 | }, [])
20 |
21 | const virtualInputRef = internalReverseResizeDir ? reverseDivRef : inputRef
22 |
23 | useEffect(() => {
24 | inputRef.current.focus()
25 |
26 | const onResizeY = () => {
27 | if (virtualInputRef.current.h !== virtualInputRef.current.offsetHeight) {
28 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
29 | if (!resizedRef.current) {
30 | resizedRef.current = true
31 | virtualInputRef.current.style.maxHeight = ''
32 | }
33 | }
34 | }
35 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
36 | virtualInputRef.current.addEventListener('mousemove', onResizeY)
37 | }, [])
38 |
39 | useEffect(() => {
40 | if (!resizedRef.current) {
41 | if (!internalReverseResizeDir) {
42 | updateRefHeight(inputRef)
43 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
44 | virtualInputRef.current.style.maxHeight = '160px'
45 | }
46 | }
47 | })
48 |
49 | useEffect(() => {
50 | if (enabled)
51 | getUserConfig().then((config) => {
52 | if (config.focusAfterAnswer) inputRef.current.focus()
53 | })
54 | }, [enabled])
55 |
56 | const handleKeyDownOrClick = (e) => {
57 | e.stopPropagation()
58 | if (e.type === 'click' || (e.keyCode === 13 && e.shiftKey === false)) {
59 | e.preventDefault()
60 | if (enabled) {
61 | if (!value) return
62 | onSubmit(value)
63 | setValue('')
64 | } else {
65 | postMessage({ stop: true })
66 | }
67 | }
68 | }
69 |
70 | return (
71 |
72 |
83 |
103 |
110 | {enabled ? t('Ask') : t('Stop')}
111 |
112 |
113 | )
114 | }
115 |
116 | InputBox.propTypes = {
117 | onSubmit: PropTypes.func.isRequired,
118 | enabled: PropTypes.bool.isRequired,
119 | reverseResizeDir: PropTypes.bool,
120 | postMessage: PropTypes.func.isRequired,
121 | }
122 |
123 | export default InputBox
124 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/Hyperlink.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import Browser from 'webextension-polyfill'
3 |
4 | export function Hyperlink({ href, children }) {
5 | const linkProperties = {
6 | target: '_blank',
7 | style: 'color: #8ab4f8; cursor: pointer;',
8 | rel: 'nofollow noopener noreferrer',
9 | }
10 |
11 | return href.includes('chatgpt.com') ||
12 | href.includes('claude.ai') ||
13 | href.includes('kimi.moonshot.cn') ? (
14 | {
17 | const url = new URL(href)
18 | url.searchParams.set('chatgptbox_notification', 'true')
19 | Browser.runtime.sendMessage({
20 | type: 'NEW_URL',
21 | data: {
22 | url: url.toString(),
23 | pinned: false,
24 | jumpBack: true,
25 | },
26 | })
27 | }}
28 | >
29 | {children}
30 |
31 | ) : (
32 |
33 | {children}
34 |
35 | )
36 | }
37 |
38 | Hyperlink.propTypes = {
39 | href: PropTypes.string.isRequired,
40 | children: PropTypes.object.isRequired,
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/Pre.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import CopyButton from '../CopyButton'
3 | import PropTypes from 'prop-types'
4 | import { changeChildrenFontSize } from '../../utils'
5 |
6 | export function Pre({ className, children }) {
7 | const preRef = useRef(null)
8 | const [fontSize, setFontSize] = useState(14)
9 | const sizeList = [10, 12, 14, 16, 18]
10 |
11 | useEffect(() => {
12 | changeChildrenFontSize(preRef.current.childNodes[1], fontSize + 'px')
13 | })
14 |
15 | return (
16 |
17 |
18 | {
22 | setFontSize(e.target.value)
23 | }}
24 | >
25 | {Object.values(sizeList).map((size) => {
26 | return (
27 |
28 | {size}px
29 |
30 | )
31 | })}
32 |
33 | preRef.current.childNodes[1].textContent} size={14} />
34 |
35 | {children}
36 |
37 | )
38 | }
39 |
40 | Pre.propTypes = {
41 | className: PropTypes.string.isRequired,
42 | children: PropTypes.object.isRequired,
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/markdown-without-katex.jsx:
--------------------------------------------------------------------------------
1 | import ReactMarkdown from 'react-markdown'
2 | import rehypeRaw from 'rehype-raw'
3 | import rehypeHighlight from 'rehype-highlight'
4 | import remarkGfm from 'remark-gfm'
5 | import remarkBreaks from 'remark-breaks'
6 | import { Pre } from './Pre'
7 | import { Hyperlink } from './Hyperlink'
8 | import { memo } from 'react'
9 |
10 | export function MarkdownRender(props) {
11 | return (
12 |
13 |
92 | {props.children}
93 |
94 |
95 | )
96 | }
97 |
98 | MarkdownRender.propTypes = {
99 | ...ReactMarkdown.propTypes,
100 | }
101 |
102 | export default memo(MarkdownRender)
103 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/markdown.jsx:
--------------------------------------------------------------------------------
1 | import './mykatex.min.css'
2 | import ReactMarkdown from 'react-markdown'
3 | import rehypeRaw from 'rehype-raw'
4 | import rehypeHighlight from 'rehype-highlight'
5 | import rehypeKatex from 'rehype-katex'
6 | import remarkMath from 'remark-math'
7 | import remarkGfm from 'remark-gfm'
8 | import remarkBreaks from 'remark-breaks'
9 | import { Pre } from './Pre'
10 | import { Hyperlink } from './Hyperlink'
11 | import { memo } from 'react'
12 |
13 | export function MarkdownRender(props) {
14 | return (
15 |
16 |
96 | {props.children}
97 |
98 |
99 | )
100 | }
101 |
102 | MarkdownRender.propTypes = {
103 | ...ReactMarkdown.propTypes,
104 | }
105 |
106 | export default memo(MarkdownRender)
107 |
--------------------------------------------------------------------------------
/src/components/ReadButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { MuteIcon, UnmuteIcon } from '@primer/octicons-react'
3 | import PropTypes from 'prop-types'
4 | import { useTranslation } from 'react-i18next'
5 | import { useConfig } from '../../hooks/use-config.mjs'
6 |
7 | ReadButton.propTypes = {
8 | contentFn: PropTypes.func.isRequired,
9 | size: PropTypes.number.isRequired,
10 | className: PropTypes.string,
11 | }
12 |
13 | const synth = window.speechSynthesis
14 |
15 | function ReadButton({ className, contentFn, size }) {
16 | const { t } = useTranslation()
17 | const [speaking, setSpeaking] = useState(false)
18 | const config = useConfig()
19 |
20 | const startSpeak = () => {
21 | synth.cancel()
22 |
23 | const text = contentFn()
24 | const utterance = new SpeechSynthesisUtterance(text)
25 | const voices = synth.getVoices()
26 |
27 | let voice
28 | if (config.preferredLanguage.includes('en') && navigator.language.includes('en'))
29 | voice = voices.find((v) => v.name.toLowerCase().includes('microsoft aria'))
30 | else if (config.preferredLanguage.includes('zh') || navigator.language.includes('zh'))
31 | voice = voices.find((v) => v.name.toLowerCase().includes('xiaoyi'))
32 | else if (config.preferredLanguage.includes('ja') || navigator.language.includes('ja'))
33 | voice = voices.find((v) => v.name.toLowerCase().includes('nanami'))
34 | if (!voice) voice = voices.find((v) => v.lang.substring(0, 2) === config.preferredLanguage)
35 | if (!voice) voice = voices.find((v) => v.lang === navigator.language)
36 |
37 | Object.assign(utterance, {
38 | rate: 1,
39 | volume: 1,
40 | onend: () => setSpeaking(false),
41 | onerror: () => setSpeaking(false),
42 | voice: voice,
43 | })
44 |
45 | synth.speak(utterance)
46 | setSpeaking(true)
47 | }
48 |
49 | const stopSpeak = () => {
50 | synth.cancel()
51 | setSpeaking(false)
52 | }
53 |
54 | return (
55 |
60 | {speaking ? : }
61 |
62 | )
63 | }
64 |
65 | export default ReadButton
66 |
--------------------------------------------------------------------------------
/src/components/WebJumpBackNotification/index.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import PropTypes from 'prop-types'
3 | import Browser from 'webextension-polyfill'
4 | import { toast, ToastContainer } from 'react-toastify'
5 | import { useEffect } from 'react'
6 | import 'react-toastify/dist/ReactToastify.css'
7 | import { useTheme } from '../../hooks/use-theme.mjs'
8 | import { getUserConfig } from '../../config/index.mjs'
9 |
10 | const WebJumpBackNotification = (props) => {
11 | const { t } = useTranslation()
12 | const [theme, config] = useTheme()
13 |
14 | const buttonStyle = {
15 | padding: '0 8px',
16 | border: '1px solid',
17 | borderRadius: '4px',
18 | whiteSpace: 'nowrap',
19 | cursor: 'pointer',
20 | color: 'inherit',
21 | backgroundColor: 'transparent',
22 | }
23 |
24 | useEffect(() => {
25 | toast(
26 |
35 |
36 | {props.chatgptMode
37 | ? t('Please keep this tab open. You can now use the web mode of ChatGPTBox')
38 | : t('You have successfully logged in for ChatGPTBox and can now return')}
39 |
40 |
41 | {props.chatgptMode && (
42 | {
45 | Browser.runtime.sendMessage({
46 | type: 'PIN_TAB',
47 | data: {
48 | saveAsChatgptConfig: true,
49 | },
50 | })
51 | }}
52 | >
53 | {t('Pin Tab')}
54 |
55 | )}
56 | {
59 | Browser.runtime.sendMessage({
60 | type: 'ACTIVATE_URL',
61 | data: {
62 | tabId: (await getUserConfig()).notificationJumpBackTabId,
63 | },
64 | })
65 | }}
66 | >
67 | {t('Go Back')}
68 |
69 |
70 |
,
71 | {
72 | toastId: 0,
73 | updateId: 0,
74 | },
75 | )
76 | }, [config.themeMode, config.preferredLanguage])
77 |
78 | return (
79 |
92 | )
93 | }
94 |
95 | WebJumpBackNotification.propTypes = {
96 | container: PropTypes.object.isRequired,
97 | chatgptMode: PropTypes.bool,
98 | }
99 |
100 | export default WebJumpBackNotification
101 |
--------------------------------------------------------------------------------
/src/components/index.mjs:
--------------------------------------------------------------------------------
1 | export * from './ConfirmButton'
2 | export * from './ConversationCard'
3 | export * from './ConversationItem'
4 | export * from './CopyButton'
5 | export * from './DecisionCard'
6 | export * from './DeleteButton'
7 | export * from './FeedbackForChatGPTWeb'
8 | export * from './FloatingToolbar'
9 | export * from './InputBox'
10 | export * from './ReadButton'
11 |
--------------------------------------------------------------------------------
/src/config/language.mjs:
--------------------------------------------------------------------------------
1 | import { languages } from 'countries-list'
2 | import { defaultConfig, getUserConfig } from './index.mjs'
3 |
4 | export const languageList = { auto: { name: 'Auto', native: 'Auto' }, ...languages }
5 | languageList.zh.name = 'Chinese (Simplified)'
6 | languageList.zh.native = '简体中文'
7 | languageList.zhHant = { ...languageList.zh }
8 | languageList.zhHant.name = 'Chinese (Traditional)'
9 | languageList.zhHant.native = '正體中文'
10 | languageList.in = {}
11 | languageList.in.name = 'Indonesia'
12 | languageList.in.native = 'Indonesia'
13 |
14 | export async function getUserLanguage() {
15 | return languageList[defaultConfig.userLanguage].name
16 | }
17 |
18 | export async function getUserLanguageNative() {
19 | return languageList[defaultConfig.userLanguage].native
20 | }
21 |
22 | export async function getPreferredLanguage() {
23 | const config = await getUserConfig()
24 | if (config.preferredLanguage === 'auto') return await getUserLanguage()
25 | return languageList[config.preferredLanguage].name
26 | }
27 |
28 | export async function getPreferredLanguageNative() {
29 | const config = await getUserConfig()
30 | if (config.preferredLanguage === 'auto') return await getUserLanguageNative()
31 | return languageList[config.preferredLanguage].native
32 | }
33 |
--------------------------------------------------------------------------------
/src/content-script/menu-tools/index.mjs:
--------------------------------------------------------------------------------
1 | import { getCoreContentText } from '../../utils/get-core-content-text'
2 | import Browser from 'webextension-polyfill'
3 | import { getUserConfig } from '../../config/index.mjs'
4 | import { openUrl } from '../../utils/open-url'
5 |
6 | export const config = {
7 | newChat: {
8 | label: 'New Chat',
9 | genPrompt: async () => {
10 | return ''
11 | },
12 | },
13 | summarizePage: {
14 | label: 'Summarize Page',
15 | genPrompt: async () => {
16 | return `The following is the text content of a web page, analyze the core content and summarize:\n${getCoreContentText()}`
17 | },
18 | },
19 | openConversationPage: {
20 | label: 'Open Conversation Page',
21 | action: async (fromBackground) => {
22 | console.debug('action is from background', fromBackground)
23 | if (fromBackground) {
24 | openUrl(Browser.runtime.getURL('IndependentPanel.html'))
25 | } else {
26 | Browser.runtime.sendMessage({
27 | type: 'OPEN_URL',
28 | data: {
29 | url: Browser.runtime.getURL('IndependentPanel.html'),
30 | },
31 | })
32 | }
33 | },
34 | },
35 | openConversationWindow: {
36 | label: 'Open Conversation Window',
37 | action: async (fromBackground) => {
38 | console.debug('action is from background', fromBackground)
39 | if (fromBackground) {
40 | const config = await getUserConfig()
41 | const url = Browser.runtime.getURL('IndependentPanel.html')
42 | const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' })
43 | if (!config.alwaysCreateNewConversationWindow && tabs.length > 0)
44 | await Browser.windows.update(tabs[0].windowId, { focused: true })
45 | else
46 | await Browser.windows.create({
47 | url: url,
48 | type: 'popup',
49 | width: 500,
50 | height: 650,
51 | })
52 | } else {
53 | Browser.runtime.sendMessage({
54 | type: 'OPEN_CHAT_WINDOW',
55 | data: {},
56 | })
57 | }
58 | },
59 | },
60 | openSidePanel: {
61 | label: 'Open Side Panel',
62 | action: async (fromBackground, tab) => {
63 | console.debug('action is from background', fromBackground)
64 | if (fromBackground) {
65 | // eslint-disable-next-line no-undef
66 | chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id })
67 | } else {
68 | // side panel is not supported
69 | }
70 | },
71 | },
72 | closeAllChats: {
73 | label: 'Close All Chats In This Page',
74 | action: async (fromBackground) => {
75 | console.debug('action is from background', fromBackground)
76 | Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
77 | Browser.tabs.sendMessage(tabs[0].id, {
78 | type: 'CLOSE_CHATS',
79 | data: {},
80 | })
81 | })
82 | },
83 | },
84 | }
85 |
--------------------------------------------------------------------------------
/src/content-script/selection-tools/index.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | CardHeading,
3 | CardList,
4 | EmojiSmile,
5 | Palette,
6 | QuestionCircle,
7 | Translate,
8 | Braces,
9 | Globe,
10 | ChatText,
11 | } from 'react-bootstrap-icons'
12 | import { getPreferredLanguage } from '../../config/language.mjs'
13 |
14 | const createGenPrompt =
15 | ({
16 | message = '',
17 | isTranslation = false,
18 | targetLanguage = '',
19 | enableBidirectional = false,
20 | includeLanguagePrefix = false,
21 | }) =>
22 | async (selection) => {
23 | let preferredLanguage = targetLanguage
24 |
25 | if (!preferredLanguage) {
26 | preferredLanguage = await getPreferredLanguage()
27 | }
28 |
29 | let fullMessage = isTranslation
30 | ? `Translate the following into ${preferredLanguage} and only show me the translated content`
31 | : message
32 | if (enableBidirectional) {
33 | fullMessage += `. If it is already in ${preferredLanguage}, translate it into English and only show me the translated content`
34 | }
35 | const prefix = includeLanguagePrefix ? `Reply in ${preferredLanguage}.` : ''
36 | return `${prefix}${fullMessage}:\n'''\n${selection}\n'''`
37 | }
38 |
39 | export const config = {
40 | explain: {
41 | icon: ,
42 | label: 'Explain',
43 | genPrompt: createGenPrompt({
44 | message: 'Explain the following',
45 | includeLanguagePrefix: true,
46 | }),
47 | },
48 | translate: {
49 | icon: ,
50 | label: 'Translate',
51 | genPrompt: createGenPrompt({
52 | isTranslation: true,
53 | }),
54 | },
55 | translateToEn: {
56 | icon: ,
57 | label: 'Translate (To English)',
58 | genPrompt: createGenPrompt({
59 | isTranslation: true,
60 | targetLanguage: 'English',
61 | }),
62 | },
63 | translateToZh: {
64 | icon: ,
65 | label: 'Translate (To Chinese)',
66 | genPrompt: createGenPrompt({
67 | isTranslation: true,
68 | targetLanguage: 'Chinese',
69 | }),
70 | },
71 | translateBidi: {
72 | icon: ,
73 | label: 'Translate (Bidirectional)',
74 | genPrompt: createGenPrompt({
75 | isTranslation: true,
76 | enableBidirectional: true,
77 | }),
78 | },
79 | summary: {
80 | icon: ,
81 | label: 'Summary',
82 | genPrompt: createGenPrompt({
83 | message: 'Summarize the following as concisely as possible',
84 | includeLanguagePrefix: true,
85 | }),
86 | },
87 | polish: {
88 | icon: ,
89 | label: 'Polish',
90 | genPrompt: createGenPrompt({
91 | message:
92 | 'Check the following content for possible diction and grammar problems, and polish it carefully',
93 | }),
94 | },
95 | sentiment: {
96 | icon: ,
97 | label: 'Sentiment Analysis',
98 | genPrompt: createGenPrompt({
99 | message:
100 | 'Analyze the sentiments expressed in the following content and make a brief summary of the sentiments',
101 | includeLanguagePrefix: true,
102 | }),
103 | },
104 | divide: {
105 | icon: ,
106 | label: 'Divide Paragraphs',
107 | genPrompt: createGenPrompt({
108 | message: 'Divide the following into paragraphs that are easy to read and understand',
109 | }),
110 | },
111 | code: {
112 | icon: ,
113 | label: 'Code Explain',
114 | genPrompt: createGenPrompt({
115 | message: 'Explain the following code',
116 | includeLanguagePrefix: true,
117 | }),
118 | },
119 | ask: {
120 | icon: ,
121 | label: 'Ask',
122 | genPrompt: createGenPrompt({
123 | message: 'Analyze the following content and express your opinion, or give your answer',
124 | includeLanguagePrefix: true,
125 | }),
126 | },
127 | }
128 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/arxiv/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('.title')?.textContent.trim()
7 | const authors = document.querySelector('.authors')?.textContent
8 | const abstract = document.querySelector('blockquote.abstract')?.textContent.trim()
9 |
10 | return await cropText(
11 | `Below is the paper abstract from a preprint site, summarize the key findings, methodology, and conclusions, especially highlight the contributions.` +
12 | `\n${title}\n${authors}\n${abstract}`,
13 | )
14 | } catch (e) {
15 | console.log(e)
16 | }
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/baidu/index.mjs:
--------------------------------------------------------------------------------
1 | import { config } from '../index'
2 |
3 | export default {
4 | init: async (hostname, userConfig, getInput, mountComponent) => {
5 | try {
6 | const targetNode = document.getElementById('wrapper_wrapper')
7 | const observer = new MutationObserver(async (records) => {
8 | if (
9 | records.some(
10 | (record) =>
11 | record.type === 'childList' &&
12 | [...record.addedNodes].some((node) => node.id === 'container'),
13 | )
14 | ) {
15 | const searchValue = await getInput(config.baidu.inputQuery)
16 | if (searchValue) {
17 | mountComponent(config.baidu)
18 | }
19 | }
20 | })
21 | observer.observe(targetNode, { childList: true })
22 | } catch (e) {
23 | /* empty */
24 | }
25 | return true
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/bilibili/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText, waitForElementToExistAndSelect } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | export default {
5 | init: async (hostname, userConfig, getInput, mountComponent) => {
6 | if (location.pathname.includes('/bangumi')) return false
7 | try {
8 | // B站页面是SSR的,如果插入过早,页面 js 检测到实际 Dom 和期望 Dom 不一致,会导致重新渲染
9 | await waitForElementToExistAndSelect('img.bili-avatar-img')
10 | const getVideoPath = () =>
11 | location.pathname + `?p=${new URLSearchParams(location.search).get('p') || 1}`
12 | let oldPath = getVideoPath()
13 | const checkPathChange = async () => {
14 | const newPath = getVideoPath()
15 | if (newPath !== oldPath) {
16 | oldPath = newPath
17 | mountComponent(config.bilibili)
18 | }
19 | }
20 | window.setInterval(checkPathChange, 500)
21 | } catch (e) {
22 | /* empty */
23 | }
24 | return true
25 | },
26 | inputQuery: async () => {
27 | try {
28 | const bvid = location.pathname.replace('video', '').replaceAll('/', '')
29 | const p = Number(new URLSearchParams(location.search).get('p') || 1) - 1
30 |
31 | const pagelistResponse = await fetch(
32 | `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`,
33 | )
34 | const pagelistData = await pagelistResponse.json()
35 | const videoList = pagelistData.data
36 | const cid = videoList[p].cid
37 | const title = videoList[p].part
38 |
39 | const infoResponse = await fetch(
40 | `https://api.bilibili.com/x/player/v2?bvid=${bvid}&cid=${cid}`,
41 | {
42 | credentials: 'include',
43 | },
44 | )
45 | const infoData = await infoResponse.json()
46 | let subtitleUrl = infoData.data.subtitle.subtitles[0].subtitle_url
47 | if (subtitleUrl.startsWith('//')) subtitleUrl = 'https:' + subtitleUrl
48 | else if (!subtitleUrl.startsWith('http')) subtitleUrl = 'https://' + subtitleUrl
49 |
50 | const subtitleResponse = await fetch(subtitleUrl)
51 | const subtitleData = await subtitleResponse.json()
52 | const subtitles = subtitleData.body
53 |
54 | let subtitleContent = ''
55 | for (let i = 0; i < subtitles.length; i++) {
56 | if (i === subtitles.length - 1) subtitleContent += subtitles[i].content
57 | else subtitleContent += subtitles[i].content + ','
58 | }
59 |
60 | return await cropText(
61 | `用尽量简练的语言,联系视频标题,对视频进行内容摘要,同时仍要保留重要细节和标题信息,如果可能的话,使用markdown语法将视频内容总结为结构化信息,视频标题为:"${title}",字幕内容为:\n${subtitleContent}`,
62 | )
63 | } catch (e) {
64 | console.log(e)
65 | }
66 | },
67 | }
68 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/brave/index.mjs:
--------------------------------------------------------------------------------
1 | import { waitForElementToExistAndSelect } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | export default {
5 | init: async (hostname, userConfig) => {
6 | const selector = userConfig.insertAtTop
7 | ? config.brave.resultsContainerQuery[0]
8 | : config.brave.sidebarContainerQuery[0]
9 | await waitForElementToExistAndSelect(selector, 5)
10 | return true
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/duckduckgo/index.mjs:
--------------------------------------------------------------------------------
1 | import { waitForElementToExistAndSelect } from '../../../utils/index.mjs'
2 | import { config } from '../index'
3 |
4 | export default {
5 | init: async (hostname, userConfig) => {
6 | if (userConfig.insertAtTop) {
7 | return !!(await waitForElementToExistAndSelect(config.duckduckgo.resultsContainerQuery[0], 5))
8 | }
9 | return true
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/followin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const author = document.querySelector('main article a > span')?.textContent
7 | const description =
8 | document.querySelector('#article-content')?.textContent ||
9 | document.querySelector('#thead-gallery')?.textContent
10 | if (author && description) {
11 | const title = document.querySelector('main article h1')?.textContent
12 | if (title) {
13 | return await cropText(
14 | `以下是一篇文章,请给出文章的结论和3到5个要点.标题是:"${title}",作者是:"${author}",内容是:\n"${description}".
15 | `,
16 | )
17 | } else {
18 | return await cropText(
19 | `以下是一篇长推文,请给出文章的结论和3到5个要点.作者是:"${author}",内容是:\n"${description}".
20 | `,
21 | )
22 | }
23 | }
24 | } catch (e) {
25 | console.log(e)
26 | }
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/github/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText, limitedFetch } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | const getPatchUrl = async () => {
5 | const patchUrl = location.origin + location.pathname + '.patch'
6 | const response = await fetch(patchUrl, { method: 'HEAD' }).catch(() => ({}))
7 | if (response.ok) return patchUrl
8 | return ''
9 | }
10 |
11 | const getPatchData = async (patchUrl) => {
12 | if (!patchUrl) return
13 |
14 | let patchData = await limitedFetch(patchUrl, 1024 * 40)
15 | patchData = patchData.substring(patchData.indexOf('---'))
16 | return patchData
17 | }
18 |
19 | const isPull = () => {
20 | return location.href.match(/\/pull\/\d+$/)
21 | }
22 |
23 | const isIssue = () => {
24 | return location.href.match(/\/issues\/\d+$/)
25 | }
26 |
27 | function parseGitHubIssueData() {
28 | // Function to parse a single comment
29 | function parseComment(commentElement) {
30 | // Parse the date
31 | const dateElement = commentElement.querySelector('relative-time')
32 | const date = dateElement.getAttribute('datetime')
33 |
34 | // Parse the author
35 | const authorElement =
36 | commentElement.querySelector('.author') || commentElement.querySelector('.author-name')
37 | const author = authorElement.textContent.trim()
38 |
39 | // Parse the body
40 | const bodyElement = commentElement.querySelector('.comment-body')
41 | const body = bodyElement.textContent.trim()
42 |
43 | return { date, author, body }
44 | }
45 |
46 | // Function to parse all messages on the page
47 | function parseAllMessages() {
48 | // Find all comment containers
49 | const commentElements = document.querySelectorAll('.timeline-comment-group')
50 | const messages = Array.from(commentElements).map(parseComment)
51 |
52 | // The initial post is not a ".timeline-comment-group", so we need to handle it separately
53 | const initialPostElement = document.querySelector('.js-comment-container')
54 | const initialPost = parseComment(initialPostElement)
55 |
56 | // Combine the initial post with the rest of the comments
57 | return [initialPost, ...messages]
58 | }
59 |
60 | // Function to get the content of the comment input box
61 | function getCommentInputContent() {
62 | const commentInput = document.querySelector('.js-new-comment-form textarea')
63 | return commentInput ? commentInput.value : ''
64 | }
65 |
66 | // Get the issue title
67 | const title = document.querySelector('.js-issue-title').textContent.trim()
68 |
69 | // Get all messages
70 | const messages = parseAllMessages()
71 |
72 | // Get the content of the new comment box
73 | const commentBoxContent = getCommentInputContent()
74 |
75 | // Return an object with both results
76 | return {
77 | title: title,
78 | messages: messages,
79 | commentBoxContent: commentBoxContent,
80 | }
81 | }
82 |
83 | function createChatGPtSummaryPrompt(issueData, isIssue = true) {
84 | // Destructure the issueData object into messages and commentBoxContent
85 | const { title, messages, commentBoxContent } = issueData
86 |
87 | // Start crafting the prompt
88 | let prompt = ''
89 |
90 | if (isIssue) {
91 | prompt =
92 | 'Please summarize the following GitHub issue thread.\nWhat is the main issue and key points discussed in this thread?\n\n'
93 | } else {
94 | prompt =
95 | 'Please summarize the following GitHub pull request thread.\nWhat is the main issue this pull request is trying to solve?\n\n'
96 | }
97 |
98 | prompt += '---\n\n'
99 |
100 | prompt += `Title:\n${title}\n\n`
101 |
102 | // Add each message to the prompt
103 | messages.forEach((message, index) => {
104 | prompt += `Message ${index + 1} by ${message.author} on ${message.date}:\n${message.body}\n\n`
105 | })
106 |
107 | // If there's content in the comment box, add it as a draft message
108 | if (commentBoxContent) {
109 | prompt += '---\n\n'
110 | prompt += `Draft message in comment box:\n${commentBoxContent}\n\n`
111 | }
112 |
113 | // Add a request for summary at the end of the prompt
114 | // prompt += 'What is the main issue and key points discussed in this thread?'
115 |
116 | return prompt
117 | }
118 |
119 | export default {
120 | init: async (hostname, userConfig, getInput, mountComponent) => {
121 | try {
122 | let oldUrl = location.href
123 | const checkUrlChange = async () => {
124 | if (location.href !== oldUrl) {
125 | oldUrl = location.href
126 | if (isPull() || isIssue()) {
127 | mountComponent(config.github)
128 | return
129 | }
130 |
131 | const patchUrl = await getPatchUrl()
132 | if (patchUrl) {
133 | mountComponent(config.github)
134 | }
135 | }
136 | }
137 | window.setInterval(checkUrlChange, 500)
138 | } catch (e) {
139 | /* empty */
140 | }
141 | return (await getPatchUrl()) || isPull() || isIssue()
142 | },
143 | inputQuery: async () => {
144 | try {
145 | if (isPull() || isIssue()) {
146 | const issueData = parseGitHubIssueData()
147 | const summaryPrompt = createChatGPtSummaryPrompt(issueData, isIssue())
148 |
149 | return await cropText(summaryPrompt)
150 | }
151 | const patchUrl = await getPatchUrl()
152 | const patchData = await getPatchData(patchUrl)
153 | if (!patchData) return
154 |
155 | return await cropText(
156 | `Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +
157 | `The patch contents of this commit are as follows:\n${patchData}`,
158 | )
159 | } catch (e) {
160 | console.log(e)
161 | }
162 | },
163 | }
164 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/gitlab/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText, limitedFetch } from '../../../utils'
2 |
3 | const getPatchUrl = async () => {
4 | const patchUrl = location.origin + location.pathname + '.patch'
5 | const response = await fetch(patchUrl, { method: 'HEAD' })
6 | if (response.ok) return patchUrl
7 | return ''
8 | }
9 |
10 | const getPatchData = async (patchUrl) => {
11 | if (!patchUrl) return
12 |
13 | let patchData = await limitedFetch(patchUrl, 1024 * 40)
14 | patchData = patchData.substring(patchData.indexOf('---'))
15 | return patchData
16 | }
17 |
18 | export default {
19 | inputQuery: async () => {
20 | try {
21 | if (location.pathname.includes('/blob')) {
22 | const fileData = await limitedFetch(location.href.replace('/blob/', '/raw/'), 1024 * 40)
23 | if (!fileData) return
24 |
25 | return await cropText(
26 | `Analyze the following file content and explain it. Use markdown syntax to make your answer more readable, such as code blocks, bold, list:` +
27 | `\n\`\`\`\n${fileData}\n\`\`\``,
28 | )
29 | } else {
30 | const patchUrl = await getPatchUrl()
31 | const patchData = await getPatchData(patchUrl)
32 | if (!patchData) return
33 |
34 | return await cropText(
35 | `Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +
36 | `The patch contents of this commit are as follows:\n${patchData}`,
37 | )
38 | }
39 | } catch (e) {
40 | console.log(e)
41 | }
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/juejin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector(
7 | '#juejin > div.view-container > main > div > div.main-area.article-area > article > h1',
8 | )?.textContent
9 | const description = document.querySelector(
10 | '#juejin > div.view-container > main > div > div.main-area.article-area > article > div.article-content',
11 | )?.textContent
12 | if (title && description) {
13 | const author = document.querySelector(
14 | '#juejin > div.view-container > main > div > div.main-area.article-area > article > div.author-info-block > div > div.author-name > a > span.name',
15 | )?.textContent
16 | const comments = document.querySelectorAll(
17 | 'div.content-box > div.comment-main > div.content',
18 | )
19 | let comment = ''
20 | for (let i = 1; i <= comments.length && i <= 4; i++) {
21 | comment += `answer${i}: ${comment[i - 1].textContent}|`
22 | }
23 | return await cropText(
24 | `以下是一篇文章,标题是:"${title}",作者是:"${author}",内容是:\n"${description}".各个评论如下:\n${comment}.请以如下格式输出你的回答:
25 | {文章摘要和文章作者}
26 | ======
27 | {文章总结和对文章的看法}
28 | ======
29 | {对评论的总结}
30 | `,
31 | )
32 | }
33 | } catch (e) {
34 | console.log(e)
35 | }
36 | },
37 | }
38 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/quora/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | if (location.pathname === '/') return
7 |
8 | const texts = document.querySelectorAll('.q-box.qu-userSelect--text')
9 | let title
10 | if (texts.length > 0) title = texts[0].textContent
11 | let answers = ''
12 | if (texts.length > 1)
13 | for (let i = 1; i < texts.length; i++) {
14 | answers += `answer${i}:${texts[i].textContent}|`
15 | }
16 |
17 | return await cropText(
18 | `Below is the content from a question and answer platform,giving the corresponding summary and your opinion on it.` +
19 | `The question is:'${title}',` +
20 | `Some answers are as follows:\n${answers}`,
21 | )
22 | } catch (e) {
23 | console.log(e)
24 | }
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/reddit/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('[id*="post-title"]')?.textContent
7 | const description = document.querySelector(
8 | 'shreddit-post > div.text-neutral-content',
9 | )?.textContent
10 | const texts = document.querySelectorAll('shreddit-comment div.md')
11 | let answers = ''
12 | for (let i = 0; i < texts.length; i++) {
13 | answers += `answer${i}:${texts[i].textContent}|`
14 | }
15 |
16 | return await cropText(
17 | `Below is the content from a social forum,giving the corresponding summary and your opinion on it.` +
18 | `The title is:'${title}',and the further description of the title is:'${description}'.` +
19 | `Some answers are as follows:\n${answers}`,
20 | )
21 | } catch (e) {
22 | console.log(e)
23 | }
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/stackoverflow/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('#question-header .question-hyperlink')?.textContent
7 | if (title) {
8 | const description = document.querySelector('.postcell .s-prose')?.textContent
9 | let answer = ''
10 | const answers = document.querySelectorAll('.answercell .s-prose')
11 | if (answers.length > 0)
12 | for (let i = 1; i <= answers.length && i <= 2; i++) {
13 | answer += `answer${i}: ${answers[i - 1].textContent}|`
14 | }
15 |
16 | return await cropText(
17 | `Below is the content from a developer Q&A platform. Analyze answers and provide a brief solution that can solve the question first,` +
18 | `then give an overview of all answers. The question is: "${title}", and the further description of the question is: "${description}".` +
19 | `The answers are as follows:\n${answer}`,
20 | )
21 | }
22 | } catch (e) {
23 | console.log(e)
24 | }
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/weixin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('#activity-name')?.textContent
7 | const description = document.querySelector('#js_content')?.textContent
8 | if (title && description) {
9 | const author = document.querySelector('#js_name')?.textContent
10 |
11 | const sidebar = document.querySelector('.qr_code_pc')
12 | if (sidebar) {
13 | sidebar.style.right = '-400px'
14 | sidebar.style.width = '400px'
15 | sidebar.style.textAlign = 'left'
16 | sidebar.style.alignItems = 'center'
17 | sidebar.style.display = 'flex'
18 | sidebar.style.flexDirection = 'column'
19 | sidebar.style.background = 'transparent'
20 | }
21 |
22 | return await cropText(
23 | `以下是一篇文章,标题是:"${title}",文章来源是:"${author}公众号",内容是:\n"${description}".请以如下格式输出你的回答:
24 | {文章来源和文章摘要}
25 | ======
26 | {文章总结}
27 | ======
28 | {对文章的看法}
29 | `,
30 | )
31 | }
32 | } catch (e) {
33 | console.log(e)
34 | }
35 | },
36 | }
37 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/youtube/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | // This function was written by ChatGPT and modified by iamsirsammy
5 | function replaceHtmlEntities(htmlString) {
6 | const doc = new DOMParser().parseFromString(htmlString.replaceAll('&', '&'), 'text/html')
7 | return doc.documentElement.innerText
8 | }
9 |
10 | export default {
11 | init: async (hostname, userConfig, getInput, mountComponent) => {
12 | try {
13 | let oldUrl = location.href
14 | const checkUrlChange = async () => {
15 | if (location.href !== oldUrl) {
16 | oldUrl = location.href
17 | mountComponent(config.youtube)
18 | }
19 | }
20 | window.setInterval(checkUrlChange, 500)
21 | } catch (e) {
22 | /* empty */
23 | }
24 | return true
25 | },
26 | inputQuery: async () => {
27 | try {
28 | const docText = await (
29 | await fetch(location.href, {
30 | credentials: 'include',
31 | })
32 | ).text()
33 |
34 | const subtitleUrlStartAt = docText.indexOf('https://www.youtube.com/api/timedtext')
35 | if (subtitleUrlStartAt === -1) return
36 |
37 | let subtitleUrl = docText.substring(subtitleUrlStartAt)
38 | subtitleUrl = subtitleUrl.substring(0, subtitleUrl.indexOf('"'))
39 | subtitleUrl = subtitleUrl.replaceAll('\\u0026', '&')
40 |
41 | let title = docText.substring(docText.indexOf('"title":"') + '"title":"'.length)
42 | title = title.substring(0, title.indexOf('","'))
43 |
44 | const subtitleResponse = await fetch(subtitleUrl)
45 | if (!subtitleResponse.ok) return
46 | let subtitleData = await subtitleResponse.text()
47 |
48 | let subtitleContent = ''
49 | while (subtitleData.indexOf('">') !== -1) {
50 | subtitleData = subtitleData.substring(subtitleData.indexOf('">') + 2)
51 | subtitleContent += subtitleData.substring(0, subtitleData.indexOf('<')) + ','
52 | }
53 |
54 | subtitleContent = replaceHtmlEntities(subtitleContent)
55 |
56 | return await cropText(
57 | `Provide a structured summary of the following video in markdown format, focusing on key takeaways and crucial information, and ensuring to include the video title. The summary should be easy to read and concise, yet comprehensive.` +
58 | `The video title is "${title}". The subtitle content is as follows:\n${subtitleContent}`,
59 | )
60 | } catch (e) {
61 | console.log(e)
62 | }
63 | },
64 | }
65 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/zhihu/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('.QuestionHeader-title')?.textContent
7 | if (title) {
8 | const description = document.querySelector('.QuestionRichText')?.textContent
9 | const answerQuery = '.AnswerItem .RichText'
10 |
11 | let answer = ''
12 | if (location.pathname.includes('answer')) {
13 | answer = document.querySelector(answerQuery)?.textContent
14 | return await cropText(
15 | `以下是一个问答平台的提问与回答内容,给出相应的摘要,以及你对此的看法.问题是:"${title}",问题的进一步描述是:"${description}".` +
16 | `其中一个回答如下:\n${answer}`,
17 | )
18 | } else {
19 | const answers = document.querySelectorAll(answerQuery)
20 | for (let i = 1; i <= answers.length && i <= 4; i++) {
21 | answer += `answer${i}: ${answers[i - 1].textContent}|`
22 | }
23 | return await cropText(
24 | `以下是一个问答平台的提问与回答内容,给出相应的摘要,以及你对此的看法.问题是:"${title}",问题的进一步描述是:"${description}".` +
25 | `各个回答如下:\n${answer}`,
26 | )
27 | }
28 | } else {
29 | const title = document.querySelector('.Post-Title')?.textContent
30 | const description = document.querySelector('.Post-RichText')?.textContent
31 |
32 | if (title) {
33 | return await cropText(
34 | `以下是一篇文章,给出相应的摘要,以及你对此的看法.标题是:"${title}",内容是:\n"${description}"`,
35 | )
36 | }
37 | }
38 | } catch (e) {
39 | console.log(e)
40 | }
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2
--------------------------------------------------------------------------------
/src/fonts/styles.css:
--------------------------------------------------------------------------------
1 | /* arabic */
2 | @font-face {
3 | font-family: 'Cairo';
4 | font-style: normal;
5 | font-weight: 400;
6 | font-display: swap;
7 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2) format('woff2');
8 | unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
9 | }
10 | /* latin-ext */
11 | @font-face {
12 | font-family: 'Cairo';
13 | font-style: normal;
14 | font-weight: 400;
15 | font-display: swap;
16 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2) format('woff2');
17 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
18 | U+2C60-2C7F, U+A720-A7FF;
19 | }
20 | /* latin */
21 | @font-face {
22 | font-family: 'Cairo';
23 | font-style: normal;
24 | font-weight: 400;
25 | font-display: swap;
26 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2) format('woff2');
27 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
28 | U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
29 | }
30 | /* arabic */
31 | @font-face {
32 | font-family: 'Cairo';
33 | font-style: normal;
34 | font-weight: 700;
35 | font-display: swap;
36 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2) format('woff2');
37 | unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
38 | }
39 | /* latin-ext */
40 | @font-face {
41 | font-family: 'Cairo';
42 | font-style: normal;
43 | font-weight: 700;
44 | font-display: swap;
45 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2) format('woff2');
46 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
47 | U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Cairo';
52 | font-style: normal;
53 | font-weight: 700;
54 | font-display: swap;
55 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2) format('woff2');
56 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
57 | U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
58 | }
59 |
--------------------------------------------------------------------------------
/src/hooks/use-clamp-window-size.mjs:
--------------------------------------------------------------------------------
1 | import { useWindowSize } from './use-window-size.mjs'
2 |
3 | export function useClampWindowSize(widthRange = [0, Infinity], heightRange = [0, Infinity]) {
4 | const windowSize = useWindowSize()
5 | windowSize[0] = Math.min(widthRange[1], Math.max(windowSize[0], widthRange[0]))
6 | windowSize[1] = Math.min(heightRange[1], Math.max(windowSize[1], heightRange[0]))
7 | return windowSize
8 | }
9 |
--------------------------------------------------------------------------------
/src/hooks/use-config.mjs:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { defaultConfig, getUserConfig } from '../config/index.mjs'
3 | import Browser from 'webextension-polyfill'
4 |
5 | export function useConfig(initFn, ignoreSession = true) {
6 | const [config, setConfig] = useState(defaultConfig)
7 | useEffect(() => {
8 | getUserConfig().then((config) => {
9 | setConfig(config)
10 | if (initFn) initFn()
11 | })
12 | }, [])
13 | useEffect(() => {
14 | const listener = (changes) => {
15 | if (ignoreSession) if (Object.keys(changes).length === 1 && 'sessions' in changes) return
16 |
17 | const changedItems = Object.keys(changes)
18 | let newConfig = {}
19 | for (const key of changedItems) {
20 | newConfig[key] = changes[key].newValue
21 | }
22 | setConfig({ ...config, ...newConfig })
23 | }
24 | Browser.storage.local.onChanged.addListener(listener)
25 | return () => {
26 | Browser.storage.local.onChanged.removeListener(listener)
27 | }
28 | }, [config])
29 | return config
30 | }
31 |
--------------------------------------------------------------------------------
/src/hooks/use-theme.mjs:
--------------------------------------------------------------------------------
1 | import { useConfig } from './use-config.mjs'
2 | import { useWindowTheme } from './use-window-theme.mjs'
3 |
4 | export function useTheme() {
5 | const config = useConfig()
6 | const theme = useWindowTheme()
7 | return [config.themeMode === 'auto' ? theme : config.themeMode, config]
8 | }
9 |
--------------------------------------------------------------------------------
/src/hooks/use-window-size.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
2 |
3 | import { useLayoutEffect, useState } from 'react'
4 |
5 | export function useWindowSize() {
6 | const [size, setSize] = useState([0, 0])
7 | useLayoutEffect(() => {
8 | function updateSize() {
9 | setSize([window.innerWidth, window.innerHeight])
10 | }
11 | window.addEventListener('resize', updateSize)
12 | updateSize()
13 | return () => window.removeEventListener('resize', updateSize)
14 | }, [])
15 | return size
16 | }
17 |
--------------------------------------------------------------------------------
/src/hooks/use-window-theme.mjs:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export function useWindowTheme() {
4 | const [theme, setTheme] = useState(
5 | window.matchMedia
6 | ? window.matchMedia('(prefers-color-scheme: dark)').matches
7 | ? 'dark'
8 | : 'light'
9 | : 'light',
10 | )
11 | useEffect(() => {
12 | if (!window.matchMedia) return
13 | const listener = (e) => {
14 | setTheme(e.matches ? 'dark' : 'light')
15 | }
16 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener)
17 | return () =>
18 | window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', listener)
19 | }, [])
20 | return theme
21 | }
22 |
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josStorer/chatGPTBox/86940f6d88dc91337a5234462e365eebf8d494cd/src/logo.png
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ChatGPTBox",
3 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4 | "version": "2.5.8",
5 | "manifest_version": 3,
6 | "icons": {
7 | "16": "logo.png",
8 | "32": "logo.png",
9 | "48": "logo.png",
10 | "128": "logo.png"
11 | },
12 | "host_permissions": [
13 | "https://*.chatgpt.com/*",
14 | "https://*.openai.com/*",
15 | "https://*.bing.com/*",
16 | "https://*.poe.com/*",
17 | "https://*.google.com/*",
18 | "https://claude.ai/*",
19 | "https://*.moonshot.cn/*",
20 | ""
21 | ],
22 | "permissions": [
23 | "cookies",
24 | "storage",
25 | "contextMenus",
26 | "unlimitedStorage",
27 | "tabs",
28 | "webRequest",
29 | "declarativeNetRequestWithHostAccess",
30 | "sidePanel"
31 | ],
32 | "optional_permissions": [
33 | "background"
34 | ],
35 | "background": {
36 | "service_worker": "background.js"
37 | },
38 | "action": {
39 | "default_popup": "popup.html"
40 | },
41 | "side_panel": {
42 | "default_path": "IndependentPanel.html"
43 | },
44 | "declarative_net_request": {
45 | "rule_resources": [
46 | {
47 | "id": "ruleset",
48 | "enabled": true,
49 | "path": "rules.json"
50 | }
51 | ]
52 | },
53 | "options_ui": {
54 | "page": "popup.html",
55 | "open_in_tab": true
56 | },
57 | "content_scripts": [
58 | {
59 | "matches": [
60 | "https://*/*",
61 | "http://*/*",
62 | "file://*/*"
63 | ],
64 | "js": [
65 | "shared.js",
66 | "content-script.js"
67 | ],
68 | "css": [
69 | "content-script.css"
70 | ]
71 | }
72 | ],
73 | "web_accessible_resources": [
74 | {
75 | "resources": [
76 | "logo.png"
77 | ],
78 | "matches": [
79 | ""
80 | ]
81 | }
82 | ],
83 | "commands": {
84 | "newChat": {
85 | "suggested_key": {
86 | "default": "Ctrl+B",
87 | "mac": "MacCtrl+B"
88 | },
89 | "description": "Create a new chat"
90 | },
91 | "summarizePage": {
92 | "suggested_key": {
93 | "default": "Alt+B",
94 | "mac": "Alt+B"
95 | },
96 | "description": "Summarize this page"
97 | },
98 | "openConversationPage": {
99 | "suggested_key": {
100 | "default": "Ctrl+Shift+H",
101 | "mac": "MacCtrl+Shift+H"
102 | },
103 | "description": "Open the independent conversation page"
104 | },
105 | "openConversationWindow": {
106 | "description": "Open the independent conversation window"
107 | },
108 | "openSidePanel": {
109 | "description": "Open the independent conversation side panel"
110 | },
111 | "closeAllChats": {
112 | "description": "Close all chats in this page"
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/src/manifest.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ChatGPTBox",
3 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4 | "version": "2.5.8",
5 | "manifest_version": 2,
6 | "icons": {
7 | "16": "logo.png",
8 | "32": "logo.png",
9 | "48": "logo.png",
10 | "128": "logo.png"
11 | },
12 | "permissions": [
13 | "cookies",
14 | "storage",
15 | "contextMenus",
16 | "unlimitedStorage",
17 | "tabs",
18 | "webRequest",
19 | "https://*.chatgpt.com/*",
20 | "https://*.openai.com/",
21 | "https://*.bing.com/",
22 | "wss://*.bing.com/*",
23 | "https://*.poe.com/",
24 | "https://*.google.com/",
25 | "https://claude.ai/",
26 | "https://*.moonshot.cn/*",
27 | ""
28 | ],
29 | "background": {
30 | "scripts": [
31 | "background.js"
32 | ],
33 | "persistent": true
34 | },
35 | "browser_action": {
36 | "default_popup": "popup.html?popup=true"
37 | },
38 | "options_ui": {
39 | "page": "popup.html",
40 | "open_in_tab": true
41 | },
42 | "content_scripts": [
43 | {
44 | "matches": [
45 | "https://*/*",
46 | "http://*/*",
47 | "file://*/*"
48 | ],
49 | "js": [
50 | "shared.js",
51 | "content-script.js"
52 | ],
53 | "css": [
54 | "content-script.css"
55 | ]
56 | }
57 | ],
58 | "web_accessible_resources": [
59 | "logo.png"
60 | ],
61 | "commands": {
62 | "newChat": {
63 | "suggested_key": {
64 | "default": "Ctrl+B",
65 | "mac": "MacCtrl+X"
66 | },
67 | "description": "Create a new chat"
68 | },
69 | "summarizePage": {
70 | "suggested_key": {
71 | "default": "Alt+B",
72 | "mac": "Alt+B"
73 | },
74 | "description": "Summarize this page"
75 | },
76 | "openConversationPage": {
77 | "suggested_key": {
78 | "default": "Ctrl+Shift+H",
79 | "mac": "MacCtrl+Shift+H"
80 | },
81 | "description": "Open the independent conversation page"
82 | },
83 | "openConversationWindow": {
84 | "description": "Open the independent conversation window"
85 | },
86 | "closeAllChats": {
87 | "description": "Close all chats in this page"
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPTBox
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/index.jsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import '../../_locales/i18n-react'
3 | import App from './App'
4 | import Browser from 'webextension-polyfill'
5 | import { changeLanguage } from 'i18next'
6 | import { getPreferredLanguageKey } from '../../config/index.mjs'
7 |
8 | document.body.style.margin = 0
9 | document.body.style.overflow = 'hidden'
10 | getPreferredLanguageKey().then((lang) => {
11 | changeLanguage(lang)
12 | })
13 | Browser.runtime.onMessage.addListener(async (message) => {
14 | if (message.type === 'CHANGE_LANG') {
15 | const data = message.data
16 | changeLanguage(data.lang)
17 | }
18 | })
19 | render( , document.getElementById('app'))
20 |
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/styles.scss:
--------------------------------------------------------------------------------
1 | [data-theme='auto'] {
2 | @media screen and (prefers-color-scheme: dark) {
3 | --font-color: #c9d1d9;
4 | --font-active-color: #ffffff;
5 | --theme-color: #202124;
6 | --theme-border-color: #3c4043;
7 | --active-color: #3c4043;
8 | }
9 | @media screen and (prefers-color-scheme: light) {
10 | --font-color: #24292f;
11 | --font-active-color: #cc3333;
12 | --theme-color: #ffffff;
13 | --theme-border-color: #aeafb2;
14 | --active-color: #d0d4da;
15 | }
16 | }
17 |
18 | [data-theme='dark'] {
19 | --font-color: #c9d1d9;
20 | --font-active-color: #ffffff;
21 | --theme-color: #202124;
22 | --theme-border-color: #3c4043;
23 | --active-color: #3c4043;
24 | }
25 |
26 | [data-theme='light'] {
27 | --font-color: #24292f;
28 | --font-active-color: #cc3333;
29 | --theme-color: #ffffff;
30 | --theme-border-color: #aeafb2;
31 | --active-color: #d0d4da;
32 | }
33 |
34 | .IndependentPanel * {
35 | font-family: 'Cairo', sans-serif;
36 | font-size: 14px;
37 | }
38 |
39 | .IndependentPanel {
40 | .chat-container {
41 | display: flex;
42 | width: 100%;
43 | height: 100%;
44 | }
45 |
46 | .chat-sidebar {
47 | display: flex;
48 | flex-direction: column;
49 | min-width: 250px;
50 | width: 250px;
51 | background-color: var(--theme-color);
52 | transition: width 0.3s, min-width 0.3s;
53 | padding: 10px;
54 |
55 | ::-webkit-scrollbar {
56 | background-color: var(--theme-color);
57 | width: 9px;
58 | }
59 | ::-webkit-scrollbar-thumb {
60 | background-color: var(--theme-border-color);
61 | border-radius: 20px;
62 | border: transparent;
63 | }
64 | ::-webkit-scrollbar-corner {
65 | background: transparent;
66 | }
67 | }
68 |
69 | .chat-sidebar.collapsed {
70 | min-width: 60px;
71 | width: 60px;
72 | }
73 |
74 | .chat-sidebar:hover,
75 | .chat-sidebar:not(.collapsed) {
76 | min-width: 250px;
77 | width: 250px;
78 | }
79 |
80 | .chat-sidebar-button-group {
81 | display: flex;
82 | flex-direction: column;
83 | padding: 0;
84 | background-color: var(--theme-color);
85 | gap: 15px;
86 | }
87 |
88 | .chat-list {
89 | display: flex;
90 | flex-direction: column;
91 | flex-grow: 1;
92 | padding: 0 2px 0 0;
93 | background-color: var(--theme-color);
94 | overflow-y: auto;
95 | overflow-x: hidden;
96 | gap: 15px;
97 | }
98 |
99 | .chat-content {
100 | flex-grow: 1;
101 | border: 1px solid var(--theme-border-color);
102 | background-color: var(--theme-color);
103 | }
104 |
105 | .normal-button {
106 | width: 100%;
107 | min-height: 40px;
108 | padding: 1px 6px;
109 | border: 1px solid;
110 | border-color: var(--theme-border-color);
111 | background-color: var(--theme-color);
112 | color: var(--font-color);
113 | border-radius: 5px;
114 | cursor: pointer;
115 | white-space: nowrap;
116 | text-overflow: ellipsis;
117 | overflow: hidden;
118 | }
119 |
120 | .gpt-util-group {
121 | display: flex;
122 | gap: 15px;
123 | align-items: center;
124 | }
125 |
126 | .gpt-util-icon {
127 | display: flex;
128 | cursor: pointer;
129 | align-items: center;
130 | color: var(--font-color);
131 | }
132 | .gpt-util-icon:hover {
133 | color: var(--font-active-color);
134 | }
135 |
136 | .normal-button.active,
137 | .normal-button:hover {
138 | background-color: var(--active-color);
139 | }
140 |
141 | hr {
142 | height: 1px;
143 | background-color: var(--theme-border-color);
144 | border: none;
145 | margin: 15px 0;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/pages/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'IndependentPanel/styles.scss';
2 |
--------------------------------------------------------------------------------
/src/popup/Popup.jsx:
--------------------------------------------------------------------------------
1 | import '@picocss/pico'
2 | import { useEffect, useState } from 'react'
3 | import {
4 | defaultConfig,
5 | getPreferredLanguageKey,
6 | getUserConfig,
7 | setUserConfig,
8 | } from '../config/index.mjs'
9 | import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
10 | import 'react-tabs/style/react-tabs.css'
11 | import './styles.scss'
12 | import { MarkGithubIcon } from '@primer/octicons-react'
13 | import Browser from 'webextension-polyfill'
14 | import { useWindowTheme } from '../hooks/use-window-theme.mjs'
15 | import { isMobile } from '../utils/index.mjs'
16 | import { useTranslation } from 'react-i18next'
17 | import { GeneralPart } from './sections/GeneralPart'
18 | import { FeaturePages } from './sections/FeaturePages'
19 | import { AdvancedPart } from './sections/AdvancedPart'
20 | import { ModulesPart } from './sections/ModulesPart'
21 |
22 | // eslint-disable-next-line react/prop-types
23 | function Footer({ currentVersion, latestVersion }) {
24 | const { t } = useTranslation()
25 |
26 | return (
27 |
28 |
29 | {`${t('Current Version')}: ${currentVersion} `}
30 | {currentVersion >= latestVersion ? (
31 | `(${t('Latest')})`
32 | ) : (
33 | <>
34 | ({`${t('Latest')}: `}
35 |
40 | {latestVersion}
41 |
42 | )
43 | >
44 | )}
45 |
46 |
56 |
57 | )
58 | }
59 |
60 | function Popup() {
61 | const { t, i18n } = useTranslation()
62 | const [config, setConfig] = useState(defaultConfig)
63 | const [currentVersion, setCurrentVersion] = useState('')
64 | const [latestVersion, setLatestVersion] = useState('')
65 | const [tabIndex, setTabIndex] = useState(0)
66 | const theme = useWindowTheme()
67 |
68 | const updateConfig = async (value) => {
69 | setConfig({ ...config, ...value })
70 | await setUserConfig(value)
71 | }
72 |
73 | useEffect(() => {
74 | getPreferredLanguageKey().then((lang) => {
75 | i18n.changeLanguage(lang)
76 | })
77 | getUserConfig().then((config) => {
78 | setConfig(config)
79 | setCurrentVersion(Browser.runtime.getManifest().version.replace('v', ''))
80 | fetch('https://api.github.com/repos/josstorer/chatGPTBox/releases/latest').then((response) =>
81 | response.json().then((data) => {
82 | setLatestVersion(data.tag_name.replace('v', ''))
83 | }),
84 | )
85 | })
86 | }, [])
87 |
88 | useEffect(() => {
89 | document.documentElement.dataset.theme = config.themeMode === 'auto' ? theme : config.themeMode
90 | }, [config.themeMode, theme])
91 |
92 | const search = new URLSearchParams(window.location.search)
93 | const popup = !isMobile() && search.get('popup') // manifest v2
94 |
95 | return (
96 |
129 | )
130 | }
131 |
132 | export default Popup
133 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPTBox
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/popup/index.jsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import Popup from './Popup'
3 | import '../_locales/i18n-react'
4 | import { getUserConfig } from '../config/index.mjs'
5 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
6 | import Browser from 'webextension-polyfill'
7 |
8 | getUserConfig().then(async (config) => {
9 | if (config.clickIconAction === 'popup' || (window.innerWidth > 100 && window.innerHeight > 100)) {
10 | render( , document.getElementById('app'))
11 | } else {
12 | const message = {
13 | itemId: config.clickIconAction,
14 | selectionText: '',
15 | useMenuPosition: false,
16 | }
17 | console.debug('custom icon action triggered', message)
18 |
19 | if (config.clickIconAction in menuConfig) {
20 | const currentTab = (await Browser.tabs.query({ active: true, currentWindow: true }))[0]
21 |
22 | if (menuConfig[config.clickIconAction].action) {
23 | menuConfig[config.clickIconAction].action(false, currentTab)
24 | }
25 |
26 | if (menuConfig[config.clickIconAction].genPrompt) {
27 | Browser.tabs.sendMessage(currentTab.id, {
28 | type: 'CREATE_CHAT',
29 | data: message,
30 | })
31 | }
32 | }
33 | window.close()
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/src/popup/sections/FeaturePages.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { useState } from 'react'
3 | import { isEdge, isFirefox, isMobile, isSafari, openUrl } from '../../utils/index.mjs'
4 | import Browser from 'webextension-polyfill'
5 | import PropTypes from 'prop-types'
6 |
7 | FeaturePages.propTypes = {
8 | config: PropTypes.object.isRequired,
9 | updateConfig: PropTypes.func.isRequired,
10 | }
11 |
12 | export function FeaturePages({ config, updateConfig }) {
13 | const { t } = useTranslation()
14 | const [backgroundPermission, setBackgroundPermission] = useState(false)
15 |
16 | if (!isMobile() && !isFirefox() && !isSafari())
17 | Browser.permissions.contains({ permissions: ['background'] }).then((result) => {
18 | setBackgroundPermission(result)
19 | })
20 |
21 | return (
22 |
23 | {!isMobile() && !isFirefox() && !isSafari() && (
24 | {
27 | if (isEdge()) openUrl('edge://extensions/shortcuts')
28 | else openUrl('chrome://extensions/shortcuts')
29 | }}
30 | >
31 | {t('Keyboard Shortcuts')}
32 |
33 | )}
34 | {
37 | Browser.runtime.sendMessage({
38 | type: 'OPEN_URL',
39 | data: {
40 | url: Browser.runtime.getURL('IndependentPanel.html'),
41 | },
42 | })
43 | }}
44 | >
45 | {t('Open Conversation Page')}
46 |
47 | {!isMobile() && (
48 | {
51 | Browser.runtime.sendMessage({
52 | type: 'OPEN_CHAT_WINDOW',
53 | data: {},
54 | })
55 | }}
56 | >
57 | {t('Open Conversation Window')}
58 |
59 | )}
60 | {!isMobile() && !isFirefox() && !isSafari() && (
61 |
62 | {
66 | const checked = e.target.checked
67 | if (checked)
68 | Browser.permissions.request({ permissions: ['background'] }).then((result) => {
69 | setBackgroundPermission(result)
70 | })
71 | else
72 | Browser.permissions.remove({ permissions: ['background'] }).then((result) => {
73 | setBackgroundPermission(result)
74 | })
75 | }}
76 | />
77 | {t('Keep Conversation Window in Background')}
78 |
79 | )}
80 | {!isMobile() && (
81 |
82 | {
86 | const checked = e.target.checked
87 | updateConfig({ alwaysCreateNewConversationWindow: checked })
88 | }}
89 | />
90 | {t('Always Create New Conversation Window')}
91 |
92 | )}
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/src/popup/sections/ModulesPart.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import PropTypes from 'prop-types'
3 | import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
4 | import { ApiModes } from './ApiModes'
5 | import { SelectionTools } from './SelectionTools'
6 | import { SiteAdapters } from './SiteAdapters'
7 |
8 | ModulesPart.propTypes = {
9 | config: PropTypes.object.isRequired,
10 | updateConfig: PropTypes.func.isRequired,
11 | }
12 |
13 | export function ModulesPart({ config, updateConfig }) {
14 | const { t } = useTranslation()
15 |
16 | return (
17 | <>
18 |
19 |
20 | {t('API Modes')}
21 | {t('Selection Tools')}
22 | {t('Sites')}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | >
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/popup/sections/SiteAdapters.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | SiteAdapters.propTypes = {
4 | config: PropTypes.object.isRequired,
5 | updateConfig: PropTypes.func.isRequired,
6 | }
7 |
8 | export function SiteAdapters({ config, updateConfig }) {
9 | return (
10 | <>
11 | {config.siteAdapters.map((key) => (
12 |
13 | {
17 | const checked = e.target.checked
18 | const activeSiteAdapters = config.activeSiteAdapters.filter((i) => i !== key)
19 | if (checked) activeSiteAdapters.push(key)
20 | updateConfig({ activeSiteAdapters })
21 | }}
22 | />
23 | {key}
24 |
25 | ))}
26 | >
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/popup/styles.scss:
--------------------------------------------------------------------------------
1 | [data-theme='auto'] {
2 | @media screen and (prefers-color-scheme: dark) {
3 | --font-color: #c9d1d9;
4 | --theme-color: #202124;
5 | --active-color: #3c4043;
6 | }
7 | @media screen and (prefers-color-scheme: light) {
8 | --font-color: #24292f;
9 | --theme-color: #ffffff;
10 | --active-color: #eaecf0;
11 | }
12 | }
13 |
14 | [data-theme='dark'] {
15 | --font-color: #c9d1d9;
16 | --theme-color: #202124;
17 | --active-color: #3c4043;
18 | }
19 |
20 | [data-theme='light'] {
21 | --font-color: #24292f;
22 | --theme-color: #ffffff;
23 | --active-color: #eaecf0;
24 | }
25 |
26 | .container-page-mode {
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | min-width: 460px;
31 | min-height: 560px;
32 | width: 100%;
33 | height: 100%;
34 | padding: 20px;
35 | overflow-y: auto;
36 | }
37 |
38 | .container-popup-mode {
39 | display: flex;
40 | flex-direction: column;
41 | align-items: center;
42 | width: 460px;
43 | height: 560px;
44 | padding: 20px;
45 | overflow-y: auto;
46 | }
47 |
48 | .container legend {
49 | font-weight: bold;
50 | }
51 |
52 | .container form {
53 | margin-bottom: 0;
54 | }
55 |
56 | .container fieldset {
57 | margin-bottom: 0;
58 | }
59 |
60 | .footer {
61 | width: 90%;
62 | position: fixed;
63 | bottom: 10px;
64 | display: flex;
65 | flex-direction: row;
66 | justify-content: space-between;
67 | align-items: center;
68 | background-color: var(--active-color);
69 | border-radius: 5px;
70 | padding: 6px;
71 | z-index: 2147483647;
72 | font-size: 12px;
73 | }
74 |
75 | .popup-tab {
76 | display: inline-block;
77 | position: relative;
78 | list-style: none;
79 | padding: 6px 12px 0;
80 | cursor: pointer;
81 | border-radius: 5px;
82 | margin-right: 5px;
83 | font-size: 14px;
84 | background-color: var(--theme-color);
85 | color: var(--font-color);
86 |
87 | &--selected {
88 | background: var(--active-color);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/rules.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "action": {
5 | "type": "modifyHeaders",
6 | "requestHeaders": [
7 | {
8 | "operation": "set",
9 | "header": "origin",
10 | "value": "https://www.bing.com"
11 | },
12 | {
13 | "operation": "set",
14 | "header": "referer",
15 | "value": "https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx"
16 | }
17 | ]
18 | },
19 | "condition": {
20 | "requestDomains": ["sydney.bing.com", "www.bing.com"],
21 | "resourceTypes": ["xmlhttprequest", "websocket"]
22 | }
23 | },
24 | {
25 | "id": 2,
26 | "action": {
27 | "type": "modifyHeaders",
28 | "requestHeaders": [
29 | {
30 | "operation": "set",
31 | "header": "origin",
32 | "value": "https://chatgpt.com"
33 | },
34 | {
35 | "operation": "set",
36 | "header": "referer",
37 | "value": "https://chatgpt.com"
38 | }
39 | ]
40 | },
41 | "condition": {
42 | "requestDomains": ["chatgpt.com"],
43 | "resourceTypes": ["xmlhttprequest"]
44 | }
45 | },
46 | {
47 | "id": 3,
48 | "action": {
49 | "type": "modifyHeaders",
50 | "requestHeaders": [
51 | {
52 | "operation": "set",
53 | "header": "origin",
54 | "value": "https://tcr9i.chat.openai.com"
55 | },
56 | {
57 | "operation": "set",
58 | "header": "referer",
59 | "value": "https://tcr9i.chat.openai.com/v2/2.9.0/enforcement.b3b1c9343f2ef3887d61d74272d6a3af.html"
60 | }
61 | ]
62 | },
63 | "condition": {
64 | "requestDomains": ["https://tcr9i.chat.openai.com"],
65 | "resourceTypes": ["xmlhttprequest"]
66 | }
67 | }
68 | ]
69 |
--------------------------------------------------------------------------------
/src/services/apis/azure-openai-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { pushRecord, setAbortController } from './shared.mjs'
3 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
4 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
5 | import { isEmpty } from 'lodash-es'
6 | import { getModelValue } from '../../utils/model-name-convert.mjs'
7 |
8 | /**
9 | * @param {Runtime.Port} port
10 | * @param {string} question
11 | * @param {Session} session
12 | */
13 | export async function generateAnswersWithAzureOpenaiApi(port, question, session) {
14 | const { controller, messageListener, disconnectListener } = setAbortController(port)
15 | const config = await getUserConfig()
16 | let model = getModelValue(session)
17 | if (!model) model = config.azureDeploymentName
18 |
19 | const prompt = getConversationPairs(
20 | session.conversationRecords.slice(-config.maxConversationContextLength),
21 | false,
22 | )
23 | prompt.push({ role: 'user', content: question })
24 |
25 | let answer = ''
26 | await fetchSSE(
27 | `${config.azureEndpoint.replace(
28 | /\/$/,
29 | '',
30 | )}/openai/deployments/${model}/chat/completions?api-version=2024-02-01`,
31 | {
32 | method: 'POST',
33 | signal: controller.signal,
34 | headers: {
35 | 'Content-Type': 'application/json',
36 | 'api-key': config.azureApiKey,
37 | },
38 | body: JSON.stringify({
39 | messages: prompt,
40 | stream: true,
41 | max_tokens: config.maxResponseTokenLength,
42 | temperature: config.temperature,
43 | }),
44 | onMessage(message) {
45 | console.debug('sse message', message)
46 | let data
47 | try {
48 | data = JSON.parse(message)
49 | } catch (error) {
50 | console.debug('json error', error)
51 | return
52 | }
53 | if (
54 | data.choices &&
55 | data.choices.length > 0 &&
56 | data.choices[0] &&
57 | data.choices[0].delta &&
58 | 'content' in data.choices[0].delta
59 | ) {
60 | answer += data.choices[0].delta.content
61 | port.postMessage({ answer: answer, done: false, session: null })
62 | }
63 |
64 | if (data.choices && data.choices.length > 0 && data.choices[0]?.finish_reason) {
65 | pushRecord(session, question, answer)
66 | console.debug('conversation history', { content: session.conversationRecords })
67 | port.postMessage({ answer: null, done: true, session: session })
68 | }
69 | },
70 | async onStart() {},
71 | async onEnd() {
72 | port.postMessage({ done: true })
73 | port.onMessage.removeListener(messageListener)
74 | port.onDisconnect.removeListener(disconnectListener)
75 | },
76 | async onError(resp) {
77 | port.onMessage.removeListener(messageListener)
78 | port.onDisconnect.removeListener(disconnectListener)
79 | if (resp instanceof Error) throw resp
80 | const error = await resp.json().catch(() => ({}))
81 | throw new Error(
82 | !isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`,
83 | )
84 | },
85 | },
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/services/apis/bard-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord } from './shared.mjs'
2 | import Bard from '../clients/bard'
3 |
4 | /**
5 | * @param {Runtime.Port} port
6 | * @param {string} question
7 | * @param {Session} session
8 | * @param {string} cookies
9 | */
10 | export async function generateAnswersWithBardWebApi(port, question, session, cookies) {
11 | // const { controller, messageListener, disconnectListener } = setAbortController(port)
12 | const bot = new Bard(cookies)
13 |
14 | // eslint-disable-next-line
15 | try {
16 | const { answer, conversationObj } = await bot.ask(question, session.bard_conversationObj || {})
17 | session.bard_conversationObj = conversationObj
18 | pushRecord(session, question, answer)
19 | console.debug('conversation history', { content: session.conversationRecords })
20 | // port.onMessage.removeListener(messageListener)
21 | // port.onDisconnect.removeListener(disconnectListener)
22 | port.postMessage({ answer: answer, done: true, session: session })
23 | } catch (err) {
24 | // port.onMessage.removeListener(messageListener)
25 | // port.onDisconnect.removeListener(disconnectListener)
26 | throw err
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/apis/bing-web.mjs:
--------------------------------------------------------------------------------
1 | import BingAIClient from '../clients/bing/index.mjs'
2 | import { getUserConfig } from '../../config/index.mjs'
3 | import { pushRecord, setAbortController } from './shared.mjs'
4 | import { getModelValue } from '../../utils/model-name-convert.mjs'
5 |
6 | /**
7 | * @param {Runtime.Port} port
8 | * @param {string} question
9 | * @param {Session} session
10 | * @param {string} accessToken
11 | * @param {boolean} sydneyMode
12 | */
13 | export async function generateAnswersWithBingWebApi(
14 | port,
15 | question,
16 | session,
17 | accessToken,
18 | sydneyMode = false,
19 | ) {
20 | const { controller, messageListener, disconnectListener } = setAbortController(port)
21 | const config = await getUserConfig()
22 | let modelMode = getModelValue(session)
23 | if (!modelMode) modelMode = config.modelMode
24 |
25 | console.debug('mode', modelMode)
26 |
27 | const bingAIClient = new BingAIClient({ userToken: accessToken, features: { genImage: false } })
28 | if (session.bingWeb_jailbreakConversationCache)
29 | bingAIClient.conversationsCache.set(
30 | session.bingWeb_jailbreakConversationId,
31 | session.bingWeb_jailbreakConversationCache,
32 | )
33 |
34 | let answer = ''
35 | const response = await bingAIClient
36 | .sendMessage(question, {
37 | abortController: controller,
38 | toneStyle: modelMode,
39 | jailbreakConversationId: sydneyMode,
40 | onProgress: (message) => {
41 | answer = message
42 | // reference markers [^number^]
43 | answer = answer.replaceAll(/\[\^(\d+)\^\]/g, '$1 ')
44 | port.postMessage({ answer: answer, done: false, session: null })
45 | },
46 | ...(session.bingWeb_conversationId
47 | ? {
48 | conversationId: session.bingWeb_conversationId,
49 | encryptedConversationSignature: session.bingWeb_encryptedConversationSignature,
50 | clientId: session.bingWeb_clientId,
51 | invocationId: session.bingWeb_invocationId,
52 | }
53 | : session.bingWeb_jailbreakConversationId
54 | ? {
55 | jailbreakConversationId: session.bingWeb_jailbreakConversationId,
56 | parentMessageId: session.bingWeb_parentMessageId,
57 | }
58 | : {}),
59 | })
60 | .catch((err) => {
61 | port.onMessage.removeListener(messageListener)
62 | port.onDisconnect.removeListener(disconnectListener)
63 | throw err
64 | })
65 |
66 | if (!sydneyMode) {
67 | session.bingWeb_encryptedConversationSignature = response.encryptedConversationSignature
68 | session.bingWeb_conversationId = response.conversationId
69 | session.bingWeb_clientId = response.clientId
70 | session.bingWeb_invocationId = response.invocationId
71 | } else {
72 | session.bingWeb_jailbreakConversationId = response.jailbreakConversationId
73 | session.bingWeb_parentMessageId = response.messageId
74 | session.bingWeb_jailbreakConversationCache = bingAIClient.conversationsCache.get(
75 | response.jailbreakConversationId,
76 | )
77 | }
78 |
79 | if (response.details.sourceAttributions && response.details.sourceAttributions.length > 0) {
80 | const footnotes =
81 | '\n\\-\n' +
82 | response.details.sourceAttributions
83 | .map((attr, index) => `\\[${index + 1}]: [${attr.providerDisplayName}](${attr.seeMoreUrl})`)
84 | .join('\n')
85 | answer += footnotes
86 | }
87 |
88 | pushRecord(session, question, answer)
89 | console.debug('conversation history', { content: session.conversationRecords })
90 | port.onMessage.removeListener(messageListener)
91 | port.onDisconnect.removeListener(disconnectListener)
92 | port.postMessage({ answer: answer, done: true, session: session })
93 | }
94 |
--------------------------------------------------------------------------------
/src/services/apis/chatglm-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { getToken } from '../../utils/jwt-token-generator.mjs'
3 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
4 |
5 | /**
6 | * @param {Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | */
10 | export async function generateAnswersWithChatGLMApi(port, question, session) {
11 | const baseUrl = 'https://open.bigmodel.cn/api/paas/v4'
12 | const config = await getUserConfig()
13 | return generateAnswersWithChatgptApiCompat(
14 | baseUrl,
15 | port,
16 | question,
17 | session,
18 | getToken(config.chatglmApiKey),
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/services/apis/claude-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { pushRecord, setAbortController } from './shared.mjs'
3 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
4 | import { isEmpty } from 'lodash-es'
5 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
6 | import { getModelValue } from '../../utils/model-name-convert.mjs'
7 |
8 | /**
9 | * @param {Runtime.Port} port
10 | * @param {string} question
11 | * @param {Session} session
12 | */
13 | export async function generateAnswersWithClaudeApi(port, question, session) {
14 | const { controller, messageListener, disconnectListener } = setAbortController(port)
15 | const config = await getUserConfig()
16 | const apiUrl = config.customClaudeApiUrl
17 | const model = getModelValue(session)
18 |
19 | const prompt = getConversationPairs(
20 | session.conversationRecords.slice(-config.maxConversationContextLength),
21 | false,
22 | )
23 | prompt.push({ role: 'user', content: question })
24 |
25 | let answer = ''
26 | await fetchSSE(`${apiUrl}/v1/messages`, {
27 | method: 'POST',
28 | signal: controller.signal,
29 | headers: {
30 | 'Content-Type': 'application/json',
31 | 'anthropic-version': '2023-06-01',
32 | 'x-api-key': config.claudeApiKey,
33 | 'anthropic-dangerous-direct-browser-access': true,
34 | },
35 | body: JSON.stringify({
36 | model,
37 | messages: prompt,
38 | stream: true,
39 | max_tokens: config.maxResponseTokenLength,
40 | temperature: config.temperature,
41 | }),
42 | onMessage(message) {
43 | console.debug('sse message', message)
44 |
45 | let data
46 | try {
47 | data = JSON.parse(message)
48 | } catch (error) {
49 | console.debug('json error', error)
50 | return
51 | }
52 | if (data?.type === 'message_stop') {
53 | pushRecord(session, question, answer)
54 | console.debug('conversation history', { content: session.conversationRecords })
55 | port.postMessage({ answer: null, done: true, session: session })
56 | return
57 | }
58 |
59 | const delta = data?.delta?.text
60 | if (delta) {
61 | answer += delta
62 | port.postMessage({ answer: answer, done: false, session: null })
63 | }
64 | },
65 | async onStart() {},
66 | async onEnd() {
67 | port.postMessage({ done: true })
68 | port.onMessage.removeListener(messageListener)
69 | port.onDisconnect.removeListener(disconnectListener)
70 | },
71 | async onError(resp) {
72 | port.onMessage.removeListener(messageListener)
73 | port.onDisconnect.removeListener(disconnectListener)
74 | if (resp instanceof Error) throw resp
75 | const error = await resp.json().catch(() => ({}))
76 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
77 | },
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/src/services/apis/claude-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import Claude from '../clients/claude'
3 | import { getModelValue } from '../../utils/model-name-convert.mjs'
4 |
5 | /**
6 | * @param {Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | * @param {string} sessionKey
10 | */
11 | export async function generateAnswersWithClaudeWebApi(port, question, session, sessionKey) {
12 | const bot = new Claude({ sessionKey })
13 | await bot.init()
14 | const { controller, cleanController } = setAbortController(port)
15 | const model = getModelValue(session)
16 |
17 | let answer = ''
18 | const progressFunc = ({ completion }) => {
19 | answer = completion
20 | port.postMessage({ answer: answer, done: false, session: null })
21 | }
22 |
23 | const doneFunc = () => {
24 | pushRecord(session, question, answer)
25 | console.debug('conversation history', { content: session.conversationRecords })
26 | port.postMessage({ answer: answer, done: true, session: session })
27 | }
28 |
29 | const params = {
30 | progress: progressFunc,
31 | done: doneFunc,
32 | model,
33 | signal: controller.signal,
34 | }
35 |
36 | if (!session.claude_conversation)
37 | await bot
38 | .startConversation(question, params)
39 | .then((conversation) => {
40 | conversation.request = null
41 | conversation.claude = null
42 | session.claude_conversation = conversation
43 | port.postMessage({ answer: answer, done: true, session: session })
44 | cleanController()
45 | })
46 | .catch((err) => {
47 | cleanController()
48 | throw err
49 | })
50 | else
51 | await bot
52 | .sendMessage(question, {
53 | conversation: session.claude_conversation,
54 | ...params,
55 | })
56 | .then(cleanController)
57 | .catch((err) => {
58 | cleanController()
59 | throw err
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/src/services/apis/custom-api.mjs:
--------------------------------------------------------------------------------
1 | // custom api version
2 |
3 | // There is a lot of duplicated code here, but it is very easy to refactor.
4 | // The current state is mainly convenient for making targeted changes at any time,
5 | // and it has not yet had a negative impact on maintenance.
6 | // If necessary, I will refactor.
7 |
8 | import { getUserConfig } from '../../config/index.mjs'
9 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
10 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
11 | import { isEmpty } from 'lodash-es'
12 | import { pushRecord, setAbortController } from './shared.mjs'
13 |
14 | /**
15 | * @param {Browser.Runtime.Port} port
16 | * @param {string} question
17 | * @param {Session} session
18 | * @param {string} apiUrl
19 | * @param {string} apiKey
20 | * @param {string} modelName
21 | */
22 | export async function generateAnswersWithCustomApi(
23 | port,
24 | question,
25 | session,
26 | apiUrl,
27 | apiKey,
28 | modelName,
29 | ) {
30 | const { controller, messageListener, disconnectListener } = setAbortController(port)
31 |
32 | const config = await getUserConfig()
33 | const prompt = getConversationPairs(
34 | session.conversationRecords.slice(-config.maxConversationContextLength),
35 | false,
36 | )
37 | prompt.push({ role: 'user', content: question })
38 |
39 | let answer = ''
40 | let finished = false
41 | const finish = () => {
42 | finished = true
43 | pushRecord(session, question, answer)
44 | console.debug('conversation history', { content: session.conversationRecords })
45 | port.postMessage({ answer: null, done: true, session: session })
46 | }
47 | await fetchSSE(apiUrl, {
48 | method: 'POST',
49 | signal: controller.signal,
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | Authorization: `Bearer ${apiKey}`,
53 | },
54 | body: JSON.stringify({
55 | messages: prompt,
56 | model: modelName,
57 | stream: true,
58 | max_tokens: config.maxResponseTokenLength,
59 | temperature: config.temperature,
60 | }),
61 | onMessage(message) {
62 | console.debug('sse message', message)
63 | if (finished) return
64 | if (message.trim() === '[DONE]') {
65 | finish()
66 | return
67 | }
68 | let data
69 | try {
70 | data = JSON.parse(message)
71 | } catch (error) {
72 | console.debug('json error', error)
73 | return
74 | }
75 |
76 | if (data.response) answer = data.response
77 | else {
78 | const delta = data.choices[0]?.delta?.content
79 | const content = data.choices[0]?.message?.content
80 | const text = data.choices[0]?.text
81 | if (delta !== undefined) {
82 | answer += delta
83 | } else if (content) {
84 | answer = content
85 | } else if (text) {
86 | answer += text
87 | }
88 | }
89 | port.postMessage({ answer: answer, done: false, session: null })
90 |
91 | if (data.choices[0]?.finish_reason) {
92 | finish()
93 | return
94 | }
95 | },
96 | async onStart() {},
97 | async onEnd() {
98 | port.postMessage({ done: true })
99 | port.onMessage.removeListener(messageListener)
100 | port.onDisconnect.removeListener(disconnectListener)
101 | },
102 | async onError(resp) {
103 | port.onMessage.removeListener(messageListener)
104 | port.onDisconnect.removeListener(disconnectListener)
105 | if (resp instanceof Error) throw resp
106 | const error = await resp.json().catch(() => ({}))
107 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
108 | },
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/src/services/apis/moonshot-api.mjs:
--------------------------------------------------------------------------------
1 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
2 |
3 | /**
4 | * @param {Browser.Runtime.Port} port
5 | * @param {string} question
6 | * @param {Session} session
7 | * @param {string} apiKey
8 | */
9 | export async function generateAnswersWithMoonshotCompletionApi(port, question, session, apiKey) {
10 | const baseUrl = 'https://api.moonshot.cn/v1'
11 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, apiKey)
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/apis/ollama-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
3 | import { getModelValue } from '../../utils/model-name-convert.mjs'
4 |
5 | /**
6 | * @param {Browser.Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | */
10 | export async function generateAnswersWithOllamaApi(port, question, session) {
11 | const config = await getUserConfig()
12 | const model = getModelValue(session)
13 | return generateAnswersWithChatgptApiCompat(
14 | config.ollamaEndpoint + '/v1',
15 | port,
16 | question,
17 | session,
18 | config.ollamaApiKey,
19 | ).then(() =>
20 | fetch(config.ollamaEndpoint + '/api/generate', {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | Authorization: `Bearer ${config.ollamaApiKey}`,
25 | },
26 | body: JSON.stringify({
27 | model,
28 | prompt: 't',
29 | options: {
30 | num_predict: 1,
31 | },
32 | keep_alive: config.ollamaKeepAliveTime === '-1' ? -1 : config.ollamaKeepAliveTime,
33 | }),
34 | }),
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/services/apis/poe-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import PoeAiClient from '../clients/poe/index.mjs'
3 |
4 | /**
5 | * @param {Runtime.Port} port
6 | * @param {string} question
7 | * @param {Session} session
8 | * @param {string} modelName
9 | */
10 | export async function generateAnswersWithPoeWebApi(port, question, session, modelName) {
11 | const bot = new PoeAiClient(session.poe_chatId)
12 | const { messageListener, disconnectListener } = setAbortController(
13 | port,
14 | () => {
15 | bot.close()
16 | },
17 | () => {
18 | bot.breakMsg()
19 | bot.close()
20 | },
21 | )
22 |
23 | let answer = ''
24 | await bot
25 | .ask(
26 | question,
27 | modelName,
28 | (msg) => {
29 | answer += msg
30 | port.postMessage({ answer: answer, done: false, session: null })
31 | },
32 | () => {
33 | if (bot.chatId) session.poe_chatId = bot.chatId
34 |
35 | pushRecord(session, question, answer)
36 | console.debug('conversation history', { content: session.conversationRecords })
37 | port.onMessage.removeListener(messageListener)
38 | if (session.conversationRecords.length > 1)
39 | port.onDisconnect.removeListener(disconnectListener)
40 | port.postMessage({ answer: answer, done: true, session: session })
41 | bot.close()
42 | },
43 | )
44 | .catch((err) => {
45 | port.onMessage.removeListener(messageListener)
46 | port.onDisconnect.removeListener(disconnectListener)
47 | bot.close()
48 | throw err
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/services/apis/shared.mjs:
--------------------------------------------------------------------------------
1 | export const getChatSystemPromptBase = async () => {
2 | return `You are a helpful, creative, clever, and very friendly assistant. You are familiar with various languages in the world.`
3 | }
4 |
5 | export const getCompletionPromptBase = async () => {
6 | return (
7 | `The following is a conversation with an AI assistant.` +
8 | `The assistant is helpful, creative, clever, and very friendly. The assistant is familiar with various languages in the world.\n\n` +
9 | `Human: Hello, who are you?\n` +
10 | `AI: I am an AI assistant. How can I help you today?\n`
11 | )
12 | }
13 |
14 | export const getCustomApiPromptBase = async () => {
15 | return `I am a helpful, creative, clever, and very friendly assistant. I am familiar with various languages in the world.`
16 | }
17 |
18 | export function setAbortController(port, onStop, onDisconnect) {
19 | const controller = new AbortController()
20 | const messageListener = (msg) => {
21 | if (msg.stop) {
22 | port.onMessage.removeListener(messageListener)
23 | console.debug('stop generating')
24 | port.postMessage({ done: true })
25 | controller.abort()
26 | if (onStop) onStop()
27 | }
28 | }
29 | port.onMessage.addListener(messageListener)
30 |
31 | const disconnectListener = () => {
32 | port.onDisconnect.removeListener(disconnectListener)
33 | console.debug('port disconnected')
34 | controller.abort()
35 | if (onDisconnect) onDisconnect()
36 | }
37 | port.onDisconnect.addListener(disconnectListener)
38 |
39 | const cleanController = () => {
40 | try {
41 | port.onMessage.removeListener(messageListener)
42 | port.onDisconnect.removeListener(disconnectListener)
43 | } catch (e) {
44 | // ignore
45 | }
46 | }
47 |
48 | return { controller, cleanController, messageListener, disconnectListener }
49 | }
50 |
51 | export function pushRecord(session, question, answer) {
52 | const recordLength = session.conversationRecords.length
53 | let lastRecord
54 | if (recordLength > 0) lastRecord = session.conversationRecords[recordLength - 1]
55 |
56 | if (session.isRetry && lastRecord && lastRecord.question === question) lastRecord.answer = answer
57 | else session.conversationRecords.push({ question: question, answer: answer })
58 | }
59 |
--------------------------------------------------------------------------------
/src/services/apis/waylaidwanderer-api.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import { getUserConfig } from '../../config/index.mjs'
3 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
4 | import { isEmpty } from 'lodash-es'
5 |
6 | /**
7 | * @param {Runtime.Port} port
8 | * @param {string} question
9 | * @param {Session} session
10 | */
11 | export async function generateAnswersWithWaylaidwandererApi(port, question, session) {
12 | const { controller, messageListener, disconnectListener } = setAbortController(port)
13 |
14 | const config = await getUserConfig()
15 |
16 | let answer = ''
17 | await fetchSSE(config.githubThirdPartyUrl, {
18 | method: 'POST',
19 | signal: controller.signal,
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | },
23 | body: JSON.stringify({
24 | message: question,
25 | stream: true,
26 | ...(session.bingWeb_encryptedConversationSignature && {
27 | conversationId: session.bingWeb_conversationId,
28 | encryptedConversationSignature: session.bingWeb_encryptedConversationSignature,
29 | clientId: session.bingWeb_clientId,
30 | invocationId: session.bingWeb_invocationId,
31 | }),
32 | ...(session.parentMessageId && {
33 | conversationId: session.conversationId,
34 | parentMessageId: session.parentMessageId,
35 | }),
36 | }),
37 | onMessage(message) {
38 | console.debug('sse message', message)
39 | if (message.trim() === '[DONE]') {
40 | pushRecord(session, question, answer)
41 | console.debug('conversation history', { content: session.conversationRecords })
42 | port.postMessage({ answer: null, done: true, session: session })
43 | return
44 | }
45 | let data
46 | try {
47 | data = JSON.parse(message)
48 | } catch (error) {
49 | console.debug('json error', error)
50 | return
51 | }
52 | if (data.conversationId) session.conversationId = data.conversationId
53 | if (data.parentMessageId) session.parentMessageId = data.parentMessageId
54 | if (data.encryptedConversationSignature)
55 | session.bingWeb_encryptedConversationSignature = data.encryptedConversationSignature
56 | if (data.conversationId) session.bingWeb_conversationId = data.conversationId
57 | if (data.clientId) session.bingWeb_clientId = data.clientId
58 | if (data.invocationId) session.bingWeb_invocationId = data.invocationId
59 |
60 | if (typeof data === 'string') {
61 | answer += data
62 | port.postMessage({ answer: answer, done: false, session: null })
63 | }
64 | },
65 | async onStart() {},
66 | async onEnd() {
67 | port.postMessage({ done: true })
68 | port.onMessage.removeListener(messageListener)
69 | port.onDisconnect.removeListener(disconnectListener)
70 | },
71 | async onError(resp) {
72 | port.onMessage.removeListener(messageListener)
73 | port.onDisconnect.removeListener(disconnectListener)
74 | if (resp instanceof Error) throw resp
75 | const error = await resp.json().catch(() => ({}))
76 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
77 | },
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/src/services/clients/bard/index.mjs:
--------------------------------------------------------------------------------
1 | // https://github.com/PawanOsman/GoogleBard
2 |
3 | export default class Bard {
4 | cookies = ''
5 |
6 | constructor(cookies) {
7 | this.cookies = cookies
8 | }
9 |
10 | ParseResponse(text) {
11 | let resData = {
12 | r: '',
13 | c: '',
14 | rc: '',
15 | responses: [],
16 | }
17 | try {
18 | let parseData = (data) => {
19 | if (typeof data === 'string') {
20 | if (data?.startsWith('c_')) {
21 | resData.c = data
22 | return
23 | }
24 | if (data?.startsWith('r_')) {
25 | resData.r = data
26 | return
27 | }
28 | if (data?.startsWith('rc_')) {
29 | resData.rc = data
30 | return
31 | }
32 | resData.responses.push(data)
33 | }
34 | if (Array.isArray(data)) {
35 | data.forEach((item) => {
36 | parseData(item)
37 | })
38 | }
39 | }
40 | try {
41 | const lines = text.split('\n')
42 | for (let i in lines) {
43 | const line = lines[i]
44 | if (line.includes('wrb.fr')) {
45 | let data = JSON.parse(line)
46 | let responsesData = JSON.parse(data[0][2])
47 | responsesData.forEach((response) => {
48 | parseData(response)
49 | })
50 | }
51 | }
52 | } catch (e) {
53 | throw new Error(
54 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
55 | )
56 | }
57 | } catch (err) {
58 | throw new Error(
59 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
60 | )
61 | }
62 | return resData
63 | }
64 |
65 | async GetRequestParams() {
66 | try {
67 | const response = await fetch('https://gemini.google.com', {
68 | headers: {
69 | Cookie: this.cookies,
70 | },
71 | })
72 | const text = await response.text()
73 | const cfb2h = text.match(/"cfb2h":\s*"([^"]+)"/)?.[1]
74 | const SNlM0e = text.match(/"SNlM0e":\s*"([^"]+)"/)?.[1]
75 | const context = { googleData: { cfb2h, SNlM0e } }
76 | const at = context.googleData.SNlM0e
77 | const bl = context.googleData.cfb2h
78 | return { at, bl }
79 | } catch (e) {
80 | throw new Error(
81 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
82 | )
83 | }
84 | }
85 |
86 | async ask(prompt, conversationObj) {
87 | return await this.send(prompt, conversationObj)
88 | }
89 |
90 | async send(prompt, conversationObj) {
91 | let conversation = {
92 | id: conversationObj.id || '',
93 | c: conversationObj.c || '',
94 | r: conversationObj.r || '',
95 | rc: conversationObj.rc || '',
96 | lastActive: Date.now(),
97 | }
98 | // eslint-disable-next-line
99 | try {
100 | let { at, bl } = await this.GetRequestParams()
101 | const response = await fetch(
102 | 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?' +
103 | new URLSearchParams({
104 | bl: bl,
105 | rt: 'c',
106 | _reqid: 0,
107 | }),
108 | {
109 | method: 'POST',
110 | body: new URLSearchParams({
111 | at: at,
112 | 'f.req': JSON.stringify([
113 | null,
114 | `[[${JSON.stringify(prompt)}],null,${JSON.stringify([
115 | conversation.c,
116 | conversation.r,
117 | conversation.rc,
118 | ])}]`,
119 | ]),
120 | }),
121 | headers: {
122 | Cookie: this.cookies,
123 | },
124 | },
125 | )
126 | const data = await response.text()
127 | let parsedResponse = this.ParseResponse(data)
128 | conversation.c = parsedResponse.c
129 | conversation.r = parsedResponse.r
130 | conversation.rc = parsedResponse.rc
131 | const conversationObj = { c: conversation.c, r: conversation.r, rc: conversation.rc }
132 | return { answer: parsedResponse.responses[3], conversationObj: conversationObj }
133 | } catch (e) {
134 | throw e
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AddHumanMessageMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AddHumanMessageMutation(
2 | $chatId: BigInt!
3 | $bot: String!
4 | $query: String!
5 | $source: MessageSource
6 | $withChatBreak: Boolean! = false
7 | ) {
8 | messageCreateWithStatus(
9 | chatId: $chatId
10 | bot: $bot
11 | query: $query
12 | source: $source
13 | withChatBreak: $withChatBreak
14 | ) {
15 | message {
16 | id
17 | __typename
18 | messageId
19 | text
20 | linkifiedText
21 | authorNickname
22 | state
23 | vote
24 | voteReason
25 | creationTime
26 | suggestedReplies
27 | chat {
28 | id
29 | shouldShowDisclaimer
30 | }
31 | }
32 | messageLimit{
33 | canSend
34 | numMessagesRemaining
35 | resetTime
36 | shouldShowReminder
37 | }
38 | chatBreak {
39 | id
40 | __typename
41 | messageId
42 | text
43 | linkifiedText
44 | authorNickname
45 | state
46 | vote
47 | voteReason
48 | creationTime
49 | suggestedReplies
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AddMessageBreakMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AddMessageBreakMutation($chatId: BigInt!) {
2 | messageBreakCreate(chatId: $chatId) {
3 | message {
4 | id
5 | __typename
6 | messageId
7 | text
8 | linkifiedText
9 | authorNickname
10 | state
11 | vote
12 | voteReason
13 | creationTime
14 | suggestedReplies
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AutoSubscriptionMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AutoSubscriptionMutation($subscriptions: [AutoSubscriptionQuery!]!) {
2 | autoSubscribe(subscriptions: $subscriptions) {
3 | viewer {
4 | id
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/BioFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment BioFragment on Viewer {
2 | id
3 | poeUser {
4 | id
5 | uid
6 | bio
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatAddedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription ChatAddedSubscription {
2 | chatAdded {
3 | ...ChatFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment ChatFragment on Chat {
2 | id
3 | chatId
4 | defaultBotNickname
5 | shouldShowDisclaimer
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatPaginationQuery.graphql:
--------------------------------------------------------------------------------
1 | query ChatPaginationQuery($bot: String!, $before: String, $last: Int! = 10) {
2 | chatOfBot(bot: $bot) {
3 | id
4 | __typename
5 | messagesConnection(before: $before, last: $last) {
6 | pageInfo {
7 | hasPreviousPage
8 | }
9 | edges {
10 | node {
11 | id
12 | __typename
13 | messageId
14 | text
15 | linkifiedText
16 | authorNickname
17 | state
18 | vote
19 | voteReason
20 | creationTime
21 | suggestedReplies
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatViewQuery.graphql:
--------------------------------------------------------------------------------
1 | query ChatViewQuery($bot: String!) {
2 | chatOfBot(bot: $bot) {
3 | id
4 | chatId
5 | defaultBotNickname
6 | shouldShowDisclaimer
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/DeleteHumanMessagesMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation DeleteHumanMessagesMutation($messageIds: [BigInt!]!) {
2 | messagesDelete(messageIds: $messageIds) {
3 | viewer {
4 | id
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/HandleFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment HandleFragment on Viewer {
2 | id
3 | poeUser {
4 | id
5 | uid
6 | handle
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/LoginWithVerificationCodeMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation LoginWithVerificationCodeMutation(
2 | $verificationCode: String!
3 | $emailAddress: String
4 | $phoneNumber: String
5 | ) {
6 | loginWithVerificationCode(
7 | verificationCode: $verificationCode
8 | emailAddress: $emailAddress
9 | phoneNumber: $phoneNumber
10 | ) {
11 | status
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageAddedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription MessageAddedSubscription($chatId: BigInt!) {
2 | messageAdded(chatId: $chatId) {
3 | ...MessageFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageDeletedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription MessageDeletedSubscription($chatId: BigInt!) {
2 | messageDeleted(chatId: $chatId) {
3 | id
4 | messageId
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment MessageFragment on Message {
2 | id
3 | __typename
4 | messageId
5 | text
6 | linkifiedText
7 | authorNickname
8 | state
9 | vote
10 | voteReason
11 | creationTime
12 | suggestedReplies
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageRemoveVoteMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation MessageRemoveVoteMutation($messageId: BigInt!) {
2 | messageRemoveVote(messageId: $messageId) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageSetVoteMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation MessageSetVoteMutation($messageId: BigInt!, $voteType: VoteType!, $reason: String) {
2 | messageSetVote(messageId: $messageId, voteType: $voteType, reason: $reason) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SendVerificationCodeForLoginMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation SendVerificationCodeForLoginMutation(
2 | $emailAddress: String
3 | $phoneNumber: String
4 | ) {
5 | sendVerificationCode(
6 | verificationReason: login
7 | emailAddress: $emailAddress
8 | phoneNumber: $phoneNumber
9 | ) {
10 | status
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ShareMessagesMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation ShareMessagesMutation(
2 | $chatId: BigInt!
3 | $messageIds: [BigInt!]!
4 | $comment: String
5 | ) {
6 | messagesShare(chatId: $chatId, messageIds: $messageIds, comment: $comment) {
7 | shareCode
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SignupWithVerificationCodeMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation SignupWithVerificationCodeMutation(
2 | $verificationCode: String!
3 | $emailAddress: String
4 | $phoneNumber: String
5 | ) {
6 | signupWithVerificationCode(
7 | verificationCode: $verificationCode
8 | emailAddress: $emailAddress
9 | phoneNumber: $phoneNumber
10 | ) {
11 | status
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/StaleChatUpdateMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation StaleChatUpdateMutation($chatId: BigInt!) {
2 | staleChatUpdate(chatId: $chatId) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizePlainPostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizePlainPostQuery($comment: String!) {
2 | summarizePlainPost(comment: $comment)
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizeQuotePostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizeQuotePostQuery($comment: String, $quotedPostId: BigInt!) {
2 | summarizeQuotePost(comment: $comment, quotedPostId: $quotedPostId)
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizeSharePostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizeSharePostQuery($comment: String!, $chatId: BigInt!, $messageIds: [BigInt!]!) {
2 | summarizeSharePost(comment: $comment, chatId: $chatId, messageIds: $messageIds)
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/UserSnippetFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment UserSnippetFragment on PoeUser {
2 | id
3 | uid
4 | bio
5 | handle
6 | fullName
7 | viewerIsFollowing
8 | isPoeOnlyUser
9 | profilePhotoURLTiny: profilePhotoUrl(size: tiny)
10 | profilePhotoURLSmall: profilePhotoUrl(size: small)
11 | profilePhotoURLMedium: profilePhotoUrl(size: medium)
12 | profilePhotoURLLarge: profilePhotoUrl(size: large)
13 | isFollowable
14 | }
15 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerInfoQuery.graphql:
--------------------------------------------------------------------------------
1 | query ViewerInfoQuery {
2 | viewer {
3 | id
4 | uid
5 | ...ViewerStateFragment
6 | ...BioFragment
7 | ...HandleFragment
8 | hasCompletedMultiplayerNux
9 | poeUser {
10 | id
11 | ...UserSnippetFragment
12 | }
13 | messageLimit{
14 | canSend
15 | numMessagesRemaining
16 | resetTime
17 | shouldShowReminder
18 | }
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerStateFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment ViewerStateFragment on Viewer {
2 | id
3 | __typename
4 | iosMinSupportedVersion: integerGate(gateName: "poe_ios_min_supported_version")
5 | iosMinEncouragedVersion: integerGate(
6 | gateName: "poe_ios_min_encouraged_version"
7 | )
8 | macosMinSupportedVersion: integerGate(
9 | gateName: "poe_macos_min_supported_version"
10 | )
11 | macosMinEncouragedVersion: integerGate(
12 | gateName: "poe_macos_min_encouraged_version"
13 | )
14 | showPoeDebugPanel: booleanGate(gateName: "poe_show_debug_panel")
15 | enableCommunityFeed: booleanGate(gateName: "enable_poe_shares_feed")
16 | linkifyText: booleanGate(gateName: "poe_linkify_response")
17 | enableSuggestedReplies: booleanGate(gateName: "poe_suggested_replies")
18 | removeInviteLimit: booleanGate(gateName: "poe_remove_invite_limit")
19 | enableInAppPurchases: booleanGate(gateName: "poe_enable_in_app_purchases")
20 | availableBots {
21 | nickname
22 | displayName
23 | profilePicture
24 | isDown
25 | disclaimer
26 | subtitle
27 | poweredBy
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerStateUpdatedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription ViewerStateUpdatedSubscription {
2 | viewerStateUpdated {
3 | ...ViewerStateFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/clients/poe/websocket.js:
--------------------------------------------------------------------------------
1 | import * as diff from 'diff'
2 |
3 | const getSocketUrl = async (settings) => {
4 | settings = settings.tchannelData
5 | const tchRand = Math.floor(100000 + Math.random() * 900000) // They're surely using 6 digit random number for ws url.
6 | const socketUrl = `wss://tch${tchRand}.tch.quora.com`
7 | const boxName = settings.boxName
8 | const minSeq = settings.minSeq
9 | const channel = settings.channel
10 | const hash = settings.channelHash
11 | return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`
12 | }
13 | export const connectWs = async (settings) => {
14 | const url = await getSocketUrl(settings)
15 | const ws = new WebSocket(url)
16 | return new Promise((resolve) => {
17 | ws.onopen = () => {
18 | console.log('Connected to websocket')
19 | return resolve(ws)
20 | }
21 | })
22 | }
23 | export const disconnectWs = async (ws) => {
24 | return new Promise((resolve) => {
25 | ws.onclose = () => {
26 | return resolve(true)
27 | }
28 | ws.close()
29 | })
30 | }
31 | export const listenWs = async (ws, onMessage, onComplete) => {
32 | let previousText = ''
33 | return new Promise((resolve) => {
34 | let complete = false
35 | ws.onmessage = (e) => {
36 | let jsonData = JSON.parse(e.data)
37 | console.log(jsonData)
38 | if (jsonData.messages && jsonData.messages.length > 0) {
39 | const messages = JSON.parse(jsonData.messages[0])
40 | const dataPayload = messages.payload.data
41 | const text = dataPayload.messageAdded.text
42 | const state = dataPayload.messageAdded.state
43 | if (state !== 'complete') {
44 | const differences = diff.diffChars(previousText, text)
45 | let result = ''
46 | differences.forEach((part) => {
47 | if (part.added) {
48 | result += part.value
49 | }
50 | })
51 | previousText = text
52 | if (onMessage) onMessage(result)
53 | } else if (dataPayload.messageAdded.author !== 'human') {
54 | if (!complete) {
55 | complete = true
56 | if (onComplete) onComplete(text)
57 | return resolve(text)
58 | }
59 | }
60 | }
61 | }
62 | })
63 | }
64 |
--------------------------------------------------------------------------------
/src/services/init-session.mjs:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid'
2 | import { apiModeToModelName, modelNameToDesc } from '../utils/model-name-convert.mjs'
3 | import { t } from 'i18next'
4 |
5 | /**
6 | * @typedef {object} Session
7 | * @property {string|null} question
8 | * @property {Object[]|null} conversationRecords
9 | * @property {string|null} sessionName
10 | * @property {string|null} sessionId
11 | * @property {string|null} createdAt
12 | * @property {string|null} updatedAt
13 | * @property {string|null} aiName
14 | * @property {string|null} modelName
15 | * @property {boolean|null} autoClean
16 | * @property {boolean} isRetry
17 | * @property {string|null} conversationId - chatGPT web mode
18 | * @property {string|null} messageId - chatGPT web mode
19 | * @property {string|null} parentMessageId - chatGPT web mode
20 | * @property {string|null} wsRequestId - chatGPT web mode
21 | * @property {string|null} bingWeb_encryptedConversationSignature
22 | * @property {string|null} bingWeb_conversationId
23 | * @property {string|null} bingWeb_clientId
24 | * @property {string|null} bingWeb_invocationId
25 | * @property {string|null} bingWeb_jailbreakConversationId
26 | * @property {string|null} bingWeb_parentMessageId
27 | * @property {Object|null} bingWeb_jailbreakConversationCache
28 | * @property {number|null} poe_chatId
29 | * @property {object|null} bard_conversationObj
30 | * @property {object|null} claude_conversation
31 | * @property {object|null} moonshot_conversation
32 | */
33 | /**
34 | * @param {string|null} question
35 | * @param {Object[]|null} conversationRecords
36 | * @param {string|null} sessionName
37 | * @param {string|null} modelName
38 | * @param {boolean|null} autoClean
39 | * @param {Object|null} apiMode
40 | * @param {string} extraCustomModelName
41 | * @returns {Session}
42 | */
43 | export function initSession({
44 | question = null,
45 | conversationRecords = [],
46 | sessionName = null,
47 | modelName = null,
48 | autoClean = false,
49 | apiMode = null,
50 | extraCustomModelName = '',
51 | } = {}) {
52 | return {
53 | // common
54 | question,
55 | conversationRecords,
56 |
57 | sessionName,
58 | sessionId: uuidv4(),
59 | createdAt: new Date().toISOString(),
60 | updatedAt: new Date().toISOString(),
61 |
62 | aiName:
63 | modelName || apiMode
64 | ? modelNameToDesc(
65 | apiMode ? apiModeToModelName(apiMode) : modelName,
66 | t,
67 | extraCustomModelName,
68 | )
69 | : null,
70 | modelName,
71 | apiMode,
72 |
73 | autoClean,
74 | isRetry: false,
75 |
76 | // chatgpt-web
77 | conversationId: null,
78 | messageId: null,
79 | parentMessageId: null,
80 | wsRequestId: null,
81 |
82 | // bing
83 | bingWeb_encryptedConversationSignature: null,
84 | bingWeb_conversationId: null,
85 | bingWeb_clientId: null,
86 | bingWeb_invocationId: null,
87 |
88 | // bing sydney
89 | bingWeb_jailbreakConversationId: null,
90 | bingWeb_parentMessageId: null,
91 | bingWeb_jailbreakConversationCache: null,
92 |
93 | // poe
94 | poe_chatId: null,
95 |
96 | // bard
97 | bard_conversationObj: null,
98 |
99 | // claude.ai
100 | claude_conversation: null,
101 | // kimi.moonshot.cn
102 | moonshot_conversation: null,
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/services/local-session.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { initSession } from './init-session.mjs'
3 | import { getUserConfig } from '../config/index.mjs'
4 |
5 | export const initDefaultSession = async () => {
6 | const config = await getUserConfig()
7 | return initSession({
8 | sessionName: new Date().toLocaleString(),
9 | modelName: config.modelName,
10 | apiMode: config.apiMode,
11 | autoClean: false,
12 | extraCustomModelName: config.customModelName,
13 | })
14 | }
15 |
16 | export const createSession = async (newSession) => {
17 | let currentSessions
18 | if (newSession) {
19 | const ret = await getSession(newSession.sessionId)
20 | currentSessions = ret.currentSessions
21 | if (ret.session)
22 | currentSessions[
23 | currentSessions.findIndex((session) => session.sessionId === newSession.sessionId)
24 | ] = newSession
25 | else currentSessions.unshift(newSession)
26 | } else {
27 | newSession = await initDefaultSession()
28 | currentSessions = await getSessions()
29 | currentSessions.unshift(newSession)
30 | }
31 | await Browser.storage.local.set({ sessions: currentSessions })
32 | return { session: newSession, currentSessions }
33 | }
34 |
35 | export const deleteSession = async (sessionId) => {
36 | const currentSessions = await getSessions()
37 | const index = currentSessions.findIndex((session) => session.sessionId === sessionId)
38 | currentSessions.splice(index, 1)
39 | if (currentSessions.length > 0) {
40 | await Browser.storage.local.set({ sessions: currentSessions })
41 | return currentSessions
42 | }
43 | return await resetSessions()
44 | }
45 |
46 | export const getSession = async (sessionId) => {
47 | const currentSessions = await getSessions()
48 | return {
49 | session: currentSessions.find((session) => session.sessionId === sessionId),
50 | currentSessions,
51 | }
52 | }
53 |
54 | export const updateSession = async (newSession) => {
55 | newSession.updatedAt = new Date().toISOString()
56 | const currentSessions = await getSessions()
57 | currentSessions[
58 | currentSessions.findIndex((session) => session.sessionId === newSession.sessionId)
59 | ] = newSession
60 | await Browser.storage.local.set({ sessions: currentSessions })
61 | return currentSessions
62 | }
63 |
64 | export const resetSessions = async () => {
65 | const currentSessions = [await initDefaultSession()]
66 | await Browser.storage.local.set({ sessions: currentSessions })
67 | return currentSessions
68 | }
69 |
70 | export const getSessions = async () => {
71 | const { sessions } = await Browser.storage.local.get('sessions')
72 | if (sessions && sessions.length > 0) return sessions
73 | return await resetSessions()
74 | }
75 |
--------------------------------------------------------------------------------
/src/services/wrappers.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | clearOldAccessToken,
3 | getUserConfig,
4 | isUsingBingWebModel,
5 | isUsingClaudeWebModel,
6 | setAccessToken,
7 | } from '../config/index.mjs'
8 | import Browser from 'webextension-polyfill'
9 | import { t } from 'i18next'
10 | import { apiModeToModelName, modelNameToDesc } from '../utils/model-name-convert.mjs'
11 |
12 | export async function getChatGptAccessToken() {
13 | await clearOldAccessToken()
14 | const userConfig = await getUserConfig()
15 | if (userConfig.accessToken) {
16 | return userConfig.accessToken
17 | } else {
18 | const cookie = (await Browser.cookies.getAll({ url: 'https://chatgpt.com/' }))
19 | .map((cookie) => {
20 | return `${cookie.name}=${cookie.value}`
21 | })
22 | .join('; ')
23 | const resp = await fetch('https://chatgpt.com/api/auth/session', {
24 | headers: {
25 | Cookie: cookie,
26 | },
27 | })
28 | if (resp.status === 403) {
29 | throw new Error('CLOUDFLARE')
30 | }
31 | const data = await resp.json().catch(() => ({}))
32 | if (!data.accessToken) {
33 | throw new Error('UNAUTHORIZED')
34 | }
35 | await setAccessToken(data.accessToken)
36 | return data.accessToken
37 | }
38 | }
39 |
40 | export async function getBingAccessToken() {
41 | return (await Browser.cookies.get({ url: 'https://bing.com/', name: '_U' }))?.value
42 | }
43 |
44 | export async function getBardCookies() {
45 | const token = (await Browser.cookies.get({ url: 'https://google.com/', name: '__Secure-1PSID' }))
46 | ?.value
47 | return '__Secure-1PSID=' + token
48 | }
49 |
50 | export async function getClaudeSessionKey() {
51 | return (await Browser.cookies.get({ url: 'https://claude.ai/', name: 'sessionKey' }))?.value
52 | }
53 |
54 | export function handlePortError(session, port, err) {
55 | console.error(err)
56 | if (err.message) {
57 | if (!err.message.includes('aborted')) {
58 | if (
59 | ['message you submitted was too long', 'maximum context length'].some((m) =>
60 | err.message.includes(m),
61 | )
62 | )
63 | port.postMessage({ error: t('Exceeded maximum context length') + '\n\n' + err.message })
64 | else if (['CaptchaChallenge', 'CAPTCHA'].some((m) => err.message.includes(m)))
65 | port.postMessage({ error: t('Bing CaptchaChallenge') + '\n\n' + err.message })
66 | else if (['exceeded your current quota'].some((m) => err.message.includes(m)))
67 | port.postMessage({ error: t('Exceeded quota') + '\n\n' + err.message })
68 | else if (['Rate limit reached'].some((m) => err.message.includes(m)))
69 | port.postMessage({ error: t('Rate limit') + '\n\n' + err.message })
70 | else if (['authentication token has expired'].some((m) => err.message.includes(m)))
71 | port.postMessage({ error: 'UNAUTHORIZED' })
72 | else if (
73 | isUsingClaudeWebModel(session) &&
74 | ['Invalid authorization', 'Session key required'].some((m) => err.message.includes(m))
75 | )
76 | port.postMessage({
77 | error: t('Please login at https://claude.ai first, and then click the retry button'),
78 | })
79 | else if (
80 | isUsingBingWebModel(session) &&
81 | ['/turing/conversation/create: failed to parse response body.'].some((m) =>
82 | err.message.includes(m),
83 | )
84 | )
85 | port.postMessage({ error: t('Please login at https://bing.com first') })
86 | else port.postMessage({ error: err.message })
87 | }
88 | } else {
89 | const errMsg = JSON.stringify(err)
90 | if (isUsingBingWebModel(session) && errMsg.includes('isTrusted'))
91 | port.postMessage({ error: t('Please login at https://bing.com first') })
92 | else port.postMessage({ error: errMsg ?? 'unknown error' })
93 | }
94 | }
95 |
96 | export function registerPortListener(executor) {
97 | Browser.runtime.onConnect.addListener((port) => {
98 | console.debug('connected')
99 | const onMessage = async (msg) => {
100 | console.debug('received msg', msg)
101 | const session = msg.session
102 | if (!session) return
103 | const config = await getUserConfig()
104 | if (!session.modelName) session.modelName = config.modelName
105 | if (!session.apiMode && session.modelName !== 'customModel') session.apiMode = config.apiMode
106 | if (!session.aiName)
107 | session.aiName = modelNameToDesc(
108 | session.apiMode ? apiModeToModelName(session.apiMode) : session.modelName,
109 | t,
110 | config.customModelName,
111 | )
112 | port.postMessage({ session })
113 | try {
114 | await executor(session, port, config)
115 | } catch (err) {
116 | handlePortError(session, port, err)
117 | }
118 | }
119 |
120 | const onDisconnect = () => {
121 | console.debug('port disconnected, remove listener')
122 | port.onMessage.removeListener(onMessage)
123 | port.onDisconnect.removeListener(onDisconnect)
124 | }
125 |
126 | port.onMessage.addListener(onMessage)
127 | port.onDisconnect.addListener(onDisconnect)
128 | })
129 | }
130 |
--------------------------------------------------------------------------------
/src/utils/change-children-font-size.mjs:
--------------------------------------------------------------------------------
1 | export function changeChildrenFontSize(element, size) {
2 | try {
3 | element.style.fontSize = size
4 | } catch {
5 | /* empty */
6 | }
7 | for (let i = 0; i < element.childNodes.length; i++) {
8 | changeChildrenFontSize(element.childNodes[i], size)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/create-element-at-position.mjs:
--------------------------------------------------------------------------------
1 | export function createElementAtPosition(x = 0, y = 0, zIndex = 2147483647) {
2 | const element = document.createElement('div')
3 | element.style.position = 'fixed'
4 | element.style.zIndex = zIndex
5 | element.style.left = x + 'px'
6 | element.style.top = y + 'px'
7 | document.documentElement.appendChild(element)
8 | return element
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/crop-text.mjs:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2023 josStorer
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 |
23 | import { encode } from '@nem035/gpt-3-encoder'
24 | import { getUserConfig } from '../config/index.mjs'
25 | import { apiModeToModelName, modelNameToDesc } from './model-name-convert.mjs'
26 |
27 | const clamp = (v, min, max) => {
28 | return Math.min(Math.max(v, min), max)
29 | }
30 |
31 | export async function cropText(
32 | text,
33 | maxLength = 4000,
34 | startLength = 400,
35 | endLength = 300,
36 | tiktoken = true,
37 | ) {
38 | const userConfig = await getUserConfig()
39 | const k = modelNameToDesc(
40 | userConfig.apiMode ? apiModeToModelName(userConfig.apiMode) : userConfig.modelName,
41 | null,
42 | userConfig.customModelName,
43 | ).match(/[- (]*([0-9]+)k/)?.[1]
44 | if (k) {
45 | maxLength = Number(k) * 1000
46 | maxLength -= 100 + clamp(userConfig.maxResponseTokenLength, 1, maxLength - 1000)
47 | } else {
48 | maxLength -= 100 + clamp(userConfig.maxResponseTokenLength, 1, maxLength - 1000)
49 | }
50 |
51 | const splits = text.split(/[,,。??!!;;]/).map((s) => s.trim())
52 | const splitsLength = splits.map((s) => (tiktoken ? encode(s).length : s.length))
53 | const length = splitsLength.reduce((sum, length) => sum + length, 0)
54 |
55 | const cropLength = length - startLength - endLength
56 | const cropTargetLength = maxLength - startLength - endLength
57 | const cropPercentage = cropTargetLength / cropLength
58 | const cropStep = Math.max(0, 1 / cropPercentage - 1)
59 |
60 | if (cropStep === 0) return text
61 |
62 | let croppedText = ''
63 | let currentLength = 0
64 | let currentIndex = 0
65 | let currentStep = 0
66 |
67 | for (; currentIndex < splits.length; currentIndex++) {
68 | if (currentLength + splitsLength[currentIndex] + 1 <= startLength) {
69 | croppedText += splits[currentIndex] + ','
70 | currentLength += splitsLength[currentIndex] + 1
71 | } else if (currentLength + splitsLength[currentIndex] + 1 + endLength <= maxLength) {
72 | if (currentStep < cropStep) {
73 | currentStep++
74 | } else {
75 | croppedText += splits[currentIndex] + ','
76 | currentLength += splitsLength[currentIndex] + 1
77 | currentStep = currentStep - cropStep
78 | }
79 | } else {
80 | break
81 | }
82 | }
83 |
84 | let endPart = ''
85 | let endPartLength = 0
86 | for (let i = splits.length - 1; endPartLength + splitsLength[i] <= endLength; i--) {
87 | endPart = splits[i] + ',' + endPart
88 | endPartLength += splitsLength[i] + 1
89 | }
90 | currentLength += endPartLength
91 | croppedText += endPart
92 |
93 | console.log(
94 | `input maxLength: ${maxLength}\n` +
95 | `maxResponseTokenLength: ${userConfig.maxResponseTokenLength}\n` +
96 | // `croppedTextLength: ${tiktoken ? encode(croppedText).length : croppedText.length}\n` +
97 | `desiredLength: ${currentLength}\n` +
98 | `content: ${croppedText}`,
99 | )
100 | return croppedText
101 | }
102 |
--------------------------------------------------------------------------------
/src/utils/ends-with-question-mark.mjs:
--------------------------------------------------------------------------------
1 | export function endsWithQuestionMark(question) {
2 | return (
3 | question.endsWith('?') || // ASCII
4 | question.endsWith('?') || // Chinese/Japanese
5 | question.endsWith('؟') || // Arabic
6 | question.endsWith('⸮') // Arabic
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/eventsource-parser.mjs:
--------------------------------------------------------------------------------
1 | // https://www.npmjs.com/package/eventsource-parser/v/1.1.1
2 |
3 | function createParser(onParse) {
4 | let isFirstChunk
5 | let bytes
6 | let buffer
7 | let startingPosition
8 | let startingFieldLength
9 | let eventId
10 | let eventName
11 | let data
12 | let extra
13 | reset()
14 | return {
15 | feed,
16 | reset,
17 | }
18 | function reset() {
19 | isFirstChunk = true
20 | bytes = []
21 | buffer = ''
22 | startingPosition = 0
23 | startingFieldLength = -1
24 | eventId = void 0
25 | eventName = void 0
26 | data = ''
27 | }
28 |
29 | function feed(chunk) {
30 | bytes = bytes.concat(Array.from(chunk))
31 | buffer = new TextDecoder().decode(new Uint8Array(bytes))
32 | if (isFirstChunk && hasBom(buffer)) {
33 | buffer = buffer.slice(BOM.length)
34 | }
35 | isFirstChunk = false
36 | const length = buffer.length
37 | let position = 0
38 | let discardTrailingNewline = false
39 | while (position < length) {
40 | if (discardTrailingNewline) {
41 | if (buffer[position] === '\n') {
42 | ++position
43 | }
44 | discardTrailingNewline = false
45 | }
46 | let lineLength = -1
47 | let fieldLength = startingFieldLength
48 | let character
49 | for (let index = startingPosition; lineLength < 0 && index < length; ++index) {
50 | character = buffer[index]
51 | if (character === ':' && fieldLength < 0) {
52 | fieldLength = index - position
53 | } else if (character === '\r') {
54 | discardTrailingNewline = true
55 | lineLength = index - position
56 | } else if (character === '\n') {
57 | lineLength = index - position
58 | }
59 | }
60 | if (lineLength < 0) {
61 | startingPosition = length - position
62 | startingFieldLength = fieldLength
63 | break
64 | } else {
65 | startingPosition = 0
66 | startingFieldLength = -1
67 | }
68 | parseEventStreamLine(buffer, position, fieldLength, lineLength)
69 | position += lineLength + 1
70 | }
71 | if (position === length) {
72 | bytes = []
73 | buffer = ''
74 | } else if (position > 0) {
75 | bytes = bytes.slice(new TextEncoder().encode(buffer.slice(0, position)).length)
76 | buffer = buffer.slice(position)
77 | }
78 | }
79 |
80 | function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
81 | if (lineLength === 0) {
82 | if (data.length > 0 || extra) {
83 | onParse({
84 | type: 'event',
85 | id: eventId,
86 | event: eventName || void 0,
87 | data: data.slice(0, -1),
88 | extra: extra || void 0,
89 | // remove trailing newline
90 | })
91 |
92 | data = ''
93 | eventId = void 0
94 | extra = void 0
95 | }
96 | eventName = void 0
97 | return
98 | }
99 | const noValue = fieldLength < 0
100 | const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength))
101 | let step = 0
102 | if (noValue) {
103 | step = lineLength
104 | } else if (lineBuffer[index + fieldLength + 1] === ' ') {
105 | step = fieldLength + 2
106 | } else {
107 | step = fieldLength + 1
108 | }
109 | const position = index + step
110 | const valueLength = lineLength - step
111 | const value = lineBuffer.slice(position, position + valueLength).toString()
112 | if (field === 'data') {
113 | data += value ? ''.concat(value, '\n') : '\n'
114 | } else if (field === 'event') {
115 | eventName = value
116 | } else if (field === 'id' && !value.includes('\0')) {
117 | eventId = value
118 | } else if (field === 'retry') {
119 | const retry = parseInt(value, 10)
120 | if (!Number.isNaN(retry)) {
121 | onParse({
122 | type: 'reconnect-interval',
123 | value: retry,
124 | })
125 | }
126 | } else if (field === 'meta') {
127 | const str = `{"${field}":${value}}`
128 | extra = extra ?? []
129 | extra.push(JSON.parse(str))
130 | }
131 | }
132 | }
133 | const BOM = [239, 187, 191]
134 | function hasBom(buffer) {
135 | return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode)
136 | }
137 | export { createParser }
138 |
--------------------------------------------------------------------------------
/src/utils/fetch-bg.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 | /**
4 | * @param {RequestInfo|URL} input
5 | * @param {RequestInit=} init
6 | * @returns {Promise}
7 | */
8 | export function fetchBg(input, init) {
9 | return new Promise((resolve, reject) => {
10 | Browser.runtime
11 | .sendMessage({
12 | type: 'FETCH',
13 | data: { input, init },
14 | })
15 | .then((messageResponse) => {
16 | const [response, error] = messageResponse
17 | if (response === null) {
18 | reject(error)
19 | } else {
20 | const body = response.body ? new Blob([response.body]) : undefined
21 | resolve(
22 | new Response(body, {
23 | status: response.status,
24 | statusText: response.statusText,
25 | headers: new Headers(response.headers),
26 | }),
27 | )
28 | }
29 | })
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/fetch-sse.mjs:
--------------------------------------------------------------------------------
1 | import { createParser } from './eventsource-parser.mjs'
2 |
3 | export async function fetchSSE(resource, options) {
4 | const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options
5 | const resp = await fetch(resource, fetchOptions).catch(async (err) => {
6 | await onError(err)
7 | })
8 | if (!resp) return
9 | if (!resp.ok) {
10 | await onError(resp)
11 | return
12 | }
13 | const parser = createParser((event) => {
14 | if (event.type === 'event') {
15 | onMessage(event.data)
16 | }
17 | })
18 | let hasStarted = false
19 | const reader = resp.body.getReader()
20 | let result
21 | while (!(result = await reader.read()).done) {
22 | const chunk = result.value
23 | if (!hasStarted) {
24 | const str = new TextDecoder().decode(chunk)
25 | hasStarted = true
26 | await onStart(str)
27 |
28 | let fakeSseData
29 | try {
30 | const commonResponse = JSON.parse(str)
31 | fakeSseData = 'data: ' + JSON.stringify(commonResponse) + '\n\ndata: [DONE]\n\n'
32 | } catch (error) {
33 | console.debug('not common response', error)
34 | }
35 | if (fakeSseData) {
36 | parser.feed(new TextEncoder().encode(fakeSseData))
37 | break
38 | }
39 | }
40 | parser.feed(chunk)
41 | }
42 | await onEnd()
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/get-client-position.mjs:
--------------------------------------------------------------------------------
1 | export function getClientPosition(e) {
2 | const rect = e.getBoundingClientRect()
3 | return { x: rect.left, y: rect.top }
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/get-conversation-pairs.mjs:
--------------------------------------------------------------------------------
1 | export function getConversationPairs(records, isCompletion) {
2 | let pairs
3 | if (isCompletion) {
4 | pairs = ''
5 | for (const record of records) {
6 | pairs += 'Human: ' + record.question + '\nAI: ' + record.answer + '\n'
7 | }
8 | } else {
9 | pairs = []
10 | for (const record of records) {
11 | pairs.push({ role: 'user', content: record['question'] })
12 | pairs.push({ role: 'assistant', content: record['answer'] })
13 | }
14 | }
15 |
16 | return pairs
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/get-core-content-text.mjs:
--------------------------------------------------------------------------------
1 | import { getPossibleElementByQuerySelector } from './get-possible-element-by-query-selector.mjs'
2 | import { Readability, isProbablyReaderable } from '@mozilla/readability'
3 |
4 | const adapters = {
5 | 'scholar.google': ['#gs_res_ccl_mid'],
6 | google: ['#search'],
7 | csdn: ['#content_views'],
8 | bing: ['#b_results'],
9 | wikipedia: ['#mw-content-text'],
10 | faz: ['.atc-Text'],
11 | golem: ['article'],
12 | eetimes: ['article'],
13 | 'new.qq.com': ['.content-article'],
14 | }
15 |
16 | function getArea(e) {
17 | const rect = e.getBoundingClientRect()
18 | return rect.width * rect.height
19 | }
20 |
21 | function findLargestElement(e) {
22 | if (!e) {
23 | return null
24 | }
25 | let maxArea = 0
26 | let largestElement = null
27 | const limitedArea = 0.8 * getArea(e)
28 |
29 | function traverseDOM(node) {
30 | if (node.nodeType === Node.ELEMENT_NODE) {
31 | const area = getArea(node)
32 |
33 | if (area > maxArea && area < limitedArea) {
34 | maxArea = area
35 | largestElement = node
36 | }
37 |
38 | Array.from(node.children).forEach(traverseDOM)
39 | }
40 | }
41 |
42 | traverseDOM(e)
43 | return largestElement
44 | }
45 |
46 | function getTextFrom(e) {
47 | return e.innerText || e.textContent
48 | }
49 |
50 | function postProcessText(text) {
51 | return text
52 | .trim()
53 | .replaceAll(' ', '')
54 | .replaceAll('\t', '')
55 | .replaceAll('\n\n', '')
56 | .replaceAll(',,', '')
57 | }
58 |
59 | export function getCoreContentText() {
60 | for (const [siteName, selectors] of Object.entries(adapters)) {
61 | if (location.hostname.includes(siteName)) {
62 | const element = getPossibleElementByQuerySelector(selectors)
63 | if (element) return postProcessText(getTextFrom(element))
64 | break
65 | }
66 | }
67 |
68 | const element = document.querySelector('article')
69 | if (element) {
70 | return postProcessText(getTextFrom(element))
71 | }
72 |
73 | if (isProbablyReaderable(document)) {
74 | let article = new Readability(document.cloneNode(true), {
75 | keepClasses: true,
76 | }).parse()
77 | console.log('readerable')
78 | return postProcessText(article.textContent)
79 | }
80 |
81 | const largestElement = findLargestElement(document.body)
82 | const secondLargestElement = findLargestElement(largestElement)
83 | console.log(largestElement)
84 | console.log(secondLargestElement)
85 |
86 | let ret
87 | if (!largestElement) {
88 | ret = getTextFrom(document.body)
89 | console.log('use document.body')
90 | } else if (
91 | secondLargestElement &&
92 | getArea(secondLargestElement) > 0.5 * getArea(largestElement)
93 | ) {
94 | ret = getTextFrom(secondLargestElement)
95 | console.log('use second')
96 | } else {
97 | ret = getTextFrom(largestElement)
98 | console.log('use first')
99 | }
100 | return postProcessText(ret)
101 | }
102 |
--------------------------------------------------------------------------------
/src/utils/get-possible-element-by-query-selector.mjs:
--------------------------------------------------------------------------------
1 | export function getPossibleElementByQuerySelector(queryArray) {
2 | if (!queryArray) return
3 | for (const query of queryArray) {
4 | if (query) {
5 | try {
6 | const element = document.querySelector(query)
7 | if (element) {
8 | return element
9 | }
10 | } catch (e) {
11 | /* empty */
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/index.mjs:
--------------------------------------------------------------------------------
1 | export * from './change-children-font-size'
2 | export * from './create-element-at-position'
3 | export * from './crop-text'
4 | export * from './ends-with-question-mark'
5 | export * from './fetch-sse'
6 | export * from './get-client-position'
7 | export * from './get-conversation-pairs'
8 | export * from './get-core-content-text'
9 | export * from './get-possible-element-by-query-selector'
10 | export * from './is-edge'
11 | export * from './is-firefox'
12 | export * from './is-mobile'
13 | export * from './is-safari'
14 | export * from './limited-fetch'
15 | export * from './open-url'
16 | export * from './parse-float-with-clamp'
17 | export * from './parse-int-with-clamp'
18 | export * from './set-element-position-in-viewport'
19 | export * from './eventsource-parser.mjs'
20 | export * from './update-ref-height'
21 | export * from './wait-for-element-to-exist-and-select.mjs'
22 | export * from './model-name-convert.mjs'
23 |
--------------------------------------------------------------------------------
/src/utils/is-edge.mjs:
--------------------------------------------------------------------------------
1 | export function isEdge() {
2 | return navigator.userAgent.toLowerCase().includes('edg')
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/is-firefox.mjs:
--------------------------------------------------------------------------------
1 | export function isFirefox() {
2 | return navigator.userAgent.toLowerCase().includes('firefox')
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/is-mobile.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
2 |
3 | export function isMobile() {
4 | if (navigator.userAgentData) return navigator.userAgentData.mobile
5 | let check = false
6 | ;(function (a) {
7 | if (
8 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
9 | a,
10 | ) ||
11 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
12 | a.substr(0, 4),
13 | )
14 | )
15 | check = true
16 | })(navigator.userAgent || navigator.vendor || window.opera)
17 | return check
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/is-safari.mjs:
--------------------------------------------------------------------------------
1 | export function isSafari() {
2 | return navigator.vendor === 'Apple Computer, Inc.'
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/jwt-token-generator.mjs:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 |
3 | let jwtToken = null
4 | let tokenExpiration = null // Declare tokenExpiration in the module scope
5 |
6 | function generateToken(apiKey, timeoutSeconds) {
7 | const parts = apiKey.split('.')
8 | if (parts.length !== 2) {
9 | throw new Error('Invalid API key')
10 | }
11 |
12 | const ms = Date.now()
13 | const currentSeconds = Math.floor(ms / 1000)
14 | const [id, secret] = parts
15 | const payload = {
16 | api_key: id,
17 | exp: currentSeconds + timeoutSeconds,
18 | timestamp: currentSeconds,
19 | }
20 |
21 | jwtToken = jwt.sign(payload, secret, {
22 | header: {
23 | alg: 'HS256',
24 | typ: 'JWT',
25 | sign_type: 'SIGN',
26 | },
27 | })
28 | tokenExpiration = ms + timeoutSeconds * 1000
29 | }
30 |
31 | function shouldRegenerateToken() {
32 | const ms = Date.now()
33 | return !jwtToken || ms >= tokenExpiration
34 | }
35 |
36 | function getToken(apiKey) {
37 | if (shouldRegenerateToken()) {
38 | generateToken(apiKey, 86400) // Hard-coded to regenerate the token every 24 hours
39 | }
40 | return jwtToken
41 | }
42 |
43 | export { getToken }
44 |
--------------------------------------------------------------------------------
/src/utils/limited-fetch.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/64304365/stop-request-after-x-amount-is-fetched
2 |
3 | export async function limitedFetch(url, maxBytes) {
4 | return new Promise((resolve, reject) => {
5 | try {
6 | const xhr = new XMLHttpRequest()
7 | xhr.onprogress = (ev) => {
8 | if (ev.loaded < maxBytes) return
9 | resolve(ev.target.responseText.substring(0, maxBytes))
10 | xhr.abort()
11 | }
12 | xhr.onload = (ev) => {
13 | resolve(ev.target.responseText.substring(0, maxBytes))
14 | }
15 | xhr.onerror = (ev) => {
16 | reject(new Error(ev.target.status))
17 | }
18 |
19 | xhr.open('GET', url)
20 | xhr.send()
21 | } catch (err) {
22 | reject(err)
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/model-name-convert.mjs:
--------------------------------------------------------------------------------
1 | import { AlwaysCustomGroups, ModelGroups, ModelMode, Models } from '../config/index.mjs'
2 |
3 | export function modelNameToDesc(modelName, t, extraCustomModelName = '') {
4 | if (!t) t = (x) => x
5 | if (modelName in Models) {
6 | const desc = t(Models[modelName].desc)
7 | if (modelName === 'customModel' && extraCustomModelName)
8 | return `${desc} (${extraCustomModelName})`
9 | return desc
10 | }
11 |
12 | let desc = modelName
13 | if (isCustomModelName(modelName)) {
14 | const presetPart = modelNameToPresetPart(modelName)
15 | const customPart = modelNameToCustomPart(modelName)
16 | if (presetPart in Models) {
17 | if (customPart in ModelMode)
18 | desc = `${t(Models[presetPart].desc)} (${t(ModelMode[customPart])})`
19 | else desc = `${t(Models[presetPart].desc)} (${customPart})`
20 | } else if (presetPart in ModelGroups) {
21 | desc = `${t(ModelGroups[presetPart].desc)} (${customPart})`
22 | }
23 | }
24 | return desc
25 | }
26 |
27 | export function modelNameToPresetPart(modelName) {
28 | if (isCustomModelName(modelName)) {
29 | return modelName.split('-')[0]
30 | } else {
31 | return modelName
32 | }
33 | }
34 |
35 | export function modelNameToCustomPart(modelName) {
36 | if (isCustomModelName(modelName)) {
37 | return modelName.substring(modelName.indexOf('-') + 1)
38 | } else {
39 | return modelName
40 | }
41 | }
42 |
43 | export function modelNameToValue(modelName) {
44 | if (modelName in Models) return Models[modelName].value
45 |
46 | return modelNameToCustomPart(modelName)
47 | }
48 |
49 | export function getModelValue(configOrSession) {
50 | let value
51 | if (configOrSession.apiMode) value = modelNameToValue(apiModeToModelName(configOrSession.apiMode))
52 | else value = modelNameToValue(configOrSession.modelName)
53 | return value
54 | }
55 |
56 | export function isCustomModelName(modelName) {
57 | return modelName ? modelName.includes('-') : false
58 | }
59 |
60 | export function modelNameToApiMode(modelName) {
61 | const presetPart = modelNameToPresetPart(modelName)
62 | const found = getModelNameGroup(presetPart)
63 | if (found) {
64 | const [groupName] = found
65 | const isCustom = isCustomModelName(modelName)
66 | let customName = ''
67 | if (isCustom) customName = modelNameToCustomPart(modelName)
68 | return {
69 | groupName,
70 | itemName: presetPart,
71 | isCustom,
72 | customName,
73 | customUrl: '',
74 | apiKey: '',
75 | active: true,
76 | }
77 | }
78 | }
79 |
80 | export function apiModeToModelName(apiMode) {
81 | if (AlwaysCustomGroups.includes(apiMode.groupName))
82 | return apiMode.groupName + '-' + apiMode.customName
83 |
84 | if (apiMode.isCustom) {
85 | if (apiMode.itemName === 'custom') return apiMode.groupName + '-' + apiMode.customName
86 | return apiMode.itemName + '-' + apiMode.customName
87 | }
88 |
89 | return apiMode.itemName
90 | }
91 |
92 | export function getApiModesFromConfig(config, onlyActive) {
93 | const stringApiModes = config.customApiModes
94 | .map((apiMode) => {
95 | if (onlyActive) {
96 | if (apiMode.active) return apiModeToModelName(apiMode)
97 | } else return apiModeToModelName(apiMode)
98 | return false
99 | })
100 | .filter((apiMode) => apiMode)
101 | const originalApiModes = config.activeApiModes
102 | .map((modelName) => {
103 | // 'customModel' is always active
104 | if (stringApiModes.includes(modelName) || modelName === 'customModel') {
105 | return
106 | }
107 | if (modelName === 'azureOpenAi') modelName += '-' + config.azureDeploymentName
108 | if (modelName === 'ollama') modelName += '-' + config.ollamaModelName
109 | return modelNameToApiMode(modelName)
110 | })
111 | .filter((apiMode) => apiMode)
112 | return [
113 | ...originalApiModes,
114 | ...config.customApiModes.filter((apiMode) => (onlyActive ? apiMode.active : true)),
115 | ]
116 | }
117 |
118 | export function getApiModesStringArrayFromConfig(config, onlyActive) {
119 | return getApiModesFromConfig(config, onlyActive).map(apiModeToModelName)
120 | }
121 |
122 | export function isApiModeSelected(apiMode, configOrSession) {
123 | return configOrSession.apiMode
124 | ? JSON.stringify(configOrSession.apiMode) === JSON.stringify(apiMode)
125 | : configOrSession.modelName === apiModeToModelName(apiMode)
126 | }
127 |
128 | // also match custom modelName, e.g. when modelName is bingFree4, configOrSession model is bingFree4-fast, it returns true
129 | export function isUsingModelName(modelName, configOrSession) {
130 | let configOrSessionModelName = configOrSession.apiMode
131 | ? apiModeToModelName(configOrSession.apiMode)
132 | : configOrSession.modelName
133 | if (modelName === configOrSessionModelName) {
134 | return true
135 | }
136 |
137 | if (isCustomModelName(configOrSessionModelName)) {
138 | const presetPart = modelNameToPresetPart(configOrSessionModelName)
139 | if (presetPart in Models) configOrSessionModelName = presetPart
140 | else if (presetPart in ModelGroups) configOrSessionModelName = ModelGroups[presetPart].value[0]
141 | }
142 | return configOrSessionModelName === modelName
143 | }
144 |
145 | export function getModelNameGroup(modelName) {
146 | const presetPart = modelNameToPresetPart(modelName)
147 | return (
148 | Object.entries(ModelGroups).find(([k]) => presetPart === k) ||
149 | Object.entries(ModelGroups).find(([, g]) => g.value.includes(presetPart))
150 | )
151 | }
152 |
153 | export function getApiModeGroup(apiMode) {
154 | return getModelNameGroup(apiModeToModelName(apiMode))
155 | }
156 |
157 | export function isInApiModeGroup(apiModeGroup, configOrSession) {
158 | let foundGroup
159 | if (configOrSession.apiMode) foundGroup = getApiModeGroup(configOrSession.apiMode)
160 | else foundGroup = getModelNameGroup(configOrSession.modelName)
161 |
162 | if (!foundGroup) return false
163 | const [, { value: groupValue }] = foundGroup
164 | return groupValue === apiModeGroup
165 | }
166 |
--------------------------------------------------------------------------------
/src/utils/open-url.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 | export function openUrl(url) {
4 | Browser.tabs.query({ url, currentWindow: true }).then((tabs) => {
5 | if (tabs.length > 0) {
6 | Browser.tabs.update(tabs[0].id, { active: true })
7 | } else {
8 | Browser.tabs.create({ url })
9 | }
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/parse-float-with-clamp.mjs:
--------------------------------------------------------------------------------
1 | export function parseFloatWithClamp(value, defaultValue, min, max) {
2 | value = parseFloat(value)
3 |
4 | if (isNaN(value)) value = defaultValue
5 | else if (value > max) value = max
6 | else if (value < min) value = min
7 |
8 | return value
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/parse-int-with-clamp.mjs:
--------------------------------------------------------------------------------
1 | export function parseIntWithClamp(value, defaultValue, min, max) {
2 | value = parseInt(value)
3 |
4 | if (isNaN(value)) value = defaultValue
5 | else if (value > max) value = max
6 | else if (value < min) value = min
7 |
8 | return value
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/set-element-position-in-viewport.mjs:
--------------------------------------------------------------------------------
1 | export function setElementPositionInViewport(element, x = 0, y = 0) {
2 | const retX = Math.min(Math.max(0, window.innerWidth - element.offsetWidth), Math.max(0, x))
3 | const retY = Math.min(Math.max(0, window.innerHeight - element.offsetHeight), Math.max(0, y))
4 | element.style.left = retX + 'px'
5 | element.style.top = retY + 'px'
6 | return { x: retX, y: retY }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/update-ref-height.mjs:
--------------------------------------------------------------------------------
1 | export function updateRefHeight(ref) {
2 | ref.current.style.height = 'auto'
3 | const computed = window.getComputedStyle(ref.current)
4 | const height =
5 | parseInt(computed.getPropertyValue('border-top-width'), 10) +
6 | parseInt(computed.getPropertyValue('padding-top'), 10) +
7 | ref.current.scrollHeight +
8 | parseInt(computed.getPropertyValue('padding-bottom'), 10) +
9 | parseInt(computed.getPropertyValue('border-bottom-width'), 10)
10 | ref.current.style.height = `${height}px`
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/wait-for-element-to-exist-and-select.mjs:
--------------------------------------------------------------------------------
1 | export function waitForElementToExistAndSelect(selector, timeout = 0) {
2 | return new Promise((resolve) => {
3 | if (document.querySelector(selector)) {
4 | return resolve(document.querySelector(selector))
5 | }
6 |
7 | const observer = new MutationObserver(() => {
8 | if (document.querySelector(selector)) {
9 | resolve(document.querySelector(selector))
10 | observer.disconnect()
11 | }
12 | })
13 |
14 | observer.observe(document.body, {
15 | subtree: true,
16 | childList: true,
17 | })
18 |
19 | if (timeout)
20 | setTimeout(() => {
21 | observer.disconnect()
22 | resolve(null)
23 | }, timeout)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------