├── .gitattributes
├── .prettierignore
├── src
├── pages
│ ├── styles.scss
│ └── IndependentPanel
│ │ ├── index.html
│ │ ├── index.jsx
│ │ └── styles.scss
├── logo.png
├── utils
│ ├── is-edge.mjs
│ ├── is-safari.mjs
│ ├── is-firefox.mjs
│ ├── get-client-position.mjs
│ ├── ends-with-question-mark.mjs
│ ├── parse-int-with-clamp.mjs
│ ├── parse-float-with-clamp.mjs
│ ├── change-children-font-size.mjs
│ ├── open-url.mjs
│ ├── create-element-at-position.mjs
│ ├── set-element-position-in-viewport.mjs
│ ├── get-possible-element-by-query-selector.mjs
│ ├── get-conversation-pairs.mjs
│ ├── update-ref-height.mjs
│ ├── wait-for-element-to-exist-and-select.mjs
│ ├── limited-fetch.mjs
│ ├── index.mjs
│ ├── fetch-bg.mjs
│ ├── jwt-token-generator.mjs
│ ├── fetch-sse.mjs
│ ├── is-mobile.mjs
│ ├── get-core-content-text.mjs
│ ├── crop-text.mjs
│ └── eventsource-parser.mjs
├── services
│ ├── clients
│ │ ├── poe
│ │ │ ├── graphql
│ │ │ │ ├── ChatAddedSubscription.graphql
│ │ │ │ ├── SummarizePlainPostQuery.graphql
│ │ │ │ ├── ChatFragment.graphql
│ │ │ │ ├── BioFragment.graphql
│ │ │ │ ├── HandleFragment.graphql
│ │ │ │ ├── MessageAddedSubscription.graphql
│ │ │ │ ├── ViewerStateUpdatedSubscription.graphql
│ │ │ │ ├── SummarizeQuotePostQuery.graphql
│ │ │ │ ├── MessageDeletedSubscription.graphql
│ │ │ │ ├── ChatViewQuery.graphql
│ │ │ │ ├── MessageRemoveVoteMutation.graphql
│ │ │ │ ├── StaleChatUpdateMutation.graphql
│ │ │ │ ├── DeleteHumanMessagesMutation.graphql
│ │ │ │ ├── SummarizeSharePostQuery.graphql
│ │ │ │ ├── AutoSubscriptionMutation.graphql
│ │ │ │ ├── MessageSetVoteMutation.graphql
│ │ │ │ ├── MessageFragment.graphql
│ │ │ │ ├── ShareMessagesMutation.graphql
│ │ │ │ ├── SendVerificationCodeForLoginMutation.graphql
│ │ │ │ ├── LoginWithVerificationCodeMutation.graphql
│ │ │ │ ├── SignupWithVerificationCodeMutation.graphql
│ │ │ │ ├── UserSnippetFragment.graphql
│ │ │ │ ├── AddMessageBreakMutation.graphql
│ │ │ │ ├── ViewerInfoQuery.graphql
│ │ │ │ ├── ChatPaginationQuery.graphql
│ │ │ │ ├── ViewerStateFragment.graphql
│ │ │ │ └── AddHumanMessageMutation.graphql
│ │ │ └── websocket.js
│ │ └── bard
│ │ │ └── index.mjs
│ ├── apis
│ │ ├── aiml-api.mjs
│ │ ├── deepseek-api.mjs
│ │ ├── openrouter-api.mjs
│ │ ├── moonshot-api.mjs
│ │ ├── chatglm-api.mjs
│ │ ├── bard-web.mjs
│ │ ├── ollama-api.mjs
│ │ ├── poe-web.mjs
│ │ ├── claude-web.mjs
│ │ ├── shared.mjs
│ │ ├── claude-api.mjs
│ │ ├── waylaidwanderer-api.mjs
│ │ ├── azure-openai-api.mjs
│ │ ├── bing-web.mjs
│ │ └── custom-api.mjs
│ ├── local-session.mjs
│ ├── init-session.mjs
│ └── wrappers.mjs
├── _locales
│ ├── i18n.mjs
│ ├── i18n-react.mjs
│ └── resources.mjs
├── fonts
│ ├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2
│ ├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2
│ ├── SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2
│ └── styles.css
├── hooks
│ ├── use-theme.mjs
│ ├── use-clamp-window-size.mjs
│ ├── use-window-size.mjs
│ ├── use-window-theme.mjs
│ └── use-config.mjs
├── components
│ ├── index.mjs
│ ├── CopyButton
│ │ └── index.jsx
│ ├── MarkdownRender
│ │ ├── Hyperlink.jsx
│ │ ├── Pre.jsx
│ │ ├── markdown-without-katex.jsx
│ │ └── markdown.jsx
│ ├── ConfirmButton
│ │ └── index.jsx
│ ├── DeleteButton
│ │ └── index.jsx
│ ├── FeedbackForChatGPTWeb
│ │ └── index.jsx
│ ├── ReadButton
│ │ └── index.jsx
│ ├── WebJumpBackNotification
│ │ └── index.jsx
│ ├── InputBox
│ │ └── index.jsx
│ ├── DecisionCard
│ │ └── index.jsx
│ └── ConversationItem
│ │ └── index.jsx
├── popup
│ ├── index.html
│ ├── sections
│ │ ├── SiteAdapters.jsx
│ │ ├── ModulesPart.jsx
│ │ └── FeaturePages.jsx
│ ├── index.jsx
│ ├── styles.scss
│ └── Popup.jsx
├── content-script
│ ├── site-adapters
│ │ ├── duckduckgo
│ │ │ └── index.mjs
│ │ ├── brave
│ │ │ └── index.mjs
│ │ ├── baidu
│ │ │ └── index.mjs
│ │ ├── quora
│ │ │ └── index.mjs
│ │ ├── reddit
│ │ │ └── index.mjs
│ │ ├── arxiv
│ │ │ └── index.mjs
│ │ ├── juejin
│ │ │ └── index.mjs
│ │ ├── weixin
│ │ │ └── index.mjs
│ │ ├── followin
│ │ │ └── index.mjs
│ │ ├── stackoverflow
│ │ │ └── index.mjs
│ │ ├── zhihu
│ │ │ └── index.mjs
│ │ ├── gitlab
│ │ │ └── index.mjs
│ │ ├── bilibili
│ │ │ └── index.mjs
│ │ └── youtube
│ │ │ └── index.mjs
│ ├── menu-tools
│ │ └── index.mjs
│ └── selection-tools
│ │ └── index.mjs
├── background
│ ├── commands.mjs
│ └── menus.mjs
├── config
│ └── language.mjs
├── rules.json
├── manifest.v2.json
└── manifest.json
├── .gitignore
├── .github
├── CONTRIBUTING.md
├── dependabot.yml
├── workflows
│ ├── verify-configs.yml
│ ├── pr-tests.yml
│ ├── pre-release-build.yml
│ └── tagged-release.yml
└── ISSUE_TEMPLATE
│ ├── feature-request---新功能请求.md
│ └── bug-report---问题报告.md
├── screenshots
├── preview_youtube.jpg
├── preview_settings.jpg
├── preview_independentpanel.jpg
├── preview_github_rightclickmenu.jpg
├── preview_reddit_selectiontools.jpg
└── preview_google_floatingwindow_conversationbranch.jpg
├── safari
├── project.patch
├── appdmg.json
├── export-options.plist
├── project.pre.patch
└── build.sh
├── .prettierrc
├── .eslintrc.json
├── LICENSE
├── package.json
└── CURRENT_CHANGE.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/services/clients/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | src/manifest.json
3 | src/manifest.v2.json
--------------------------------------------------------------------------------
/src/pages/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'IndependentPanel/styles.scss';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | node_modules/
4 | build/
5 | .DS_Store
6 | *.zip
7 |
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/src/logo.png
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See https://github.com/ChatGPTBox-dev/chatGPTBox/wiki/Development&Contributing
--------------------------------------------------------------------------------
/screenshots/preview_youtube.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_youtube.jpg
--------------------------------------------------------------------------------
/src/utils/is-edge.mjs:
--------------------------------------------------------------------------------
1 | export function isEdge() {
2 | return navigator.userAgent.toLowerCase().includes('edg')
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/is-safari.mjs:
--------------------------------------------------------------------------------
1 | export function isSafari() {
2 | return navigator.vendor === 'Apple Computer, Inc.'
3 | }
4 |
--------------------------------------------------------------------------------
/screenshots/preview_settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_settings.jpg
--------------------------------------------------------------------------------
/src/utils/is-firefox.mjs:
--------------------------------------------------------------------------------
1 | export function isFirefox() {
2 | return navigator.userAgent.toLowerCase().includes('firefox')
3 | }
4 |
--------------------------------------------------------------------------------
/screenshots/preview_independentpanel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_independentpanel.jpg
--------------------------------------------------------------------------------
/screenshots/preview_github_rightclickmenu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_github_rightclickmenu.jpg
--------------------------------------------------------------------------------
/screenshots/preview_reddit_selectiontools.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_reddit_selectiontools.jpg
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatAddedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription ChatAddedSubscription {
2 | chatAdded {
3 | ...ChatFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/_locales/i18n.mjs:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import { resources } from './resources'
3 |
4 | i18n.init({
5 | resources,
6 | fallbackLng: 'en',
7 | })
8 |
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2
--------------------------------------------------------------------------------
/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/src/fonts/SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizePlainPostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizePlainPostQuery($comment: String!) {
2 | summarizePlainPost(comment: $comment)
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/get-client-position.mjs:
--------------------------------------------------------------------------------
1 | export function getClientPosition(e) {
2 | const rect = e.getBoundingClientRect()
3 | return { x: rect.left, y: rect.top }
4 | }
5 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment ChatFragment on Chat {
2 | id
3 | chatId
4 | defaultBotNickname
5 | shouldShowDisclaimer
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/BioFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment BioFragment on Viewer {
2 | id
3 | poeUser {
4 | id
5 | uid
6 | bio
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/safari/project.patch:
--------------------------------------------------------------------------------
1 | --- a/build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj/project.pbxproj
2 | +++ b/build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj/project.pbxproj
3 |
--------------------------------------------------------------------------------
/screenshots/preview_google_floatingwindow_conversationbranch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChatGPTBox-dev/chatGPTBox/HEAD/screenshots/preview_google_floatingwindow_conversationbranch.jpg
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/HandleFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment HandleFragment on Viewer {
2 | id
3 | poeUser {
4 | id
5 | uid
6 | handle
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageAddedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription MessageAddedSubscription($chatId: BigInt!) {
2 | messageAdded(chatId: $chatId) {
3 | ...MessageFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerStateUpdatedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription ViewerStateUpdatedSubscription {
2 | viewerStateUpdated {
3 | ...ViewerStateFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizeQuotePostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizeQuotePostQuery($comment: String, $quotedPostId: BigInt!) {
2 | summarizeQuotePost(comment: $comment, quotedPostId: $quotedPostId)
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageDeletedSubscription.graphql:
--------------------------------------------------------------------------------
1 | subscription MessageDeletedSubscription($chatId: BigInt!) {
2 | messageDeleted(chatId: $chatId) {
3 | id
4 | messageId
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | commit-message:
8 | prefix: "chore"
9 | include: "scope"
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatViewQuery.graphql:
--------------------------------------------------------------------------------
1 | query ChatViewQuery($bot: String!) {
2 | chatOfBot(bot: $bot) {
3 | id
4 | chatId
5 | defaultBotNickname
6 | shouldShowDisclaimer
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageRemoveVoteMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation MessageRemoveVoteMutation($messageId: BigInt!) {
2 | messageRemoveVote(messageId: $messageId) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/StaleChatUpdateMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation StaleChatUpdateMutation($chatId: BigInt!) {
2 | staleChatUpdate(chatId: $chatId) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/DeleteHumanMessagesMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation DeleteHumanMessagesMutation($messageIds: [BigInt!]!) {
2 | messagesDelete(messageIds: $messageIds) {
3 | viewer {
4 | id
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SummarizeSharePostQuery.graphql:
--------------------------------------------------------------------------------
1 | query SummarizeSharePostQuery($comment: String!, $chatId: BigInt!, $messageIds: [BigInt!]!) {
2 | summarizeSharePost(comment: $comment, chatId: $chatId, messageIds: $messageIds)
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AutoSubscriptionMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AutoSubscriptionMutation($subscriptions: [AutoSubscriptionQuery!]!) {
2 | autoSubscribe(subscriptions: $subscriptions) {
3 | viewer {
4 | id
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/safari/appdmg.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Fission - ChatBox",
3 | "icon": "../src/logo.png",
4 | "contents": [
5 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" },
6 | { "x": 192, "y": 344, "type": "file", "path": "../build/Fission - ChatBox.app" }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/safari/export-options.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | method
6 | mac-application
7 |
8 |
--------------------------------------------------------------------------------
/src/utils/ends-with-question-mark.mjs:
--------------------------------------------------------------------------------
1 | export function endsWithQuestionMark(question) {
2 | return (
3 | question.endsWith('?') || // ASCII
4 | question.endsWith('?') || // Chinese/Japanese
5 | question.endsWith('؟') || // Arabic
6 | question.endsWith('⸮') // Arabic
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageSetVoteMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation MessageSetVoteMutation($messageId: BigInt!, $voteType: VoteType!, $reason: String) {
2 | messageSetVote(messageId: $messageId, voteType: $voteType, reason: $reason) {
3 | message {
4 | ...MessageFragment
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/parse-int-with-clamp.mjs:
--------------------------------------------------------------------------------
1 | export function parseIntWithClamp(value, defaultValue, min, max) {
2 | value = parseInt(value)
3 |
4 | if (isNaN(value)) value = defaultValue
5 | else if (value > max) value = max
6 | else if (value < min) value = min
7 |
8 | return value
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/parse-float-with-clamp.mjs:
--------------------------------------------------------------------------------
1 | export function parseFloatWithClamp(value, defaultValue, min, max) {
2 | value = parseFloat(value)
3 |
4 | if (isNaN(value)) value = defaultValue
5 | else if (value > max) value = max
6 | else if (value < min) value = min
7 |
8 | return value
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/MessageFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment MessageFragment on Message {
2 | id
3 | __typename
4 | messageId
5 | text
6 | linkifiedText
7 | authorNickname
8 | state
9 | vote
10 | voteReason
11 | creationTime
12 | suggestedReplies
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ShareMessagesMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation ShareMessagesMutation(
2 | $chatId: BigInt!
3 | $messageIds: [BigInt!]!
4 | $comment: String
5 | ) {
6 | messagesShare(chatId: $chatId, messageIds: $messageIds, comment: $comment) {
7 | shareCode
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/use-theme.mjs:
--------------------------------------------------------------------------------
1 | import { useConfig } from './use-config.mjs'
2 | import { useWindowTheme } from './use-window-theme.mjs'
3 |
4 | export function useTheme() {
5 | const config = useConfig()
6 | const theme = useWindowTheme()
7 | return [config.themeMode === 'auto' ? theme : config.themeMode, config]
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/change-children-font-size.mjs:
--------------------------------------------------------------------------------
1 | export function changeChildrenFontSize(element, size) {
2 | try {
3 | element.style.fontSize = size
4 | } catch {
5 | /* empty */
6 | }
7 | for (let i = 0; i < element.childNodes.length; i++) {
8 | changeChildrenFontSize(element.childNodes[i], size)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "bracketSpacing": true,
8 | "overrides": [
9 | {
10 | "files": ".prettierrc",
11 | "options": {
12 | "parser": "json"
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/_locales/i18n-react.mjs:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import { initReactI18next } from 'react-i18next'
3 | import { resources } from './resources'
4 |
5 | i18n.use(initReactI18next).init({
6 | resources,
7 | fallbackLng: 'en',
8 | interpolation: {
9 | escapeValue: false, // not needed for react as it escapes by default
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/src/utils/open-url.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 | export function openUrl(url) {
4 | Browser.tabs.query({ url, currentWindow: true }).then((tabs) => {
5 | if (tabs.length > 0) {
6 | Browser.tabs.update(tabs[0].id, { active: true })
7 | } else {
8 | Browser.tabs.create({ url })
9 | }
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/index.mjs:
--------------------------------------------------------------------------------
1 | export * from './ConfirmButton'
2 | export * from './ConversationCard'
3 | export * from './ConversationItem'
4 | export * from './CopyButton'
5 | export * from './DecisionCard'
6 | export * from './DeleteButton'
7 | export * from './FeedbackForChatGPTWeb'
8 | export * from './FloatingToolbar'
9 | export * from './InputBox'
10 | export * from './ReadButton'
11 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SendVerificationCodeForLoginMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation SendVerificationCodeForLoginMutation(
2 | $emailAddress: String
3 | $phoneNumber: String
4 | ) {
5 | sendVerificationCode(
6 | verificationReason: login
7 | emailAddress: $emailAddress
8 | phoneNumber: $phoneNumber
9 | ) {
10 | status
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/create-element-at-position.mjs:
--------------------------------------------------------------------------------
1 | export function createElementAtPosition(x = 0, y = 0, zIndex = 2147483647) {
2 | const element = document.createElement('div')
3 | element.style.position = 'fixed'
4 | element.style.zIndex = zIndex
5 | element.style.left = x + 'px'
6 | element.style.top = y + 'px'
7 | document.documentElement.appendChild(element)
8 | return element
9 | }
10 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPTBox
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/verify-configs.yml:
--------------------------------------------------------------------------------
1 | name: verify-configs
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: "0 6 * * *"
6 |
7 | jobs:
8 | verify_configs:
9 | runs-on: ubuntu-22.04
10 |
11 | steps:
12 | - uses: actions/checkout@v5
13 | - uses: actions/setup-node@v5
14 | with:
15 | node-version: 20
16 | - run: npm ci
17 | - run: npm run verify
18 |
--------------------------------------------------------------------------------
/src/utils/set-element-position-in-viewport.mjs:
--------------------------------------------------------------------------------
1 | export function setElementPositionInViewport(element, x = 0, y = 0) {
2 | const retX = Math.min(Math.max(0, window.innerWidth - element.offsetWidth), Math.max(0, x))
3 | const retY = Math.min(Math.max(0, window.innerHeight - element.offsetHeight), Math.max(0, y))
4 | element.style.left = retX + 'px'
5 | element.style.top = retY + 'px'
6 | return { x: retX, y: retY }
7 | }
8 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/duckduckgo/index.mjs:
--------------------------------------------------------------------------------
1 | import { waitForElementToExistAndSelect } from '../../../utils/index.mjs'
2 | import { config } from '../index'
3 |
4 | export default {
5 | init: async (hostname, userConfig) => {
6 | if (userConfig.insertAtTop) {
7 | return !!(await waitForElementToExistAndSelect(config.duckduckgo.resultsContainerQuery[0], 5))
8 | }
9 | return true
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/use-clamp-window-size.mjs:
--------------------------------------------------------------------------------
1 | import { useWindowSize } from './use-window-size.mjs'
2 |
3 | export function useClampWindowSize(widthRange = [0, Infinity], heightRange = [0, Infinity]) {
4 | const windowSize = useWindowSize()
5 | windowSize[0] = Math.min(widthRange[1], Math.max(windowSize[0], widthRange[0]))
6 | windowSize[1] = Math.min(heightRange[1], Math.max(windowSize[1], heightRange[0]))
7 | return windowSize
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/LoginWithVerificationCodeMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation LoginWithVerificationCodeMutation(
2 | $verificationCode: String!
3 | $emailAddress: String
4 | $phoneNumber: String
5 | ) {
6 | loginWithVerificationCode(
7 | verificationCode: $verificationCode
8 | emailAddress: $emailAddress
9 | phoneNumber: $phoneNumber
10 | ) {
11 | status
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/SignupWithVerificationCodeMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation SignupWithVerificationCodeMutation(
2 | $verificationCode: String!
3 | $emailAddress: String
4 | $phoneNumber: String
5 | ) {
6 | signupWithVerificationCode(
7 | verificationCode: $verificationCode
8 | emailAddress: $emailAddress
9 | phoneNumber: $phoneNumber
10 | ) {
11 | status
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/get-possible-element-by-query-selector.mjs:
--------------------------------------------------------------------------------
1 | export function getPossibleElementByQuerySelector(queryArray) {
2 | if (!queryArray) return
3 | for (const query of queryArray) {
4 | if (query) {
5 | try {
6 | const element = document.querySelector(query)
7 | if (element) {
8 | return element
9 | }
10 | } catch (e) {
11 | /* empty */
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/brave/index.mjs:
--------------------------------------------------------------------------------
1 | import { waitForElementToExistAndSelect } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | export default {
5 | init: async (hostname, userConfig) => {
6 | const selector = userConfig.insertAtTop
7 | ? config.brave.resultsContainerQuery[0]
8 | : config.brave.sidebarContainerQuery[0]
9 | await waitForElementToExistAndSelect(selector, 5)
10 | return true
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/UserSnippetFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment UserSnippetFragment on PoeUser {
2 | id
3 | uid
4 | bio
5 | handle
6 | fullName
7 | viewerIsFollowing
8 | isPoeOnlyUser
9 | profilePhotoURLTiny: profilePhotoUrl(size: tiny)
10 | profilePhotoURLSmall: profilePhotoUrl(size: small)
11 | profilePhotoURLMedium: profilePhotoUrl(size: medium)
12 | profilePhotoURLLarge: profilePhotoUrl(size: large)
13 | isFollowable
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPTBox
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/services/apis/aiml-api.mjs:
--------------------------------------------------------------------------------
1 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
2 |
3 | /**
4 | * @param {Browser.Runtime.Port} port
5 | * @param {string} question
6 | * @param {Session} session
7 | * @param {string} apiKey
8 | */
9 | export async function generateAnswersWithAimlApi(port, question, session, apiKey) {
10 | const baseUrl = 'https://api.aimlapi.com/v1'
11 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, apiKey)
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/apis/deepseek-api.mjs:
--------------------------------------------------------------------------------
1 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
2 |
3 | /**
4 | * @param {Browser.Runtime.Port} port
5 | * @param {string} question
6 | * @param {Session} session
7 | * @param {string} apiKey
8 | */
9 | export async function generateAnswersWithDeepSeekApi(port, question, session, apiKey) {
10 | const baseUrl = 'https://api.deepseek.com'
11 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, apiKey)
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AddMessageBreakMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AddMessageBreakMutation($chatId: BigInt!) {
2 | messageBreakCreate(chatId: $chatId) {
3 | message {
4 | id
5 | __typename
6 | messageId
7 | text
8 | linkifiedText
9 | authorNickname
10 | state
11 | vote
12 | voteReason
13 | creationTime
14 | suggestedReplies
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/services/apis/openrouter-api.mjs:
--------------------------------------------------------------------------------
1 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
2 |
3 | /**
4 | * @param {Browser.Runtime.Port} port
5 | * @param {string} question
6 | * @param {Session} session
7 | * @param {string} apiKey
8 | */
9 | export async function generateAnswersWithOpenRouterApi(port, question, session, apiKey) {
10 | const baseUrl = 'https://openrouter.ai/api/v1'
11 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, apiKey)
12 | }
13 |
--------------------------------------------------------------------------------
/src/services/apis/moonshot-api.mjs:
--------------------------------------------------------------------------------
1 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
2 |
3 | /**
4 | * @param {Browser.Runtime.Port} port
5 | * @param {string} question
6 | * @param {Session} session
7 | * @param {string} apiKey
8 | */
9 | export async function generateAnswersWithMoonshotCompletionApi(port, question, session, apiKey) {
10 | const baseUrl = 'https://api.moonshot.cn/v1'
11 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, apiKey)
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/pr-tests.yml:
--------------------------------------------------------------------------------
1 | name: pr-tests
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - "opened"
7 | - "reopened"
8 | - "synchronize"
9 | paths:
10 | - "src/**"
11 | - "build.mjs"
12 |
13 | jobs:
14 | tests:
15 | runs-on: ubuntu-22.04
16 |
17 | steps:
18 | - uses: actions/checkout@v5
19 | - uses: actions/setup-node@v5
20 | with:
21 | node-version: 20
22 | - run: npm ci
23 | - run: npm run lint
24 | - run: npm run build
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:react/recommended"],
7 | "overrides": [],
8 | "parserOptions": {
9 | "ecmaVersion": "latest",
10 | "sourceType": "module"
11 | },
12 | "rules": {
13 | "react/react-in-jsx-scope": "off"
14 | },
15 | "ignorePatterns": ["build/**", "build.mjs", "src/utils/is-mobile.mjs"],
16 | "settings": {
17 | "react": {
18 | "version": "detect"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerInfoQuery.graphql:
--------------------------------------------------------------------------------
1 | query ViewerInfoQuery {
2 | viewer {
3 | id
4 | uid
5 | ...ViewerStateFragment
6 | ...BioFragment
7 | ...HandleFragment
8 | hasCompletedMultiplayerNux
9 | poeUser {
10 | id
11 | ...UserSnippetFragment
12 | }
13 | messageLimit{
14 | canSend
15 | numMessagesRemaining
16 | resetTime
17 | shouldShowReminder
18 | }
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/utils/get-conversation-pairs.mjs:
--------------------------------------------------------------------------------
1 | export function getConversationPairs(records, isCompletion) {
2 | let pairs
3 | if (isCompletion) {
4 | pairs = ''
5 | for (const record of records) {
6 | pairs += 'Human: ' + record.question + '\nAI: ' + record.answer + '\n'
7 | }
8 | } else {
9 | pairs = []
10 | for (const record of records) {
11 | pairs.push({ role: 'user', content: record['question'] })
12 | pairs.push({ role: 'assistant', content: record['answer'] })
13 | }
14 | }
15 |
16 | return pairs
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/update-ref-height.mjs:
--------------------------------------------------------------------------------
1 | export function updateRefHeight(ref) {
2 | ref.current.style.height = 'auto'
3 | const computed = window.getComputedStyle(ref.current)
4 | const height =
5 | parseInt(computed.getPropertyValue('border-top-width'), 10) +
6 | parseInt(computed.getPropertyValue('padding-top'), 10) +
7 | ref.current.scrollHeight +
8 | parseInt(computed.getPropertyValue('padding-bottom'), 10) +
9 | parseInt(computed.getPropertyValue('border-bottom-width'), 10)
10 | ref.current.style.height = `${height}px`
11 | }
12 |
--------------------------------------------------------------------------------
/safari/project.pre.patch:
--------------------------------------------------------------------------------
1 | --- a/src/manifest.v2.json
2 | +++ b/src/manifest.v2.json
3 | @@ -1,5 +1,5 @@
4 | {
5 | - "name": "ChatGPTBox",
6 | + "name": "Fission - ChatBox",
7 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
8 | "version": "0.0.0",
9 | "manifest_version": 2,
10 | @@ -28,7 +28,7 @@
11 | "scripts": [
12 | "background.js"
13 | ],
14 | - "persistent": true
15 | + "persistent": true
16 | },
17 | "browser_action": {
18 | "default_popup": "popup.html?popup=true"
19 |
--------------------------------------------------------------------------------
/src/hooks/use-window-size.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
2 |
3 | import { useLayoutEffect, useState } from 'react'
4 |
5 | export function useWindowSize() {
6 | const [size, setSize] = useState([0, 0])
7 | useLayoutEffect(() => {
8 | function updateSize() {
9 | setSize([window.innerWidth, window.innerHeight])
10 | }
11 | window.addEventListener('resize', updateSize)
12 | updateSize()
13 | return () => window.removeEventListener('resize', updateSize)
14 | }, [])
15 | return size
16 | }
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request---新功能请求.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request / 新功能请求
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | **新功能是否与解决某个问题相关, 请描述**
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | **Describe the solution you'd like**
15 | **你期望的新功能实现方案**
16 | A clear and concise description of what you want to happen.
17 |
18 | **Additional context**
19 | **其他**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/services/apis/chatglm-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | // import { getToken } from '../../utils/jwt-token-generator.mjs'
3 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
4 |
5 | /**
6 | * @param {Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | */
10 | export async function generateAnswersWithChatGLMApi(port, question, session) {
11 | const baseUrl = 'https://open.bigmodel.cn/api/paas/v4'
12 | const config = await getUserConfig()
13 | return generateAnswersWithChatgptApiCompat(baseUrl, port, question, session, config.chatglmApiKey)
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/index.jsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import '../../_locales/i18n-react'
3 | import App from './App'
4 | import Browser from 'webextension-polyfill'
5 | import { changeLanguage } from 'i18next'
6 | import { getPreferredLanguageKey } from '../../config/index.mjs'
7 |
8 | document.body.style.margin = 0
9 | document.body.style.overflow = 'hidden'
10 | getPreferredLanguageKey().then((lang) => {
11 | changeLanguage(lang)
12 | })
13 | Browser.runtime.onMessage.addListener(async (message) => {
14 | if (message.type === 'CHANGE_LANG') {
15 | const data = message.data
16 | changeLanguage(data.lang)
17 | }
18 | })
19 | render( , document.getElementById('app'))
20 |
--------------------------------------------------------------------------------
/src/hooks/use-window-theme.mjs:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export function useWindowTheme() {
4 | const [theme, setTheme] = useState(
5 | window.matchMedia
6 | ? window.matchMedia('(prefers-color-scheme: dark)').matches
7 | ? 'dark'
8 | : 'light'
9 | : 'light',
10 | )
11 | useEffect(() => {
12 | if (!window.matchMedia) return
13 | const listener = (e) => {
14 | setTheme(e.matches ? 'dark' : 'light')
15 | }
16 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener)
17 | return () =>
18 | window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', listener)
19 | }, [])
20 | return theme
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/wait-for-element-to-exist-and-select.mjs:
--------------------------------------------------------------------------------
1 | export function waitForElementToExistAndSelect(selector, timeout = 0) {
2 | return new Promise((resolve) => {
3 | if (document.querySelector(selector)) {
4 | return resolve(document.querySelector(selector))
5 | }
6 |
7 | const observer = new MutationObserver(() => {
8 | if (document.querySelector(selector)) {
9 | resolve(document.querySelector(selector))
10 | observer.disconnect()
11 | }
12 | })
13 |
14 | observer.observe(document.body, {
15 | subtree: true,
16 | childList: true,
17 | })
18 |
19 | if (timeout)
20 | setTimeout(() => {
21 | observer.disconnect()
22 | resolve(null)
23 | }, timeout)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/safari/build.sh:
--------------------------------------------------------------------------------
1 | git apply safari/project.pre.patch
2 | npm run build
3 | xcrun safari-web-extension-converter ./build/firefox \
4 | --project-location ./build/safari --app-name "Fission - ChatBox" \
5 | --bundle-identifier dev.josStorer.chatGPTBox --force --no-prompt --no-open
6 | git apply safari/project.patch
7 | xcodebuild archive -project "./build/safari/Fission - ChatBox/Fission - ChatBox.xcodeproj" \
8 | -scheme "Fission - ChatBox (macOS)" -configuration Release -archivePath "./build/safari/Fission - ChatBox.xcarchive"
9 | xcodebuild -exportArchive -archivePath "./build/safari/Fission - ChatBox.xcarchive" \
10 | -exportOptionsPlist ./safari/export-options.plist -exportPath ./build
11 | npm install -D appdmg
12 | rm ./build/safari.dmg
13 | appdmg ./safari/appdmg.json ./build/safari.dmg
--------------------------------------------------------------------------------
/src/utils/limited-fetch.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/64304365/stop-request-after-x-amount-is-fetched
2 |
3 | export async function limitedFetch(url, maxBytes) {
4 | return new Promise((resolve, reject) => {
5 | try {
6 | const xhr = new XMLHttpRequest()
7 | xhr.onprogress = (ev) => {
8 | if (ev.loaded < maxBytes) return
9 | resolve(ev.target.responseText.substring(0, maxBytes))
10 | xhr.abort()
11 | }
12 | xhr.onload = (ev) => {
13 | resolve(ev.target.responseText.substring(0, maxBytes))
14 | }
15 | xhr.onerror = (ev) => {
16 | reject(new Error(ev.target.status))
17 | }
18 |
19 | xhr.open('GET', url)
20 | xhr.send()
21 | } catch (err) {
22 | reject(err)
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ChatPaginationQuery.graphql:
--------------------------------------------------------------------------------
1 | query ChatPaginationQuery($bot: String!, $before: String, $last: Int! = 10) {
2 | chatOfBot(bot: $bot) {
3 | id
4 | __typename
5 | messagesConnection(before: $before, last: $last) {
6 | pageInfo {
7 | hasPreviousPage
8 | }
9 | edges {
10 | node {
11 | id
12 | __typename
13 | messageId
14 | text
15 | linkifiedText
16 | authorNickname
17 | state
18 | vote
19 | voteReason
20 | creationTime
21 | suggestedReplies
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/popup/sections/SiteAdapters.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | SiteAdapters.propTypes = {
4 | config: PropTypes.object.isRequired,
5 | updateConfig: PropTypes.func.isRequired,
6 | }
7 |
8 | export function SiteAdapters({ config, updateConfig }) {
9 | return (
10 | <>
11 | {config.siteAdapters.map((key) => (
12 |
13 | {
17 | const checked = e.target.checked
18 | const activeSiteAdapters = config.activeSiteAdapters.filter((i) => i !== key)
19 | if (checked) activeSiteAdapters.push(key)
20 | updateConfig({ activeSiteAdapters })
21 | }}
22 | />
23 | {key}
24 |
25 | ))}
26 | >
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/background/commands.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
3 |
4 | export function registerCommands() {
5 | Browser.commands.onCommand.addListener(async (command, tab) => {
6 | const message = {
7 | itemId: command,
8 | selectionText: '',
9 | useMenuPosition: false,
10 | }
11 | console.debug('command triggered', message)
12 |
13 | if (command in menuConfig) {
14 | if (menuConfig[command].action) {
15 | menuConfig[command].action(true, tab)
16 | }
17 |
18 | if (menuConfig[command].genPrompt) {
19 | const currentTab = (await Browser.tabs.query({ active: true, currentWindow: true }))[0]
20 | Browser.tabs.sendMessage(currentTab.id, {
21 | type: 'CREATE_CHAT',
22 | data: message,
23 | })
24 | }
25 | }
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/baidu/index.mjs:
--------------------------------------------------------------------------------
1 | import { config } from '../index'
2 |
3 | export default {
4 | init: async (hostname, userConfig, getInput, mountComponent) => {
5 | try {
6 | const targetNode = document.getElementById('wrapper_wrapper')
7 | const observer = new MutationObserver(async (records) => {
8 | if (
9 | records.some(
10 | (record) =>
11 | record.type === 'childList' &&
12 | [...record.addedNodes].some((node) => node.id === 'container'),
13 | )
14 | ) {
15 | const searchValue = await getInput(config.baidu.inputQuery)
16 | if (searchValue) {
17 | mountComponent('baidu', config.baidu)
18 | }
19 | }
20 | })
21 | observer.observe(targetNode, { childList: true })
22 | } catch (e) {
23 | /* empty */
24 | }
25 | return true
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report---问题报告.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report / 问题报告
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | **问题描述**
12 | A clear and concise description of what the bug is.
13 |
14 | **To Reproduce**
15 | **如何复现**
16 | Steps to reproduce the behavior:
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 | **期望行为**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | **截图说明**
28 | If applicable, add screenshots to help explain your problem.
29 |
30 | **Please complete the following information):**
31 | **请补全以下内容**
32 | - OS: [e.g. Windows]
33 | - Browser: [e.g. chrome, safari]
34 | - Extension Version: [e.g. v2.0.2]
35 |
36 | **Additional context**
37 | **其他**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/utils/index.mjs:
--------------------------------------------------------------------------------
1 | export * from './change-children-font-size'
2 | export * from './create-element-at-position'
3 | export * from './crop-text'
4 | export * from './ends-with-question-mark'
5 | export * from './fetch-sse'
6 | export * from './get-client-position'
7 | export * from './get-conversation-pairs'
8 | export * from './get-core-content-text'
9 | export * from './get-possible-element-by-query-selector'
10 | export * from './is-edge'
11 | export * from './is-firefox'
12 | export * from './is-mobile'
13 | export * from './is-safari'
14 | export * from './limited-fetch'
15 | export * from './open-url'
16 | export * from './parse-float-with-clamp'
17 | export * from './parse-int-with-clamp'
18 | export * from './set-element-position-in-viewport'
19 | export * from './eventsource-parser.mjs'
20 | export * from './update-ref-height'
21 | export * from './wait-for-element-to-exist-and-select.mjs'
22 | export * from './model-name-convert.mjs'
23 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/quora/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | if (location.pathname === '/') return
7 |
8 | const texts = document.querySelectorAll('.q-box.qu-userSelect--text')
9 | let title
10 | if (texts.length > 0) title = texts[0].textContent
11 | let answers = ''
12 | if (texts.length > 1)
13 | for (let i = 1; i < texts.length; i++) {
14 | answers += `answer${i}:${texts[i].textContent}|`
15 | }
16 |
17 | return await cropText(
18 | `You are an insightful analyst of Q&A discussions. ` +
19 | `Below is content from a Q&A platform. Please provide a summary of the discussion and your opinion on it.\n` +
20 | `Question: '${title}'\n` +
21 | `Answers:\n${answers}`,
22 | )
23 | } catch (e) {
24 | console.log(e)
25 | }
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/fetch-bg.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 | /**
4 | * @param {RequestInfo|URL} input
5 | * @param {RequestInit=} init
6 | * @returns {Promise}
7 | */
8 | export function fetchBg(input, init) {
9 | return new Promise((resolve, reject) => {
10 | Browser.runtime
11 | .sendMessage({
12 | type: 'FETCH',
13 | data: { input, init },
14 | })
15 | .then((messageResponse) => {
16 | const [response, error] = messageResponse
17 | if (response === null) {
18 | reject(error)
19 | } else {
20 | const body = response.body ? new Blob([response.body]) : undefined
21 | resolve(
22 | new Response(body, {
23 | status: response.status,
24 | statusText: response.statusText,
25 | headers: new Headers(response.headers),
26 | }),
27 | )
28 | }
29 | })
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/reddit/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('[id*="post-title"]')?.textContent
7 | const description = document.querySelector(
8 | 'shreddit-post > div.text-neutral-content',
9 | )?.textContent
10 | const texts = document.querySelectorAll('shreddit-comment div.md')
11 | let answers = ''
12 | for (let i = 0; i < texts.length; i++) {
13 | answers += `answer${i}:${texts[i].textContent}|`
14 | }
15 |
16 | return await cropText(
17 | `You are an expert in analyzing online forum discussions. ` +
18 | `Below is content from a social forum (Reddit). Please provide a summary of the discussion and your opinion on it.\n` +
19 | `Title: '${title}'\n` +
20 | `Description: '${description}'\n` +
21 | `Comments:\n${answers}`,
22 | )
23 | } catch (e) {
24 | console.log(e)
25 | }
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks/use-config.mjs:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { defaultConfig, getUserConfig } from '../config/index.mjs'
3 | import Browser from 'webextension-polyfill'
4 |
5 | export function useConfig(initFn, ignoreSession = true) {
6 | const [config, setConfig] = useState(defaultConfig)
7 | useEffect(() => {
8 | getUserConfig().then((config) => {
9 | setConfig(config)
10 | if (initFn) initFn()
11 | })
12 | }, [])
13 | useEffect(() => {
14 | const listener = (changes) => {
15 | if (ignoreSession) if (Object.keys(changes).length === 1 && 'sessions' in changes) return
16 |
17 | const changedItems = Object.keys(changes)
18 | let newConfig = {}
19 | for (const key of changedItems) {
20 | newConfig[key] = changes[key].newValue
21 | }
22 | setConfig({ ...config, ...newConfig })
23 | }
24 | Browser.storage.local.onChanged.addListener(listener)
25 | return () => {
26 | Browser.storage.local.onChanged.removeListener(listener)
27 | }
28 | }, [config])
29 | return config
30 | }
31 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/arxiv/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('.title')?.textContent.trim()
7 | const authors = document.querySelector('.authors')?.textContent
8 | const abstract = document.querySelector('blockquote.abstract')?.textContent.trim()
9 |
10 | return await cropText(
11 | `You are a research assistant skilled in academic paper analysis. ` +
12 | `Based on the provided paper abstract from a preprint site, generate a structured summary. ` +
13 | `The summary should clearly outline: key findings, methodology, and conclusions. ` +
14 | `Pay special attention to highlighting the main contributions of the paper. ` +
15 | `Ensure the summary is concise and maintains an academic tone.\n` +
16 | `Title: ${title}\n` +
17 | `Authors: ${authors}\n` +
18 | `Abstract: ${abstract}`,
19 | )
20 | } catch (e) {
21 | console.log(e)
22 | }
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/CopyButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { CheckIcon, CopyIcon } from '@primer/octicons-react'
3 | import PropTypes from 'prop-types'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | CopyButton.propTypes = {
7 | contentFn: PropTypes.func.isRequired,
8 | size: PropTypes.number.isRequired,
9 | className: PropTypes.string,
10 | }
11 |
12 | function CopyButton({ className, contentFn, size }) {
13 | const { t } = useTranslation()
14 | const [copied, setCopied] = useState(false)
15 |
16 | const onClick = () => {
17 | navigator.clipboard
18 | .writeText(contentFn())
19 | .then(() => setCopied(true))
20 | .then(() =>
21 | setTimeout(() => {
22 | setCopied(false)
23 | }, 600),
24 | )
25 | }
26 |
27 | return (
28 |
33 | {copied ? : }
34 |
35 | )
36 | }
37 |
38 | export default CopyButton
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 josStorer
4 | Copyright (c) 2022 wong2
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/utils/jwt-token-generator.mjs:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 |
3 | let jwtToken = null
4 | let tokenExpiration = null // Declare tokenExpiration in the module scope
5 |
6 | function generateToken(apiKey, timeoutSeconds) {
7 | const parts = apiKey.split('.')
8 | if (parts.length !== 2) {
9 | throw new Error('Invalid API key')
10 | }
11 |
12 | const ms = Date.now()
13 | const currentSeconds = Math.floor(ms / 1000)
14 | const [id, secret] = parts
15 | const payload = {
16 | api_key: id,
17 | exp: currentSeconds + timeoutSeconds,
18 | timestamp: currentSeconds,
19 | }
20 |
21 | jwtToken = jwt.sign(payload, secret, {
22 | header: {
23 | alg: 'HS256',
24 | typ: 'JWT',
25 | sign_type: 'SIGN',
26 | },
27 | })
28 | tokenExpiration = ms + timeoutSeconds * 1000
29 | }
30 |
31 | function shouldRegenerateToken() {
32 | const ms = Date.now()
33 | return !jwtToken || ms >= tokenExpiration
34 | }
35 |
36 | function getToken(apiKey) {
37 | if (shouldRegenerateToken()) {
38 | generateToken(apiKey, 86400) // Hard-coded to regenerate the token every 24 hours
39 | }
40 | return jwtToken
41 | }
42 |
43 | export { getToken }
44 |
--------------------------------------------------------------------------------
/src/_locales/resources.mjs:
--------------------------------------------------------------------------------
1 | import de from './de/main.json'
2 | import en from './en/main.json'
3 | import es from './es/main.json'
4 | import fr from './fr/main.json'
5 | import inTrans from './in/main.json'
6 | import it from './it/main.json'
7 | import ja from './ja/main.json'
8 | import ko from './ko/main.json'
9 | import pt from './pt/main.json'
10 | import ru from './ru/main.json'
11 | import tr from './tr/main.json'
12 | import zhHans from './zh-hans/main.json'
13 | import zhHant from './zh-hant/main.json'
14 |
15 | export const resources = {
16 | de: {
17 | translation: de,
18 | },
19 | en: {
20 | translation: en,
21 | },
22 | es: {
23 | translation: es,
24 | },
25 | fr: {
26 | translation: fr,
27 | },
28 | in: {
29 | translation: inTrans,
30 | },
31 | it: {
32 | translation: it,
33 | },
34 | ja: {
35 | translation: ja,
36 | },
37 | ko: {
38 | translation: ko,
39 | },
40 | pt: {
41 | translation: pt,
42 | },
43 | ru: {
44 | translation: ru,
45 | },
46 | tr: {
47 | translation: tr,
48 | },
49 | zh: {
50 | translation: zhHans,
51 | },
52 | zhHant: {
53 | translation: zhHant,
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/src/services/apis/bard-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord } from './shared.mjs'
2 | import Bard from '../clients/bard'
3 |
4 | /**
5 | * @param {Runtime.Port} port
6 | * @param {string} question
7 | * @param {Session} session
8 | * @param {string} cookies
9 | */
10 | export async function generateAnswersWithBardWebApi(port, question, session, cookies) {
11 | // const { controller, messageListener, disconnectListener } = setAbortController(port)
12 | const bot = new Bard(cookies)
13 |
14 | // eslint-disable-next-line
15 | try {
16 | const { answer, conversationObj } = await bot.ask(question, session.bard_conversationObj || {})
17 | session.bard_conversationObj = conversationObj
18 | pushRecord(session, question, answer)
19 | console.debug('conversation history', { content: session.conversationRecords })
20 | // port.onMessage.removeListener(messageListener)
21 | // port.onDisconnect.removeListener(disconnectListener)
22 | port.postMessage({ answer: answer, done: true, session: session })
23 | } catch (err) {
24 | // port.onMessage.removeListener(messageListener)
25 | // port.onDisconnect.removeListener(disconnectListener)
26 | throw err
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/ViewerStateFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment ViewerStateFragment on Viewer {
2 | id
3 | __typename
4 | iosMinSupportedVersion: integerGate(gateName: "poe_ios_min_supported_version")
5 | iosMinEncouragedVersion: integerGate(
6 | gateName: "poe_ios_min_encouraged_version"
7 | )
8 | macosMinSupportedVersion: integerGate(
9 | gateName: "poe_macos_min_supported_version"
10 | )
11 | macosMinEncouragedVersion: integerGate(
12 | gateName: "poe_macos_min_encouraged_version"
13 | )
14 | showPoeDebugPanel: booleanGate(gateName: "poe_show_debug_panel")
15 | enableCommunityFeed: booleanGate(gateName: "enable_poe_shares_feed")
16 | linkifyText: booleanGate(gateName: "poe_linkify_response")
17 | enableSuggestedReplies: booleanGate(gateName: "poe_suggested_replies")
18 | removeInviteLimit: booleanGate(gateName: "poe_remove_invite_limit")
19 | enableInAppPurchases: booleanGate(gateName: "poe_enable_in_app_purchases")
20 | availableBots {
21 | nickname
22 | displayName
23 | profilePicture
24 | isDown
25 | disclaimer
26 | subtitle
27 | poweredBy
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/Hyperlink.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import Browser from 'webextension-polyfill'
3 |
4 | export function Hyperlink({ href, children }) {
5 | const linkProperties = {
6 | target: '_blank',
7 | style: 'color: #8ab4f8; cursor: pointer;',
8 | rel: 'nofollow noopener noreferrer',
9 | }
10 |
11 | return href.includes('chatgpt.com') ||
12 | href.includes('claude.ai') ||
13 | href.includes('kimi.moonshot.cn') ||
14 | href.includes('kimi.com') ? (
15 | {
18 | const url = new URL(href)
19 | url.searchParams.set('chatgptbox_notification', 'true')
20 | Browser.runtime.sendMessage({
21 | type: 'NEW_URL',
22 | data: {
23 | url: url.toString(),
24 | pinned: false,
25 | jumpBack: true,
26 | },
27 | })
28 | }}
29 | >
30 | {children}
31 |
32 | ) : (
33 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | Hyperlink.propTypes = {
40 | href: PropTypes.string.isRequired,
41 | children: PropTypes.object.isRequired,
42 | }
43 |
--------------------------------------------------------------------------------
/src/services/apis/ollama-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { generateAnswersWithChatgptApiCompat } from './openai-api.mjs'
3 | import { getModelValue } from '../../utils/model-name-convert.mjs'
4 |
5 | /**
6 | * @param {Browser.Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | */
10 | export async function generateAnswersWithOllamaApi(port, question, session) {
11 | const config = await getUserConfig()
12 | const model = getModelValue(session)
13 | return generateAnswersWithChatgptApiCompat(
14 | config.ollamaEndpoint + '/v1',
15 | port,
16 | question,
17 | session,
18 | config.ollamaApiKey,
19 | ).then(() =>
20 | fetch(config.ollamaEndpoint + '/api/generate', {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | Authorization: `Bearer ${config.ollamaApiKey}`,
25 | },
26 | body: JSON.stringify({
27 | model,
28 | prompt: 't',
29 | options: {
30 | num_predict: 1,
31 | },
32 | keep_alive: config.ollamaKeepAliveTime === '-1' ? -1 : config.ollamaKeepAliveTime,
33 | }),
34 | }),
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/config/language.mjs:
--------------------------------------------------------------------------------
1 | import { languages } from 'countries-list'
2 | import { defaultConfig, getUserConfig } from './index.mjs'
3 |
4 | export const languageList = { auto: { name: 'Auto', native: 'Auto' }, ...languages }
5 | languageList.zh.name = 'Chinese (Simplified)'
6 | languageList.zh.native = '简体中文'
7 | languageList.zhHant = { ...languageList.zh }
8 | languageList.zhHant.name = 'Chinese (Traditional)'
9 | languageList.zhHant.native = '正體中文'
10 | languageList.in = {}
11 | languageList.in.name = 'Indonesia'
12 | languageList.in.native = 'Indonesia'
13 |
14 | export async function getUserLanguage() {
15 | return languageList[defaultConfig.userLanguage].name
16 | }
17 |
18 | export async function getUserLanguageNative() {
19 | return languageList[defaultConfig.userLanguage].native
20 | }
21 |
22 | export async function getPreferredLanguage() {
23 | const config = await getUserConfig()
24 | if (config.preferredLanguage === 'auto') return await getUserLanguage()
25 | return languageList[config.preferredLanguage].name
26 | }
27 |
28 | export async function getPreferredLanguageNative() {
29 | const config = await getUserConfig()
30 | if (config.preferredLanguage === 'auto') return await getUserLanguageNative()
31 | return languageList[config.preferredLanguage].native
32 | }
33 |
--------------------------------------------------------------------------------
/src/popup/index.jsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import Popup from './Popup'
3 | import '../_locales/i18n-react'
4 | import { getUserConfig } from '../config/index.mjs'
5 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
6 | import Browser from 'webextension-polyfill'
7 |
8 | getUserConfig().then(async (config) => {
9 | if (config.clickIconAction === 'popup' || (window.innerWidth > 100 && window.innerHeight > 100)) {
10 | render( , document.getElementById('app'))
11 | } else {
12 | const message = {
13 | itemId: config.clickIconAction,
14 | selectionText: '',
15 | useMenuPosition: false,
16 | }
17 | console.debug('custom icon action triggered', message)
18 |
19 | if (config.clickIconAction in menuConfig) {
20 | const currentTab = (await Browser.tabs.query({ active: true, currentWindow: true }))[0]
21 |
22 | if (menuConfig[config.clickIconAction].action) {
23 | menuConfig[config.clickIconAction].action(false, currentTab)
24 | }
25 |
26 | if (menuConfig[config.clickIconAction].genPrompt) {
27 | Browser.tabs.sendMessage(currentTab.id, {
28 | type: 'CREATE_CHAT',
29 | data: message,
30 | })
31 | }
32 | }
33 | window.close()
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/src/popup/sections/ModulesPart.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import PropTypes from 'prop-types'
3 | import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
4 | import { ApiModes } from './ApiModes'
5 | import { SelectionTools } from './SelectionTools'
6 | import { SiteAdapters } from './SiteAdapters'
7 |
8 | ModulesPart.propTypes = {
9 | config: PropTypes.object.isRequired,
10 | updateConfig: PropTypes.func.isRequired,
11 | }
12 |
13 | export function ModulesPart({ config, updateConfig }) {
14 | const { t } = useTranslation()
15 |
16 | return (
17 | <>
18 |
19 |
20 | {t('API Modes')}
21 | {t('Selection Tools')}
22 | {t('Sites')}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | >
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/juejin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('#juejin .article-title')?.innerText
7 | const description = document.querySelector('#juejin #article-root')?.innerText
8 | if (title && description) {
9 | const author = document.querySelector('#juejin .author-block .info-box span')?.innerText
10 | const comments = document.querySelectorAll('.comment-list .comment-content')
11 | let comment = ''
12 | for (let i = 1; i <= comments.length && i <= 4; i++) {
13 | comment += `answer${i}: ${comment[i - 1].innerText}|`
14 | }
15 | return await cropText(
16 | `You are an expert content analyst and summarizer. ` +
17 | `Please analyze the following Juejin article and its comments. Provide a summary of the article (including author), your opinion on it, and a summary of the comments.\n` +
18 | `Article Title: "${title}"\n` +
19 | `Author: "${author}"\n` +
20 | `Content:\n"${description}"\n\n` +
21 | `Selected comments:\n${comment}`,
22 | )
23 | }
24 | } catch (e) {
25 | console.log(e)
26 | }
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/fetch-sse.mjs:
--------------------------------------------------------------------------------
1 | import { createParser } from './eventsource-parser.mjs'
2 |
3 | export async function fetchSSE(resource, options) {
4 | const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options
5 | const resp = await fetch(resource, fetchOptions).catch(async (err) => {
6 | await onError(err)
7 | })
8 | if (!resp) return
9 | if (!resp.ok) {
10 | await onError(resp)
11 | return
12 | }
13 | const parser = createParser((event) => {
14 | if (event.type === 'event') {
15 | onMessage(event.data)
16 | }
17 | })
18 | let hasStarted = false
19 | const reader = resp.body.getReader()
20 | let result
21 | while (!(result = await reader.read()).done) {
22 | const chunk = result.value
23 | if (!hasStarted) {
24 | const str = new TextDecoder().decode(chunk)
25 | hasStarted = true
26 | await onStart(str)
27 |
28 | let fakeSseData
29 | try {
30 | const commonResponse = JSON.parse(str)
31 | fakeSseData = 'data: ' + JSON.stringify(commonResponse) + '\n\ndata: [DONE]\n\n'
32 | } catch (error) {
33 | console.debug('not common response', error)
34 | }
35 | if (fakeSseData) {
36 | parser.feed(new TextEncoder().encode(fakeSseData))
37 | break
38 | }
39 | }
40 | parser.feed(chunk)
41 | }
42 | await onEnd()
43 | }
44 |
--------------------------------------------------------------------------------
/src/services/clients/poe/graphql/AddHumanMessageMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AddHumanMessageMutation(
2 | $chatId: BigInt!
3 | $bot: String!
4 | $query: String!
5 | $source: MessageSource
6 | $withChatBreak: Boolean! = false
7 | ) {
8 | messageCreateWithStatus(
9 | chatId: $chatId
10 | bot: $bot
11 | query: $query
12 | source: $source
13 | withChatBreak: $withChatBreak
14 | ) {
15 | message {
16 | id
17 | __typename
18 | messageId
19 | text
20 | linkifiedText
21 | authorNickname
22 | state
23 | vote
24 | voteReason
25 | creationTime
26 | suggestedReplies
27 | chat {
28 | id
29 | shouldShowDisclaimer
30 | }
31 | }
32 | messageLimit{
33 | canSend
34 | numMessagesRemaining
35 | resetTime
36 | shouldShowReminder
37 | }
38 | chatBreak {
39 | id
40 | __typename
41 | messageId
42 | text
43 | linkifiedText
44 | authorNickname
45 | state
46 | vote
47 | voteReason
48 | creationTime
49 | suggestedReplies
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/weixin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('#activity-name')?.textContent
7 | const description = document.querySelector('#js_content')?.textContent
8 | if (title && description) {
9 | const author = document.querySelector('#js_name')?.textContent
10 |
11 | const sidebar = document.querySelector('.qr_code_pc')
12 | if (sidebar) {
13 | sidebar.style.right = '-400px'
14 | sidebar.style.width = '400px'
15 | sidebar.style.textAlign = 'left'
16 | sidebar.style.alignItems = 'center'
17 | sidebar.style.display = 'flex'
18 | sidebar.style.flexDirection = 'column'
19 | sidebar.style.background = 'transparent'
20 | }
21 |
22 | return await cropText(
23 | `You are an expert article analyst and summarizer. ` +
24 | `Please analyze the following WeChat Official Account article. Provide the source, a summary of the article, its main conclusions, and your opinion on it.\n` +
25 | `Article Title: "${title}"\n` +
26 | `Source: "${author} Official Account"\n` +
27 | `Content:\n"${description}"`,
28 | )
29 | }
30 | } catch (e) {
31 | console.log(e)
32 | }
33 | },
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/Pre.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import CopyButton from '../CopyButton'
3 | import PropTypes from 'prop-types'
4 | import { changeChildrenFontSize } from '../../utils'
5 |
6 | export function Pre({ className, children }) {
7 | const preRef = useRef(null)
8 | const [fontSize, setFontSize] = useState(14)
9 | const sizeList = [10, 12, 14, 16, 18]
10 |
11 | useEffect(() => {
12 | changeChildrenFontSize(preRef.current.childNodes[1], fontSize + 'px')
13 | })
14 |
15 | return (
16 |
17 |
18 | {
22 | setFontSize(e.target.value)
23 | }}
24 | >
25 | {Object.values(sizeList).map((size) => {
26 | return (
27 |
28 | {size}px
29 |
30 | )
31 | })}
32 |
33 | preRef.current.childNodes[1].textContent} size={14} />
34 |
35 | {children}
36 |
37 | )
38 | }
39 |
40 | Pre.propTypes = {
41 | className: PropTypes.string.isRequired,
42 | children: PropTypes.object.isRequired,
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/ConfirmButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { useEffect, useRef, useState } from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | ConfirmButton.propTypes = {
6 | onConfirm: PropTypes.func.isRequired,
7 | text: PropTypes.string.isRequired,
8 | }
9 |
10 | function ConfirmButton({ onConfirm, text }) {
11 | const { t } = useTranslation()
12 | const [waitConfirm, setWaitConfirm] = useState(false)
13 | const confirmRef = useRef(null)
14 |
15 | useEffect(() => {
16 | if (waitConfirm) confirmRef.current.focus()
17 | }, [waitConfirm])
18 |
19 | return (
20 |
21 | {
29 | e.preventDefault()
30 | e.stopPropagation()
31 | }}
32 | onBlur={() => {
33 | setWaitConfirm(false)
34 | }}
35 | onClick={() => {
36 | setWaitConfirm(false)
37 | onConfirm()
38 | }}
39 | >
40 | {t('Confirm')}
41 |
42 | {
49 | setWaitConfirm(true)
50 | }}
51 | >
52 | {text}
53 |
54 |
55 | )
56 | }
57 |
58 | export default ConfirmButton
59 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/followin/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const author = document.querySelector('main article a > span')?.textContent
7 | const description =
8 | document.querySelector('#article-content')?.textContent ||
9 | document.querySelector('#thead-gallery')?.textContent
10 | if (author && description) {
11 | const title = document.querySelector('main article h1')?.textContent
12 | if (title) {
13 | return await cropText(
14 | `You are an expert content summarizer. Please carefully read the following article. ` +
15 | `Provide a conclusion and 3 to 5 main points, presented as a markdown list. ` +
16 | `The summary should be concise, clear, and accurately reflect the core content.\n` +
17 | `Title: "${title}"\n` +
18 | `Author: "${author}"\n` +
19 | `Content:\n"${description}"`,
20 | )
21 | } else {
22 | return await cropText(
23 | `You are an expert content summarizer. Please carefully read the following long tweet. ` +
24 | `Provide a conclusion and 3 to 5 main points, presented as a markdown list. ` +
25 | `The summary should be concise, clear, and accurately reflect the core content.\n` +
26 | `Author: "${author}"\n` +
27 | `Content:\n"${description}"`,
28 | )
29 | }
30 | }
31 | } catch (e) {
32 | console.log(e)
33 | }
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/src/services/apis/poe-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import PoeAiClient from '../clients/poe/index.mjs'
3 |
4 | /**
5 | * @param {Runtime.Port} port
6 | * @param {string} question
7 | * @param {Session} session
8 | * @param {string} modelName
9 | */
10 | export async function generateAnswersWithPoeWebApi(port, question, session, modelName) {
11 | const bot = new PoeAiClient(session.poe_chatId)
12 | const { messageListener, disconnectListener } = setAbortController(
13 | port,
14 | () => {
15 | bot.close()
16 | },
17 | () => {
18 | bot.breakMsg()
19 | bot.close()
20 | },
21 | )
22 |
23 | let answer = ''
24 | await bot
25 | .ask(
26 | question,
27 | modelName,
28 | (msg) => {
29 | answer += msg
30 | port.postMessage({ answer: answer, done: false, session: null })
31 | },
32 | () => {
33 | if (bot.chatId) session.poe_chatId = bot.chatId
34 |
35 | pushRecord(session, question, answer)
36 | console.debug('conversation history', { content: session.conversationRecords })
37 | port.onMessage.removeListener(messageListener)
38 | if (session.conversationRecords.length > 1)
39 | port.onDisconnect.removeListener(disconnectListener)
40 | port.postMessage({ answer: answer, done: true, session: session })
41 | bot.close()
42 | },
43 | )
44 | .catch((err) => {
45 | port.onMessage.removeListener(messageListener)
46 | port.onDisconnect.removeListener(disconnectListener)
47 | bot.close()
48 | throw err
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/stackoverflow/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('#question-header .question-hyperlink')?.textContent
7 | if (title) {
8 | const description = document.querySelector('.postcell .s-prose')?.textContent
9 | let answer = ''
10 | const answers = document.querySelectorAll('.answercell .s-prose')
11 | if (answers.length > 0)
12 | for (let i = 1; i <= answers.length && i <= 2; i++) {
13 | answer += `answer${i}: ${answers[i - 1].textContent}|`
14 | }
15 |
16 | return await cropText(
17 | `You are an expert software developer and technical problem solver. ` +
18 | `The following content is from a developer Q&A platform (Stack Overflow).\n\n` +
19 | `Question: "${title}"\n` +
20 | `Question Description: "${description}"\n\n` +
21 | `Provided Answers:\n${answer}\n\n` +
22 | `Please perform the following tasks:\n` +
23 | `1. **Direct Solution:** Based on the provided answers, formulate a concise and effective solution to the question. ` +
24 | `If applicable, include a brief code snippet (using markdown for formatting).\n` +
25 | `2. **Overview of Answers:** Provide an overview of the different approaches or key points mentioned in the provided answers. ` +
26 | `You can highlight any notable variations, pros, or cons if apparent.`,
27 | )
28 | }
29 | } catch (e) {
30 | console.log(e)
31 | }
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/DeleteButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useTranslation } from 'react-i18next'
4 | import { TrashIcon } from '@primer/octicons-react'
5 |
6 | DeleteButton.propTypes = {
7 | onConfirm: PropTypes.func.isRequired,
8 | size: PropTypes.number.isRequired,
9 | text: PropTypes.string.isRequired,
10 | }
11 |
12 | function DeleteButton({ onConfirm, size, text }) {
13 | const { t } = useTranslation()
14 | const [waitConfirm, setWaitConfirm] = useState(false)
15 | const confirmRef = useRef(null)
16 |
17 | useEffect(() => {
18 | if (waitConfirm) confirmRef.current.focus()
19 | }, [waitConfirm])
20 |
21 | return (
22 |
23 | {
32 | e.preventDefault()
33 | e.stopPropagation()
34 | }}
35 | onBlur={() => {
36 | setWaitConfirm(false)
37 | }}
38 | onClick={() => {
39 | setWaitConfirm(false)
40 | onConfirm()
41 | }}
42 | >
43 | {t('Confirm')}
44 |
45 | {
50 | setWaitConfirm(true)
51 | }}
52 | >
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default DeleteButton
60 |
--------------------------------------------------------------------------------
/src/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://claude.ai"
55 | },
56 | {
57 | "operation": "set",
58 | "header": "referer",
59 | "value": "https://claude.ai"
60 | }
61 | ]
62 | },
63 | "condition": {
64 | "requestDomains": ["claude.ai"],
65 | "resourceTypes": ["xmlhttprequest"]
66 | }
67 | }
68 | ]
69 |
--------------------------------------------------------------------------------
/src/services/apis/claude-web.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import Claude from '../clients/claude'
3 | import { getModelValue } from '../../utils/model-name-convert.mjs'
4 |
5 | /**
6 | * @param {Runtime.Port} port
7 | * @param {string} question
8 | * @param {Session} session
9 | * @param {string} sessionKey
10 | */
11 | export async function generateAnswersWithClaudeWebApi(port, question, session, sessionKey) {
12 | const bot = new Claude({ sessionKey })
13 | await bot.init()
14 | const { controller, cleanController } = setAbortController(port)
15 | const model = getModelValue(session)
16 |
17 | let answer = ''
18 | const progressFunc = ({ completion }) => {
19 | answer = completion
20 | port.postMessage({ answer: answer, done: false, session: null })
21 | }
22 |
23 | const doneFunc = () => {
24 | pushRecord(session, question, answer)
25 | console.debug('conversation history', { content: session.conversationRecords })
26 | port.postMessage({ answer: answer, done: true, session: session })
27 | }
28 |
29 | const params = {
30 | progress: progressFunc,
31 | done: doneFunc,
32 | model,
33 | signal: controller.signal,
34 | }
35 |
36 | if (!session.claude_conversation)
37 | await bot
38 | .startConversation(question, params)
39 | .then((conversation) => {
40 | conversation.request = null
41 | conversation.claude = null
42 | session.claude_conversation = conversation
43 | port.postMessage({ answer: answer, done: true, session: session })
44 | cleanController()
45 | })
46 | .catch((err) => {
47 | cleanController()
48 | throw err
49 | })
50 | else
51 | await bot
52 | .sendMessage(question, {
53 | conversation: session.claude_conversation,
54 | ...params,
55 | })
56 | .then(cleanController)
57 | .catch((err) => {
58 | cleanController()
59 | throw err
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/src/popup/styles.scss:
--------------------------------------------------------------------------------
1 | [data-theme='auto'] {
2 | @media screen and (prefers-color-scheme: dark) {
3 | --font-color: #c9d1d9;
4 | --theme-color: #202124;
5 | --active-color: #3c4043;
6 | }
7 | @media screen and (prefers-color-scheme: light) {
8 | --font-color: #24292f;
9 | --theme-color: #ffffff;
10 | --active-color: #eaecf0;
11 | }
12 | }
13 |
14 | [data-theme='dark'] {
15 | --font-color: #c9d1d9;
16 | --theme-color: #202124;
17 | --active-color: #3c4043;
18 | }
19 |
20 | [data-theme='light'] {
21 | --font-color: #24292f;
22 | --theme-color: #ffffff;
23 | --active-color: #eaecf0;
24 | }
25 |
26 | .container-page-mode {
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | min-width: 460px;
31 | min-height: 560px;
32 | width: 100%;
33 | height: 100%;
34 | padding: 20px;
35 | overflow-y: auto;
36 | }
37 |
38 | .container-popup-mode {
39 | display: flex;
40 | flex-direction: column;
41 | align-items: center;
42 | width: 460px;
43 | height: 560px;
44 | padding: 20px;
45 | overflow-y: auto;
46 | }
47 |
48 | .container legend {
49 | font-weight: bold;
50 | }
51 |
52 | .container form {
53 | margin-bottom: 0;
54 | }
55 |
56 | .container fieldset {
57 | margin-bottom: 0;
58 | }
59 |
60 | .footer {
61 | width: 90%;
62 | position: fixed;
63 | bottom: 10px;
64 | display: flex;
65 | flex-direction: row;
66 | justify-content: space-between;
67 | align-items: center;
68 | background-color: var(--active-color);
69 | border-radius: 5px;
70 | padding: 6px;
71 | z-index: 2147483647;
72 | font-size: 12px;
73 | }
74 |
75 | .popup-tab {
76 | display: inline-block;
77 | position: relative;
78 | list-style: none;
79 | padding: 6px 12px 0;
80 | cursor: pointer;
81 | border-radius: 5px;
82 | margin-right: 5px;
83 | font-size: 14px;
84 | background-color: var(--theme-color);
85 | color: var(--font-color);
86 |
87 | &--selected {
88 | background: var(--active-color);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release-build.yml:
--------------------------------------------------------------------------------
1 | name: pre-release
2 | on:
3 | workflow_dispatch:
4 | # push:
5 | # branches:
6 | # - master
7 | # paths:
8 | # - "src/**"
9 | # - "!src/**/*.json"
10 | # - "build.mjs"
11 | # tags-ignore:
12 | # - "v*"
13 |
14 | permissions:
15 | id-token: "write"
16 | contents: "write"
17 |
18 | jobs:
19 | build_and_release:
20 | runs-on: ubuntu-22.04
21 |
22 | steps:
23 | - uses: actions/checkout@v5
24 | - uses: actions/setup-node@v5
25 | with:
26 | node-version: 20
27 | - run: npm ci
28 | - run: npm run build
29 |
30 | - uses: josStorer/get-current-time@v2
31 | id: current-time
32 | with:
33 | format: YYYY_MMDD_HHmm
34 |
35 | - uses: actions/upload-artifact@v4
36 | with:
37 | name: Chromium_Build_${{ steps.current-time.outputs.formattedTime }}
38 | path: build/chromium/*
39 |
40 | - uses: actions/upload-artifact@v4
41 | with:
42 | name: Firefox_Build_${{ steps.current-time.outputs.formattedTime }}
43 | path: build/firefox/*
44 |
45 | - uses: actions/upload-artifact@v4
46 | with:
47 | name: Chromium_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}
48 | path: build/chromium-without-katex-and-tiktoken/*
49 |
50 | - uses: actions/upload-artifact@v4
51 | with:
52 | name: Firefox_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}
53 | path: build/firefox-without-katex-and-tiktoken/*
54 |
55 | - uses: marvinpinto/action-automatic-releases@v1.2.1
56 | with:
57 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
58 | automatic_release_tag: "latest"
59 | prerelease: true
60 | title: "Development Build"
61 | files: |
62 | build/chromium.zip
63 | build/firefox.zip
64 | build/chromium-without-katex-and-tiktoken.zip
65 | build/firefox-without-katex-and-tiktoken.zip
66 |
--------------------------------------------------------------------------------
/src/components/FeedbackForChatGPTWeb/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { memo, useCallback, useState } from 'react'
3 | import { ThumbsupIcon, ThumbsdownIcon } from '@primer/octicons-react'
4 | import Browser from 'webextension-polyfill'
5 | import { useTranslation } from 'react-i18next'
6 |
7 | const FeedbackForChatGPTWeb = (props) => {
8 | const { t } = useTranslation()
9 | const [action, setAction] = useState(null)
10 |
11 | const clickThumbsUp = useCallback(async () => {
12 | if (action) {
13 | return
14 | }
15 | setAction('thumbsUp')
16 | await Browser.runtime.sendMessage({
17 | type: 'FEEDBACK',
18 | data: {
19 | conversation_id: props.conversationId,
20 | message_id: props.messageId,
21 | rating: 'thumbsUp',
22 | },
23 | })
24 | }, [props, action])
25 |
26 | const clickThumbsDown = useCallback(async () => {
27 | if (action) {
28 | return
29 | }
30 | setAction('thumbsDown')
31 | await Browser.runtime.sendMessage({
32 | type: 'FEEDBACK',
33 | data: {
34 | conversation_id: props.conversationId,
35 | message_id: props.messageId,
36 | rating: 'thumbsDown',
37 | text: '',
38 | tags: [],
39 | },
40 | })
41 | }, [props, action])
42 |
43 | return (
44 |
45 |
49 |
50 |
51 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | FeedbackForChatGPTWeb.propTypes = {
64 | messageId: PropTypes.string.isRequired,
65 | conversationId: PropTypes.string.isRequired,
66 | }
67 |
68 | export default memo(FeedbackForChatGPTWeb)
69 |
--------------------------------------------------------------------------------
/src/fonts/styles.css:
--------------------------------------------------------------------------------
1 | /* arabic */
2 | @font-face {
3 | font-family: 'Cairo';
4 | font-style: normal;
5 | font-weight: 400;
6 | font-display: swap;
7 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2) format('woff2');
8 | unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
9 | }
10 | /* latin-ext */
11 | @font-face {
12 | font-family: 'Cairo';
13 | font-style: normal;
14 | font-weight: 400;
15 | font-display: swap;
16 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2) format('woff2');
17 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
18 | U+2C60-2C7F, U+A720-A7FF;
19 | }
20 | /* latin */
21 | @font-face {
22 | font-family: 'Cairo';
23 | font-style: normal;
24 | font-weight: 400;
25 | font-display: swap;
26 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2) format('woff2');
27 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
28 | U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
29 | }
30 | /* arabic */
31 | @font-face {
32 | font-family: 'Cairo';
33 | font-style: normal;
34 | font-weight: 700;
35 | font-display: swap;
36 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscQyyS4J0.woff2) format('woff2');
37 | unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
38 | }
39 | /* latin-ext */
40 | @font-face {
41 | font-family: 'Cairo';
42 | font-style: normal;
43 | font-weight: 700;
44 | font-display: swap;
45 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscSCyS4J0.woff2) format('woff2');
46 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
47 | U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Cairo';
52 | font-style: normal;
53 | font-weight: 700;
54 | font-display: swap;
55 | src: url(SLXVc1nY6HkvangtZmpQdkhzfH5lkSscRiyS.woff2) format('woff2');
56 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
57 | U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
58 | }
59 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/zhihu/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 |
3 | export default {
4 | inputQuery: async () => {
5 | try {
6 | const title = document.querySelector('.QuestionHeader-title')?.textContent
7 | if (title) {
8 | const description = document.querySelector('.QuestionRichText')?.textContent
9 | const answerQuery = '.AnswerItem .RichText'
10 |
11 | let answer = ''
12 | if (location.pathname.includes('answer')) {
13 | answer = document.querySelector(answerQuery)?.textContent
14 | return await cropText(
15 | `You are an insightful analyst of Q&A discussions. ` +
16 | `Below is content from Zhihu, a Q&A platform. Please provide a summary of the question and answer, and your opinion on them.\n` +
17 | `Question: "${title}"\n` +
18 | `Description: "${description}"\n` +
19 | `Answer:\n${answer}`,
20 | )
21 | } else {
22 | const answers = document.querySelectorAll(answerQuery)
23 | for (let i = 1; i <= answers.length && i <= 4; i++) {
24 | answer += `answer${i}: ${answers[i - 1].textContent}|`
25 | }
26 | return await cropText(
27 | `You are an insightful analyst of Q&A discussions. ` +
28 | `Below is content from Zhihu, a Q&A platform. Please provide a summary of the question and answers, and your opinion on them.\n` +
29 | `Question: "${title}"\n` +
30 | `Description: "${description}"\n` +
31 | `Answers:\n${answer}`,
32 | )
33 | }
34 | } else {
35 | const title = document.querySelector('.Post-Title')?.textContent
36 | const description = document.querySelector('.Post-RichText')?.textContent
37 |
38 | if (title) {
39 | return await cropText(
40 | `You are an expert article analyst. ` +
41 | `Below is an article from Zhihu. Please provide a summary of the article and your opinion on it.\n` +
42 | `Title: "${title}"\n` +
43 | `Content:\n"${description}"`,
44 | )
45 | }
46 | }
47 | } catch (e) {
48 | console.log(e)
49 | }
50 | },
51 | }
52 |
--------------------------------------------------------------------------------
/src/manifest.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ChatGPTBox",
3 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4 | "version": "2.5.9",
5 | "manifest_version": 2,
6 | "icons": {
7 | "16": "logo.png",
8 | "32": "logo.png",
9 | "48": "logo.png",
10 | "128": "logo.png"
11 | },
12 | "permissions": [
13 | "cookies",
14 | "storage",
15 | "contextMenus",
16 | "unlimitedStorage",
17 | "tabs",
18 | "webRequest",
19 | "https://*.chatgpt.com/*",
20 | "https://*.openai.com/",
21 | "https://*.bing.com/",
22 | "wss://*.bing.com/*",
23 | "https://*.poe.com/",
24 | "https://*.google.com/",
25 | "https://claude.ai/",
26 | "https://*.moonshot.cn/*",
27 | ""
28 | ],
29 | "background": {
30 | "scripts": [
31 | "background.js"
32 | ],
33 | "persistent": true
34 | },
35 | "browser_action": {
36 | "default_popup": "popup.html?popup=true"
37 | },
38 | "options_ui": {
39 | "page": "popup.html",
40 | "open_in_tab": true
41 | },
42 | "content_scripts": [
43 | {
44 | "matches": [
45 | "https://*/*",
46 | "http://*/*",
47 | "file://*/*"
48 | ],
49 | "js": [
50 | "shared.js",
51 | "content-script.js"
52 | ],
53 | "css": [
54 | "content-script.css"
55 | ]
56 | }
57 | ],
58 | "web_accessible_resources": [
59 | "logo.png"
60 | ],
61 | "commands": {
62 | "newChat": {
63 | "suggested_key": {
64 | "default": "Ctrl+B",
65 | "mac": "MacCtrl+X"
66 | },
67 | "description": "Create a new chat"
68 | },
69 | "summarizePage": {
70 | "suggested_key": {
71 | "default": "Alt+B",
72 | "mac": "Alt+B"
73 | },
74 | "description": "Summarize this page"
75 | },
76 | "openConversationPage": {
77 | "suggested_key": {
78 | "default": "Ctrl+Shift+H",
79 | "mac": "MacCtrl+Shift+H"
80 | },
81 | "description": "Open the independent conversation page"
82 | },
83 | "openConversationWindow": {
84 | "description": "Open the independent conversation window"
85 | },
86 | "closeAllChats": {
87 | "description": "Close all chats in this page"
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/components/ReadButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { MuteIcon, UnmuteIcon } from '@primer/octicons-react'
3 | import PropTypes from 'prop-types'
4 | import { useTranslation } from 'react-i18next'
5 | import { useConfig } from '../../hooks/use-config.mjs'
6 |
7 | ReadButton.propTypes = {
8 | contentFn: PropTypes.func.isRequired,
9 | size: PropTypes.number.isRequired,
10 | className: PropTypes.string,
11 | }
12 |
13 | const synth = window.speechSynthesis
14 |
15 | function ReadButton({ className, contentFn, size }) {
16 | const { t } = useTranslation()
17 | const [speaking, setSpeaking] = useState(false)
18 | const config = useConfig()
19 |
20 | const startSpeak = () => {
21 | synth.cancel()
22 |
23 | const text = contentFn()
24 | const utterance = new SpeechSynthesisUtterance(text)
25 | const voices = synth.getVoices()
26 |
27 | let voice
28 | if (config.preferredLanguage.includes('en') && navigator.language.includes('en'))
29 | voice = voices.find((v) => v.name.toLowerCase().includes('microsoft aria'))
30 | else if (config.preferredLanguage.includes('zh') || navigator.language.includes('zh'))
31 | voice = voices.find((v) => v.name.toLowerCase().includes('xiaoyi'))
32 | else if (config.preferredLanguage.includes('ja') || navigator.language.includes('ja'))
33 | voice = voices.find((v) => v.name.toLowerCase().includes('nanami'))
34 | if (!voice) voice = voices.find((v) => v.lang.substring(0, 2) === config.preferredLanguage)
35 | if (!voice) voice = voices.find((v) => v.lang === navigator.language)
36 |
37 | Object.assign(utterance, {
38 | rate: 1,
39 | volume: 1,
40 | onend: () => setSpeaking(false),
41 | onerror: () => setSpeaking(false),
42 | voice: voice,
43 | })
44 |
45 | synth.speak(utterance)
46 | setSpeaking(true)
47 | }
48 |
49 | const stopSpeak = () => {
50 | synth.cancel()
51 | setSpeaking(false)
52 | }
53 |
54 | return (
55 |
60 | {speaking ? : }
61 |
62 | )
63 | }
64 |
65 | export default ReadButton
66 |
--------------------------------------------------------------------------------
/src/services/apis/shared.mjs:
--------------------------------------------------------------------------------
1 | export const getChatSystemPromptBase = async () => {
2 | return `You are a helpful, creative, clever, and very friendly assistant. You are familiar with various languages in the world.`
3 | }
4 |
5 | export const getCompletionPromptBase = async () => {
6 | return (
7 | `The following is a conversation with an AI assistant.` +
8 | `The assistant is helpful, creative, clever, and very friendly. The assistant is familiar with various languages in the world.\n\n` +
9 | `Human: Hello, who are you?\n` +
10 | `AI: I am an AI assistant. How can I help you today?\n`
11 | )
12 | }
13 |
14 | export const getCustomApiPromptBase = async () => {
15 | return `I am a helpful, creative, clever, and very friendly assistant. I am familiar with various languages in the world.`
16 | }
17 |
18 | export function setAbortController(port, onStop, onDisconnect) {
19 | const controller = new AbortController()
20 | const messageListener = (msg) => {
21 | if (msg.stop) {
22 | port.onMessage.removeListener(messageListener)
23 | console.debug('stop generating')
24 | port.postMessage({ done: true })
25 | controller.abort()
26 | if (onStop) onStop()
27 | }
28 | }
29 | port.onMessage.addListener(messageListener)
30 |
31 | const disconnectListener = () => {
32 | port.onDisconnect.removeListener(disconnectListener)
33 | console.debug('port disconnected')
34 | controller.abort()
35 | if (onDisconnect) onDisconnect()
36 | }
37 | port.onDisconnect.addListener(disconnectListener)
38 |
39 | const cleanController = () => {
40 | try {
41 | port.onMessage.removeListener(messageListener)
42 | port.onDisconnect.removeListener(disconnectListener)
43 | } catch (e) {
44 | // ignore
45 | }
46 | }
47 |
48 | return { controller, cleanController, messageListener, disconnectListener }
49 | }
50 |
51 | export function pushRecord(session, question, answer) {
52 | const recordLength = session.conversationRecords.length
53 | let lastRecord
54 | if (recordLength > 0) lastRecord = session.conversationRecords[recordLength - 1]
55 |
56 | if (session.isRetry && lastRecord && lastRecord.question === question) lastRecord.answer = answer
57 | else session.conversationRecords.push({ question: question, answer: answer })
58 | }
59 |
--------------------------------------------------------------------------------
/src/services/clients/poe/websocket.js:
--------------------------------------------------------------------------------
1 | import * as diff from 'diff'
2 |
3 | const getSocketUrl = async (settings) => {
4 | settings = settings.tchannelData
5 | const tchRand = Math.floor(100000 + Math.random() * 900000) // They're surely using 6 digit random number for ws url.
6 | const socketUrl = `wss://tch${tchRand}.tch.quora.com`
7 | const boxName = settings.boxName
8 | const minSeq = settings.minSeq
9 | const channel = settings.channel
10 | const hash = settings.channelHash
11 | return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`
12 | }
13 | export const connectWs = async (settings) => {
14 | const url = await getSocketUrl(settings)
15 | const ws = new WebSocket(url)
16 | return new Promise((resolve) => {
17 | ws.onopen = () => {
18 | console.log('Connected to websocket')
19 | return resolve(ws)
20 | }
21 | })
22 | }
23 | export const disconnectWs = async (ws) => {
24 | return new Promise((resolve) => {
25 | ws.onclose = () => {
26 | return resolve(true)
27 | }
28 | ws.close()
29 | })
30 | }
31 | export const listenWs = async (ws, onMessage, onComplete) => {
32 | let previousText = ''
33 | return new Promise((resolve) => {
34 | let complete = false
35 | ws.onmessage = (e) => {
36 | let jsonData = JSON.parse(e.data)
37 | console.log(jsonData)
38 | if (jsonData.messages && jsonData.messages.length > 0) {
39 | const messages = JSON.parse(jsonData.messages[0])
40 | const dataPayload = messages.payload.data
41 | const text = dataPayload.messageAdded.text
42 | const state = dataPayload.messageAdded.state
43 | if (state !== 'complete') {
44 | const differences = diff.diffChars(previousText, text)
45 | let result = ''
46 | differences.forEach((part) => {
47 | if (part.added) {
48 | result += part.value
49 | }
50 | })
51 | previousText = text
52 | if (onMessage) onMessage(result)
53 | } else if (dataPayload.messageAdded.author !== 'human') {
54 | if (!complete) {
55 | complete = true
56 | if (onComplete) onComplete(text)
57 | return resolve(text)
58 | }
59 | }
60 | }
61 | }
62 | })
63 | }
64 |
--------------------------------------------------------------------------------
/src/utils/is-mobile.mjs:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
2 |
3 | export function isMobile() {
4 | if (navigator.userAgentData) return navigator.userAgentData.mobile
5 | let check = false
6 | ;(function (a) {
7 | if (
8 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
9 | a,
10 | ) ||
11 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
12 | a.substr(0, 4),
13 | )
14 | )
15 | check = true
16 | })(navigator.userAgent || navigator.vendor || window.opera)
17 | return check
18 | }
19 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/gitlab/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText, limitedFetch } from '../../../utils'
2 |
3 | const getPatchUrl = async () => {
4 | const patchUrl = location.origin + location.pathname + '.patch'
5 | const response = await fetch(patchUrl, { method: 'HEAD' })
6 | if (response.ok) return patchUrl
7 | return ''
8 | }
9 |
10 | const getPatchData = async (patchUrl) => {
11 | if (!patchUrl) return
12 |
13 | let patchData = await limitedFetch(patchUrl, 1024 * 40)
14 | patchData = patchData.substring(patchData.indexOf('---'))
15 | return patchData
16 | }
17 |
18 | export default {
19 | inputQuery: async () => {
20 | try {
21 | if (location.pathname.includes('/blob')) {
22 | const fileData = await limitedFetch(location.href.replace('/blob/', '/raw/'), 1024 * 40)
23 | if (!fileData) return
24 |
25 | return await cropText(
26 | `You are a senior software engineer and code reviewer. ` +
27 | `Analyze the following file content thoroughly. ` +
28 | `Explain its purpose, main functionalities, and how different parts of the code contribute to its overall behavior. ` +
29 | `Identify any potential issues, areas for improvement, or notable design patterns. ` +
30 | `Use markdown syntax (e.g., code blocks, bolding, lists) to structure your explanation for better readability.\n\n` +
31 | `File content:\n\`\`\`\n${fileData}\n\`\`\``,
32 | )
33 | } else {
34 | const patchUrl = await getPatchUrl()
35 | const patchData = await getPatchData(patchUrl)
36 | if (!patchData) return
37 |
38 | return await cropText(
39 | `You are an expert in analyzing git commits and crafting clear, concise commit messages. ` +
40 | `Based on the following git patch, please perform two tasks:\n` +
41 | `1. Generate a suitable commit message. It should follow standard conventions: a short imperative subject line (max 50 chars), ` +
42 | `followed by a blank line and a more detailed body if necessary, explaining the "what" and "why" of the changes.\n` +
43 | `2. Provide a brief summary of the changes introduced in this commit, highlighting the main purpose and impact.\n\n` +
44 | `The patch contents are as follows:\n${patchData}`,
45 | )
46 | }
47 | } catch (e) {
48 | console.log(e)
49 | }
50 | },
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/tagged-release.yml:
--------------------------------------------------------------------------------
1 | name: tagged-release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | permissions:
8 | id-token: "write"
9 | contents: "write"
10 | env:
11 | GH_TOKEN: ${{ github.token }}
12 |
13 | jobs:
14 | build_and_release:
15 | runs-on: macos-14
16 |
17 | steps:
18 | - run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
19 | - uses: actions/checkout@v5
20 | with:
21 | ref: master
22 |
23 | - name: Update manifest.json version
24 | uses: jossef/action-set-json-field@v2.2
25 | with:
26 | file: src/manifest.json
27 | field: version
28 | value: ${{ env.VERSION }}
29 |
30 | - name: Update manifest.v2.json version
31 | uses: jossef/action-set-json-field@v2.2
32 | with:
33 | file: src/manifest.v2.json
34 | field: version
35 | value: ${{ env.VERSION }}
36 |
37 | - name: Push files
38 | continue-on-error: true
39 | run: |
40 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
41 | git config --global user.name "github-actions[bot]"
42 | git commit -am "release v${{ env.VERSION }}"
43 | git push
44 |
45 | - run: |
46 | gh release create ${{github.ref_name}} -d -F CURRENT_CHANGE.md -t ${{github.ref_name}}
47 |
48 | - uses: actions/setup-node@v5
49 | with:
50 | node-version: 20
51 | - run: npm ci
52 |
53 | - uses: actions/setup-python@v6
54 | with:
55 | python-version: '3.10' # for appdmg
56 | - uses: maxim-lobanov/setup-xcode@v1
57 | with:
58 | xcode-version: 16.2
59 | - run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.pre.patch
60 | - run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.patch
61 | - run: npm run build:safari
62 |
63 | - run: |
64 | gh release upload ${{github.ref_name}} build/chromium.zip
65 | gh release upload ${{github.ref_name}} build/firefox.zip
66 | gh release upload ${{github.ref_name}} build/safari.dmg
67 | gh release upload ${{github.ref_name}} build/chromium-without-katex-and-tiktoken.zip
68 | gh release upload ${{github.ref_name}} build/firefox-without-katex-and-tiktoken.zip
69 |
70 | - run: |
71 | gh release edit ${{github.ref_name}} --draft=false
72 |
--------------------------------------------------------------------------------
/src/background/menus.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { defaultConfig, getPreferredLanguageKey, getUserConfig } from '../config/index.mjs'
3 | import { changeLanguage, t } from 'i18next'
4 | import { config as menuConfig } from '../content-script/menu-tools/index.mjs'
5 |
6 | const menuId = 'ChatGPTBox-Menu'
7 | const onClickMenu = (info, tab) => {
8 | Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
9 | const currentTab = tabs[0]
10 | const message = {
11 | itemId: info.menuItemId.replace(menuId, ''),
12 | selectionText: info.selectionText,
13 | useMenuPosition: tab.id === currentTab.id,
14 | }
15 | console.debug('menu clicked', message)
16 |
17 | if (defaultConfig.selectionTools.includes(message.itemId)) {
18 | Browser.tabs.sendMessage(currentTab.id, {
19 | type: 'CREATE_CHAT',
20 | data: message,
21 | })
22 | } else if (message.itemId in menuConfig) {
23 | if (menuConfig[message.itemId].action) {
24 | menuConfig[message.itemId].action(true, tab)
25 | }
26 |
27 | if (menuConfig[message.itemId].genPrompt) {
28 | Browser.tabs.sendMessage(currentTab.id, {
29 | type: 'CREATE_CHAT',
30 | data: message,
31 | })
32 | }
33 | }
34 | })
35 | }
36 | export function refreshMenu() {
37 | if (Browser.contextMenus.onClicked.hasListener(onClickMenu))
38 | Browser.contextMenus.onClicked.removeListener(onClickMenu)
39 | Browser.contextMenus.removeAll().then(async () => {
40 | if ((await getUserConfig()).hideContextMenu) return
41 |
42 | await getPreferredLanguageKey().then((lang) => {
43 | changeLanguage(lang)
44 | })
45 | Browser.contextMenus.create({
46 | id: menuId,
47 | title: 'ChatGPTBox',
48 | contexts: ['all'],
49 | })
50 |
51 | for (const [k, v] of Object.entries(menuConfig)) {
52 | Browser.contextMenus.create({
53 | id: menuId + k,
54 | parentId: menuId,
55 | title: t(v.label),
56 | contexts: ['all'],
57 | })
58 | }
59 | Browser.contextMenus.create({
60 | id: menuId + 'separator1',
61 | parentId: menuId,
62 | contexts: ['selection'],
63 | type: 'separator',
64 | })
65 | for (const index in defaultConfig.selectionTools) {
66 | const key = defaultConfig.selectionTools[index]
67 | const desc = defaultConfig.selectionToolsDesc[index]
68 | Browser.contextMenus.create({
69 | id: menuId + key,
70 | parentId: menuId,
71 | title: t(desc),
72 | contexts: ['selection'],
73 | })
74 | }
75 |
76 | Browser.contextMenus.onClicked.addListener(onClickMenu)
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/src/services/local-session.mjs:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { initSession } from './init-session.mjs'
3 | import { getUserConfig } from '../config/index.mjs'
4 |
5 | export const initDefaultSession = async () => {
6 | const config = await getUserConfig()
7 | return initSession({
8 | sessionName: new Date().toLocaleString(),
9 | modelName: config.modelName,
10 | apiMode: config.apiMode,
11 | autoClean: false,
12 | extraCustomModelName: config.customModelName,
13 | })
14 | }
15 |
16 | export const createSession = async (newSession) => {
17 | let currentSessions
18 | if (newSession) {
19 | const ret = await getSession(newSession.sessionId)
20 | currentSessions = ret.currentSessions
21 | if (ret.session)
22 | currentSessions[
23 | currentSessions.findIndex((session) => session.sessionId === newSession.sessionId)
24 | ] = newSession
25 | else currentSessions.unshift(newSession)
26 | } else {
27 | newSession = await initDefaultSession()
28 | currentSessions = await getSessions()
29 | currentSessions.unshift(newSession)
30 | }
31 | await Browser.storage.local.set({ sessions: currentSessions })
32 | return { session: newSession, currentSessions }
33 | }
34 |
35 | export const deleteSession = async (sessionId) => {
36 | const currentSessions = await getSessions()
37 | const index = currentSessions.findIndex((session) => session.sessionId === sessionId)
38 | currentSessions.splice(index, 1)
39 | if (currentSessions.length > 0) {
40 | await Browser.storage.local.set({ sessions: currentSessions })
41 | return currentSessions
42 | }
43 | return await resetSessions()
44 | }
45 |
46 | export const getSession = async (sessionId) => {
47 | const currentSessions = await getSessions()
48 | return {
49 | session: currentSessions.find((session) => session.sessionId === sessionId),
50 | currentSessions,
51 | }
52 | }
53 |
54 | export const updateSession = async (newSession) => {
55 | newSession.updatedAt = new Date().toISOString()
56 | const currentSessions = await getSessions()
57 | currentSessions[
58 | currentSessions.findIndex((session) => session.sessionId === newSession.sessionId)
59 | ] = newSession
60 | await Browser.storage.local.set({ sessions: currentSessions })
61 | return currentSessions
62 | }
63 |
64 | export const resetSessions = async () => {
65 | const currentSessions = [await initDefaultSession()]
66 | await Browser.storage.local.set({ sessions: currentSessions })
67 | return currentSessions
68 | }
69 |
70 | export const getSessions = async () => {
71 | const { sessions } = await Browser.storage.local.get('sessions')
72 | if (sessions && sessions.length > 0) return sessions
73 | return await resetSessions()
74 | }
75 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/bilibili/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText, waitForElementToExistAndSelect } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | export default {
5 | init: async (hostname, userConfig, getInput, mountComponent) => {
6 | if (location.pathname.includes('/bangumi')) return false
7 | try {
8 | // B站页面是SSR的,如果插入过早,页面 js 检测到实际 Dom 和期望 Dom 不一致,会导致重新渲染
9 | await waitForElementToExistAndSelect('img.bili-avatar-img')
10 | const getVideoPath = () =>
11 | location.pathname + `?p=${new URLSearchParams(location.search).get('p') || 1}`
12 | let oldPath = getVideoPath()
13 | const checkPathChange = async () => {
14 | const newPath = getVideoPath()
15 | if (newPath !== oldPath) {
16 | oldPath = newPath
17 | mountComponent('bilibili', config.bilibili)
18 | }
19 | }
20 | window.setInterval(checkPathChange, 500)
21 | } catch (e) {
22 | /* empty */
23 | }
24 | return true
25 | },
26 | inputQuery: async () => {
27 | try {
28 | const bvid = location.pathname.replace('video', '').replaceAll('/', '')
29 | const p = Number(new URLSearchParams(location.search).get('p') || 1) - 1
30 |
31 | const pagelistResponse = await fetch(
32 | `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`,
33 | )
34 | const pagelistData = await pagelistResponse.json()
35 | const videoList = pagelistData.data
36 | const cid = videoList[p].cid
37 | const title = videoList[p].part
38 |
39 | const infoResponse = await fetch(
40 | `https://api.bilibili.com/x/player/wbi/v2?bvid=${bvid}&cid=${cid}`,
41 | {
42 | credentials: 'include',
43 | },
44 | )
45 | const infoData = await infoResponse.json()
46 | let subtitleUrl = infoData.data.subtitle.subtitles[0].subtitle_url
47 | if (subtitleUrl.startsWith('//')) subtitleUrl = 'https:' + subtitleUrl
48 | else if (!subtitleUrl.startsWith('http')) subtitleUrl = 'https://' + subtitleUrl
49 |
50 | const subtitleResponse = await fetch(subtitleUrl)
51 | const subtitleData = await subtitleResponse.json()
52 | const subtitles = subtitleData.body
53 |
54 | let subtitleContent = ''
55 | for (let i = 0; i < subtitles.length; i++) {
56 | if (i === subtitles.length - 1) subtitleContent += subtitles[i].content
57 | else subtitleContent += subtitles[i].content + ','
58 | }
59 |
60 | return await cropText(
61 | `You are an expert video summarizer. Create a comprehensive summary of the following Bilibili video in markdown format, ` +
62 | `highlighting key takeaways, crucial information, and main topics. Include the video title.\n` +
63 | `Video Title: "${title}"\n` +
64 | `Subtitle content:\n${subtitleContent}`,
65 | )
66 | } catch (e) {
67 | console.log(e)
68 | }
69 | },
70 | }
71 |
--------------------------------------------------------------------------------
/src/services/apis/claude-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { pushRecord, setAbortController } from './shared.mjs'
3 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
4 | import { isEmpty } from 'lodash-es'
5 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
6 | import { getModelValue } from '../../utils/model-name-convert.mjs'
7 |
8 | /**
9 | * @param {Runtime.Port} port
10 | * @param {string} question
11 | * @param {Session} session
12 | */
13 | export async function generateAnswersWithClaudeApi(port, question, session) {
14 | const { controller, messageListener, disconnectListener } = setAbortController(port)
15 | const config = await getUserConfig()
16 | const apiUrl = config.customClaudeApiUrl
17 | const model = getModelValue(session)
18 |
19 | const prompt = getConversationPairs(
20 | session.conversationRecords.slice(-config.maxConversationContextLength),
21 | false,
22 | )
23 | prompt.push({ role: 'user', content: question })
24 |
25 | let answer = ''
26 | await fetchSSE(`${apiUrl}/v1/messages`, {
27 | method: 'POST',
28 | signal: controller.signal,
29 | headers: {
30 | 'Content-Type': 'application/json',
31 | 'anthropic-version': '2023-06-01',
32 | 'x-api-key': config.claudeApiKey,
33 | 'anthropic-dangerous-direct-browser-access': true,
34 | },
35 | body: JSON.stringify({
36 | model,
37 | messages: prompt,
38 | stream: true,
39 | max_tokens: config.maxResponseTokenLength,
40 | temperature: config.temperature,
41 | }),
42 | onMessage(message) {
43 | console.debug('sse message', message)
44 |
45 | let data
46 | try {
47 | data = JSON.parse(message)
48 | } catch (error) {
49 | console.debug('json error', error)
50 | return
51 | }
52 | if (data?.type === 'message_stop') {
53 | pushRecord(session, question, answer)
54 | console.debug('conversation history', { content: session.conversationRecords })
55 | port.postMessage({ answer: null, done: true, session: session })
56 | return
57 | }
58 |
59 | const delta = data?.delta?.text
60 | if (delta) {
61 | answer += delta
62 | port.postMessage({ answer: answer, done: false, session: null })
63 | }
64 | },
65 | async onStart() {},
66 | async onEnd() {
67 | port.postMessage({ done: true })
68 | port.onMessage.removeListener(messageListener)
69 | port.onDisconnect.removeListener(disconnectListener)
70 | },
71 | async onError(resp) {
72 | port.onMessage.removeListener(messageListener)
73 | port.onDisconnect.removeListener(disconnectListener)
74 | if (resp instanceof Error) throw resp
75 | const error = await resp.json().catch(() => ({}))
76 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
77 | },
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ChatGPTBox",
3 | "description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4 | "version": "2.5.9",
5 | "manifest_version": 3,
6 | "icons": {
7 | "16": "logo.png",
8 | "32": "logo.png",
9 | "48": "logo.png",
10 | "128": "logo.png"
11 | },
12 | "host_permissions": [
13 | "https://*.chatgpt.com/*",
14 | "https://*.openai.com/*",
15 | "https://*.bing.com/*",
16 | "https://*.poe.com/*",
17 | "https://*.google.com/*",
18 | "https://claude.ai/*",
19 | "https://*.moonshot.cn/*",
20 | ""
21 | ],
22 | "permissions": [
23 | "cookies",
24 | "storage",
25 | "contextMenus",
26 | "unlimitedStorage",
27 | "tabs",
28 | "webRequest",
29 | "declarativeNetRequestWithHostAccess",
30 | "sidePanel"
31 | ],
32 | "optional_permissions": [
33 | "background"
34 | ],
35 | "background": {
36 | "service_worker": "background.js"
37 | },
38 | "action": {
39 | "default_popup": "popup.html"
40 | },
41 | "side_panel": {
42 | "default_path": "IndependentPanel.html"
43 | },
44 | "declarative_net_request": {
45 | "rule_resources": [
46 | {
47 | "id": "ruleset",
48 | "enabled": true,
49 | "path": "rules.json"
50 | }
51 | ]
52 | },
53 | "options_ui": {
54 | "page": "popup.html",
55 | "open_in_tab": true
56 | },
57 | "content_scripts": [
58 | {
59 | "matches": [
60 | "https://*/*",
61 | "http://*/*",
62 | "file://*/*"
63 | ],
64 | "js": [
65 | "shared.js",
66 | "content-script.js"
67 | ],
68 | "css": [
69 | "content-script.css"
70 | ]
71 | }
72 | ],
73 | "web_accessible_resources": [
74 | {
75 | "resources": [
76 | "logo.png"
77 | ],
78 | "matches": [
79 | ""
80 | ]
81 | }
82 | ],
83 | "commands": {
84 | "newChat": {
85 | "suggested_key": {
86 | "default": "Ctrl+B",
87 | "mac": "MacCtrl+B"
88 | },
89 | "description": "Create a new chat"
90 | },
91 | "summarizePage": {
92 | "suggested_key": {
93 | "default": "Alt+B",
94 | "mac": "Alt+B"
95 | },
96 | "description": "Summarize this page"
97 | },
98 | "openConversationPage": {
99 | "suggested_key": {
100 | "default": "Ctrl+Shift+H",
101 | "mac": "MacCtrl+Shift+H"
102 | },
103 | "description": "Open the independent conversation page"
104 | },
105 | "openConversationWindow": {
106 | "description": "Open the independent conversation window"
107 | },
108 | "openSidePanel": {
109 | "description": "Open the independent conversation side panel"
110 | },
111 | "closeAllChats": {
112 | "description": "Close all chats in this page"
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/src/content-script/menu-tools/index.mjs:
--------------------------------------------------------------------------------
1 | import { getCoreContentText } from '../../utils/get-core-content-text'
2 | import Browser from 'webextension-polyfill'
3 | import { getUserConfig } from '../../config/index.mjs'
4 | import { openUrl } from '../../utils/open-url'
5 |
6 | export const config = {
7 | newChat: {
8 | label: 'New Chat',
9 | genPrompt: async () => {
10 | return ''
11 | },
12 | },
13 | summarizePage: {
14 | label: 'Summarize Page',
15 | genPrompt: async () => {
16 | return `You are an expert summarizer. Carefully analyze the following web page content and provide a concise summary focusing on the key points:\n${getCoreContentText()}`
17 | },
18 | },
19 | openConversationPage: {
20 | label: 'Open Conversation Page',
21 | action: async (fromBackground) => {
22 | console.debug('action is from background', fromBackground)
23 | if (fromBackground) {
24 | openUrl(Browser.runtime.getURL('IndependentPanel.html'))
25 | } else {
26 | Browser.runtime.sendMessage({
27 | type: 'OPEN_URL',
28 | data: {
29 | url: Browser.runtime.getURL('IndependentPanel.html'),
30 | },
31 | })
32 | }
33 | },
34 | },
35 | openConversationWindow: {
36 | label: 'Open Conversation Window',
37 | action: async (fromBackground) => {
38 | console.debug('action is from background', fromBackground)
39 | if (fromBackground) {
40 | const config = await getUserConfig()
41 | const url = Browser.runtime.getURL('IndependentPanel.html')
42 | const tabs = await Browser.tabs.query({ url: url, windowType: 'popup' })
43 | if (!config.alwaysCreateNewConversationWindow && tabs.length > 0)
44 | await Browser.windows.update(tabs[0].windowId, { focused: true })
45 | else
46 | await Browser.windows.create({
47 | url: url,
48 | type: 'popup',
49 | width: 500,
50 | height: 650,
51 | })
52 | } else {
53 | Browser.runtime.sendMessage({
54 | type: 'OPEN_CHAT_WINDOW',
55 | data: {},
56 | })
57 | }
58 | },
59 | },
60 | openSidePanel: {
61 | label: 'Open Side Panel',
62 | action: async (fromBackground, tab) => {
63 | console.debug('action is from background', fromBackground)
64 | if (fromBackground) {
65 | // eslint-disable-next-line no-undef
66 | chrome.sidePanel.open({ windowId: tab.windowId, tabId: tab.id })
67 | } else {
68 | // side panel is not supported
69 | }
70 | },
71 | },
72 | closeAllChats: {
73 | label: 'Close All Chats In This Page',
74 | action: async (fromBackground) => {
75 | console.debug('action is from background', fromBackground)
76 | Browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
77 | Browser.tabs.sendMessage(tabs[0].id, {
78 | type: 'CLOSE_CHATS',
79 | data: {},
80 | })
81 | })
82 | },
83 | },
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/WebJumpBackNotification/index.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import PropTypes from 'prop-types'
3 | import Browser from 'webextension-polyfill'
4 | import { toast, ToastContainer } from 'react-toastify'
5 | import { useEffect } from 'react'
6 | import 'react-toastify/dist/ReactToastify.css'
7 | import { useTheme } from '../../hooks/use-theme.mjs'
8 | import { getUserConfig } from '../../config/index.mjs'
9 |
10 | const WebJumpBackNotification = (props) => {
11 | const { t } = useTranslation()
12 | const [theme, config] = useTheme()
13 |
14 | const buttonStyle = {
15 | padding: '0 8px',
16 | border: '1px solid',
17 | borderRadius: '4px',
18 | whiteSpace: 'nowrap',
19 | cursor: 'pointer',
20 | color: 'inherit',
21 | backgroundColor: 'transparent',
22 | }
23 |
24 | useEffect(() => {
25 | toast(
26 |
35 |
36 | {props.chatgptMode
37 | ? t('Please keep this tab open. You can now use the web mode of ChatGPTBox')
38 | : t('You have successfully logged in for ChatGPTBox and can now return')}
39 |
40 |
41 | {props.chatgptMode && (
42 | {
45 | Browser.runtime.sendMessage({
46 | type: 'PIN_TAB',
47 | data: {
48 | saveAsChatgptConfig: true,
49 | },
50 | })
51 | }}
52 | >
53 | {t('Pin Tab')}
54 |
55 | )}
56 | {
59 | Browser.runtime.sendMessage({
60 | type: 'ACTIVATE_URL',
61 | data: {
62 | tabId: (await getUserConfig()).notificationJumpBackTabId,
63 | },
64 | })
65 | }}
66 | >
67 | {t('Go Back')}
68 |
69 |
70 |
,
71 | {
72 | toastId: 0,
73 | updateId: 0,
74 | },
75 | )
76 | }, [config.themeMode, config.preferredLanguage])
77 |
78 | return (
79 |
92 | )
93 | }
94 |
95 | WebJumpBackNotification.propTypes = {
96 | container: PropTypes.object.isRequired,
97 | chatgptMode: PropTypes.bool,
98 | }
99 |
100 | export default WebJumpBackNotification
101 |
--------------------------------------------------------------------------------
/src/utils/get-core-content-text.mjs:
--------------------------------------------------------------------------------
1 | import { getPossibleElementByQuerySelector } from './get-possible-element-by-query-selector.mjs'
2 | import { Readability, isProbablyReaderable } from '@mozilla/readability'
3 |
4 | const adapters = {
5 | 'scholar.google': ['#gs_res_ccl_mid'],
6 | google: ['#search'],
7 | csdn: ['#content_views'],
8 | bing: ['#b_results'],
9 | wikipedia: ['#mw-content-text'],
10 | faz: ['.atc-Text'],
11 | golem: ['article'],
12 | eetimes: ['article'],
13 | 'new.qq.com': ['.content-article'],
14 | }
15 |
16 | function getArea(e) {
17 | const rect = e.getBoundingClientRect()
18 | return rect.width * rect.height
19 | }
20 |
21 | function findLargestElement(e) {
22 | if (!e) {
23 | return null
24 | }
25 | let maxArea = 0
26 | let largestElement = null
27 | const limitedArea = 0.8 * getArea(e)
28 |
29 | function traverseDOM(node) {
30 | if (node.nodeType === Node.ELEMENT_NODE) {
31 | const area = getArea(node)
32 |
33 | if (area > maxArea && area < limitedArea) {
34 | maxArea = area
35 | largestElement = node
36 | }
37 |
38 | Array.from(node.children).forEach(traverseDOM)
39 | }
40 | }
41 |
42 | traverseDOM(e)
43 | return largestElement
44 | }
45 |
46 | function getTextFrom(e) {
47 | return e.innerText || e.textContent
48 | }
49 |
50 | function postProcessText(text) {
51 | return text
52 | .trim()
53 | .replaceAll(' ', '')
54 | .replaceAll('\t', '')
55 | .replaceAll('\n\n', '')
56 | .replaceAll(',,', '')
57 | }
58 |
59 | export function getCoreContentText() {
60 | for (const [siteName, selectors] of Object.entries(adapters)) {
61 | if (location.hostname.includes(siteName)) {
62 | const element = getPossibleElementByQuerySelector(selectors)
63 | if (element) return postProcessText(getTextFrom(element))
64 | break
65 | }
66 | }
67 |
68 | const element = document.querySelector('article')
69 | if (element) {
70 | return postProcessText(getTextFrom(element))
71 | }
72 |
73 | if (isProbablyReaderable(document)) {
74 | let article = new Readability(document.cloneNode(true), {
75 | keepClasses: true,
76 | }).parse()
77 | if (article?.textContent) {
78 | console.log('readerable: successfully extracted content')
79 | return postProcessText(article.textContent)
80 | } else {
81 | console.log('readerable: parsing failed despite probability check')
82 | }
83 | }
84 |
85 | const largestElement = findLargestElement(document.body)
86 | const secondLargestElement = findLargestElement(largestElement)
87 | console.log(largestElement)
88 | console.log(secondLargestElement)
89 |
90 | let ret
91 | if (!largestElement) {
92 | ret = getTextFrom(document.body)
93 | console.log('use document.body')
94 | } else if (
95 | secondLargestElement &&
96 | getArea(secondLargestElement) > 0.5 * getArea(largestElement)
97 | ) {
98 | ret = getTextFrom(secondLargestElement)
99 | console.log('use second')
100 | } else {
101 | ret = getTextFrom(largestElement)
102 | console.log('use first')
103 | }
104 | return postProcessText(ret)
105 | }
106 |
--------------------------------------------------------------------------------
/src/services/apis/waylaidwanderer-api.mjs:
--------------------------------------------------------------------------------
1 | import { pushRecord, setAbortController } from './shared.mjs'
2 | import { getUserConfig } from '../../config/index.mjs'
3 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
4 | import { isEmpty } from 'lodash-es'
5 |
6 | /**
7 | * @param {Runtime.Port} port
8 | * @param {string} question
9 | * @param {Session} session
10 | */
11 | export async function generateAnswersWithWaylaidwandererApi(port, question, session) {
12 | const { controller, messageListener, disconnectListener } = setAbortController(port)
13 |
14 | const config = await getUserConfig()
15 |
16 | let answer = ''
17 | await fetchSSE(config.githubThirdPartyUrl, {
18 | method: 'POST',
19 | signal: controller.signal,
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | },
23 | body: JSON.stringify({
24 | message: question,
25 | stream: true,
26 | ...(session.bingWeb_encryptedConversationSignature && {
27 | conversationId: session.bingWeb_conversationId,
28 | encryptedConversationSignature: session.bingWeb_encryptedConversationSignature,
29 | clientId: session.bingWeb_clientId,
30 | invocationId: session.bingWeb_invocationId,
31 | }),
32 | ...(session.parentMessageId && {
33 | conversationId: session.conversationId,
34 | parentMessageId: session.parentMessageId,
35 | }),
36 | }),
37 | onMessage(message) {
38 | console.debug('sse message', message)
39 | if (message.trim() === '[DONE]') {
40 | pushRecord(session, question, answer)
41 | console.debug('conversation history', { content: session.conversationRecords })
42 | port.postMessage({ answer: null, done: true, session: session })
43 | return
44 | }
45 | let data
46 | try {
47 | data = JSON.parse(message)
48 | } catch (error) {
49 | console.debug('json error', error)
50 | return
51 | }
52 | if (data.conversationId) session.conversationId = data.conversationId
53 | if (data.parentMessageId) session.parentMessageId = data.parentMessageId
54 | if (data.encryptedConversationSignature)
55 | session.bingWeb_encryptedConversationSignature = data.encryptedConversationSignature
56 | if (data.conversationId) session.bingWeb_conversationId = data.conversationId
57 | if (data.clientId) session.bingWeb_clientId = data.clientId
58 | if (data.invocationId) session.bingWeb_invocationId = data.invocationId
59 |
60 | if (typeof data === 'string') {
61 | answer += data
62 | port.postMessage({ answer: answer, done: false, session: null })
63 | }
64 | },
65 | async onStart() {},
66 | async onEnd() {
67 | port.postMessage({ done: true })
68 | port.onMessage.removeListener(messageListener)
69 | port.onDisconnect.removeListener(disconnectListener)
70 | },
71 | async onError(resp) {
72 | port.onMessage.removeListener(messageListener)
73 | port.onDisconnect.removeListener(disconnectListener)
74 | if (resp instanceof Error) throw resp
75 | const error = await resp.json().catch(() => ({}))
76 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
77 | },
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/src/popup/sections/FeaturePages.jsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { useState } from 'react'
3 | import { isEdge, isFirefox, isMobile, isSafari, openUrl } from '../../utils/index.mjs'
4 | import Browser from 'webextension-polyfill'
5 | import PropTypes from 'prop-types'
6 |
7 | FeaturePages.propTypes = {
8 | config: PropTypes.object.isRequired,
9 | updateConfig: PropTypes.func.isRequired,
10 | }
11 |
12 | export function FeaturePages({ config, updateConfig }) {
13 | const { t } = useTranslation()
14 | const [backgroundPermission, setBackgroundPermission] = useState(false)
15 |
16 | if (!isMobile() && !isFirefox() && !isSafari())
17 | Browser.permissions.contains({ permissions: ['background'] }).then((result) => {
18 | setBackgroundPermission(result)
19 | })
20 |
21 | return (
22 |
23 | {!isMobile() && !isFirefox() && !isSafari() && (
24 | {
27 | if (isEdge()) openUrl('edge://extensions/shortcuts')
28 | else openUrl('chrome://extensions/shortcuts')
29 | }}
30 | >
31 | {t('Keyboard Shortcuts')}
32 |
33 | )}
34 | {
37 | Browser.runtime.sendMessage({
38 | type: 'OPEN_URL',
39 | data: {
40 | url: Browser.runtime.getURL('IndependentPanel.html'),
41 | },
42 | })
43 | }}
44 | >
45 | {t('Open Conversation Page')}
46 |
47 | {!isMobile() && (
48 | {
51 | Browser.runtime.sendMessage({
52 | type: 'OPEN_CHAT_WINDOW',
53 | data: {},
54 | })
55 | }}
56 | >
57 | {t('Open Conversation Window')}
58 |
59 | )}
60 | {!isMobile() && !isFirefox() && !isSafari() && (
61 |
62 | {
66 | const checked = e.target.checked
67 | if (checked)
68 | Browser.permissions.request({ permissions: ['background'] }).then((result) => {
69 | setBackgroundPermission(result)
70 | })
71 | else
72 | Browser.permissions.remove({ permissions: ['background'] }).then((result) => {
73 | setBackgroundPermission(result)
74 | })
75 | }}
76 | />
77 | {t('Keep Conversation Window in Background')}
78 |
79 | )}
80 | {!isMobile() && (
81 |
82 | {
86 | const checked = e.target.checked
87 | updateConfig({ alwaysCreateNewConversationWindow: checked })
88 | }}
89 | />
90 | {t('Always Create New Conversation Window')}
91 |
92 | )}
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/src/services/apis/azure-openai-api.mjs:
--------------------------------------------------------------------------------
1 | import { getUserConfig } from '../../config/index.mjs'
2 | import { pushRecord, setAbortController } from './shared.mjs'
3 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
4 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
5 | import { isEmpty } from 'lodash-es'
6 | import { getModelValue } from '../../utils/model-name-convert.mjs'
7 |
8 | /**
9 | * @param {Runtime.Port} port
10 | * @param {string} question
11 | * @param {Session} session
12 | */
13 | export async function generateAnswersWithAzureOpenaiApi(port, question, session) {
14 | const { controller, messageListener, disconnectListener } = setAbortController(port)
15 | const config = await getUserConfig()
16 | let model = getModelValue(session)
17 | if (!model) model = config.azureDeploymentName
18 |
19 | const prompt = getConversationPairs(
20 | session.conversationRecords.slice(-config.maxConversationContextLength),
21 | false,
22 | )
23 | prompt.push({ role: 'user', content: question })
24 |
25 | let answer = ''
26 | await fetchSSE(
27 | `${config.azureEndpoint.replace(
28 | /\/$/,
29 | '',
30 | )}/openai/deployments/${model}/chat/completions?api-version=2024-02-01`,
31 | {
32 | method: 'POST',
33 | signal: controller.signal,
34 | headers: {
35 | 'Content-Type': 'application/json',
36 | 'api-key': config.azureApiKey,
37 | },
38 | body: JSON.stringify({
39 | messages: prompt,
40 | stream: true,
41 | max_tokens: config.maxResponseTokenLength,
42 | temperature: config.temperature,
43 | }),
44 | onMessage(message) {
45 | console.debug('sse message', message)
46 | let data
47 | try {
48 | data = JSON.parse(message)
49 | } catch (error) {
50 | console.debug('json error', error)
51 | return
52 | }
53 | if (
54 | data.choices &&
55 | data.choices.length > 0 &&
56 | data.choices[0] &&
57 | data.choices[0].delta &&
58 | 'content' in data.choices[0].delta
59 | ) {
60 | answer += data.choices[0].delta.content
61 | port.postMessage({ answer: answer, done: false, session: null })
62 | }
63 |
64 | if (data.choices && data.choices.length > 0 && data.choices[0]?.finish_reason) {
65 | pushRecord(session, question, answer)
66 | console.debug('conversation history', { content: session.conversationRecords })
67 | port.postMessage({ answer: null, done: true, session: session })
68 | }
69 | },
70 | async onStart() {},
71 | async onEnd() {
72 | port.postMessage({ done: true })
73 | port.onMessage.removeListener(messageListener)
74 | port.onDisconnect.removeListener(disconnectListener)
75 | },
76 | async onError(resp) {
77 | port.onMessage.removeListener(messageListener)
78 | port.onDisconnect.removeListener(disconnectListener)
79 | if (resp instanceof Error) throw resp
80 | const error = await resp.json().catch(() => ({}))
81 | throw new Error(
82 | !isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`,
83 | )
84 | },
85 | },
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgptbox",
3 | "scripts": {
4 | "build": "node build.mjs --production",
5 | "build:safari": "bash ./safari/build.sh",
6 | "dev": "node build.mjs --development",
7 | "analyze": "node build.mjs --analyze",
8 | "lint": "eslint --ext .js,.mjs,.jsx .",
9 | "lint:fix": "eslint --ext .js,.mjs,.jsx . --fix",
10 | "pretty": "prettier --write ./**/*.{js,mjs,jsx,json,css,scss}",
11 | "stage": "run-script-os",
12 | "stage:default": "git add $(git diff --name-only --cached --diff-filter=d)",
13 | "stage:win32": "powershell git add $(git diff --name-only --cached --diff-filter=d)",
14 | "verify": "node .github/workflows/scripts/verify-search-engine-configs.mjs"
15 | },
16 | "pre-commit": [
17 | "pretty",
18 | "stage",
19 | "lint"
20 | ],
21 | "dependencies": {
22 | "@mozilla/readability": "^0.6.0",
23 | "@nem035/gpt-3-encoder": "^1.1.7",
24 | "@picocss/pico": "^1.5.13",
25 | "@primer/octicons-react": "^18.3.0",
26 | "buffer": "^6.0.3",
27 | "countries-list": "^2.6.1",
28 | "crypto-browserify": "^3.12.0",
29 | "diff": "^5.2.0",
30 | "file-saver": "^2.0.5",
31 | "github-markdown-css": "^5.6.1",
32 | "gpt-3-encoder": "^1.1.4",
33 | "graphql": "^16.9.0",
34 | "i18next": "^22.4.15",
35 | "js-sha3": "^0.9.3",
36 | "jsonwebtoken": "9.0.2",
37 | "katex": "^0.16.11",
38 | "lodash-es": "^4.17.21",
39 | "md5": "^2.3.0",
40 | "parse5": "^6.0.1",
41 | "preact": "^10.22.1",
42 | "process": "^0.11.10",
43 | "prop-types": "^15.8.1",
44 | "random-int": "^3.0.0",
45 | "react": "npm:@preact/compat@^17.1.2",
46 | "react-bootstrap-icons": "^1.11.4",
47 | "react-dom": "npm:@preact/compat@^17.1.2",
48 | "react-draggable": "^4.4.6",
49 | "react-i18next": "^12.2.0",
50 | "react-markdown": "^8.0.7",
51 | "react-tabs": "^4.3.0",
52 | "react-toastify": "^9.1.3",
53 | "rehype-highlight": "^6.0.0",
54 | "rehype-katex": "^6.0.3",
55 | "rehype-raw": "^6.1.1",
56 | "remark-breaks": "^3.0.3",
57 | "remark-gfm": "^3.0.1",
58 | "remark-math": "^5.1.1",
59 | "stream-browserify": "^3.0.0",
60 | "util": "^0.12.5",
61 | "uuid": "^9.0.1",
62 | "webextension-polyfill": "^0.12.0"
63 | },
64 | "devDependencies": {
65 | "@babel/core": "^7.24.7",
66 | "@babel/plugin-transform-react-jsx": "^7.24.7",
67 | "@babel/plugin-transform-runtime": "^7.24.7",
68 | "@babel/preset-env": "^7.24.7",
69 | "@types/archiver": "^5.3.4",
70 | "@types/fs-extra": "^11.0.4",
71 | "@types/jsdom": "^21.1.7",
72 | "@types/webextension-polyfill": "^0.10.7",
73 | "archiver": "^5.3.2",
74 | "babel-loader": "^9.1.3",
75 | "css-loader": "^6.11.0",
76 | "css-minimizer-webpack-plugin": "^5.0.1",
77 | "eslint": "^8.57.1",
78 | "eslint-plugin-react": "^7.34.3",
79 | "fs-extra": "^11.2.0",
80 | "graphql-tag": "^2.12.6",
81 | "jsdom": "^21.1.2",
82 | "less-loader": "^11.1.4",
83 | "mini-css-extract-plugin": "^2.9.0",
84 | "node-fetch": "^3.3.2",
85 | "pre-commit": "^1.2.2",
86 | "prettier": "^2.8.8",
87 | "progress-bar-webpack-plugin": "^2.1.0",
88 | "run-script-os": "^1.1.6",
89 | "sass": "^1.77.6",
90 | "sass-loader": "^13.3.3",
91 | "string-replace-loader": "^3.1.0",
92 | "terser-webpack-plugin": "^5.3.10",
93 | "webpack": "^5.92.1",
94 | "webpack-bundle-analyzer": "^4.10.2"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/services/init-session.mjs:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid'
2 | import { apiModeToModelName, modelNameToDesc } from '../utils/model-name-convert.mjs'
3 | import { t } from 'i18next'
4 |
5 | /**
6 | * @typedef {object} Session
7 | * @property {string|null} question
8 | * @property {Object[]|null} conversationRecords
9 | * @property {string|null} sessionName
10 | * @property {string|null} sessionId
11 | * @property {string|null} createdAt
12 | * @property {string|null} updatedAt
13 | * @property {string|null} aiName
14 | * @property {string|null} modelName
15 | * @property {boolean|null} autoClean
16 | * @property {boolean} isRetry
17 | * @property {string|null} conversationId - chatGPT web mode
18 | * @property {string|null} messageId - chatGPT web mode
19 | * @property {string|null} parentMessageId - chatGPT web mode
20 | * @property {string|null} wsRequestId - chatGPT web mode
21 | * @property {string|null} bingWeb_encryptedConversationSignature
22 | * @property {string|null} bingWeb_conversationId
23 | * @property {string|null} bingWeb_clientId
24 | * @property {string|null} bingWeb_invocationId
25 | * @property {string|null} bingWeb_jailbreakConversationId
26 | * @property {string|null} bingWeb_parentMessageId
27 | * @property {Object|null} bingWeb_jailbreakConversationCache
28 | * @property {number|null} poe_chatId
29 | * @property {object|null} bard_conversationObj
30 | * @property {object|null} claude_conversation
31 | * @property {object|null} moonshot_conversation
32 | */
33 | /**
34 | * @param {string|null} question
35 | * @param {Object[]|null} conversationRecords
36 | * @param {string|null} sessionName
37 | * @param {string|null} modelName
38 | * @param {boolean|null} autoClean
39 | * @param {Object|null} apiMode
40 | * @param {string} extraCustomModelName
41 | * @returns {Session}
42 | */
43 | export function initSession({
44 | question = null,
45 | conversationRecords = [],
46 | sessionName = null,
47 | modelName = null,
48 | autoClean = false,
49 | apiMode = null,
50 | extraCustomModelName = '',
51 | } = {}) {
52 | return {
53 | // common
54 | question,
55 | conversationRecords,
56 |
57 | sessionName,
58 | sessionId: uuidv4(),
59 | createdAt: new Date().toISOString(),
60 | updatedAt: new Date().toISOString(),
61 |
62 | aiName:
63 | modelName || apiMode
64 | ? modelNameToDesc(
65 | apiMode ? apiModeToModelName(apiMode) : modelName,
66 | t,
67 | extraCustomModelName,
68 | )
69 | : null,
70 | modelName,
71 | apiMode,
72 |
73 | autoClean,
74 | isRetry: false,
75 |
76 | // chatgpt-web
77 | conversationId: null,
78 | messageId: null,
79 | parentMessageId: null,
80 | wsRequestId: null,
81 |
82 | // bing
83 | bingWeb_encryptedConversationSignature: null,
84 | bingWeb_conversationId: null,
85 | bingWeb_clientId: null,
86 | bingWeb_invocationId: null,
87 |
88 | // bing sydney
89 | bingWeb_jailbreakConversationId: null,
90 | bingWeb_parentMessageId: null,
91 | bingWeb_jailbreakConversationCache: null,
92 |
93 | // poe
94 | poe_chatId: null,
95 |
96 | // bard
97 | bard_conversationObj: null,
98 |
99 | // claude.ai
100 | claude_conversation: null,
101 | // kimi.com
102 | moonshot_conversation: null,
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/content-script/site-adapters/youtube/index.mjs:
--------------------------------------------------------------------------------
1 | import { cropText } from '../../../utils'
2 | import { config } from '../index.mjs'
3 |
4 | // This function was written by ChatGPT and modified by iamsirsammy
5 | function replaceHtmlEntities(htmlString) {
6 | const doc = new DOMParser().parseFromString(htmlString.replaceAll('&', '&'), 'text/html')
7 | return doc.documentElement.innerText
8 | }
9 |
10 | export default {
11 | init: async (hostname, userConfig, getInput, mountComponent) => {
12 | try {
13 | let oldUrl = location.href
14 | const checkUrlChange = async () => {
15 | if (location.href !== oldUrl) {
16 | oldUrl = location.href
17 | mountComponent('youtube', config.youtube)
18 | }
19 | }
20 | window.setInterval(checkUrlChange, 500)
21 | } catch (e) {
22 | /* empty */
23 | }
24 | return true
25 | },
26 | inputQuery: async () => {
27 | try {
28 | const docText = await (
29 | await fetch(location.href, {
30 | credentials: 'include',
31 | })
32 | ).text()
33 |
34 | const subtitleUrlStartAt = docText.indexOf('https://www.youtube.com/api/timedtext')
35 | if (subtitleUrlStartAt === -1) return
36 |
37 | let subtitleUrl = docText.substring(subtitleUrlStartAt)
38 | subtitleUrl = subtitleUrl.substring(0, subtitleUrl.indexOf('"'))
39 | subtitleUrl = subtitleUrl.replaceAll('\\u0026', '&')
40 |
41 | let title = docText.substring(docText.indexOf('"title":"') + '"title":"'.length)
42 | title = title.substring(0, title.indexOf('","'))
43 |
44 | let potokenSource = performance
45 | .getEntriesByType('resource')
46 | .filter((a) => a?.name.includes('/api/timedtext?'))
47 | .pop()
48 | if (!potokenSource) {
49 | //TODO use waitUntil function in refactor version
50 | await new Promise((r) => setTimeout(r, 500))
51 | document.querySelector('button.ytp-subtitles-button.ytp-button').click()
52 | await new Promise((r) => setTimeout(r, 100))
53 | document.querySelector('button.ytp-subtitles-button.ytp-button').click()
54 | }
55 | await new Promise((r) => setTimeout(r, 500))
56 | potokenSource = performance
57 | .getEntriesByType('resource')
58 | .filter((a) => a?.name.includes('/api/timedtext?'))
59 | .pop()
60 | if (!potokenSource) return
61 | const potoken = new URL(potokenSource.name).searchParams.get('pot')
62 |
63 | const subtitleResponse = await fetch(`${subtitleUrl}&pot=${potoken}&c=WEB`)
64 | if (!subtitleResponse.ok) return
65 | let subtitleData = await subtitleResponse.text()
66 |
67 | let subtitleContent = ''
68 | while (subtitleData.indexOf('">') !== -1) {
69 | subtitleData = subtitleData.substring(subtitleData.indexOf('">') + 2)
70 | subtitleContent += subtitleData.substring(0, subtitleData.indexOf('<')) + ','
71 | }
72 |
73 | subtitleContent = replaceHtmlEntities(subtitleContent)
74 |
75 | return await cropText(
76 | `You are an expert video summarizer. Create a comprehensive summary of the following YouTube video in markdown format, ` +
77 | `highlighting key takeaways, crucial information, and main topics. Include the video title.\n` +
78 | `Video Title: "${title}"\n` +
79 | `Subtitle content:\n${subtitleContent}`,
80 | )
81 | } catch (e) {
82 | console.log(e)
83 | }
84 | },
85 | }
86 |
--------------------------------------------------------------------------------
/src/pages/IndependentPanel/styles.scss:
--------------------------------------------------------------------------------
1 | [data-theme='auto'] {
2 | @media screen and (prefers-color-scheme: dark) {
3 | --font-color: #c9d1d9;
4 | --font-active-color: #ffffff;
5 | --theme-color: #202124;
6 | --theme-border-color: #3c4043;
7 | --active-color: #3c4043;
8 | }
9 | @media screen and (prefers-color-scheme: light) {
10 | --font-color: #24292f;
11 | --font-active-color: #cc3333;
12 | --theme-color: #ffffff;
13 | --theme-border-color: #aeafb2;
14 | --active-color: #d0d4da;
15 | }
16 | }
17 |
18 | [data-theme='dark'] {
19 | --font-color: #c9d1d9;
20 | --font-active-color: #ffffff;
21 | --theme-color: #202124;
22 | --theme-border-color: #3c4043;
23 | --active-color: #3c4043;
24 | }
25 |
26 | [data-theme='light'] {
27 | --font-color: #24292f;
28 | --font-active-color: #cc3333;
29 | --theme-color: #ffffff;
30 | --theme-border-color: #aeafb2;
31 | --active-color: #d0d4da;
32 | }
33 |
34 | .IndependentPanel * {
35 | font-family: 'Cairo', sans-serif;
36 | font-size: 14px;
37 | }
38 |
39 | .IndependentPanel {
40 | .chat-container {
41 | display: flex;
42 | width: 100%;
43 | height: 100%;
44 | }
45 |
46 | .chat-sidebar {
47 | display: flex;
48 | flex-direction: column;
49 | min-width: 250px;
50 | width: 250px;
51 | background-color: var(--theme-color);
52 | transition: width 0.3s, min-width 0.3s;
53 | padding: 10px;
54 |
55 | ::-webkit-scrollbar {
56 | background-color: var(--theme-color);
57 | width: 9px;
58 | }
59 | ::-webkit-scrollbar-thumb {
60 | background-color: var(--theme-border-color);
61 | border-radius: 20px;
62 | border: transparent;
63 | }
64 | ::-webkit-scrollbar-corner {
65 | background: transparent;
66 | }
67 | }
68 |
69 | .chat-sidebar.collapsed {
70 | min-width: 60px;
71 | width: 60px;
72 | }
73 |
74 | .chat-sidebar:hover,
75 | .chat-sidebar:not(.collapsed) {
76 | min-width: 250px;
77 | width: 250px;
78 | }
79 |
80 | .chat-sidebar-button-group {
81 | display: flex;
82 | flex-direction: column;
83 | padding: 0;
84 | background-color: var(--theme-color);
85 | gap: 15px;
86 | }
87 |
88 | .chat-list {
89 | display: flex;
90 | flex-direction: column;
91 | flex-grow: 1;
92 | padding: 0 2px 0 0;
93 | background-color: var(--theme-color);
94 | overflow-y: auto;
95 | overflow-x: hidden;
96 | gap: 15px;
97 | }
98 |
99 | .chat-content {
100 | flex-grow: 1;
101 | border: 1px solid var(--theme-border-color);
102 | background-color: var(--theme-color);
103 | }
104 |
105 | .normal-button {
106 | width: 100%;
107 | min-height: 40px;
108 | padding: 1px 6px;
109 | border: 1px solid;
110 | border-color: var(--theme-border-color);
111 | background-color: var(--theme-color);
112 | color: var(--font-color);
113 | border-radius: 5px;
114 | cursor: pointer;
115 | white-space: nowrap;
116 | text-overflow: ellipsis;
117 | overflow: hidden;
118 | }
119 |
120 | .gpt-util-group {
121 | display: flex;
122 | gap: 15px;
123 | align-items: center;
124 | }
125 |
126 | .gpt-util-icon {
127 | display: flex;
128 | cursor: pointer;
129 | align-items: center;
130 | color: var(--font-color);
131 | }
132 | .gpt-util-icon:hover {
133 | color: var(--font-active-color);
134 | }
135 |
136 | .normal-button.active,
137 | .normal-button:hover {
138 | background-color: var(--active-color);
139 | }
140 |
141 | hr {
142 | height: 1px;
143 | background-color: var(--theme-border-color);
144 | border: none;
145 | margin: 15px 0;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/services/apis/bing-web.mjs:
--------------------------------------------------------------------------------
1 | import BingAIClient from '../clients/bing/index.mjs'
2 | import { getUserConfig } from '../../config/index.mjs'
3 | import { pushRecord, setAbortController } from './shared.mjs'
4 | import { getModelValue } from '../../utils/model-name-convert.mjs'
5 |
6 | /**
7 | * @param {Runtime.Port} port
8 | * @param {string} question
9 | * @param {Session} session
10 | * @param {string} accessToken
11 | * @param {boolean} sydneyMode
12 | */
13 | export async function generateAnswersWithBingWebApi(
14 | port,
15 | question,
16 | session,
17 | accessToken,
18 | sydneyMode = false,
19 | ) {
20 | const { controller, messageListener, disconnectListener } = setAbortController(port)
21 | const config = await getUserConfig()
22 | let modelMode = getModelValue(session)
23 | if (!modelMode) modelMode = config.modelMode
24 |
25 | console.debug('mode', modelMode)
26 |
27 | const bingAIClient = new BingAIClient({ userToken: accessToken, features: { genImage: false } })
28 | if (session.bingWeb_jailbreakConversationCache)
29 | bingAIClient.conversationsCache.set(
30 | session.bingWeb_jailbreakConversationId,
31 | session.bingWeb_jailbreakConversationCache,
32 | )
33 |
34 | let answer = ''
35 | const response = await bingAIClient
36 | .sendMessage(question, {
37 | abortController: controller,
38 | toneStyle: modelMode,
39 | jailbreakConversationId: sydneyMode,
40 | onProgress: (message) => {
41 | answer = message
42 | // reference markers [^number^]
43 | answer = answer.replaceAll(/\[\^(\d+)\^\]/g, '$1 ')
44 | port.postMessage({ answer: answer, done: false, session: null })
45 | },
46 | ...(session.bingWeb_conversationId
47 | ? {
48 | conversationId: session.bingWeb_conversationId,
49 | encryptedConversationSignature: session.bingWeb_encryptedConversationSignature,
50 | clientId: session.bingWeb_clientId,
51 | invocationId: session.bingWeb_invocationId,
52 | }
53 | : session.bingWeb_jailbreakConversationId
54 | ? {
55 | jailbreakConversationId: session.bingWeb_jailbreakConversationId,
56 | parentMessageId: session.bingWeb_parentMessageId,
57 | }
58 | : {}),
59 | })
60 | .catch((err) => {
61 | port.onMessage.removeListener(messageListener)
62 | port.onDisconnect.removeListener(disconnectListener)
63 | throw err
64 | })
65 |
66 | if (!sydneyMode) {
67 | session.bingWeb_encryptedConversationSignature = response.encryptedConversationSignature
68 | session.bingWeb_conversationId = response.conversationId
69 | session.bingWeb_clientId = response.clientId
70 | session.bingWeb_invocationId = response.invocationId
71 | } else {
72 | session.bingWeb_jailbreakConversationId = response.jailbreakConversationId
73 | session.bingWeb_parentMessageId = response.messageId
74 | session.bingWeb_jailbreakConversationCache = bingAIClient.conversationsCache.get(
75 | response.jailbreakConversationId,
76 | )
77 | }
78 |
79 | if (response.details.sourceAttributions && response.details.sourceAttributions.length > 0) {
80 | const footnotes =
81 | '\n\\-\n' +
82 | response.details.sourceAttributions
83 | .map((attr, index) => `\\[${index + 1}]: [${attr.providerDisplayName}](${attr.seeMoreUrl})`)
84 | .join('\n')
85 | answer += footnotes
86 | }
87 |
88 | pushRecord(session, question, answer)
89 | console.debug('conversation history', { content: session.conversationRecords })
90 | port.onMessage.removeListener(messageListener)
91 | port.onDisconnect.removeListener(disconnectListener)
92 | port.postMessage({ answer: answer, done: true, session: session })
93 | }
94 |
--------------------------------------------------------------------------------
/src/services/apis/custom-api.mjs:
--------------------------------------------------------------------------------
1 | // custom api version
2 |
3 | // There is a lot of duplicated code here, but it is very easy to refactor.
4 | // The current state is mainly convenient for making targeted changes at any time,
5 | // and it has not yet had a negative impact on maintenance.
6 | // If necessary, I will refactor.
7 |
8 | import { getUserConfig } from '../../config/index.mjs'
9 | import { fetchSSE } from '../../utils/fetch-sse.mjs'
10 | import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
11 | import { isEmpty } from 'lodash-es'
12 | import { pushRecord, setAbortController } from './shared.mjs'
13 |
14 | /**
15 | * @param {Browser.Runtime.Port} port
16 | * @param {string} question
17 | * @param {Session} session
18 | * @param {string} apiUrl
19 | * @param {string} apiKey
20 | * @param {string} modelName
21 | */
22 | export async function generateAnswersWithCustomApi(
23 | port,
24 | question,
25 | session,
26 | apiUrl,
27 | apiKey,
28 | modelName,
29 | ) {
30 | const { controller, messageListener, disconnectListener } = setAbortController(port)
31 |
32 | const config = await getUserConfig()
33 | const prompt = getConversationPairs(
34 | session.conversationRecords.slice(-config.maxConversationContextLength),
35 | false,
36 | )
37 | prompt.push({ role: 'user', content: question })
38 |
39 | let answer = ''
40 | let finished = false
41 | const finish = () => {
42 | finished = true
43 | pushRecord(session, question, answer)
44 | console.debug('conversation history', { content: session.conversationRecords })
45 | port.postMessage({ answer: null, done: true, session: session })
46 | }
47 | await fetchSSE(apiUrl, {
48 | method: 'POST',
49 | signal: controller.signal,
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | Authorization: `Bearer ${apiKey}`,
53 | },
54 | body: JSON.stringify({
55 | messages: prompt,
56 | model: modelName,
57 | stream: true,
58 | max_tokens: config.maxResponseTokenLength,
59 | temperature: config.temperature,
60 | }),
61 | onMessage(message) {
62 | console.debug('sse message', message)
63 | if (finished) return
64 | if (message.trim() === '[DONE]') {
65 | finish()
66 | return
67 | }
68 | let data
69 | try {
70 | data = JSON.parse(message)
71 | } catch (error) {
72 | console.debug('json error', error)
73 | return
74 | }
75 |
76 | if (data.response) answer = data.response
77 | else {
78 | const delta = data.choices[0]?.delta?.content
79 | const content = data.choices[0]?.message?.content
80 | const text = data.choices[0]?.text
81 | if (delta !== undefined) {
82 | answer += delta
83 | } else if (content) {
84 | answer = content
85 | } else if (text) {
86 | answer += text
87 | }
88 | }
89 | port.postMessage({ answer: answer, done: false, session: null })
90 |
91 | if (data.choices[0]?.finish_reason) {
92 | finish()
93 | return
94 | }
95 | },
96 | async onStart() {},
97 | async onEnd() {
98 | port.postMessage({ done: true })
99 | port.onMessage.removeListener(messageListener)
100 | port.onDisconnect.removeListener(disconnectListener)
101 | },
102 | async onError(resp) {
103 | port.onMessage.removeListener(messageListener)
104 | port.onDisconnect.removeListener(disconnectListener)
105 | if (resp instanceof Error) throw resp
106 | const error = await resp.json().catch(() => ({}))
107 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
108 | },
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/InputBox/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { isFirefox, isMobile, isSafari, updateRefHeight } from '../../utils'
4 | import { useTranslation } from 'react-i18next'
5 | import { getUserConfig } from '../../config/index.mjs'
6 |
7 | export function InputBox({ onSubmit, enabled, postMessage, reverseResizeDir }) {
8 | const { t } = useTranslation()
9 | const [value, setValue] = useState('')
10 | const reverseDivRef = useRef(null)
11 | const inputRef = useRef(null)
12 | const resizedRef = useRef(false)
13 | const [internalReverseResizeDir, setInternalReverseResizeDir] = useState(reverseResizeDir)
14 |
15 | useEffect(() => {
16 | setInternalReverseResizeDir(
17 | !isSafari() && !isFirefox() && !isMobile() ? internalReverseResizeDir : false,
18 | )
19 | }, [])
20 |
21 | const virtualInputRef = internalReverseResizeDir ? reverseDivRef : inputRef
22 |
23 | useEffect(() => {
24 | inputRef.current.focus()
25 |
26 | const onResizeY = () => {
27 | if (virtualInputRef.current.h !== virtualInputRef.current.offsetHeight) {
28 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
29 | if (!resizedRef.current) {
30 | resizedRef.current = true
31 | virtualInputRef.current.style.maxHeight = ''
32 | }
33 | }
34 | }
35 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
36 | virtualInputRef.current.addEventListener('mousemove', onResizeY)
37 | }, [])
38 |
39 | useEffect(() => {
40 | if (!resizedRef.current) {
41 | if (!internalReverseResizeDir) {
42 | updateRefHeight(inputRef)
43 | virtualInputRef.current.h = virtualInputRef.current.offsetHeight
44 | virtualInputRef.current.style.maxHeight = '160px'
45 | }
46 | }
47 | })
48 |
49 | useEffect(() => {
50 | if (enabled)
51 | getUserConfig().then((config) => {
52 | if (config.focusAfterAnswer) inputRef.current.focus()
53 | })
54 | }, [enabled])
55 |
56 | const handleKeyDownOrClick = (e) => {
57 | e.stopPropagation()
58 | if (e.type === 'click' || (e.keyCode === 13 && e.shiftKey === false)) {
59 | e.preventDefault()
60 | if (enabled) {
61 | if (!value) return
62 | onSubmit(value)
63 | setValue('')
64 | } else {
65 | postMessage({ stop: true })
66 | }
67 | }
68 | }
69 |
70 | return (
71 |
72 |
83 |
103 |
110 | {enabled ? t('Ask') : t('Stop')}
111 |
112 |
113 | )
114 | }
115 |
116 | InputBox.propTypes = {
117 | onSubmit: PropTypes.func.isRequired,
118 | enabled: PropTypes.bool.isRequired,
119 | reverseResizeDir: PropTypes.bool,
120 | postMessage: PropTypes.func.isRequired,
121 | }
122 |
123 | export default InputBox
124 |
--------------------------------------------------------------------------------
/src/utils/crop-text.mjs:
--------------------------------------------------------------------------------
1 | // MIT License
2 | //
3 | // Copyright (c) 2023 josStorer
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { encode } from '@nem035/gpt-3-encoder'
24 | import { getUserConfig } from '../config/index.mjs'
25 | import { apiModeToModelName, modelNameToDesc } from './model-name-convert.mjs'
26 |
27 | const clamp = (v, min, max) => {
28 | return Math.min(Math.max(v, min), max)
29 | }
30 |
31 | export async function cropText(
32 | text,
33 | maxLength = 8000,
34 | startLength = 800,
35 | endLength = 600,
36 | tiktoken = true,
37 | ) {
38 | const userConfig = await getUserConfig()
39 | if (!userConfig.cropText) return text
40 |
41 | const k = modelNameToDesc(
42 | userConfig.apiMode ? apiModeToModelName(userConfig.apiMode) : userConfig.modelName,
43 | null,
44 | userConfig.customModelName,
45 | ).match(/[- (]*([0-9]+)k/)?.[1]
46 | if (k) {
47 | maxLength = Number(k) * 1000
48 | maxLength -= 100 + clamp(userConfig.maxResponseTokenLength, 1, maxLength - 2000)
49 | } else {
50 | maxLength -= 100 + clamp(userConfig.maxResponseTokenLength, 1, maxLength - 2000)
51 | }
52 |
53 | const splits = text.split(/[,,。??!!;;]/).map((s) => s.trim())
54 | const splitsLength = splits.map((s) => (tiktoken ? encode(s).length : s.length))
55 | const length = splitsLength.reduce((sum, length) => sum + length, 0)
56 |
57 | const cropLength = length - startLength - endLength
58 | const cropTargetLength = maxLength - startLength - endLength
59 | const cropPercentage = cropTargetLength / cropLength
60 | const cropStep = Math.max(0, 1 / cropPercentage - 1)
61 |
62 | if (cropStep === 0) return text
63 |
64 | let croppedText = ''
65 | let currentLength = 0
66 | let currentIndex = 0
67 | let currentStep = 0
68 |
69 | for (; currentIndex < splits.length; currentIndex++) {
70 | if (currentLength + splitsLength[currentIndex] + 1 <= startLength) {
71 | croppedText += splits[currentIndex] + ','
72 | currentLength += splitsLength[currentIndex] + 1
73 | } else if (currentLength + splitsLength[currentIndex] + 1 + endLength <= maxLength) {
74 | if (currentStep < cropStep) {
75 | currentStep++
76 | } else {
77 | croppedText += splits[currentIndex] + ','
78 | currentLength += splitsLength[currentIndex] + 1
79 | currentStep = currentStep - cropStep
80 | }
81 | } else {
82 | break
83 | }
84 | }
85 |
86 | let endPart = ''
87 | let endPartLength = 0
88 | for (let i = splits.length - 1; endPartLength + splitsLength[i] <= endLength; i--) {
89 | endPart = splits[i] + ',' + endPart
90 | endPartLength += splitsLength[i] + 1
91 | }
92 | currentLength += endPartLength
93 | croppedText += endPart
94 |
95 | console.log(
96 | `input maxLength: ${maxLength}\n` +
97 | `maxResponseTokenLength: ${userConfig.maxResponseTokenLength}\n` +
98 | // `croppedTextLength: ${tiktoken ? encode(croppedText).length : croppedText.length}\n` +
99 | `desiredLength: ${currentLength}\n` +
100 | `content: ${croppedText}`,
101 | )
102 | return croppedText
103 | }
104 |
--------------------------------------------------------------------------------
/CURRENT_CHANGE.md:
--------------------------------------------------------------------------------
1 | Long time no see — ChatGPTBox is back! Nearly every feature that had broken due to page updates or API changes has been fixed, and we’ve also introduced some new features.
2 |
3 | Over the past year the LLM landscape has shifted dramatically, and the key players are now fairly clear. Regarding ChatGPTBox’s free web APIs, some providers are still actively trying to block reverse-engineering, while others remain open. At the moment, ChatGPT, Claude, and Kimi are still open, so we’ll keep maintaining the related web free APIs. The web APIs for Bing and Gemini, however, will no longer be supported; if you need some reverse-engineering web apis, please check out the work of this organization: https://github.com/LLM-Red-Team.
4 |
5 | As OpenRouter has consistently offered stable and affordable APIs, we’ve now added direct option support for it — no need to rely on custom mode and manually fill in the API URL.
6 |
7 | During this period, countless AI projects have exploded onto the scene and just as many have quietly disappeared. I’ve been tied up with various non-public projects and have neglected ChatGPTBox, while also pondering how to keep it vibrant.
8 |
9 | I have to admit that when ChatGPTBox was first created, many decisions and code designs were rather hasty and not very modern. Without much forethought, I made choices that now make it inconvenient to add new features.
10 |
11 | I’m currently rewriting ChatGPTBox from scratch using the WXT framework while ensuring full backward compatibility with old data. This will take a considerable amount of time, but I’ll keep pushing forward. I also have some commercialization ideas for ChatGPTBox; of course only server-related features would be charged, while all web APIs and user Api Key features will remain completely free, and the project will stay open-source under the MIT license.
12 |
13 | As I’m simultaneously in charge of several other non-public projects, I can’t promise when the rewrite will be finished, but I’ll keep making steady progress. In the meantime, I’ll continue to fix major issues in the current version of ChatGPTBox.
14 |
15 | ## Changes
16 |
17 | ### Features
18 |
19 | - add support for openRouter, AI/ML and DeepSeek api (previously required filling in the URL via the custom model option)
20 | - a new option has been added to the general settings to disable cropText, ensuring the full input tokens are always passed. This can improve summarization on sites like YouTube, but note that you should only disable cropText when using a model with a sufficiently long context.
21 | -
22 | - reasoning model renderer support
23 | -
24 |
25 | ### Improvements
26 | - add a range of new models recently made available by various AI providers
27 | - significantly improve the prompt templates for built-in tools. Great thanks to @PeterDaveHello
28 | - update and enhance API clients (including Claude, ChatGLM, and Kimi.Moonshot) that had become unavailable or unstable due to recent policy changes and adjustments by AI providers
29 | - increase the default input and response limits, as current LLMs generally support longer contexts
30 | - improve kimi.moonshot support and add more available models like k2, kimi-latest, k1.5, k1.5-thinking
31 | - improve google search sidebar
32 |
33 | ### Fixes
34 | - fix the issue where YouTube subtitles could not be fetched and the video summarization feature became unavailable due to the recent introduction of the "pot" parameter by YouTube
35 | - avoid crash when readability parser returns null (#865) @PeterDaveHello
36 | - fix the issue where kimi web functionality became unstable due to changes in the page and domain
37 | - fix an issue where the selected model might be not displayed correctly due to inconsistent key ordering in JSON.stringify
38 | - fix the issue of abnormal subtitle retrieval caused by changes to Bilibili API
39 |
40 | ### Chores
41 | - update adapters support for startpage, kagi, naver, wechat, juejin
42 | - update dependencies to mitigate security vulnerabilities @PeterDaveHello
43 | - update default configs
44 | - since ChatGPT has relaxed the web API request restrictions, it is no longer necessary to simulate input to retrieve data (#869)
45 | - update verify-search-engine-configs.mjs
46 |
--------------------------------------------------------------------------------
/src/utils/eventsource-parser.mjs:
--------------------------------------------------------------------------------
1 | // https://www.npmjs.com/package/eventsource-parser/v/1.1.1
2 |
3 | function createParser(onParse) {
4 | let isFirstChunk
5 | let bytes
6 | let buffer
7 | let startingPosition
8 | let startingFieldLength
9 | let eventId
10 | let eventName
11 | let data
12 | let extra
13 | reset()
14 | return {
15 | feed,
16 | reset,
17 | }
18 | function reset() {
19 | isFirstChunk = true
20 | bytes = []
21 | buffer = ''
22 | startingPosition = 0
23 | startingFieldLength = -1
24 | eventId = void 0
25 | eventName = void 0
26 | data = ''
27 | }
28 |
29 | function feed(chunk) {
30 | bytes = bytes.concat(Array.from(chunk))
31 | buffer = new TextDecoder().decode(new Uint8Array(bytes))
32 | if (isFirstChunk && hasBom(buffer)) {
33 | buffer = buffer.slice(BOM.length)
34 | }
35 | isFirstChunk = false
36 | const length = buffer.length
37 | let position = 0
38 | let discardTrailingNewline = false
39 | while (position < length) {
40 | if (discardTrailingNewline) {
41 | if (buffer[position] === '\n') {
42 | ++position
43 | }
44 | discardTrailingNewline = false
45 | }
46 | let lineLength = -1
47 | let fieldLength = startingFieldLength
48 | let character
49 | for (let index = startingPosition; lineLength < 0 && index < length; ++index) {
50 | character = buffer[index]
51 | if (character === ':' && fieldLength < 0) {
52 | fieldLength = index - position
53 | } else if (character === '\r') {
54 | discardTrailingNewline = true
55 | lineLength = index - position
56 | } else if (character === '\n') {
57 | lineLength = index - position
58 | }
59 | }
60 | if (lineLength < 0) {
61 | startingPosition = length - position
62 | startingFieldLength = fieldLength
63 | break
64 | } else {
65 | startingPosition = 0
66 | startingFieldLength = -1
67 | }
68 | parseEventStreamLine(buffer, position, fieldLength, lineLength)
69 | position += lineLength + 1
70 | }
71 | if (position === length) {
72 | bytes = []
73 | buffer = ''
74 | } else if (position > 0) {
75 | bytes = bytes.slice(new TextEncoder().encode(buffer.slice(0, position)).length)
76 | buffer = buffer.slice(position)
77 | }
78 | }
79 |
80 | function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
81 | if (lineLength === 0) {
82 | if (data.length > 0 || extra) {
83 | onParse({
84 | type: 'event',
85 | id: eventId,
86 | event: eventName || void 0,
87 | data: data.slice(0, -1),
88 | extra: extra || void 0,
89 | // remove trailing newline
90 | })
91 |
92 | data = ''
93 | eventId = void 0
94 | extra = void 0
95 | }
96 | eventName = void 0
97 | return
98 | }
99 | const noValue = fieldLength < 0
100 | const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength))
101 | let step = 0
102 | if (noValue) {
103 | step = lineLength
104 | } else if (lineBuffer[index + fieldLength + 1] === ' ') {
105 | step = fieldLength + 2
106 | } else {
107 | step = fieldLength + 1
108 | }
109 | const position = index + step
110 | const valueLength = lineLength - step
111 | const value = lineBuffer.slice(position, position + valueLength).toString()
112 | if (field === 'data') {
113 | data += value ? ''.concat(value, '\n') : '\n'
114 | } else if (field === 'event') {
115 | eventName = value
116 | } else if (field === 'id' && !value.includes('\0')) {
117 | eventId = value
118 | } else if (field === 'retry') {
119 | const retry = parseInt(value, 10)
120 | if (!Number.isNaN(retry)) {
121 | onParse({
122 | type: 'reconnect-interval',
123 | value: retry,
124 | })
125 | }
126 | } else if (field === 'meta') {
127 | const str = `{"${field}":${value}}`
128 | extra = extra ?? []
129 | extra.push(JSON.parse(str))
130 | }
131 | }
132 | }
133 | const BOM = [239, 187, 191]
134 | function hasBom(buffer) {
135 | return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode)
136 | }
137 | export { createParser }
138 |
--------------------------------------------------------------------------------
/src/content-script/selection-tools/index.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | CardHeading,
3 | CardList,
4 | EmojiSmile,
5 | Palette,
6 | QuestionCircle,
7 | Translate,
8 | Braces,
9 | Globe,
10 | ChatText,
11 | } from 'react-bootstrap-icons'
12 | import { getPreferredLanguage } from '../../config/language.mjs'
13 |
14 | const createGenPrompt =
15 | ({
16 | message = '',
17 | isTranslation = false,
18 | targetLanguage = '',
19 | enableBidirectional = false,
20 | includeLanguagePrefix = false,
21 | }) =>
22 | async (selection) => {
23 | let preferredLanguage = targetLanguage
24 |
25 | if (!preferredLanguage) {
26 | preferredLanguage = await getPreferredLanguage()
27 | }
28 |
29 | let fullMessage = isTranslation
30 | ? `You are a professional translator. Translate the following text into ${preferredLanguage}, preserving meaning, tone, and formatting. Only provide the translated result.`
31 | : message
32 | if (enableBidirectional) {
33 | fullMessage += ` If the text is already in ${preferredLanguage}, translate it into English instead following the same requirements. Only provide the translated result.`
34 | }
35 | const prefix = includeLanguagePrefix ? `Reply in ${preferredLanguage}.` : ''
36 | return `${prefix}${fullMessage}:\n'''\n${selection}\n'''`
37 | }
38 |
39 | export const config = {
40 | explain: {
41 | icon: ,
42 | label: 'Explain',
43 | genPrompt: createGenPrompt({
44 | message:
45 | 'You are an expert teacher. Explain the following content in simple terms and highlight the key points',
46 | includeLanguagePrefix: true,
47 | }),
48 | },
49 | translate: {
50 | icon: ,
51 | label: 'Translate',
52 | genPrompt: createGenPrompt({
53 | isTranslation: true,
54 | }),
55 | },
56 | translateToEn: {
57 | icon: ,
58 | label: 'Translate (To English)',
59 | genPrompt: createGenPrompt({
60 | isTranslation: true,
61 | targetLanguage: 'English',
62 | }),
63 | },
64 | translateToZh: {
65 | icon: ,
66 | label: 'Translate (To Chinese)',
67 | genPrompt: createGenPrompt({
68 | isTranslation: true,
69 | targetLanguage: 'Chinese',
70 | }),
71 | },
72 | translateBidi: {
73 | icon: ,
74 | label: 'Translate (Bidirectional)',
75 | genPrompt: createGenPrompt({
76 | isTranslation: true,
77 | enableBidirectional: true,
78 | }),
79 | },
80 | summary: {
81 | icon: ,
82 | label: 'Summary',
83 | genPrompt: createGenPrompt({
84 | message:
85 | 'You are a professional summarizer. Summarize the following content in a few sentences, focusing on the key points',
86 | includeLanguagePrefix: true,
87 | }),
88 | },
89 | polish: {
90 | icon: ,
91 | label: 'Polish',
92 | genPrompt: createGenPrompt({
93 | message:
94 | 'Act as a skilled editor. Correct grammar and word choice in the following text, improve readability and flow while preserving the original meaning, and return only the polished version',
95 | }),
96 | },
97 | sentiment: {
98 | icon: ,
99 | label: 'Sentiment Analysis',
100 | genPrompt: createGenPrompt({
101 | message:
102 | 'You are an expert in sentiment analysis. Analyze the following content and provide a brief summary of the overall emotional tone, labeling it with a short descriptive word or phrase',
103 | includeLanguagePrefix: true,
104 | }),
105 | },
106 | divide: {
107 | icon: ,
108 | label: 'Divide Paragraphs',
109 | genPrompt: createGenPrompt({
110 | message:
111 | 'You are a skilled editor. Divide the following text into clear, easy-to-read and easy-to-understand paragraphs',
112 | }),
113 | },
114 | code: {
115 | icon: ,
116 | label: 'Code Explain',
117 | genPrompt: createGenPrompt({
118 | message:
119 | 'You are a senior software engineer and system architect. Break down the following code step by step, explain how each part works and why it was designed that way, note any potential issues, and summarize the overall purpose',
120 | includeLanguagePrefix: true,
121 | }),
122 | },
123 | ask: {
124 | icon: ,
125 | label: 'Ask',
126 | genPrompt: createGenPrompt({
127 | message:
128 | 'Analyze the following content carefully and provide a concise answer or opinion with a short explanation',
129 | includeLanguagePrefix: true,
130 | }),
131 | },
132 | }
133 |
--------------------------------------------------------------------------------
/src/components/DecisionCard/index.jsx:
--------------------------------------------------------------------------------
1 | import { LightBulbIcon, SearchIcon } from '@primer/octicons-react'
2 | import { useState, useEffect } from 'react'
3 | import PropTypes from 'prop-types'
4 | import ConversationCard from '../ConversationCard'
5 | import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils'
6 | import { useTranslation } from 'react-i18next'
7 | import { useConfig } from '../../hooks/use-config.mjs'
8 |
9 | function DecisionCard(props) {
10 | const { t } = useTranslation()
11 | const [triggered, setTriggered] = useState(false)
12 | const [render, setRender] = useState(false)
13 | const config = useConfig(() => {
14 | setRender(true)
15 | })
16 |
17 | const question = props.question
18 |
19 | const updatePosition = () => {
20 | if (!render) return
21 |
22 | const container = props.container
23 | const siteConfig = props.siteConfig
24 | container.classList.remove('chatgptbox-sidebar-free')
25 |
26 | if (config.appendQuery) {
27 | const appendContainer = getPossibleElementByQuerySelector([config.appendQuery])
28 | if (appendContainer) {
29 | appendContainer.appendChild(container)
30 | return
31 | }
32 | }
33 |
34 | if (config.prependQuery) {
35 | const prependContainer = getPossibleElementByQuerySelector([config.prependQuery])
36 | if (prependContainer) {
37 | prependContainer.prepend(container)
38 | return
39 | }
40 | }
41 |
42 | if (!siteConfig) return
43 |
44 | if (config.insertAtTop) {
45 | const resultsContainerQuery = getPossibleElementByQuerySelector(
46 | siteConfig.resultsContainerQuery,
47 | )
48 | if (resultsContainerQuery) resultsContainerQuery.prepend(container)
49 | } else {
50 | const sidebarContainer = getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery)
51 | if (sidebarContainer) {
52 | sidebarContainer.prepend(container)
53 | } else {
54 | const appendContainer = getPossibleElementByQuerySelector(siteConfig.appendContainerQuery)
55 | if (appendContainer) {
56 | container.classList.add('chatgptbox-sidebar-free')
57 | appendContainer.appendChild(container)
58 | } else {
59 | const resultsContainerQuery = getPossibleElementByQuerySelector(
60 | siteConfig.resultsContainerQuery,
61 | )
62 | if (resultsContainerQuery) resultsContainerQuery.prepend(container)
63 | }
64 | }
65 | }
66 | }
67 |
68 | useEffect(() => updatePosition(), [config])
69 |
70 | return (
71 | render && (
72 |
73 | {(() => {
74 | if (question)
75 | switch (config.triggerMode) {
76 | case 'always':
77 | return
78 | case 'manually':
79 | if (triggered) {
80 | return
81 | }
82 | return (
83 |
setTriggered(true)}>
84 |
85 | {t('Ask ChatGPT')}
86 |
87 |
88 | )
89 | case 'questionMark':
90 | if (endsWithQuestionMark(question.trim())) {
91 | return
92 | }
93 | if (triggered) {
94 | return
95 | }
96 | return (
97 |
setTriggered(true)}>
98 |
99 | {t('Ask ChatGPT')}
100 |
101 |
102 | )
103 | }
104 | else
105 | return (
106 |
107 |
108 | {t('No Input Found')}
109 |
110 |
111 | )
112 | })()}
113 |
114 | )
115 | )
116 | }
117 |
118 | DecisionCard.propTypes = {
119 | session: PropTypes.object.isRequired,
120 | question: PropTypes.string.isRequired,
121 | siteConfig: PropTypes.object.isRequired,
122 | container: PropTypes.object.isRequired,
123 | }
124 |
125 | export default DecisionCard
126 |
--------------------------------------------------------------------------------
/src/popup/Popup.jsx:
--------------------------------------------------------------------------------
1 | import '@picocss/pico'
2 | import { useEffect, useState } from 'react'
3 | import {
4 | defaultConfig,
5 | getPreferredLanguageKey,
6 | getUserConfig,
7 | setUserConfig,
8 | } from '../config/index.mjs'
9 | import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
10 | import 'react-tabs/style/react-tabs.css'
11 | import './styles.scss'
12 | import { MarkGithubIcon } from '@primer/octicons-react'
13 | import Browser from 'webextension-polyfill'
14 | import { useWindowTheme } from '../hooks/use-window-theme.mjs'
15 | import { isMobile } from '../utils/index.mjs'
16 | import { useTranslation } from 'react-i18next'
17 | import { GeneralPart } from './sections/GeneralPart'
18 | import { FeaturePages } from './sections/FeaturePages'
19 | import { AdvancedPart } from './sections/AdvancedPart'
20 | import { ModulesPart } from './sections/ModulesPart'
21 |
22 | // eslint-disable-next-line react/prop-types
23 | function Footer({ currentVersion, latestVersion }) {
24 | const { t } = useTranslation()
25 |
26 | return (
27 |
28 |
29 | {`${t('Current Version')}: ${currentVersion} `}
30 | {currentVersion >= latestVersion ? (
31 | `(${t('Latest')})`
32 | ) : (
33 | <>
34 | ({`${t('Latest')}: `}
35 |
40 | {latestVersion}
41 |
42 | )
43 | >
44 | )}
45 |
46 |
56 |
57 | )
58 | }
59 |
60 | function Popup() {
61 | const { t, i18n } = useTranslation()
62 | const [config, setConfig] = useState(defaultConfig)
63 | const [currentVersion, setCurrentVersion] = useState('')
64 | const [latestVersion, setLatestVersion] = useState('')
65 | const [tabIndex, setTabIndex] = useState(0)
66 | const theme = useWindowTheme()
67 |
68 | const updateConfig = async (value) => {
69 | setConfig({ ...config, ...value })
70 | await setUserConfig(value)
71 | }
72 |
73 | useEffect(() => {
74 | getPreferredLanguageKey().then((lang) => {
75 | i18n.changeLanguage(lang)
76 | })
77 | getUserConfig().then((config) => {
78 | setConfig(config)
79 | setCurrentVersion(Browser.runtime.getManifest().version.replace('v', ''))
80 | fetch('https://api.github.com/repos/josstorer/chatGPTBox/releases/latest').then((response) =>
81 | response.json().then((data) => {
82 | setLatestVersion(data.tag_name.replace('v', ''))
83 | }),
84 | )
85 | })
86 | }, [])
87 |
88 | useEffect(() => {
89 | document.documentElement.dataset.theme = config.themeMode === 'auto' ? theme : config.themeMode
90 | }, [config.themeMode, theme])
91 |
92 | const search = new URLSearchParams(window.location.search)
93 | const popup = !isMobile() && search.get('popup') // manifest v2
94 |
95 | return (
96 |
129 | )
130 | }
131 |
132 | export default Popup
133 |
--------------------------------------------------------------------------------
/src/services/clients/bard/index.mjs:
--------------------------------------------------------------------------------
1 | // https://github.com/PawanOsman/GoogleBard
2 |
3 | export default class Bard {
4 | cookies = ''
5 |
6 | constructor(cookies) {
7 | this.cookies = cookies
8 | }
9 |
10 | ParseResponse(text) {
11 | let resData = {
12 | r: '',
13 | c: '',
14 | rc: '',
15 | responses: [],
16 | }
17 | try {
18 | let parseData = (data) => {
19 | if (typeof data === 'string') {
20 | if (data?.startsWith('c_')) {
21 | resData.c = data
22 | return
23 | }
24 | if (data?.startsWith('r_')) {
25 | resData.r = data
26 | return
27 | }
28 | if (data?.startsWith('rc_')) {
29 | resData.rc = data
30 | return
31 | }
32 | resData.responses.push(data)
33 | }
34 | if (Array.isArray(data)) {
35 | data.forEach((item) => {
36 | parseData(item)
37 | })
38 | }
39 | }
40 | try {
41 | const lines = text.split('\n')
42 | for (let i in lines) {
43 | const line = lines[i]
44 | if (line.includes('wrb.fr')) {
45 | let data = JSON.parse(line)
46 | let responsesData = JSON.parse(data[0][2])
47 | responsesData.forEach((response) => {
48 | parseData(response)
49 | })
50 | }
51 | }
52 | } catch (e) {
53 | throw new Error(
54 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
55 | )
56 | }
57 | } catch (err) {
58 | throw new Error(
59 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
60 | )
61 | }
62 | return resData
63 | }
64 |
65 | async GetRequestParams() {
66 | try {
67 | const response = await fetch('https://gemini.google.com', {
68 | headers: {
69 | Cookie: this.cookies,
70 | },
71 | })
72 | const text = await response.text()
73 | const cfb2h = text.match(/"cfb2h":\s*"([^"]+)"/)?.[1]
74 | const SNlM0e = text.match(/"SNlM0e":\s*"([^"]+)"/)?.[1]
75 | const context = { googleData: { cfb2h, SNlM0e } }
76 | const at = context.googleData.SNlM0e
77 | const bl = context.googleData.cfb2h
78 | return { at, bl }
79 | } catch (e) {
80 | throw new Error(
81 | `Error parsing response: make sure you are using the correct cookie, copy the value of "__Secure-1PSID" cookie and set it like this: \n\nnew Bard("__Secure-1PSID=")\n\nAlso using a US proxy is recommended.\n\nIf this error persists, please open an issue on github.\nhttps://github.com/PawanOsman/GoogleBard`,
82 | )
83 | }
84 | }
85 |
86 | async ask(prompt, conversationObj) {
87 | return await this.send(prompt, conversationObj)
88 | }
89 |
90 | async send(prompt, conversationObj) {
91 | let conversation = {
92 | id: conversationObj.id || '',
93 | c: conversationObj.c || '',
94 | r: conversationObj.r || '',
95 | rc: conversationObj.rc || '',
96 | lastActive: Date.now(),
97 | }
98 | // eslint-disable-next-line
99 | try {
100 | let { at, bl } = await this.GetRequestParams()
101 | const response = await fetch(
102 | 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?' +
103 | new URLSearchParams({
104 | bl: bl,
105 | rt: 'c',
106 | _reqid: 0,
107 | }),
108 | {
109 | method: 'POST',
110 | body: new URLSearchParams({
111 | at: at,
112 | 'f.req': JSON.stringify([
113 | null,
114 | `[[${JSON.stringify(prompt)}],null,${JSON.stringify([
115 | conversation.c,
116 | conversation.r,
117 | conversation.rc,
118 | ])}]`,
119 | ]),
120 | }),
121 | headers: {
122 | Cookie: this.cookies,
123 | },
124 | },
125 | )
126 | const data = await response.text()
127 | let parsedResponse = this.ParseResponse(data)
128 | conversation.c = parsedResponse.c
129 | conversation.r = parsedResponse.r
130 | conversation.rc = parsedResponse.rc
131 | const conversationObj = { c: conversation.c, r: conversation.r, rc: conversation.rc }
132 | return { answer: parsedResponse.responses[3], conversationObj: conversationObj }
133 | } catch (e) {
134 | throw e
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/components/ConversationItem/index.jsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from 'react'
2 | import { ChevronDownIcon, XCircleIcon, SyncIcon } from '@primer/octicons-react'
3 | import CopyButton from '../CopyButton'
4 | import ReadButton from '../ReadButton'
5 | import PropTypes from 'prop-types'
6 | import MarkdownRender from '../MarkdownRender/markdown.jsx'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | function AnswerTitle({ descName }) {
10 | const { t } = useTranslation()
11 |
12 | return {descName ? `${descName}:` : t('Loading...')}
13 | }
14 |
15 | AnswerTitle.propTypes = {
16 | descName: PropTypes.string,
17 | }
18 |
19 | export function ConversationItem({ type, content, descName, onRetry }) {
20 | const { t } = useTranslation()
21 | const [collapsed, setCollapsed] = useState(false)
22 |
23 | switch (type) {
24 | case 'question':
25 | return (
26 |
27 |
28 |
{t('You')}:
29 |
30 | content.replace(/\n $/, '')} size={14} />
31 | content} size={14} />
32 | {!collapsed ? (
33 | setCollapsed(true)}
37 | >
38 |
39 |
40 | ) : (
41 | setCollapsed(false)}
45 | >
46 |
47 |
48 | )}
49 |
50 |
51 | {!collapsed &&
{content} }
52 |
53 | )
54 | case 'answer':
55 | return (
56 |
57 |
58 |
59 |
60 | {onRetry && (
61 |
62 |
63 |
64 | )}
65 | {descName && (
66 | content.replace(/\n $/, '')} size={14} />
67 | )}
68 | {descName && content} size={14} />}
69 | {!collapsed ? (
70 | setCollapsed(true)}
74 | >
75 |
76 |
77 | ) : (
78 | setCollapsed(false)}
82 | >
83 |
84 |
85 | )}
86 |
87 |
88 | {!collapsed &&
{content} }
89 |
90 | )
91 | case 'error':
92 | return (
93 |
94 |
95 |
{t('Error')}:
96 |
97 | {onRetry && (
98 |
99 |
100 |
101 | )}
102 | content.replace(/\n $/, '')} size={14} />
103 | {!collapsed ? (
104 | setCollapsed(true)}
108 | >
109 |
110 |
111 | ) : (
112 | setCollapsed(false)}
116 | >
117 |
118 |
119 | )}
120 |
121 |
122 | {!collapsed &&
{content} }
123 |
124 | )
125 | }
126 | }
127 |
128 | ConversationItem.propTypes = {
129 | type: PropTypes.oneOf(['question', 'answer', 'error']).isRequired,
130 | content: PropTypes.string.isRequired,
131 | descName: PropTypes.string,
132 | onRetry: PropTypes.func,
133 | }
134 |
135 | export default memo(ConversationItem)
136 |
--------------------------------------------------------------------------------
/src/services/wrappers.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | clearOldAccessToken,
3 | getUserConfig,
4 | isUsingBingWebModel,
5 | isUsingClaudeWebModel,
6 | setAccessToken,
7 | } from '../config/index.mjs'
8 | import Browser from 'webextension-polyfill'
9 | import { t } from 'i18next'
10 | import { apiModeToModelName, modelNameToDesc } from '../utils/model-name-convert.mjs'
11 |
12 | export async function getChatGptAccessToken() {
13 | await clearOldAccessToken()
14 | const userConfig = await getUserConfig()
15 | if (userConfig.accessToken) {
16 | return userConfig.accessToken
17 | } else {
18 | const cookie = (await Browser.cookies.getAll({ url: 'https://chatgpt.com/' }))
19 | .map((cookie) => {
20 | return `${cookie.name}=${cookie.value}`
21 | })
22 | .join('; ')
23 | const resp = await fetch('https://chatgpt.com/api/auth/session', {
24 | headers: {
25 | Cookie: cookie,
26 | },
27 | })
28 | if (resp.status === 403) {
29 | throw new Error('CLOUDFLARE')
30 | }
31 | const data = await resp.json().catch(() => ({}))
32 | if (!data.accessToken) {
33 | throw new Error('UNAUTHORIZED')
34 | }
35 | await setAccessToken(data.accessToken)
36 | return data.accessToken
37 | }
38 | }
39 |
40 | export async function getBingAccessToken() {
41 | return (await Browser.cookies.get({ url: 'https://bing.com/', name: '_U' }))?.value
42 | }
43 |
44 | export async function getBardCookies() {
45 | const token = (await Browser.cookies.get({ url: 'https://google.com/', name: '__Secure-1PSID' }))
46 | ?.value
47 | return '__Secure-1PSID=' + token
48 | }
49 |
50 | export async function getClaudeSessionKey() {
51 | return (await Browser.cookies.get({ url: 'https://claude.ai/', name: 'sessionKey' }))?.value
52 | }
53 |
54 | export function handlePortError(session, port, err) {
55 | console.error(err)
56 | if (err.message) {
57 | if (!err.message.includes('aborted')) {
58 | if (
59 | ['message you submitted was too long', 'maximum context length'].some((m) =>
60 | err.message.includes(m),
61 | )
62 | )
63 | port.postMessage({ error: t('Exceeded maximum context length') + '\n\n' + err.message })
64 | else if (['CaptchaChallenge', 'CAPTCHA'].some((m) => err.message.includes(m)))
65 | port.postMessage({ error: t('Bing CaptchaChallenge') + '\n\n' + err.message })
66 | else if (['exceeded your current quota'].some((m) => err.message.includes(m)))
67 | port.postMessage({ error: t('Exceeded quota') + '\n\n' + err.message })
68 | else if (['Rate limit reached'].some((m) => err.message.includes(m)))
69 | port.postMessage({ error: t('Rate limit') + '\n\n' + err.message })
70 | else if (['authentication token has expired'].some((m) => err.message.includes(m)))
71 | port.postMessage({ error: 'UNAUTHORIZED' })
72 | else if (
73 | isUsingClaudeWebModel(session) &&
74 | ['Invalid authorization', 'Session key required'].some((m) => err.message.includes(m))
75 | )
76 | port.postMessage({
77 | error: t('Please login at https://claude.ai first, and then click the retry button'),
78 | })
79 | else if (
80 | isUsingBingWebModel(session) &&
81 | ['/turing/conversation/create: failed to parse response body.'].some((m) =>
82 | err.message.includes(m),
83 | )
84 | )
85 | port.postMessage({ error: t('Please login at https://bing.com first') })
86 | else port.postMessage({ error: err.message })
87 | }
88 | } else {
89 | const errMsg = JSON.stringify(err)
90 | if (isUsingBingWebModel(session) && errMsg.includes('isTrusted'))
91 | port.postMessage({ error: t('Please login at https://bing.com first') })
92 | else port.postMessage({ error: errMsg ?? 'unknown error' })
93 | }
94 | }
95 |
96 | export function registerPortListener(executor) {
97 | Browser.runtime.onConnect.addListener((port) => {
98 | console.debug('connected')
99 | const onMessage = async (msg) => {
100 | console.debug('received msg', msg)
101 | const session = msg.session
102 | if (!session) return
103 | const config = await getUserConfig()
104 | if (!session.modelName) session.modelName = config.modelName
105 | if (!session.apiMode && session.modelName !== 'customModel') session.apiMode = config.apiMode
106 | if (!session.aiName)
107 | session.aiName = modelNameToDesc(
108 | session.apiMode ? apiModeToModelName(session.apiMode) : session.modelName,
109 | t,
110 | config.customModelName,
111 | )
112 | port.postMessage({ session })
113 | try {
114 | await executor(session, port, config)
115 | } catch (err) {
116 | handlePortError(session, port, err)
117 | }
118 | }
119 |
120 | const onDisconnect = () => {
121 | console.debug('port disconnected, remove listener')
122 | port.onMessage.removeListener(onMessage)
123 | port.onDisconnect.removeListener(onDisconnect)
124 | }
125 |
126 | port.onMessage.addListener(onMessage)
127 | port.onDisconnect.addListener(onDisconnect)
128 | })
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/markdown-without-katex.jsx:
--------------------------------------------------------------------------------
1 | import ReactMarkdown from 'react-markdown'
2 | import rehypeRaw from 'rehype-raw'
3 | import rehypeHighlight from 'rehype-highlight'
4 | import remarkGfm from 'remark-gfm'
5 | import remarkBreaks from 'remark-breaks'
6 | import { Pre } from './Pre'
7 | import { Hyperlink } from './Hyperlink'
8 | import { memo, useState } from 'react'
9 | import { useTranslation } from 'react-i18next'
10 |
11 | // eslint-disable-next-line
12 | const ThinkComponent = ({ node, children, ...props }) => {
13 | const { t } = useTranslation()
14 | const [isExpanded, setIsExpanded] = useState(true)
15 | const isEmpty =
16 | !children ||
17 | (Array.isArray(children) &&
18 | // eslint-disable-next-line
19 | (children.length === 0 ||
20 | // eslint-disable-next-line
21 | (children.length === 1 && typeof children[0] === 'string' && children[0].trim() === '')))
22 |
23 | const toggleExpanded = () => {
24 | setIsExpanded(!isExpanded)
25 | }
26 |
27 | return isEmpty ? (
28 | <>>
29 | ) : (
30 |
40 |
55 |
56 |
65 |
66 | 💭 {t('Thinking Content')}
67 |
68 |
69 |
76 | ▼
77 |
78 |
79 |
88 |
98 | {children}
99 |
100 |
101 |
107 |
108 | )
109 | }
110 |
111 | export function MarkdownRender(props) {
112 | return (
113 |
114 |
196 | {props.children.replace('', '\n\n\n\n')}
197 |
198 |
199 | )
200 | }
201 |
202 | MarkdownRender.propTypes = {
203 | ...ReactMarkdown.propTypes,
204 | }
205 |
206 | export default memo(MarkdownRender)
207 |
--------------------------------------------------------------------------------
/src/components/MarkdownRender/markdown.jsx:
--------------------------------------------------------------------------------
1 | import './mykatex.min.css'
2 | import ReactMarkdown from 'react-markdown'
3 | import rehypeRaw from 'rehype-raw'
4 | import rehypeHighlight from 'rehype-highlight'
5 | import rehypeKatex from 'rehype-katex'
6 | import remarkMath from 'remark-math'
7 | import remarkGfm from 'remark-gfm'
8 | import remarkBreaks from 'remark-breaks'
9 | import { Pre } from './Pre'
10 | import { Hyperlink } from './Hyperlink'
11 | import { memo, useState } from 'react'
12 | import { useTranslation } from 'react-i18next'
13 |
14 | // eslint-disable-next-line
15 | const ThinkComponent = ({ node, children, ...props }) => {
16 | const { t } = useTranslation()
17 | const [isExpanded, setIsExpanded] = useState(true)
18 | const isEmpty =
19 | !children ||
20 | (Array.isArray(children) &&
21 | // eslint-disable-next-line
22 | (children.length === 0 ||
23 | // eslint-disable-next-line
24 | (children.length === 1 && typeof children[0] === 'string' && children[0].trim() === '')))
25 |
26 | const toggleExpanded = () => {
27 | setIsExpanded(!isExpanded)
28 | }
29 |
30 | return isEmpty ? (
31 | <>>
32 | ) : (
33 |
43 |
58 |
59 |
68 |
69 | 💭 {t('Thinking Content')}
70 |
71 |
72 |
79 | ▼
80 |
81 |
82 |
91 |
101 | {children}
102 |
103 |
104 |
110 |
111 | )
112 | }
113 |
114 | export function MarkdownRender(props) {
115 | return (
116 |
117 |
200 | {props.children.replace('', '\n\n\n\n')}
201 |
202 |
203 | )
204 | }
205 |
206 | MarkdownRender.propTypes = {
207 | ...ReactMarkdown.propTypes,
208 | }
209 |
210 | export default memo(MarkdownRender)
211 |
--------------------------------------------------------------------------------