├── .cursorrules ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature.yml ├── dependabot.yml └── workflows │ ├── lint.yaml │ ├── playwright.yml │ ├── release.yaml │ ├── test-build.yaml │ ├── unit-test.yaml │ └── winget.yml ├── .gitignore ├── .plasmo ├── cache │ └── parcel │ │ ├── data.mdb │ │ └── lock.mdb └── chrome-mv3.plasmo.manifest.json ├── .prettierrc.js ├── 8.gif ├── CHANGELOG-CN.md ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README-CN.md ├── README.md ├── README.md.backup ├── clip-extensions ├── popclip │ ├── Config.plist │ ├── icon.png │ └── openai-translator.sh └── snipdo │ ├── icon.png │ ├── openai-translator.json │ └── openai-translator.ps1 ├── e2e ├── common.ts ├── fixtures.ts ├── hotkey.spec.ts ├── index.spec.ts └── test.html ├── jest.config.js ├── make ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── public ├── 8.gif ├── icon.png ├── image-1.png ├── image-10.png ├── image-11.png ├── image-12.png ├── image-13.png ├── image-14.png ├── image-15.png ├── image-2.png ├── image-3.png ├── image-4.png ├── image-5.png ├── image-6.png ├── image-7.png ├── image-8.png ├── image-9.png └── rules.json ├── readingMode_1.gif ├── scripts └── release.py ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── favicon.ico │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── resources │ ├── bin │ │ ├── ocr_apple │ │ └── ocr_intel │ ├── get-selected-text-by-ax.applescript │ └── get-selected-text.applescript ├── src │ ├── config.rs │ ├── hotkey.rs │ ├── lang.rs │ ├── main.rs │ ├── ocr.rs │ ├── tray.rs │ ├── utils.rs │ └── windows.rs └── tauri.conf.json ├── src ├── browser-extension │ ├── background │ │ └── index.ts │ ├── content_script │ │ ├── consts.ts │ │ ├── index.tsx │ │ └── utils.ts │ ├── manifest.firefox.json │ ├── manifest.json │ ├── options │ │ ├── index.css │ │ ├── index.html │ │ └── index.tsx │ ├── popup │ │ ├── index.css │ │ ├── index.html │ │ └── index.tsx │ └── youglish │ │ └── youglish.html ├── common │ ├── AnswerTabs.tsx │ ├── __tests__ │ │ └── translate.test.ts │ ├── analysis.ts │ ├── anki │ │ └── anki-connect.js │ ├── arkose │ │ ├── generator.js │ │ └── index.ts │ ├── assets │ │ ├── gif │ │ │ └── drag_to_resize.gif │ │ └── images │ │ │ ├── beams.jpg │ │ │ ├── chatglm.svg │ │ │ ├── claude.svg │ │ │ ├── deepseek.svg │ │ │ ├── groq.svg │ │ │ ├── icon-large.png │ │ │ ├── icon.png │ │ │ ├── kimi.svg │ │ │ ├── moonshot-dark.png │ │ │ ├── moonshot-light.png │ │ │ ├── ollama.svg │ │ │ ├── openrouter.png │ │ │ ├── party-popper.gif │ │ │ └── rocket.gif │ ├── background │ │ ├── eventnames.ts │ │ ├── fetch.ts │ │ └── services │ │ │ ├── arkoseToken.ts │ │ │ ├── base.ts │ │ │ └── vocabulary.ts │ ├── components │ │ ├── ActionForm.tsx │ │ ├── ActionGroupForm.tsx │ │ ├── ActionList.tsx │ │ ├── ActionManager.tsx │ │ ├── ActionStore.tsx │ │ ├── AnswerManager.tsx │ │ ├── AppTutorial.tsx │ │ ├── AuthModal.tsx │ │ ├── CategorySelector.tsx │ │ ├── CheckBox.tsx │ │ ├── ConversationView.tsx │ │ ├── CopyButton.tsx │ │ ├── ErrorFallback.tsx │ │ ├── Form │ │ │ ├── form.ts │ │ │ ├── index.module.css │ │ │ ├── index.ts │ │ │ ├── item.tsx │ │ │ ├── typings.ts │ │ │ └── validators.ts │ │ ├── GlobalSuspense.tsx │ │ ├── GroupSelect.tsx │ │ ├── IconPicker.tsx │ │ ├── IpLocationNotification.tsx │ │ ├── Markdown.tsx │ │ ├── MessageCard.tsx │ │ ├── ModelSelect.tsx │ │ ├── NumberInput.tsx │ │ ├── QuickActionBar.tsx │ │ ├── QuotePreview.tsx │ │ ├── RenderingFormatSelector.tsx │ │ ├── ReviewSettings.tsx │ │ ├── Settings.tsx │ │ ├── SpeakerMotion.tsx │ │ ├── StudyChart.tsx │ │ ├── SystemMessage.tsx │ │ ├── TextAreaWithActions.tsx │ │ ├── TextParser.tsx │ │ ├── Tooltip.tsx │ │ ├── Translator.tsx │ │ ├── WordBookViewer.tsx │ │ ├── WordListMenu.tsx │ │ ├── WordListUploader.tsx │ │ ├── icons │ │ │ ├── ChatGLMIcon.tsx │ │ │ ├── ClaudeIcon.tsx │ │ │ ├── DeepSeekIcon.tsx │ │ │ ├── GroqIcon.tsx │ │ │ ├── KimiIcon.tsx │ │ │ ├── MoonshotIcon.tsx │ │ │ ├── OllamaIcon.tsx │ │ │ ├── OneAPIIcon.tsx │ │ │ └── OpenRouterIcon.tsx │ │ └── lang │ │ │ ├── data.ts │ │ │ └── lang.ts │ ├── constants.ts │ ├── docs │ │ ├── actionstore.markdown │ │ └── autoCompletePrompt.markdown │ ├── engines │ │ ├── abstract-engine.ts │ │ ├── abstract-openai.ts │ │ ├── azure.ts │ │ ├── chatglm.ts │ │ ├── chatgpt.ts │ │ ├── claude.ts │ │ ├── deepseek.ts │ │ ├── gemini.ts │ │ ├── groq.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── kimi.ts │ │ ├── minimax.ts │ │ ├── moonshot.ts │ │ ├── ollama.ts │ │ ├── oneapi.ts │ │ ├── openai.ts │ │ └── openrouter.ts │ ├── geo-data.ts │ ├── geo.ts │ ├── highlight-in-textarea │ │ ├── index.css │ │ └── index.ts │ ├── hooks │ │ ├── global.ts │ │ ├── useAutoExpand.ts │ │ ├── useCollectedWordTotal.ts │ │ ├── useCurrentThemeType.ts │ │ ├── useMemoWindow.ts │ │ ├── useSettings.ts │ │ ├── useStoredAction.ts │ │ ├── useTheme.ts │ │ ├── useThemeDetector.ts │ │ └── useThemeType.ts │ ├── i18n.js │ ├── i18n │ │ └── locales │ │ │ ├── ar │ │ │ └── translation.json │ │ │ ├── de │ │ │ └── translation.json │ │ │ ├── en │ │ │ └── translation.json │ │ │ ├── fr │ │ │ └── translation.json │ │ │ ├── hi │ │ │ └── translation.json │ │ │ ├── ja │ │ │ └── translation.json │ │ │ ├── ko │ │ │ └── translation.json │ │ │ ├── ru │ │ │ └── translation.json │ │ │ ├── th │ │ │ └── translation.json │ │ │ ├── zh-Hans │ │ │ └── translation.json │ │ │ └── zh-Hant │ │ │ └── translation.json │ ├── internal-services │ │ ├── action.ts │ │ └── db.ts │ ├── polyfills │ │ ├── electron.ts │ │ ├── tauri.ts │ │ └── userscript.ts │ ├── services │ │ ├── Arabic.json │ │ ├── Chinese.json │ │ ├── English.json │ │ ├── French.json │ │ ├── German.json │ │ ├── Hindi.json │ │ ├── Japanese.json │ │ ├── Korean.json │ │ ├── Russian.json │ │ ├── Thai.json │ │ ├── TraditionalChinese.json │ │ ├── github.ts │ │ └── vocabulary.ts │ ├── state │ │ └── UserProvider.tsx │ ├── traditional-or-simplified.ts │ ├── translate.ts │ ├── tts │ │ ├── edge-tts.ts │ │ ├── index.ts │ │ └── types.ts │ ├── types.ts │ ├── universal-fetch.ts │ ├── utils.ts │ ├── utils │ │ └── format.ts │ └── youglish │ │ ├── widget.js │ │ └── youglish.jsx ├── const │ ├── layoutTokens.ts │ ├── message.ts │ └── meta.ts ├── hooks │ └── useClerkUser.ts ├── index.d.ts ├── openai-translator.lnk ├── store │ └── file │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── middleware │ │ ├── createDevtools.ts │ │ └── createHyperStorage │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── indexedDB.test.ts │ │ │ ├── indexedDB.ts │ │ │ ├── keyMapper.ts │ │ │ ├── localStorage.ts │ │ │ ├── type.ts │ │ │ ├── urlStorage.test.ts │ │ │ └── urlStorage.ts │ │ ├── selectors.ts │ │ ├── slices │ │ ├── action │ │ │ ├── action.ts │ │ │ └── initialState.ts │ │ ├── chat │ │ │ ├── action.ts │ │ │ └── initialState.ts │ │ ├── component │ │ │ ├── action.ts │ │ │ └── initialState.ts │ │ ├── file │ │ │ ├── action.ts │ │ │ ├── initialState.ts │ │ │ └── selectors.ts │ │ ├── user │ │ │ ├── action.ts │ │ │ └── initialState.ts │ │ └── word │ │ │ ├── __tests__ │ │ │ ├── addMessageToHistory.test.ts │ │ │ └── setupTests.ts │ │ │ ├── action.ts │ │ │ └── initialState.ts │ │ └── store.ts ├── tauri │ ├── App.tsx │ ├── Window.tsx │ ├── action_manager.html │ ├── action_manager.tsx │ ├── index.html │ ├── index.tsx │ ├── thumb.html │ ├── thumb.tsx │ └── utils.ts ├── types │ ├── agent │ │ └── index.ts │ ├── fetch.ts │ ├── llm.ts │ ├── meta.ts │ ├── openai │ │ ├── chat.ts │ │ ├── functionCall.ts │ │ ├── image.ts │ │ └── plugin.ts │ ├── session.ts │ ├── settings.ts │ └── topic.ts └── utils │ ├── auth.ts │ ├── merge.ts │ ├── storeDebug.ts │ ├── swr │ └── index.ts │ └── uuid.ts ├── tsconfig.json ├── typos.toml ├── vite ├── vite-env.d.ts ├── vite.config.chromium.ts ├── vite.config.firefox.ts ├── vite.config.tauri.ts └── vite.config.userscript.ts /.cursorrules: -------------------------------------------------------------------------------- 1 | ## 项目概述 2 | 3 | 这是一个基于 React + TypeScript 构建的浏览器扩展。 4 | 5 | ## 技术栈 6 | 7 | - 前端框架: React + TypeScript 8 | - 状态管理: Zustand 9 | - UI 组件: Base Web UI 10 | - 国际化: i18next 11 | - 存储: 本地存储 + IndexedDB、 12 | - 代码规范: ESLint + Prettier 13 | - 测试: Jest 14 | 15 | src/ 16 | ├── browser-extension/ # 浏览器扩展相关代码 17 | ├── common/ 18 | │ ├── components/ # 公共组件 19 | │ ├── engines/ # AI 引擎适配层 20 | │ ├── hooks/ # 自定义 Hooks 21 | │ ├── i18n/ # 国际化配置 22 | │ ├── internal-services/ # 内部服务 23 | │ └── store/ # 状态管理 24 | │ 25 | ├── public/ # 公共资源 26 | ├── types/ # 类型 27 | ├── utils/ # 工具函数 28 | └── vite.config.ts # Vite 配置 29 | └── jest.config.ts # Jest 配置 30 | 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | node: true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | project: true, 12 | tsconfigRootDir: __dirname, 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | }, 16 | root: true, 17 | plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier', 'baseui'], 18 | rules: { 19 | 'react/prop-types': 'off', 20 | 'react/react-in-jsx-scope': 'off', 21 | 'camelcase': 'off', 22 | 'react/display-name': 'off', 23 | 'eqeqeq': ['error', 'always'], 24 | 'spaced-comment': 'error', 25 | 'no-duplicate-imports': 'error', 26 | 'baseui/deprecated-theme-api': 'warn', 27 | 'baseui/deprecated-component-api': 'warn', 28 | 'baseui/no-deep-imports': 'warn', 29 | 'prettier/prettier': 'error', 30 | 'react-hooks/rules-of-hooks': 'error', 31 | 'react-hooks/exhaustive-deps': 'warn', 32 | }, 33 | settings: { 34 | 'import/resolver': { 35 | typescript: {}, 36 | }, 37 | 'react': { 38 | version: 'detect', 39 | }, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question or get support 4 | url: https://github.com/GPT-language/gpt-tutor-for-chrome/discussions/categories/q-a 5 | about: Ask a question or request support for GPT-Tutor 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | description: Add new feature, improve code, and more 3 | labels: [ "enhancement" ] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **非常感谢您的功能建议!Thank you very much for your feature proposal!** 9 | - type: checkboxes 10 | attributes: 11 | label: Search before asking 12 | description: > 13 | 请在提交前搜索 [issues](https://github.com/yetone/openai-translator/issues),以查看您的问题是否已经被提交。 14 | 15 | Please search [issues](https://github.com/yetone/openai-translator/issues) to check if your issue has already been reported. 16 | options: 17 | - label: > 18 | 在 [issues](https://github.com/yetone/openai-translator/issues) 中没有找到类似的内容。 19 | 20 | I searched in the [issues](https://github.com/yetone/openai-translator/issues) and found nothing similar. 21 | required: true 22 | - type: input 23 | attributes: 24 | label: feature 25 | description: > 26 | 一句话概括你的功能建议。Please provide a brief description of your feature proposal. 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: 描述 Motivation 32 | description: > 33 | 解释一下这个功能将如何解决您的问题。 34 | 35 | Explain how this feature will resolve your problem, including the way it addresses the issue that you are facing. 36 | validations: 37 | required: true 38 | - type: textarea 39 | attributes: 40 | label: Solution 41 | description: 描述建议的解决方案。Describe the proposed solution. (if you have any additional information, please add it here.) 42 | - type: textarea 43 | attributes: 44 | label: 还有其他内容吗?Anything else? 45 | - type: checkboxes 46 | attributes: 47 | label: 你是否愿意提交一份 PR?Are you willing to submit a PR? 48 | description: > 49 | 我们期待开发人员和用户的帮助,以解决在 GPT-Tutor 中发现的任何问题。 如果您愿意通过提交 PR 来解决此问题,请勾选。We eagerly anticipate developers' and users' support and collaboration in resolving any issues found in GPT-Tutor. If you are willing to offer a solution by submitting a PR to fix this matter, kindly mark the checkbox provided. 50 | options: 51 | - label: 我愿意提供 PR! I'm willing to submit a PR! 52 | - type: markdown 53 | attributes: 54 | value: "非常感谢您的功能建议!Thank you very much for your feature proposal!" 55 | 56 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: npm 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | typos-check: 13 | name: Spell Check with Typos 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Actions Repository 17 | uses: actions/checkout@v4 18 | - name: Check spelling with custom config file 19 | uses: crate-ci/typos@v1.24.5 20 | with: 21 | config: ./typos.toml 22 | 23 | eslint: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: pnpm/action-setup@v4 30 | with: 31 | version: 8.6.0 32 | 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: 18 36 | cache: 'pnpm' 37 | 38 | - name: Install packages 39 | run: pnpm install --no-frozen-lockfile 40 | 41 | - name: Run type check 42 | run: pnpm tsc 43 | 44 | - name: Run eslint 45 | run: pnpm lint 46 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | e2e-test: 13 | timeout-minutes: 60 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 8.6.0 21 | 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install --no-frozen-lockfile 29 | 30 | - name: Install Playwright Browsers 31 | run: pnpm exec playwright install chromium --with-deps 32 | 33 | - name: Build Chromium extension 34 | run: pnpm vite build -c vite.config.chromium.ts 35 | 36 | - name: Run Playwright tests 37 | run: pnpm test:e2e --reporter github 38 | -------------------------------------------------------------------------------- /.github/workflows/test-build.yaml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-tauri: 8 | permissions: 9 | contents: write 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: [macos-latest, ubuntu-20.04, windows-latest] 14 | 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: pnpm/action-setup@v4 20 | with: 21 | version: 8.6.0 22 | 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 18 26 | cache: 'pnpm' 27 | 28 | - name: install Rust stable 29 | uses: dtolnay/rust-toolchain@stable 30 | 31 | - name: install dependencies (ubuntu only) 32 | if: matrix.platform == 'ubuntu-20.04' 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libx11-dev libxdo-dev libxcb-shape0-dev libxcb-xfixes0-dev 36 | 37 | - name: install dependencies (mac only) 38 | if: matrix.platform == 'macos-latest' 39 | run: | 40 | rustup target add aarch64-apple-darwin 41 | 42 | - name: install frontend dependencies 43 | run: pnpm install --no-frozen-lockfile # change this to npm or pnpm depending on which one you use 44 | 45 | - name: Build Tauri App (MacOS Universal) 46 | uses: tauri-apps/tauri-action@dev 47 | if: matrix.platform == 'macos-latest' 48 | id: tauri-action-mac 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | releaseId: test-release 53 | args: --target universal-apple-darwin 54 | 55 | - name: Build Tauri App 56 | uses: tauri-apps/tauri-action@dev 57 | if: matrix.platform != 'macos-latest' 58 | id: tauri-action 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | releaseId: test-release 63 | 64 | - name: Upload artifact 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: tauri-client-app-artifact 68 | path: | 69 | ${{ fromJSON(steps.tauri-action-mac.outputs.artifactPaths)[0] }} 70 | ${{ fromJSON(steps.tauri-action.outputs.artifactPaths)[0] }} 71 | 72 | 73 | build-browser-extension: 74 | runs-on: ubuntu-22.04 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - uses: pnpm/action-setup@v4 80 | with: 81 | version: 8.6.0 82 | 83 | - uses: actions/setup-node@v3 84 | with: 85 | node-version: 18 86 | cache: 'pnpm' 87 | 88 | - name: Install dependencies 89 | run: pnpm install --no-frozen-lockfile 90 | 91 | - name: Build 92 | run: make build-browser-extension 93 | # todo: upload browser extension artifacts 94 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | unit-test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 8.6.0 21 | 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | cache: 'pnpm' 26 | 27 | - name: Install packages 28 | run: pnpm install --no-frozen-lockfile 29 | 30 | - name: Run test 31 | run: pnpm test 32 | -------------------------------------------------------------------------------- /.github/workflows/winget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to WinGet 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | publish: 7 | # Action can only be run on windows 8 | runs-on: windows-latest 9 | steps: 10 | - uses: vedantmgoyal2009/winget-releaser@v2 11 | with: 12 | identifier: yetone.OpenAITranslator 13 | max-versions-to-keep: 5 # keep only latest 5 versions 14 | installers-regex: '\.msi$' # Only .msi files 15 | token: ${{ secrets.WINGET_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | dist/ 4 | tmp/ 5 | release/ 6 | yarn-error.log 7 | npm-error.log 8 | .DS_Store 9 | *.json-e 10 | *.js-e 11 | 12 | # 环境变量文件 13 | .env 14 | .env.local 15 | .env.* 16 | !.env.example 17 | 18 | .vscode 19 | .idea 20 | .eslintcache 21 | .yarn 22 | .yarnrc.yml 23 | .env 24 | /test-results/ 25 | /playwright-report/ 26 | /playwright/.cache/ 27 | -------------------------------------------------------------------------------- /.plasmo/cache/parcel/data.mdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/.plasmo/cache/parcel/data.mdb -------------------------------------------------------------------------------- /.plasmo/cache/parcel/lock.mdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/.plasmo/cache/parcel/lock.mdb -------------------------------------------------------------------------------- /.plasmo/chrome-mv3.plasmo.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "icons": { 3 | "16": "./gen-assets/icon16.plasmo.png", 4 | "32": "./gen-assets/icon32.plasmo.png", 5 | "48": "./gen-assets/icon48.plasmo.png", 6 | "64": "./gen-assets/icon64.plasmo.png", 7 | "128": "./gen-assets/icon128.plasmo.png" 8 | }, 9 | "manifest_version": 3, 10 | "action": { 11 | "default_icon": { 12 | "16": "./gen-assets/icon16.plasmo.png", 13 | "32": "./gen-assets/icon32.plasmo.png", 14 | "48": "./gen-assets/icon48.plasmo.png", 15 | "64": "./gen-assets/icon64.plasmo.png", 16 | "128": "./gen-assets/icon128.plasmo.png" 17 | } 18 | }, 19 | "version": "1.0.1.7", 20 | "author": "BlackStar1453", 21 | "name": "DEV | ", 22 | "description": "gpt-tutor", 23 | "permissions": [ 24 | "storage" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | jsxSingleQuote: true, 4 | semi: false, 5 | trailingComma: 'es5', 6 | tabWidth: 4, 7 | useTabs: false, 8 | quoteProps: 'consistent', 9 | bracketSpacing: true, 10 | printWidth: 120, 11 | } 12 | -------------------------------------------------------------------------------- /8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/8.gif -------------------------------------------------------------------------------- /CHANGELOG-CN.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | 本项目的所有显著变化都将记录在此文件中。 4 | 5 | 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 6 | 并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 7 | 8 | ## [Unreleased] 9 | 10 | ### 待完成 11 | - [x] 阅读模式。 12 | - [x] 功能组商店。 13 | - [ ] 内置oneAPI。 14 | - [x] 设计新的UI,现在将区分单词、句子、写作三部分,每一部分使用不同的UI和功能组。 15 | - [ ] 完成雅思学习的功能组设计。 16 | - [ ] 实现打印功能。 17 | - [ ] 实现通过AI快速实现一个功能和功能组的功能。 18 | - [ ] 实现通过GitHub来管理和远程存储功能组,避免丢失。 19 | 20 | ### 待修复 21 | - [x] 应该将添加到复习和添加到anki的按钮移动到一个更合适的位置,区分添加整个单词和单词部分内容的功能。 22 | 23 | 24 | ## [1.0.1.7] - 2024-12-08 25 | 26 | ### 🎉新增 27 | 28 | - 新增操作指引。解释主界面的使用。 29 | - 新增功能组商店。支持根据自己的需要购买和更新、定制需要的功能组。 30 | 31 | ### 🐛修复 32 | - 修复了在阅读模式下,无法使用@符号来呼出GPT-Tutor功能的问题。 33 | - 修复了答案被错误保存的问题。 34 | 35 | ### 🎉新增 36 | - 新增阅读模式。更方便地通过gpt-tutor来阅读网页内容。 37 | 38 | ## [1.0.1.4] - 2024-11-09 39 | 40 | ### 🎉新增 41 | - 新增阅读模式。更方便地通过gpt-tutor来阅读网页内容。 42 | - 新增第三方API提供商:openRouter。 43 | 44 | ![alt text](readingMode_1.gif) 45 | 46 | ### 🔄变更 47 | - 删除内置的复习功能,改为通过Anki来实现。 48 | - 删除初始页面中clerk的使用,现在不再需要登录使用。 49 | - 使用zustand来完成整个状态的管理。 50 | 51 | ### 🐛修复 52 | 53 | 54 | ## [1.0.1.3] - 2024-09-14 55 | 56 | ### 🎉新增 57 | - 重设了主页面的UI。现在使用起来将更加直观和简洁。 58 | - 通过选择顶部的Tab来选择功能组,点击右侧的“更多”按钮来查看剩余的功能组和其它设置。 59 | - 现在通过@符号可以快速呼出GPT-Tutor的相关功能,比如在选择单词的tab后,输入@可以获取单词相关的功能。 60 | - 历史记录、复习记录和单词列表默认将显示在侧边栏中,通常为隐藏状态,你可以通过点击左上角的|<来打开。 61 | 62 | ![alt text](8.gif) 63 | 64 | - 在功能管理器中,在内置的功能组中新增了一个反馈按钮,你可以在这里提交对某个功能的反馈。 65 | - 在回答框中新增了一个?按钮,如果你对当前回答有疑问,可以点击它来查看完善方案。 66 | 67 | ### 🔄变更 68 | - 将添加到复习的按钮移动到了答案框中,点击后将该单词的所有内容添加到复习中。 69 | - 现在非管理员和订阅用户无法修改内置的功能,以免出现不必要的错误。 70 | - 将上传词书的功能移动到了下拉菜单中。 71 | 72 | ### 🐛修复 73 | - 修复Youglish组件在隐藏时仍然会触发的问题。 74 | 75 | 76 | 77 | 78 | ## [1.0.1.2] - 2024-09-04 79 | 80 | ### 🎉新增 81 | - 所有内置的功能组现在将通过远程仓库获取(之前从本地加载),并且在更新后可以随时获取到最新版本。 82 | - 在动作管理器(ActionManager)中添加了 商店(Store) 组件,后续你可以在这里上传你的功能组来获得API Key的使用额度,也可以在这里购买和定制功能组 (`src/common/components/ActionStore.tsx`)。 83 | - 新增反馈功能的设置。当对某个内置的功能存在疑问时,可以在动作管理器中或者点击回答页面中的问号按钮提交反馈。 84 | 85 | ### 🔄变更 86 | - 现在区分了用户自己创建的功能和GPT-Tutor内置的功能。为保证功能能够正常使用,用户将无法删除或修改内置的功能(但能够查看)。 87 | - 重新设置了底部按钮的使用逻辑。删除“继续”和“下一个”按钮,只保留添加到复习的按钮。 88 | - 删除了动作管理器中辅助动作的设置。 89 | - 删除了动作管理器中输出格式中的JSON的设置。 90 | 91 | ### 🐛修复 92 | - 修复了在通过输入来查询(而不是通过选择右侧List中的单词)时,生成的回答没有正确显示的问题。 93 | - 修复使用ChatGLM或Kimi时,初次打开页面时会跳转到设置页面的问题。 94 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= 1.0.1.7 2 | 3 | clean: 4 | rm -rf dist 5 | 6 | change-version: 7 | sed -i -e "s/\"version\": \".*\"/\"version\": \"$(VERSION)\"/" src/browser-extension/manifest.json 8 | 9 | 10 | change-package-version: 11 | sed -i -e "s/\"version\": \".*\"/\"version\": \"$(VERSION)\"/" package.json 12 | 13 | build-browser-extension: change-version change-package-version 14 | pnpm vite build -c vite.config.chromium.ts 15 | cd dist/browser-extension/chromium && zip -r ../chromium.zip . 16 | 17 | build-userscript: change-package-version 18 | pnpm vite build -c vite.config.userscript.ts 19 | 20 | build-popclip-extension: 21 | rm -f dist/openai-translator.popclipextz 22 | mkdir -p dist/openai-translator.popclipext 23 | cp -r clip-extensions/popclip/* dist/openai-translator.popclipext 24 | cd dist && zip -r openai-translator.popclipextz openai-translator.popclipext && rm -r openai-translator.popclipext 25 | 26 | build-snipdo-extension: 27 | rm -f dist/openai-translator.pbar 28 | zip -j -r dist/openai-translator.pbar clip-extensions/snipdo/* 29 | -------------------------------------------------------------------------------- /clip-extensions/popclip/Config.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Actions 6 | 7 | 8 | Shell Script File 9 | openai-translator.sh 10 | Image File 11 | icon.png 12 | Title 13 | OpenAI Translator 14 | 15 | 16 | Credits 17 | 18 | 19 | Link 20 | https://github.com/openai-translator/openai-translator 21 | Name 22 | OpenAI Translator 23 | 24 | 25 | Extension Description 26 | Translate text with OpenAI Translator. 27 | Extension Identifier 28 | xyz.yetone.apps.openai-translator.clip-extensions.popclip 29 | Extension Image File 30 | openai-translator.png 31 | Extension Name 32 | OpenAI Translator 33 | Required OS Version 34 | 10.13 35 | 36 | 37 | -------------------------------------------------------------------------------- /clip-extensions/popclip/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/clip-extensions/popclip/icon.png -------------------------------------------------------------------------------- /clip-extensions/popclip/openai-translator.sh: -------------------------------------------------------------------------------- 1 | send_text() { 2 | curl -d "$POPCLIP_TEXT" --unix-socket /tmp/openai-translator.sock http://openai-translator 3 | } 4 | 5 | if ! send_text; then 6 | open -g -a OpenAI\ Translator 7 | sleep 2 8 | send_text 9 | fi 10 | -------------------------------------------------------------------------------- /clip-extensions/snipdo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/clip-extensions/snipdo/icon.png -------------------------------------------------------------------------------- /clip-extensions/snipdo/openai-translator.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenAI Translator", 3 | "identifier": "xyz.yetone.apps.openai-translator.clip-extensions.snipdo", 4 | "icon": "icon.png", 5 | "actions": [ 6 | { 7 | "title": "OpenAI Translator", 8 | "icon": "icon.png", 9 | "powershellFile": "openai-translator.ps1" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /clip-extensions/snipdo/openai-translator.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$PLAIN_TEXT 3 | ) 4 | 5 | $encode_text = [System.Text.Encoding]::UTF8.GetBytes($PLAIN_TEXT) 6 | 7 | curl 127.0.0.1:62007 -Method POST -Body $encode_text -UseBasicParsing 8 | -------------------------------------------------------------------------------- /e2e/common.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test' 2 | 3 | export function getOptionsPageUrl(extensionId: string) { 4 | return `chrome-extension://${extensionId}/src/browser-extension/options/index.html` 5 | } 6 | 7 | export function getPopupPageUrl(extensionId: string) { 8 | return `chrome-extension://${extensionId}/src/browser-extension/popup/index.html` 9 | } 10 | 11 | export async function selectExampleText(page: Page) { 12 | const textLocator = page.getByTestId('example-text') 13 | const boundingBox = await textLocator.boundingBox() 14 | if (boundingBox) { 15 | // select text 16 | await page.mouse.move(boundingBox.x, boundingBox.y) 17 | await page.mouse.down() 18 | await page.mouse.move(boundingBox.x + boundingBox.width, boundingBox.y + boundingBox.height) 19 | await page.mouse.up() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/fixtures.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { type BrowserContext, test as base, chromium } from '@playwright/test' 3 | 4 | export const extensionPath = path.join(__dirname, '../dist/browser-extension/chromium') 5 | 6 | export const test = base.extend<{ 7 | context: BrowserContext 8 | extensionId: string 9 | }>({ 10 | context: async ({ headless }, use) => { 11 | const context = await chromium.launchPersistentContext('', { 12 | headless, 13 | args: [ 14 | ...(headless ? ['--headless=new'] : []), 15 | `--disable-extensions-except=${extensionPath}`, 16 | `--load-extension=${extensionPath}`, 17 | ], 18 | }) 19 | await use(context) 20 | await context.close() 21 | }, 22 | extensionId: async ({ context }, use) => { 23 | // for manifest v3: 24 | let [background] = context.serviceWorkers() 25 | if (!background) background = await context.waitForEvent('serviceworker') 26 | 27 | const extensionId = background.url().split('/')[2] 28 | await use(extensionId) 29 | }, 30 | }) 31 | 32 | export const expect = test.expect 33 | -------------------------------------------------------------------------------- /e2e/hotkey.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { getOptionsPageUrl, selectExampleText } from './common' 3 | import { expect, test } from './fixtures' 4 | import { containerID, popupCardID } from '../src/browser-extension/content_script/consts' 5 | 6 | test.fixme('hotkey should work', async ({ page, extensionId }) => { 7 | await test.step('set hotkey', async () => { 8 | await page.goto(getOptionsPageUrl(extensionId)) 9 | const input = page.locator('input[name="apiKey"]') 10 | await input.fill('fake-api-key') 11 | await page.getByTestId('hotkey-recorder').click() 12 | await page.keyboard.down('Alt') 13 | await page.keyboard.down('x') 14 | await page.keyboard.up('Alt') 15 | await page.keyboard.up('x') 16 | await page.getByText('Save').click() 17 | }) 18 | 19 | const popupCard = await test.step('select example text', async () => { 20 | await page.goto(`file:${path.join(__dirname, 'test.html')}`) 21 | await selectExampleText(page) 22 | 23 | const container = page.locator(`#${containerID}`) 24 | await container.waitFor({ state: 'attached' }) 25 | 26 | await page.keyboard.down('Alt') 27 | await page.keyboard.press('x') 28 | 29 | return container.locator(`#${popupCardID}`) 30 | }) 31 | 32 | await expect(popupCard).toBeVisible() 33 | }) 34 | -------------------------------------------------------------------------------- /e2e/index.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { expect, test } from './fixtures' 3 | import { getOptionsPageUrl, getPopupPageUrl, selectExampleText } from './common' 4 | import { containerID, popupThumbID, popupCardID } from '../src/browser-extension/content_script/consts' 5 | 6 | test('popup card should be visible', async ({ page }) => { 7 | await page.goto(`file:${path.join(__dirname, 'test.html')}`) 8 | await selectExampleText(page) 9 | 10 | const container = page.locator(`#${containerID}`) 11 | const thumb = container.locator(`#${popupThumbID}`) 12 | await expect(thumb).toBeVisible() 13 | await thumb.click() 14 | const popupCard = container.locator(`#${popupCardID}`) 15 | await expect(popupCard).toBeVisible() 16 | }) 17 | 18 | test('popup page should be opened', async ({ page, extensionId }) => { 19 | await page.goto(getPopupPageUrl(extensionId)) 20 | await expect(page.getByTestId('popup-container')).toBeVisible() 21 | }) 22 | 23 | test('options page should be opened', async ({ page, extensionId }) => { 24 | await page.goto(getOptionsPageUrl(extensionId)) 25 | await expect(page.getByTestId('settings-container')).toBeVisible() 26 | }) 27 | -------------------------------------------------------------------------------- /e2e/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | E2E Testing 9 | 10 | 11 | 12 | example text 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/src/$1' 6 | }, 7 | transform: { 8 | '^.+\\.tsx?$': ['ts-jest', { 9 | tsconfig: { 10 | "jsx": "react", 11 | "esModuleInterop": true, 12 | } 13 | }] 14 | }, 15 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 16 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 17 | }; 18 | -------------------------------------------------------------------------------- /make: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/make -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://playwright.dev/docs/chrome-extensions Chrome extensions | Playwright} 3 | */ 4 | import { defineConfig } from '@playwright/test' 5 | 6 | export default defineConfig({ 7 | testDir: './e2e', 8 | retries: 2, 9 | }) 10 | -------------------------------------------------------------------------------- /public/8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/8.gif -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/icon.png -------------------------------------------------------------------------------- /public/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-1.png -------------------------------------------------------------------------------- /public/image-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-10.png -------------------------------------------------------------------------------- /public/image-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-11.png -------------------------------------------------------------------------------- /public/image-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-12.png -------------------------------------------------------------------------------- /public/image-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-13.png -------------------------------------------------------------------------------- /public/image-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-14.png -------------------------------------------------------------------------------- /public/image-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-15.png -------------------------------------------------------------------------------- /public/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-2.png -------------------------------------------------------------------------------- /public/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-3.png -------------------------------------------------------------------------------- /public/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-4.png -------------------------------------------------------------------------------- /public/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-5.png -------------------------------------------------------------------------------- /public/image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-6.png -------------------------------------------------------------------------------- /public/image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-7.png -------------------------------------------------------------------------------- /public/image-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-8.png -------------------------------------------------------------------------------- /public/image-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/public/image-9.png -------------------------------------------------------------------------------- /public/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.5.0/enforcement.13af146b6f5532afc450f0718859ea0f.html" 60 | } 61 | ] 62 | }, 63 | "condition": { 64 | "requestDomains": ["https://tcr9i.chat.openai.com"], 65 | "resourceTypes": ["xmlhttprequest"] 66 | } 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /readingMode_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/readingMode_1.gif -------------------------------------------------------------------------------- /scripts/release.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from functools import reduce 3 | 4 | def get_current_version_from_tag(): 5 | subprocess.check_output(['git', 'pull', 'origin', 'main', '-r']) 6 | subprocess.check_output(['git', 'fetch', '--tags']) 7 | """Get the current version from the latest tag in the repository.""" 8 | # Get the latest tag in the repository. 9 | tag = subprocess.check_output(['git', 'describe', '--tags', '--abbrev=0']).decode('utf-8').strip() 10 | # Get the current version from the tag. 11 | version = tag.lstrip('v') 12 | # Return the current version. 13 | return version 14 | 15 | def generate_new_version(): 16 | """Generate the new version for the current release.""" 17 | # Get the current version. 18 | previous_version = get_current_version_from_tag() 19 | # Get the new version. 20 | new_version = previous_version.split('.') 21 | new_version[-1] = str(int(new_version[-1]) + 1) 22 | new_version = '.'.join(new_version) 23 | # Return the new version. 24 | return new_version 25 | 26 | def generate_release_note(): 27 | """Generate the release note for the current version.""" 28 | # Get the current version. 29 | previous_version = get_current_version_from_tag() 30 | # Get the release note. 31 | release_note = subprocess.check_output(['git', 'log', '--pretty="%s"', 'v' + previous_version + '..HEAD']).decode('utf-8').strip() 32 | # Return the release note. 33 | return release_note 34 | 35 | def create_new_tag(): 36 | """Create a new tag for the current release.""" 37 | # Get the current version. 38 | new_version = generate_new_version() 39 | release_note = generate_release_note() 40 | 41 | release_notes = [] 42 | seen = set() 43 | for line in release_note.split('\n'): 44 | line = line.strip().strip('"') 45 | t_, _, _ = line.partition(':') 46 | if t_.lower() not in ('fix', 'feat', 'docs', 'refactor'): 47 | continue 48 | if line in seen: 49 | continue 50 | release_notes.append(line) 51 | seen.add(line) 52 | args = ['git', 'tag', '-a', 'v' + new_version] + flatten([['-m', line] for line in release_notes]) 53 | # Create a new tag for the current release. 54 | return subprocess.check_output(args) 55 | 56 | def flatten(l): 57 | return reduce(lambda x, y: x + y, l) 58 | 59 | def main(): 60 | print(create_new_tag()) 61 | 62 | if __name__ == '__main__': 63 | main() 64 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | default-run = "app" 9 | edition = "2021" 10 | rust-version = "1.59" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.2.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.2.4", features = ["clipboard-all", "dialog-message", "fs-read-dir", "fs-read-file", "fs-write-file", "global-shortcut-all", "notification-all", "shell-open", "system-tray", "updater", "window-all", "windows7-compat"] } 21 | window-shadows = "0.2.1" 22 | once_cell = "1.17.1" 23 | clipboard = "0.5.0" 24 | enigo = {git = "https://github.com/enigo-rs/enigo"} 25 | mouse_position = "0.1.3" 26 | rdev = "0.5.2" 27 | tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev", version = "0.1.0" } 28 | tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } 29 | cpuid = "0.1.1" 30 | sysinfo = "0.28.3" 31 | parking_lot = "0.12.1" 32 | mouce = "0.2.41" 33 | tauri-plugin-window-state = "0.1.0" 34 | whatlang = "0.16.2" 35 | arboard = "3.2.0" 36 | tiny_http = "0.12.0" 37 | 38 | [target.'cfg(target_os = "macos")'.dependencies] 39 | cocoa = "0.24" 40 | objc = "0.2.7" 41 | macos-accessibility-client = "0.0.1" 42 | core-graphics = "0.22.3" 43 | 44 | [target.'cfg(windows)'.dependencies] 45 | windows = {version="0.44.0",features= ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"] } 46 | 47 | [features] 48 | # by default Tauri runs in production mode 49 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 50 | default = ["custom-protocol"] 51 | # this feature is used for production builds where `devPath` points to the filesystem 52 | # DO NOT remove this 53 | custom-protocol = ["tauri/custom-protocol"] 54 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/favicon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/resources/bin/ocr_apple: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/resources/bin/ocr_apple -------------------------------------------------------------------------------- /src-tauri/resources/bin/ocr_intel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src-tauri/resources/bin/ocr_intel -------------------------------------------------------------------------------- /src-tauri/resources/get-selected-text-by-ax.applescript: -------------------------------------------------------------------------------- 1 | use sys : application "System Events" 2 | 3 | -- Use the following delay to choose an application window 4 | -- and highlight some text. Then ensure that the window remains 5 | -- in focus until the script terminates. 6 | -- delay 5 7 | 8 | set P to the first application process whose frontmost is true 9 | 10 | set appName to name of P 11 | 12 | if appName is equal to "Mail" then 13 | error "not support " & appName 14 | end 15 | 16 | if appName is equal to "Safari" then 17 | try 18 | tell application "Safari" 19 | set theText to (do JavaScript "getSelection().toString()" in document 1) 20 | end tell 21 | return theText 22 | end try 23 | error "not support Safari" 24 | end 25 | 26 | set _W to a reference to the first window of P 27 | 28 | set _U to a reference to ¬ 29 | (UI elements of P whose ¬ 30 | name of attributes contains "AXSelectedText" and ¬ 31 | value of attribute "AXSelectedText" is not "" and ¬ 32 | class of value of attribute "AXSelectedText" is not class) 33 | 34 | tell sys to if (count _U) ≠ 0 then ¬ 35 | return the value of ¬ 36 | attribute "AXSelectedText" of ¬ 37 | _U's contents's first item 38 | 39 | set _U to a reference to UI elements of _W 40 | 41 | with timeout of 1 seconds 42 | tell sys to repeat while (_U exists) 43 | tell (a reference to ¬ 44 | (_U whose ¬ 45 | name of attributes contains "AXSelectedText" and ¬ 46 | value of attribute "AXSelectedText" is not "" and ¬ 47 | class of value of attribute "AXSelectedText" is not class)) ¬ 48 | to if (count) ≠ 0 then return the value of ¬ 49 | attribute "AXSelectedText" of its contents's first item 50 | 51 | set _U to a reference to (UI elements of _U) 52 | end repeat 53 | end timeout 54 | 55 | error "not found AXSelectedText" 56 | -------------------------------------------------------------------------------- /src-tauri/resources/get-selected-text.applescript: -------------------------------------------------------------------------------- 1 | use AppleScript version "2.4" 2 | use scripting additions 3 | use framework "Foundation" 4 | use framework "AppKit" 5 | 6 | tell application "System Events" 7 | set frontmostProcess to first process whose frontmost is true 8 | set appName to name of frontmostProcess 9 | end tell 10 | 11 | if appName is equal to "OpenAI Translator" then 12 | return 13 | end if 14 | 15 | -- Back up clipboard contents: 16 | set savedClipboard to the clipboard 17 | 18 | set thePasteboard to current application's NSPasteboard's generalPasteboard() 19 | set theCount to thePasteboard's changeCount() 20 | 21 | -- Copy selected text to clipboard: 22 | tell application "System Events" to keystroke "c" using {command down} 23 | delay 0.1 -- Without this, the clipboard may have stale data. 24 | 25 | if thePasteboard's changeCount() is theCount then 26 | return "" 27 | end if 28 | 29 | set theSelectedText to the clipboard 30 | 31 | set the clipboard to savedClipboard 32 | 33 | theSelectedText 34 | -------------------------------------------------------------------------------- /src-tauri/src/config.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use tauri::api::path::config_dir; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Clone)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct Config { 9 | pub hotkey: Option, 10 | pub ocr_hotkey: Option, 11 | pub restore_previous_position: Option, 12 | pub always_show_icons: Option, 13 | pub allow_using_clipboard_when_selected_text_not_available: Option, 14 | } 15 | 16 | static CONFIG_CACHE: Mutex> = Mutex::new(None); 17 | 18 | pub fn get_config() -> Result> { 19 | if let Some(config_cache) = &*CONFIG_CACHE.lock() { 20 | return Ok(config_cache.clone()); 21 | } 22 | let config_content = get_config_content()?; 23 | let config: Config = serde_json::from_str(&config_content)?; 24 | CONFIG_CACHE.lock().replace(config.clone()); 25 | Ok(config) 26 | } 27 | 28 | #[tauri::command] 29 | pub fn clear_config_cache() { 30 | CONFIG_CACHE.lock().take(); 31 | } 32 | 33 | #[tauri::command] 34 | pub fn get_config_content() -> Result { 35 | if let Some(config_dir) = config_dir() { 36 | let app_config_dir = config_dir.join("xyz.yetone.apps.openai-translator"); 37 | if !app_config_dir.exists() { 38 | std::fs::create_dir_all(&app_config_dir).unwrap(); 39 | } 40 | let config_path = app_config_dir.join("config.json"); 41 | if config_path.exists() { 42 | match std::fs::read_to_string(config_path) { 43 | Ok(content) => Ok(content), 44 | Err(_) => Err("Failed to read config file".to_string()), 45 | } 46 | } else { 47 | std::fs::write(config_path, "{}").unwrap(); 48 | Ok("{}".to_string()) 49 | } 50 | } else { 51 | Err("Config directory not found".to_string()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/src/hotkey.rs: -------------------------------------------------------------------------------- 1 | use crate::config::get_config; 2 | use crate::ocr::ocr; 3 | use crate::windows::show_main_window_with_selected_text; 4 | use crate::APP_HANDLE; 5 | use tauri::GlobalShortcutManager; 6 | 7 | #[allow(unused, dead_code)] 8 | pub fn do_bind_hotkey() -> Result<(), Box> { 9 | let config = get_config()?; 10 | let handle = APP_HANDLE.get().ok_or("can't get app handle")?; 11 | if let Some(hotkey) = config.hotkey { 12 | if !handle.global_shortcut_manager().is_registered(&hotkey)? { 13 | handle.global_shortcut_manager().unregister(&hotkey)?; 14 | } 15 | handle 16 | .global_shortcut_manager() 17 | .register(hotkey.as_str(), show_main_window_with_selected_text)?; 18 | } 19 | if let Some(ocr_hotkey) = config.ocr_hotkey { 20 | if !handle 21 | .global_shortcut_manager() 22 | .is_registered(&ocr_hotkey)? 23 | { 24 | handle.global_shortcut_manager().unregister(&ocr_hotkey)?; 25 | } 26 | handle 27 | .global_shortcut_manager() 28 | .register(ocr_hotkey.as_str(), || { 29 | ocr(); 30 | })?; 31 | } 32 | Ok(()) 33 | } 34 | 35 | #[allow(unused, dead_code)] 36 | #[tauri::command] 37 | pub fn bind_hotkey() -> Result<(), String> { 38 | do_bind_hotkey().map_err(|e| e.to_string()) 39 | } 40 | -------------------------------------------------------------------------------- /src-tauri/src/lang.rs: -------------------------------------------------------------------------------- 1 | use whatlang::detect; 2 | 3 | #[tauri::command] 4 | pub fn detect_lang(text: String) -> String { 5 | match detect(&text) { 6 | Some(info) => info.lang().to_string(), 7 | None => "".to_string(), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/src/ocr.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_os = "macos"))] 2 | pub fn do_ocr() -> Result<(), Box> { 3 | Ok(()) 4 | } 5 | 6 | #[cfg(target_os = "macos")] 7 | pub fn do_ocr() -> Result<(), Box> { 8 | use crate::{APP_HANDLE, CPU_VENDOR}; 9 | 10 | let mut rel_path = "resources/bin/ocr_intel".to_string(); 11 | if *CPU_VENDOR.lock() == "Apple" { 12 | rel_path = "resources/bin/ocr_apple".to_string(); 13 | } 14 | 15 | let bin_path = APP_HANDLE 16 | .get() 17 | .unwrap() 18 | .path_resolver() 19 | .resolve_resource(rel_path) 20 | .expect("failed to resolve ocr binary resource"); 21 | 22 | let output = std::process::Command::new(bin_path) 23 | .args(["-l", "zh"]) 24 | .output() 25 | .expect("failed to execute ocr binary"); 26 | 27 | // check exit code 28 | if output.status.success() { 29 | // get output content 30 | let content = String::from_utf8(output.stdout).expect("failed to parse ocr binary output"); 31 | crate::utils::send_text(content); 32 | crate::windows::show_main_window(false, true); 33 | Ok(()) 34 | } else { 35 | Err("ocr binary failed".into()) 36 | } 37 | } 38 | 39 | #[tauri::command] 40 | pub fn ocr() { 41 | do_ocr().unwrap(); 42 | } 43 | -------------------------------------------------------------------------------- /src/browser-extension/content_script/consts.ts: -------------------------------------------------------------------------------- 1 | export const zIndex = '2147483647' 2 | export const popupThumbID = '__yetone-openai-translator-popup-thumb' 3 | export const popupCardID = '__yetone-openai-translator-popup-card' 4 | export const containerID = '__yetone-openai-translator' 5 | export const popupCardMinWidth = 220 6 | export const popupCardMaxWidth = 660 7 | export const documentPadding = 10 8 | -------------------------------------------------------------------------------- /src/browser-extension/content_script/utils.ts: -------------------------------------------------------------------------------- 1 | import { containerID, documentPadding, popupCardID, popupThumbID, zIndex } from './consts' 2 | 3 | function attachEventsToContainer($container: HTMLElement) { 4 | $container.addEventListener('mousedown', (event) => { 5 | event.stopPropagation() 6 | }) 7 | $container.addEventListener('mouseup', (event) => { 8 | event.stopPropagation() 9 | }) 10 | } 11 | 12 | export async function getContainer(): Promise { 13 | let $container: HTMLElement | null = document.getElementById(containerID) 14 | if (!$container) { 15 | $container = document.createElement('div') 16 | $container.id = containerID 17 | attachEventsToContainer($container) 18 | $container.style.zIndex = zIndex 19 | return new Promise((resolve, reject) => { 20 | setTimeout(() => { 21 | const $container_: HTMLElement | null = document.getElementById(containerID) 22 | if ($container_) { 23 | resolve($container_) 24 | return 25 | } 26 | if (!$container) { 27 | reject(new Error('Failed to create container')) 28 | return 29 | } 30 | const shadowRoot = $container.attachShadow({ mode: 'open' }) 31 | const $inner = document.createElement('div') 32 | shadowRoot.appendChild($inner) 33 | const $html = document.body.parentElement 34 | if ($html) { 35 | $html.appendChild($container as HTMLElement) 36 | } else { 37 | document.appendChild($container as HTMLElement) 38 | } 39 | resolve($container) 40 | }, 100) 41 | }) 42 | } 43 | return new Promise((resolve) => { 44 | resolve($container as HTMLElement) 45 | }) 46 | } 47 | 48 | export async function queryPopupThumbElement(): Promise { 49 | const $container = await getContainer() 50 | return $container.shadowRoot?.querySelector(`#${popupThumbID}`) as HTMLDivElement | null 51 | } 52 | 53 | export async function queryPopupCardElement(): Promise { 54 | const $container = await getContainer() 55 | return $container.shadowRoot?.querySelector(`#${popupCardID}`) as HTMLDivElement | null 56 | } 57 | 58 | export function calculateMaxXY($popupCard: HTMLElement): number[] { 59 | const { innerWidth, innerHeight } = window 60 | const { scrollLeft, scrollTop } = document.documentElement 61 | const { width, height } = $popupCard.getBoundingClientRect() 62 | const maxX = scrollLeft + innerWidth - width - documentPadding 63 | const maxY = scrollTop + innerHeight - height - documentPadding 64 | return [maxX, maxY] 65 | } 66 | -------------------------------------------------------------------------------- /src/browser-extension/manifest.firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "GPT Tutor", 5 | "description": "GPT-Tutor is a extension that uses the ChatGPT API for language learning.", 6 | "version": "0.1.9.7", 7 | 8 | "options_ui": { 9 | "page": "/src/browser-extension/options/index.html" 10 | }, 11 | 12 | "action": { 13 | "default_icon": "icon.png", 14 | "default_popup": "/src/browser-extension/popup/index.html" 15 | }, 16 | 17 | "content_scripts": [ 18 | { 19 | "matches": [""], 20 | "all_frames": true, 21 | "js": [ "/src/browser-extension/content_script/index.tsx"] 22 | } 23 | ], 24 | 25 | "background": { 26 | "scripts": ["/src/browser-extension/background/index.ts"] 27 | }, 28 | 29 | "permissions": ["tabs", "storage", "contextMenus", "sidePanel","webRequest", "declarativeNetRequestWithHostAccess"], 30 | 31 | "host_permissions": [ 32 | "https://*.openai.com/", 33 | "https://*.openai.azure.com/", 34 | "https://*.ingest.sentry.io/", 35 | "*://speech.platform.bing.com/", 36 | "https://*.google-analytics.com/", 37 | "https://*.chat.openai.com/" 38 | ], 39 | 40 | "browser_specific_settings": { 41 | "gecko": { 42 | "id": "yaozeng1999@gmail.com" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/browser-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "GPT Tutor", 5 | "description": "GPT-Tutor is a extension that uses the ChatGPT API for language learning.", 6 | "version": "1.0.1.7", 7 | 8 | "icons": { 9 | "16": "icon.png", 10 | "32": "icon.png", 11 | "48": "icon.png", 12 | "128": "icon.png" 13 | }, 14 | 15 | "options_ui": { 16 | "page": "/src/browser-extension/options/index.html" 17 | }, 18 | 19 | "action": { 20 | "default_icon": "icon.png", 21 | "default_popup": "/src/browser-extension/popup/index.html" 22 | }, 23 | 24 | "side_panel": { 25 | "default_path": "/src/browser-extension/popup/index.html" 26 | }, 27 | 28 | "declarative_net_request": { 29 | "rule_resources": [ 30 | { 31 | "id": "ruleset", 32 | "enabled": true, 33 | "path": "rules.json" 34 | } 35 | ] 36 | }, 37 | 38 | "content_scripts": [ 39 | { 40 | "matches": [""], 41 | "all_frames": true, 42 | "js": [ "/src/browser-extension/content_script/index.tsx"] 43 | } 44 | ], 45 | 46 | "web_accessible_resources": [ 47 | { 48 | "resources": ["rules.json", "assets/images/*"], 49 | "matches": [""] 50 | } 51 | ], 52 | 53 | "background": { 54 | "service_worker": "/src/browser-extension/background/index.ts" 55 | }, 56 | "permissions": ["tabs", "storage", "contextMenus", "sidePanel","webRequest", "cookies", "declarativeNetRequestWithHostAccess"], 57 | 58 | "commands": { 59 | "open-popup": { 60 | "suggested_key": { 61 | "default": "Ctrl+Shift+Y", 62 | "mac": "Command+Shift+Y" 63 | }, 64 | "description": "Open the popup" 65 | } 66 | }, 67 | 68 | "host_permissions": [ 69 | "http://127.0.0.1:8765/", 70 | "https://*.openai.com/", 71 | "https://*.tcr9i.chat.openai.com/", 72 | "https://*.openai.azure.com/", 73 | "https://*.ingest.sentry.io/", 74 | "*://speech.platform.bing.com/", 75 | "https://chatgpt.com/", 76 | "https://*.chatglm.cn/", 77 | "https://*.moonshot.cn/", 78 | "https://*.volces.com/", 79 | "https://*.deepseek.com/" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /src/browser-extension/options/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/browser-extension/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GPT Tutor Options 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/browser-extension/options/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { Settings } from '../../common/components/Settings' 4 | import { Client as Styletron } from 'styletron-engine-atomic' 5 | import '../../common/i18n.js' 6 | import './index.css' 7 | 8 | const engine = new Styletron() 9 | 10 | const Options = () => { 11 | return 12 | } 13 | 14 | const root = createRoot(document.getElementById('root') as HTMLElement) 15 | 16 | root.render( 17 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /src/browser-extension/popup/index.css: -------------------------------------------------------------------------------- 1 | /* @tailwind base; */ 2 | /* @tailwind components; */ 3 | /* @tailwind utilities; */ 4 | html, body { 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | .popup { 11 | background: #1f1f1f; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/browser-extension/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GPT Tutor 6 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/browser-extension/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import { Translator } from '../../common/components/Translator' 3 | import { Client as Styletron } from 'styletron-engine-atomic' 4 | import '../../common/i18n.js' 5 | import './index.css' 6 | import { PREFIX } from '../../common/constants' 7 | import { useTheme } from '../../common/hooks/useTheme' 8 | import { useChatStore } from '@/store/file/store' 9 | import { useEffect, useState } from 'react' 10 | import AppTutorial from '@/common/components/AppTutorial' 11 | 12 | const root = createRoot(document.getElementById('root') as HTMLElement) 13 | 14 | const engine = new Styletron({ 15 | prefix: `${PREFIX}-styletron-`, 16 | }) 17 | 18 | function App() { 19 | const { theme } = useTheme() 20 | const { settings, showSettings } = useChatStore() 21 | const [shouldShowTutorial, setShouldShowTutorial] = useState(false) 22 | const [defaultShowSettings, setDefaultShowSettings] = useState(false) 23 | 24 | // 处理首次使用逻辑 25 | useEffect(() => { 26 | if (settings?.isFirstTimeUse) { 27 | setDefaultShowSettings(false) 28 | } else { 29 | setDefaultShowSettings(true) 30 | } 31 | }, [settings?.isFirstTimeUse]) 32 | 33 | // 监听设置界面的关闭 34 | useEffect(() => { 35 | if (showSettings) { 36 | // 当设置页面打开时,确保不显示教程 37 | setShouldShowTutorial(false) 38 | } else if (!showSettings && settings?.isFirstTimeUse && !settings?.tutorialCompleted) { 39 | // 当设置页面关闭且满足条件时,延迟显示教程 40 | // 给予足够时间让主界面组件完全渲染 41 | const timer = setTimeout(() => { 42 | setShouldShowTutorial(true) 43 | }, 500) // 添加适当的延迟 44 | 45 | return () => clearTimeout(timer) 46 | } 47 | }, [showSettings, settings?.isFirstTimeUse, settings?.tutorialCompleted]) 48 | 49 | return ( 50 |
58 | 59 | {shouldShowTutorial && } 60 |
61 | ) 62 | } 63 | 64 | root.render() 65 | -------------------------------------------------------------------------------- /src/browser-extension/youglish/youglish.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 56 | 57 | 58 | 59 | <!DOCTYPE html> 60 | -------------------------------------------------------------------------------- /src/common/AnswerTabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Block } from 'baseui-sd/block' 3 | import { Tabs, Tab } from 'baseui-sd/tabs' 4 | import { Delete } from 'baseui-sd/icon' 5 | import { useTranslation } from 'react-i18next' 6 | import toast from 'react-hot-toast' 7 | import { useChatStore } from '@/store/file/store' 8 | 9 | const AnswerTabs: React.FC = () => { 10 | const { t } = useTranslation() 11 | const { currentFileId, answers, setAnswers, selectedWord, updateWordAnswers } = useChatStore() 12 | 13 | const [activeKey, setActiveKey] = useState('') 14 | 15 | const handleTabDelete = async (actionName: string, event: React.MouseEvent) => { 16 | event.stopPropagation() 17 | const updatedAnswers = { ...answers } 18 | delete updatedAnswers[actionName] 19 | 20 | if (selectedWord && currentFileId) { 21 | try { 22 | await updateWordAnswers(updatedAnswers) 23 | setAnswers(updatedAnswers) 24 | toast.success(t('Deleted successfully')) 25 | } catch (error) { 26 | console.error('删除失败:', error) 27 | toast.error(t('Failed to delete')) 28 | } 29 | } else { 30 | setAnswers(updatedAnswers) 31 | } 32 | } 33 | 34 | return ( 35 | setActiveKey(activeKey as string)} 38 | overrides={{ 39 | Root: { 40 | style: { 41 | width: '100%', 42 | }, 43 | }, 44 | }} 45 | > 46 | {Object.entries(answers).map(([actionName]) => ( 47 | 51 | {actionName} 52 | handleTabDelete(actionName, e)} 55 | $style={{ 56 | 'cursor': 'pointer', 57 | ':hover': { opacity: 0.7 }, 58 | }} 59 | > 60 | 61 | 62 | 63 | } 64 | > 65 | ))} 66 | 67 | ) 68 | } 69 | 70 | export default AnswerTabs 71 | -------------------------------------------------------------------------------- /src/common/analysis.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import { getSettings, isDesktopApp, isUserscript } from './utils' 3 | 4 | export async function setupAnalysis() { 5 | if (isUserscript()) { 6 | return 7 | } 8 | doSetupAnalysis() 9 | } 10 | 11 | let isAnalysisSetupped = false 12 | 13 | export async function doSetupAnalysis() { 14 | if (isAnalysisSetupped) { 15 | return 16 | } 17 | isAnalysisSetupped = true 18 | const settings = await getSettings() 19 | if (settings.disableCollectingStatistics) { 20 | return 21 | } 22 | if (isDesktopApp()) { 23 | Sentry.init({ 24 | dsn: 'https://477519542bd6491cb347ca3f55fcdce6@o441417.ingest.sentry.io/4505051776090112', 25 | integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], 26 | // Performance Monitoring 27 | tracesSampleRate: 0.5, // Capture 100% of the transactions, reduce in production! 28 | // Session Replay 29 | replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. 30 | replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/arkose/generator.js: -------------------------------------------------------------------------------- 1 | import Browser from 'webextension-polyfill' 2 | 3 | class ArkoseTokenGenerator { 4 | constructor() { 5 | this.enforcement = undefined 6 | this.pendingPromises = [] 7 | window.useArkoseSetupEnforcement = this.useArkoseSetupEnforcement.bind(this) 8 | this.injectScript() 9 | } 10 | 11 | useArkoseSetupEnforcement(enforcement) { 12 | this.enforcement = enforcement 13 | enforcement.setConfig({ 14 | onCompleted: (r) => { 15 | console.debug('enforcement.onCompleted', r) 16 | this.pendingPromises.forEach((promise) => { 17 | promise.resolve(r.token) 18 | }) 19 | this.pendingPromises = [] 20 | }, 21 | onReady: () => { 22 | console.debug('enforcement.onReady') 23 | }, 24 | onError: (r) => { 25 | console.debug('enforcement.onError', r) 26 | this.pendingPromises.forEach((promise) => { 27 | promise.reject(new Error('Error generating arkose token')) 28 | }) 29 | }, 30 | onFailed: (r) => { 31 | console.debug('enforcement.onFailed', r) 32 | this.pendingPromises.forEach((promise) => { 33 | promise.reject(new Error('Failed to generate arkose token')) 34 | }) 35 | }, 36 | }) 37 | } 38 | 39 | injectScript() { 40 | const script = document.createElement('script') 41 | script.src = Browser.runtime.getURL('/js/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js') 42 | script.async = true 43 | script.defer = true 44 | script.setAttribute('data-callback', 'useArkoseSetupEnforcement') 45 | document.body.appendChild(script) 46 | } 47 | 48 | async generate() { 49 | if (!this.enforcement) { 50 | return 51 | } 52 | return new Promise((resolve, reject) => { 53 | this.pendingPromises = [{ resolve, reject }] // store only one promise for now. 54 | this.enforcement.run() 55 | }) 56 | } 57 | } 58 | 59 | export const arkoseTokenGenerator = new ArkoseTokenGenerator() 60 | -------------------------------------------------------------------------------- /src/common/arkose/index.ts: -------------------------------------------------------------------------------- 1 | import { arkoseTokenGenerator } from './generator.js' 2 | 3 | export async function ArkoseToken() { 4 | const token = await arkoseTokenGenerator.generate() 5 | if (token) { 6 | return token 7 | } else { 8 | console.log('Fail to get arkosetoken!') 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/common/assets/gif/drag_to_resize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/gif/drag_to_resize.gif -------------------------------------------------------------------------------- /src/common/assets/images/beams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/beams.jpg -------------------------------------------------------------------------------- /src/common/assets/images/chatglm.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/common/assets/images/claude.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/common/assets/images/groq.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/common/assets/images/icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/icon-large.png -------------------------------------------------------------------------------- /src/common/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/icon.png -------------------------------------------------------------------------------- /src/common/assets/images/kimi.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/common/assets/images/moonshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/moonshot-dark.png -------------------------------------------------------------------------------- /src/common/assets/images/moonshot-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/moonshot-light.png -------------------------------------------------------------------------------- /src/common/assets/images/openrouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/openrouter.png -------------------------------------------------------------------------------- /src/common/assets/images/party-popper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/party-popper.gif -------------------------------------------------------------------------------- /src/common/assets/images/rocket.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GPT-language/gpt-tutor-for-chrome/3c51f2d13773890de2a17319066758938b7181f8/src/common/assets/images/rocket.gif -------------------------------------------------------------------------------- /src/common/background/eventnames.ts: -------------------------------------------------------------------------------- 1 | export const BackgroundEventNames = { 2 | fetch: 'fetch', 3 | } 4 | -------------------------------------------------------------------------------- /src/common/background/services/arkoseToken.ts: -------------------------------------------------------------------------------- 1 | import { arkoseToken } from '../../internal-services/db' 2 | import { IarkoseTokenInternalService } from '../../internal-services/arkoseToken' 3 | import { callMethod } from './base' 4 | 5 | class BackgroundTokenService implements IarkoseTokenInternalService { 6 | async putItem(item: arkoseToken): Promise { 7 | return await callMethod('tokenInternalService', 'putItem', [item]) 8 | } 9 | 10 | async getNextValidToken(): Promise { 11 | return await callMethod('tokenInternalService', 'getNextValidToken', []) 12 | } 13 | async initializeTokens(): Promise { 14 | return await callMethod('tokenInternalService', 'getNextValidToken', []) 15 | } 16 | initializeTokenInternalService(): void { 17 | callMethod('tokenInternalService', 'initializeTokenInternalService', []) 18 | } 19 | 20 | getTokenInternalService(): TokenInternalService | null { 21 | return callMethod('tokenInternalService', 'getTokenInternalService', []) 22 | } 23 | } 24 | 25 | export const backgroundTokenService = new BackgroundTokenService() 26 | -------------------------------------------------------------------------------- /src/common/background/services/base.ts: -------------------------------------------------------------------------------- 1 | import { BackgroundEventNames } from '../eventnames' 2 | 3 | export async function callMethod( 4 | eventType: keyof typeof BackgroundEventNames, 5 | methodName: string, 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | args: any[] 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | ): Promise { 10 | const browser = (await import('webextension-polyfill')).default 11 | const resp = await browser.runtime.sendMessage({ 12 | type: BackgroundEventNames[eventType], 13 | method: methodName, 14 | args: args, 15 | }) 16 | return resp.result 17 | } 18 | -------------------------------------------------------------------------------- /src/common/background/services/vocabulary.ts: -------------------------------------------------------------------------------- 1 | import { VocabularyItem } from '../../internal-services/db' 2 | import { IVocabularyInternalService } from '../../internal-services/vocabulary' 3 | import { callMethod } from './base' 4 | 5 | class BackgroundVocabularyService implements IVocabularyInternalService { 6 | async putItem(item: VocabularyItem): Promise { 7 | return await callMethod('vocabularyService', 'putItem', [item]) 8 | } 9 | 10 | async getItem(word: string): Promise { 11 | return await callMethod('vocabularyService', 'getItem', [word]) 12 | } 13 | 14 | async deleteItem(word: string): Promise { 15 | return await callMethod('vocabularyService', 'deleteItem', [word]) 16 | } 17 | 18 | async countItems(): Promise { 19 | return await callMethod('vocabularyService', 'countItems', []) 20 | } 21 | 22 | async listItems(): Promise { 23 | return await callMethod('vocabularyService', 'listItems', []) 24 | } 25 | 26 | async listRandomItems(limit: number): Promise { 27 | return await callMethod('vocabularyService', 'listRandomItems', [limit]) 28 | } 29 | 30 | async listFrequencyItems(limit: number): Promise { 31 | return await callMethod('vocabularyService', 'listFrequencyItems', [limit]) 32 | } 33 | 34 | async isCollected(word: string): Promise { 35 | return await callMethod('vocabularyService', 'isCollected', [word]) 36 | } 37 | } 38 | 39 | export const backgroundVocabularyService = new BackgroundVocabularyService() 40 | -------------------------------------------------------------------------------- /src/common/components/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Checkbox, LABEL_PLACEMENT } from 'baseui-sd/checkbox' 3 | 4 | export interface ICheckBox { 5 | label?: string 6 | value?: boolean 7 | onChange?: (value: boolean) => void 8 | labelSmall?: string 9 | } 10 | 11 | export function CheckBox({ label, value, onChange, labelSmall }: ICheckBox) { 12 | return ( 13 | onChange?.(!value)} labelPlacement={LABEL_PLACEMENT.right}> 14 | {label || ''} 15 | {labelSmall &&
{labelSmall}
} 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/common/components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from 'baseui-sd/tooltip' 2 | import { CopyToClipboard } from 'react-copy-to-clipboard' 3 | import { RxCopy } from 'react-icons/rx' 4 | import { useTranslation } from 'react-i18next' 5 | import toast from 'react-hot-toast' 6 | 7 | export function CopyButton({ text, styles }: { text: string; styles: { actionButton: string } }) { 8 | const { t } = useTranslation() 9 | return ( 10 | <> 11 | 12 |
13 | { 16 | toast(t('Copy to clipboard'), { 17 | duration: 3000, 18 | icon: '👏', 19 | }) 20 | }} 21 | options={{ format: 'text/plain' }} 22 | > 23 |
24 | 25 |
26 |
27 |
28 |
29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/common/components/ErrorFallback.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) { 4 | return ( 5 |
12 |

17 | Something went wrong: 18 |

19 |

24 | {error.message} 25 |

26 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/common/components/Form/index.module.css: -------------------------------------------------------------------------------- 1 | .formItem { 2 | margin-bottom: 10px; 3 | } 4 | 5 | .formItem:last-child { 6 | margin-bottom: 0; 7 | } 8 | 9 | .error { 10 | color: red; 11 | font-size: 12px; 12 | } 13 | 14 | .help { 15 | color: orangered; 16 | font-size: 12px; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | import * as validators from './validators' 2 | 3 | export { validators } 4 | export * from './form' 5 | -------------------------------------------------------------------------------- /src/common/components/Form/item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FieldProps } from 'rc-field-form/lib/Field' 3 | import { Field } from 'rc-field-form' 4 | import styles from './index.module.css' 5 | 6 | export interface IFormItemProps extends FieldProps { 7 | label?: React.ReactNode 8 | required?: boolean 9 | style?: React.CSSProperties 10 | } 11 | 12 | export const FormItem = ({ label: label_, required, style, children, ...restProps }: IFormItemProps) => { 13 | let label = label_ 14 | if (required) { 15 | label = {label} * 16 | } 17 | return ( 18 |
19 | {/* eslint-disable-next-line react/jsx-props-no-spreading */} 20 | 21 | {(control, meta, form) => { 22 | const childNode = 23 | typeof children === 'function' 24 | ? children(control, meta, form) 25 | : React.cloneElement(children as React.ReactElement, { 26 | label, 27 | errorMessage: meta.errors.join(';'), 28 | ...control, 29 | }) 30 | return <>{childNode} 31 | }} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/common/components/Form/typings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | /* eslint-disable @typescript-eslint/ban-types */ 4 | 5 | type Cons = T extends readonly any[] 6 | ? ((h: H, ...t: T) => void) extends (...r: infer R) => void 7 | ? R 8 | : never 9 | : never 10 | 11 | type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]] 12 | 13 | // https://stackoverflow.com/a/58436959 14 | export type Paths = [D] extends [never] 15 | ? never 16 | : T extends object 17 | ? { 18 | [K in keyof T]-?: [K] | (Paths extends infer P ? (P extends [] ? never : Cons) : never) 19 | }[keyof T] 20 | : [] 21 | 22 | interface NextInt { 23 | 0: 1 24 | 1: 2 25 | 2: 3 26 | 3: 4 27 | 4: 5 28 | [rest: number]: number 29 | } 30 | 31 | export type PathType = { 32 | [K in keyof P & number & Index]: P[K] extends undefined 33 | ? T 34 | : P[K] extends keyof T 35 | ? NextInt[K] extends keyof P & number 36 | ? PathType> 37 | : T[P[K]] 38 | : never 39 | }[Index] 40 | 41 | export type NamePath = keyof T | Paths 42 | 43 | export interface Control { 44 | value?: T 45 | onChange?: (value: T) => void 46 | } 47 | -------------------------------------------------------------------------------- /src/common/components/GlobalSuspense.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | 3 | export function GlobalSuspense({ children }: { children: React.ReactNode }) { 4 | // TODO: a global loading fallback 5 | return {children} 6 | } 7 | -------------------------------------------------------------------------------- /src/common/components/IpLocationNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Trans } from 'react-i18next' 3 | import { Notification, KIND as NOTIFICATION_KIND } from 'baseui-sd/notification' 4 | import { StyledLink } from 'baseui-sd/link' 5 | import { IpLocation, getIpLocationInfo } from '../geo' 6 | import { isUsingOpenAIOfficial } from '../utils' 7 | 8 | export default function IpLocationNotification(props: { showSettings: boolean }) { 9 | const [ipLocation, setIpLocation] = useState(null) 10 | useEffect( 11 | () => { 12 | ;(async () => { 13 | setIpLocation( 14 | (await isUsingOpenAIOfficial()) 15 | ? await getIpLocationInfo() 16 | : null /* Not directly connecting to OpenAI */ 17 | ) 18 | })() 19 | }, 20 | [props.showSettings] // refresh on provider / API endpoint change 21 | ) 22 | 23 | if (ipLocation === null || ipLocation.supported) return <> 24 | 25 | const referenceLink = 26 | 27 | return ipLocation.name ? ( 28 | 35 | 42 | 43 | ) : ( 44 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/common/components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown' 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 3 | import oneDark from 'react-syntax-highlighter/dist/esm/styles/prism/one-dark' 4 | import oneLight from 'react-syntax-highlighter/dist/esm/styles/prism/one-light' 5 | import { useTheme } from '../hooks/useTheme' 6 | 7 | export interface IMarkdownProps { 8 | children: string 9 | } 10 | 11 | export function Markdown({ children }: IMarkdownProps) { 12 | const { themeType } = useTheme() 13 | 14 | return ( 15 | 27 | {String(children).replace(/\n$/, '')} 28 | 29 | ) : ( 30 | 31 | {children} 32 | 33 | ) 34 | }, 35 | }} 36 | > 37 | {children} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/common/components/MessageCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { MessageCard } from 'baseui-sd/message-card' 3 | import { useChatStore } from '@/store/file/store' 4 | import { useTranslation } from 'react-i18next' 5 | import * as utils from '../utils' 6 | import { ISettings } from '../types' 7 | import { HeadingXSmall } from 'baseui-sd/typography' 8 | export default function MessageCardsContainer() { 9 | const { isShowMessageCard, setShowActionManager, setShowSettings } = useChatStore() 10 | const { t } = useTranslation() 11 | const [targetLang, setTargetLang] = useState('zh-Hans') 12 | 13 | useEffect(() => { 14 | const getLanguage = async () => { 15 | const settings: ISettings = await utils.getSettings() 16 | if (settings && settings.i18n) { 17 | console.log(settings.i18n) 18 | setTargetLang(settings.i18n) 19 | } 20 | } 21 | getLanguage() 22 | }, []) 23 | 24 | const handleFeedbackClick = () => { 25 | window.open( 26 | 'https://github.com/GPT-language/gpt-tutor-resources/discussions/categories/prompt-related', 27 | '_blank' 28 | ) 29 | } 30 | 31 | if (!isShowMessageCard) return null 32 | 33 | return ( 34 |
35 | {t('Not satisfied with the results? Try the following:')} 36 |
37 | setShowSettings(true)} 41 | paragraph={t('Use a better model(gpt-4o or claude-3.5-opus) to get more accurate results')} 42 | /> 43 | setShowActionManager(true)} 47 | paragraph={t('Adjust your prompt in ActionManager')} 48 | /> 49 | 55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/common/components/ModelSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Select, TYPE, OnChangeParams } from 'baseui-sd/select' 3 | import { getModels } from '../utils' 4 | import { IModel } from '../engines/interfaces' 5 | 6 | export interface IModelSelectProps { 7 | value?: string 8 | onChange?: (value: string | undefined) => void 9 | } 10 | 11 | export default function ModelSelect({ value, onChange }: IModelSelectProps) { 12 | const [groupedModels, setGroupedModels] = useState([]) 13 | // Function to group actions into options 14 | // 将actions根据group属性分组 15 | useEffect(() => { 16 | // Fetch all models for each provider 17 | const fetchAllModels = async () => { 18 | const models = await getModels() 19 | console.log('models', models) 20 | setGroupedModels(models) 21 | } 22 | fetchAllModels() 23 | }, []) 24 | 25 | useEffect(() => { 26 | console.log('groupedModels', groupedModels) 27 | console.log('value is ' + value) 28 | }, [groupedModels, value]) 29 | 30 | return ( 31 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/common/components/RenderingFormatSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Select, SelectProps } from 'baseui-sd/select' 2 | import { ActionOutputRenderingFormat } from '../internal-services/db' 3 | 4 | export interface IRenderingFormatSelector extends Omit { 5 | value?: ActionOutputRenderingFormat 6 | onChange?: (value: ActionOutputRenderingFormat) => void 7 | } 8 | 9 | export function RenderingFormatSelector({ value, onChange, ...props }: IRenderingFormatSelector) { 10 | const options = [ 11 | { 12 | id: 'text', 13 | label: 'Text', 14 | }, 15 | { 16 | id: 'markdown', 17 | label: 'Markdown', 18 | }, 19 | { 20 | id: 'latex', 21 | label: 'LaTeX', 22 | }, 23 | ] as { 24 | id: ActionOutputRenderingFormat 25 | label: React.ReactNode 26 | }[] 27 | 28 | return ( 29 |