[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true\n  },\n  \"extends\": [\"eslint:recommended\", \"plugin:react/recommended\"],\n  \"overrides\": [],\n  \"parserOptions\": {\n    \"ecmaVersion\": \"latest\",\n    \"sourceType\": \"module\"\n  },\n  \"rules\": {\n    \"react/react-in-jsx-scope\": \"off\"\n  },\n  \"ignorePatterns\": [\"build/**\", \"build.mjs\", \"src/utils/is-mobile.mjs\"],\n  \"settings\": {\n    \"react\": {\n      \"version\": \"detect\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\""
  },
  {
    "path": ".github/workflows/pr-tests.yml",
    "content": "name: pr-tests\n\non:\n  pull_request:\n    types:\n      - \"opened\"\n      - \"reopened\"\n      - \"synchronize\"\n    paths:\n      - \"src/**\"\n      - \"build.mjs\"\n\njobs:\n  tests:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      - run: npm ci\n      - run: npm run lint\n      - run: npm run build"
  },
  {
    "path": ".github/workflows/pre-release-build.yml",
    "content": "name: pre-release\non:\n  workflow_dispatch:\n#  push:\n#    branches:\n#      - main\n#    paths:\n#      - \"src/**\"\n#      - \"!src/**/*.json\"\n#      - \"build.mjs\"\n#    tags-ignore:\n#      - \"v*\"\n\njobs:\n  build_and_release:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      - run: npm ci\n      - run: npm run build\n\n      - uses: josStorer/get-current-time@v2\n        id: current-time\n        with:\n          format: YYYY_MMDD_HHmm\n\n      - uses: actions/upload-artifact@v3\n        with:\n          name: Chromium_Build_${{ steps.current-time.outputs.formattedTime }}\n          path: build/chromium/*\n\n      - uses: actions/upload-artifact@v3\n        with:\n          name: Firefox_Build_${{ steps.current-time.outputs.formattedTime }}\n          path: build/firefox/*\n\n      - uses: actions/upload-artifact@v3\n        with:\n          name: Chromium_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}\n          path: build/chromium-without-katex/*\n\n      - uses: actions/upload-artifact@v3\n        with:\n          name: Firefox_Build_WithoutKatex_${{ steps.current-time.outputs.formattedTime }}\n          path: build/firefox-without-katex/*\n\n      - uses: marvinpinto/action-automatic-releases@v1.2.1\n        with:\n          repo_token: \"${{ secrets.GITHUB_TOKEN }}\"\n          automatic_release_tag: \"latest\"\n          prerelease: true\n          title: \"Development Build\"\n          files: |\n            build/chromium.zip\n            build/firefox.zip\n            build/chromium-without-katex.zip\n            build/firefox-without-katex.zip\n"
  },
  {
    "path": ".github/workflows/scripts/verify-search-engine-configs.mjs",
    "content": "import { JSDOM } from 'jsdom'\nimport fetch, { Headers } from 'node-fetch'\n\nconst config = {\n  google: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#rhs'],\n    appendContainerQuery: ['#rcnt'],\n    resultsContainerQuery: ['#rso'],\n  },\n  bing: {\n    inputQuery: [\"[name='q']\"],\n    sidebarContainerQuery: ['#b_context'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#b_results'],\n  },\n  yahoo: {\n    inputQuery: [\"input[name='p']\"],\n    sidebarContainerQuery: ['#right', '.Contents__inner.Contents__inner--sub'],\n    appendContainerQuery: ['#cols', '#contents__wrap'],\n    resultsContainerQuery: [\n      '#main-algo',\n      '.searchCenterMiddle',\n      '.Contents__inner.Contents__inner--main',\n      '#contents',\n    ],\n  },\n  duckduckgo: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.results--sidebar.js-results-sidebar'],\n    appendContainerQuery: ['#links_wrapper'],\n    resultsContainerQuery: ['.results'],\n  },\n  startpage: {\n    inputQuery: [\"input[name='query']\"],\n    sidebarContainerQuery: ['.layout-web__sidebar.layout-web__sidebar--web'],\n    appendContainerQuery: ['.layout-web__body.layout-web__body--desktop'],\n    resultsContainerQuery: ['.mainline-results'],\n  },\n  baidu: {\n    inputQuery: [\"input[id='kw']\"],\n    sidebarContainerQuery: ['#content_right'],\n    appendContainerQuery: ['#container'],\n    resultsContainerQuery: ['#content_left', '#results'],\n  },\n  kagi: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.right-content-box._0_right_sidebar'],\n    appendContainerQuery: ['#_0_app_content'],\n    resultsContainerQuery: ['#main', '#app'],\n  },\n  yandex: {\n    inputQuery: [\"input[name='text']\"],\n    sidebarContainerQuery: ['#search-result-aside'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#search-result'],\n  },\n  naver: {\n    inputQuery: [\"input[name='query']\"],\n    sidebarContainerQuery: ['#sub_pack'],\n    appendContainerQuery: ['#content'],\n    resultsContainerQuery: ['#main_pack', '#ct'],\n  },\n  brave: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#side-right'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#results'],\n  },\n  searx: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#sidebar_results', '#sidebar'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#urls', '#main_results', '#results'],\n  },\n  ecosia: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.sidebar.web__sidebar'],\n    appendContainerQuery: ['#main'],\n    resultsContainerQuery: ['.mainline'],\n  },\n  neeva: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.result-group-layout__stickyContainer-iDIO8'],\n    appendContainerQuery: ['.search-index__searchHeaderContainer-2JD6q'],\n    resultsContainerQuery: ['.result-group-layout__component-1jzTe', '#search'],\n  },\n}\n\nconst urls = {\n  google: ['https://www.google.com/search?q=hello'],\n  bing: ['https://www.bing.com/search?q=hello', 'https://cn.bing.com/search?q=hello'],\n  yahoo: ['https://search.yahoo.com/search?p=hello', 'https://search.yahoo.co.jp/search?p=hello'],\n  duckduckgo: ['https://duckduckgo.com/s?q=hello'],\n  startpage: [], // need redirect and post https://www.startpage.com/do/search?query=hello\n  baidu: ['https://www.baidu.com/s?wd=hello'],\n  kagi: [], // need login https://kagi.com/search?q=hello\n  yandex: [], // need cookie https://yandex.com/search/?text=hello\n  naver: ['https://search.naver.com/search.naver?query=hello'],\n  brave: ['https://search.brave.com/search?q=hello'],\n  searx: ['https://searx.tiekoetter.com/search?q=hello'],\n  ecosia: [], // unknown verify method https://www.ecosia.org/search?q=hello\n  neeva: [], // unknown verify method(FetchError: maximum redirect reached) https://neeva.com/search?q=hello\n}\n\nconst commonHeaders = {\n  Accept:\n    'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',\n  Connection: 'keep-alive',\n  'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', // for baidu\n}\n\nconst desktopHeaders = new Headers({\n  'User-Agent':\n    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/108.0.1462.76',\n  ...commonHeaders,\n})\n\nconst mobileHeaders = {\n  'User-Agent':\n    'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36 Edg/108.0.1462.76',\n  ...commonHeaders,\n}\n\nconst desktopQueryNames = [\n  'inputQuery',\n  'sidebarContainerQuery',\n  'appendContainerQuery',\n  'resultsContainerQuery',\n]\n\nconst mobileQueryNames = ['inputQuery', 'resultsContainerQuery']\n\nlet errors = ''\n\nasync function verify(errorTag, urls, headers, queryNames) {\n  await Promise.all(\n    Object.entries(urls).map(([siteName, urlArray]) =>\n      Promise.all(\n        urlArray.map((url) =>\n          fetch(url, {\n            method: 'GET',\n            headers: headers,\n          })\n            .then((response) => response.text())\n            .then((text) => {\n              const dom = new JSDOM(text)\n              for (const queryName of queryNames) {\n                const queryArray = config[siteName][queryName]\n                if (queryArray.length === 0) continue\n\n                let foundQuery\n                for (const query of queryArray) {\n                  const element = dom.window.document.querySelector(query)\n                  if (element) {\n                    foundQuery = query\n                    break\n                  }\n                }\n                if (foundQuery) {\n                  console.log(`${siteName} ${url} ${queryName}: ${foundQuery} passed`)\n                } else {\n                  const error = `${siteName} ${url} ${queryName} failed`\n                  errors += errorTag + error + '\\n'\n                }\n              }\n            })\n            .catch((error) => {\n              errors += errorTag + error + '\\n'\n            }),\n        ),\n      ),\n    ),\n  )\n}\n\nasync function main() {\n  console.log('Verify desktop search engine configs:')\n  await verify('desktop: ', urls, desktopHeaders, desktopQueryNames)\n  console.log('\\nVerify mobile search engine configs:')\n  await verify('mobile: ', urls, mobileHeaders, mobileQueryNames)\n\n  if (errors.length > 0) throw new Error('\\n' + errors)\n  else console.log('\\nAll passed')\n}\n\nmain()\n"
  },
  {
    "path": ".github/workflows/tagged-release.yml",
    "content": "name: tagged-release\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build_and_release:\n    runs-on: macos-12\n\n    steps:\n      - run: echo \"VERSION=${GITHUB_REF_NAME#v}\" >> $GITHUB_ENV\n      - uses: actions/checkout@v3\n        with:\n          ref: main\n\n      - name: Update manifest.json version\n        uses: jossef/action-set-json-field@v2.1\n        with:\n          file: src/manifest.json\n          field: version\n          value: ${{ env.VERSION }}\n\n      - name: Update manifest.v2.json version\n        uses: jossef/action-set-json-field@v2.1\n        with:\n          file: src/manifest.v2.json\n          field: version\n          value: ${{ env.VERSION }}\n\n      - name: Push files\n        run: |\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n          git config --global user.name \"github-actions[bot]\"\n          git commit -am \"release v${{ env.VERSION }}\"\n          git push\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      - run: npm ci\n      - run: npm run build\n\n      - uses: maxim-lobanov/setup-xcode@v1\n        with:\n          xcode-version: 14.2\n      - run: sed -i '' \"s/0.0.0/${{ env.VERSION }}/g\" safari/project.patch\n      - run: npm run build:safari\n\n      - uses: marvinpinto/action-automatic-releases@v1.2.1\n        with:\n          repo_token: \"${{ secrets.GITHUB_TOKEN }}\"\n          prerelease: false\n          files: |\n            build/chromium.zip\n            build/firefox.zip\n            build/safari.dmg\n            build/chromium-without-katex.zip\n            build/firefox-without-katex.zip\n"
  },
  {
    "path": ".github/workflows/verify-configs.yml",
    "content": "name: verify-configs\non:\n  workflow_dispatch:\n\njobs:\n  verify_configs:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - run: npm ci\n      - run: npm run verify\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.vscode/\nnode_modules/\nbuild/\n.DS_Store\n*.zip\n"
  },
  {
    "path": ".prettierignore",
    "content": "build/\nsrc/manifest.json\nsrc/manifest.v2.json"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 100,\n  \"semi\": false,\n  \"tabWidth\": 2,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"bracketSpacing\": true,\n  \"overrides\": [\n    {\n      \"files\": \".prettierrc\",\n      \"options\": {\n        \"parser\": \"json\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 josStorer\nCopyright (c) 2022 wong2\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# This repo has moved to [ChatGPTBox](https://github.com/josStorer/chatGPTBox). Due to the upstream repo being acquired and closed source, and there has been a period of time where issues and PRs in the upstream have gone unhandled. I decided to publish this extension to the store and keep open-source\n\n[![Verify search engine configs](https://github.com/josStorer/chatGPT-search-engine-extension/workflows/verify-configs/badge.svg)](https://github.com/josStorer/chatGPT-search-engine-extension/actions/workflows/verify-configs.yml)\n[![GitHub release](https://img.shields.io/github/release/josStorer/chatGPT-search-engine-extension.svg)](https://github.com/josStorer/chatGPT-search-engine-extension/releases/latest)\n\n[Installation](#installation)\n\nA browser extension to display ChatGPT response alongside Search Engine results, supports Chrome/Edge/Firefox/Safari(macOS) and\nAndroid.\n\nSupport most search engines, including Google, Bing, Yahoo, DuckDuckGo, StartPage, Baidu, Kagi, Yandex, Naver, Brave,\nSearx, Ecosia, Neeva in total.\n\nRequest more search engine support in [#6](https://github.com/josStorer/chatGPT-search-engine-extension/issues/6)\n\nSee more in [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases)\nand [Pre-release build](https://github.com/josStorer/chatGPT-search-engine-extension/actions/workflows/pre-release-build.yml)\n\n## Notice\n\nThis repository exists only to support some features that are not supported or denied\nin [upstream](https://github.com/wong2/chat-gpt-google-extension), and for ethical reasons, I have not uploaded it to\nany app store. It isn't related to any extensions of the same name that may exist in some app store\n\n## Diff with upstream\n\n<details>\n<summary>Details:</summary>\n\n- Support StartPage, Ecosia, Neeva, Searx(searx.tiekoetter.com, searx.fmac.xyz, searx.be and more)\n- Android support\n- Safari(macOS) support\n- Custom mount point (e.g. for some unsupported engines)\n- Preview your setting (e.g. theme, mount point) in realtime\n- Katex: [upstream#75](https://github.com/wong2/chat-gpt-google-extension/pull/75)\n- Linkify in ReactMarkdown\n- Interactive mode: [upstream#103](https://github.com/wong2/chat-gpt-google-extension/pull/103), now support generating\n  separate sessions for each page\n- Fix answer being overwritten due to \"network error\" or other errors\n- Theme switcher: [#9](https://github.com/josStorer/chatGPT-search-engine-extension/pull/9)\n- Collapse answers\n- Popup Setting Window (Upstream has switched to a standalone options page)\n- Allow `Insert chatGPT at the top of search results` in Setting Window\n- Switch to webpack\n- Javascript\n- See more in [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases)\n\n</details>\n\n## Upstream supports, but not here\n\n<details>\n<summary>Details:</summary>\n\n(I don't think these contents are very valuable, but I could be wrong, so if you think of some suitable application\nscenario or related need, please create an issue)\n\n1. Upstream supports setting the desired language, and will force the relevant words to be inserted at the end after you\n   enter the question\n\n    - but I think, users always expect to get the language corresponding to their question, when you want to get a\n      different language, you should take the initiative to specify when searching, which is also consistent with the\n      habits of using search engines, and this fork supports interactive mode, you can also continue to tell chatGPT\n      what you want. Once you set up forced insertion, it will change the actual content of the user's question, for\n      example, when you configure French and search in English, chatGPT will always reply to you in French, when you\n      expect a reply in English, you will have to open the settings page, make changes, then refresh and ask the\n      question again, which I think is a very bad process\n\n2. The upstream extension popup window has an embedded chatGPT page (iframe)\n\n    - but you have to open the chatGPT website and log in to use it, so I think, in that case, why not use it directly\n      on the official website? In addition, interactive mode is already supported here, and each page can be used as a\n      separate session, so this feature is less necessary\n\n</details>\n\n## Preview\n\n- [SearchEngines](screenshot/engines/README.md)\n- Code highlight, interactive mode, dark mode, copy/collapse answers, theme switcher and more\n\n  (Click on the extension icon to open the setting window)\n  ![code-highlight](screenshot/code-highlight.png)\n- LaTeX\n  ![latex](screenshot/latex.png)\n- Android\n  ![android](screenshot/android.jpg)\n\n## Installation\n\n### Install to Chrome/Edge\n\n1. Download `chromium.zip` from [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases).\n2. Unzip the file.\n3. In Chrome/Edge go to the extensions page (`chrome://extensions` or `edge://extensions`).\n4. Enable Developer Mode.\n5. Drag the unzipped folder anywhere on the page to import it (do not delete the folder afterwards).\n\n### Install to Firefox\n\n1. Download `firefox.zip` from [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases).\n2. Unzip the file.\n3. Go to `about:debugging`, click \"This Firefox\" on the sidebar.\n4. Click \"Load Temporary Add-on\" button, then select any file in the unzipped folder.\n\n### Install to Android\n\n1. Install [Kiwi Browser](https://play.google.com/store/apps/details?id=com.kiwibrowser.browser) or other mobile browsers that support installing extensions from local files.\n2. Download `chromium.zip` from [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases) on\n   your phone.\n3. Go to `Extensions` and enable developer mode.\n4. Click `+ (from .zip/.crx/.user.js)` button and load the downloaded zip file.\n5. Click the browser option button, scroll down and click on the extension icon to open the setting window.\n6. Enable `Insert chatGPT at the top of search results`.\n\n### Install to Safari(macOS)\n\n1. Download `safari.dmg` from [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases).\n2. Double-click `safari.dmg` to open it and drag the extension’s icon to your Applications folder\n3. Run this extension in your Applications folder\n4. Click `Quit and Open Safari Settings...`\n5. Click `Advanced` in Safari Settings and then turn on `Show Develop menu in menu bar`\n6. Click `Develop` in Safari menu bar and then turn on `Allow Unsigned Extensions`\n7. You will see this extension in Extensions of Safari Settings, turn on it\n8. Click `Always Allow on Every Website`\n\n## Enable for single website\n\n1. Click on the extension icon to open the popup setting window.\n2. Click `Advanced`.\n3. Input the website name (of the hostname) in `Custom Site Regex`, e.g. google\n4. Enable `Only use Custom Site Regex...`\n\n## Build from source\n\n1. Clone the repo\n2. Install dependencies with `npm install`\n3. `npm run build`\n4. Load `build/chromium/` or `build/firefox/` directory to your browser\n\n## My contributions\n\n- [Pull Requests](https://github.com/wong2/chat-gpt-google-extension/pulls?q=is%3Apr+author%3AjosStorer+)\n- ### Other\n    - Merge and improve some PRs\n    - Support for most search engines\n    - Android support\n    - Safari(macOS) support\n    - Custom mount point\n    - Preview your setting in realtime\n    - Fix answer being overwritten due to \"network error\" or other errors\n    - Linkify in ReactMarkdown\n    - Generate separate sessions for each page\n    - Code highlight\n    - Collapse answers\n    - Copy answers\n    - Allow insert chatGPT at the top of search results\n    - Automated build workflow (with esbuild/webpack)\n    - Verify search engine configs automatically\n    - See more in [Releases](https://github.com/josStorer/chatGPT-search-engine-extension/releases)\n\n## Credit\n\nThis project is forked from [wong2/chat-gpt-google-extension](https://github.com/wong2/chat-gpt-google-extension) and\ndetached since 14 December of 2022\n\nThe original repository is inspired by [ZohaibAhmed/ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) ([upstream-c54528b](https://github.com/wong2/chatgpt-google-extension/commit/c54528b0e13058ab78bfb433c92603db017d1b6b))\n"
  },
  {
    "path": "build.mjs",
    "content": "import archiver from 'archiver'\nimport fs from 'fs-extra'\nimport path from 'path'\nimport webpack from 'webpack'\nimport ProgressBarPlugin from 'progress-bar-webpack-plugin'\nimport CssMinimizerPlugin from 'css-minimizer-webpack-plugin'\nimport MiniCssExtractPlugin from 'mini-css-extract-plugin'\nimport TerserPlugin from 'terser-webpack-plugin'\nimport { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'\n\nconst outdir = 'build'\n\nconst __dirname = path.resolve()\nconst isProduction = process.argv[2] !== '--development' // --production and --analyze are both production\nconst isAnalyzing = process.argv[2] === '--analyze'\n\nasync function deleteOldDir() {\n  await fs.rm(outdir, { recursive: true, force: true })\n}\n\nasync function runWebpack(isWithoutKatex, callback) {\n  const compiler = webpack({\n    entry: {\n      'content-script': {\n        import: './src/content-script/index.jsx',\n        dependOn: 'shared',\n      },\n      background: {\n        import: './src/background/index.mjs',\n      },\n      popup: {\n        import: './src/popup/index.jsx',\n        dependOn: 'shared',\n      },\n      shared: [\n        'preact',\n        'webextension-polyfill',\n        '@primer/octicons-react',\n        'react-bootstrap-icons',\n        './src/utils',\n      ],\n    },\n    output: {\n      filename: '[name].js',\n      path: path.resolve(__dirname, outdir),\n    },\n    mode: isProduction ? 'production' : 'development',\n    devtool: isProduction ? false : 'inline-source-map',\n    optimization: {\n      minimizer: [\n        new TerserPlugin({\n          terserOptions: {\n            output: { ascii_only: true },\n          },\n        }),\n        new CssMinimizerPlugin(),\n      ],\n      concatenateModules: !isAnalyzing,\n    },\n    plugins: [\n      new ProgressBarPlugin({\n        format: '  build [:bar] :percent (:elapsed seconds)',\n        clear: false,\n      }),\n      new MiniCssExtractPlugin({\n        filename: '[name].css',\n      }),\n      new BundleAnalyzerPlugin({\n        analyzerMode: isAnalyzing ? 'static' : 'disable',\n      }),\n      ...(isWithoutKatex\n        ? [\n            new webpack.NormalModuleReplacementPlugin(/markdown\\.jsx/, (result) => {\n              if (result.request) {\n                result.request = result.request.replace(\n                  'markdown.jsx',\n                  'markdown-without-katex.jsx',\n                )\n              }\n            }),\n          ]\n        : []),\n    ],\n    resolve: {\n      extensions: ['.jsx', '.mjs', '.js'],\n      alias: {\n        parse5: path.resolve(__dirname, 'node_modules/parse5'),\n      },\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.m?jsx?$/,\n          exclude: /(node_modules)/,\n          resolve: {\n            fullySpecified: false,\n          },\n          use: [\n            {\n              loader: 'babel-loader',\n              options: {\n                presets: [\n                  '@babel/preset-env',\n                  {\n                    plugins: ['@babel/plugin-transform-runtime'],\n                  },\n                ],\n                plugins: [\n                  [\n                    '@babel/plugin-transform-react-jsx',\n                    {\n                      runtime: 'automatic',\n                      importSource: 'preact',\n                    },\n                  ],\n                ],\n              },\n            },\n          ],\n        },\n        {\n          test: /\\.s[ac]ss$/,\n          use: [\n            MiniCssExtractPlugin.loader,\n            {\n              loader: 'css-loader',\n              options: {\n                importLoaders: 1,\n              },\n            },\n            {\n              loader: 'sass-loader',\n            },\n          ],\n        },\n        {\n          test: /\\.less$/,\n          use: [\n            MiniCssExtractPlugin.loader,\n            {\n              loader: 'css-loader',\n              options: {\n                importLoaders: 1,\n              },\n            },\n            {\n              loader: 'less-loader',\n            },\n          ],\n        },\n        {\n          test: /\\.css$/,\n          use: [\n            MiniCssExtractPlugin.loader,\n            {\n              loader: 'css-loader',\n            },\n          ],\n        },\n        {\n          test: /\\.(woff|ttf)$/,\n          type: 'asset/resource',\n          generator: {\n            emit: false,\n          },\n        },\n        {\n          test: /\\.woff2$/,\n          type: 'asset/inline',\n        },\n        {\n          test: /\\.jpg$/,\n          type: 'asset/inline',\n        },\n      ],\n    },\n  })\n  if (isProduction) compiler.run(callback)\n  else compiler.watch({}, callback)\n}\n\nasync function zipFolder(dir) {\n  const output = fs.createWriteStream(`${dir}.zip`)\n  const archive = archiver('zip', {\n    zlib: { level: 9 },\n  })\n  archive.pipe(output)\n  archive.directory(dir, false)\n  await archive.finalize()\n}\n\nasync function copyFiles(entryPoints, targetDir) {\n  if (!fs.existsSync(targetDir)) await fs.mkdir(targetDir)\n  await Promise.all(\n    entryPoints.map(async (entryPoint) => {\n      await fs.copy(entryPoint.src, `${targetDir}/${entryPoint.dst}`)\n    }),\n  )\n}\n\nasync function finishOutput(outputDirSuffix) {\n  const commonFiles = [\n    { src: 'build/shared.js', dst: 'shared.js' },\n    { src: 'build/content-script.js', dst: 'content-script.js' },\n    { src: 'build/content-script.css', dst: 'content-script.css' },\n    { src: 'build/background.js', dst: 'background.js' },\n    { src: 'build/popup.js', dst: 'popup.js' },\n    { src: 'build/popup.css', dst: 'popup.css' },\n    { src: 'src/popup/index.html', dst: 'popup.html' },\n    { src: 'src/logo.png', dst: 'logo.png' },\n  ]\n\n  // chromium\n  const chromiumOutputDir = `./${outdir}/chromium${outputDirSuffix}`\n  await copyFiles(\n    [...commonFiles, { src: 'src/manifest.json', dst: 'manifest.json' }],\n    chromiumOutputDir,\n  )\n  if (isProduction) await zipFolder(chromiumOutputDir)\n\n  // firefox\n  const firefoxOutputDir = `./${outdir}/firefox${outputDirSuffix}`\n  await copyFiles(\n    [...commonFiles, { src: 'src/manifest.v2.json', dst: 'manifest.json' }],\n    firefoxOutputDir,\n  )\n  if (isProduction) await zipFolder(firefoxOutputDir)\n}\n\nfunction generateWebpackCallback(finishOutputFunc) {\n  return async function webpackCallback(err, stats) {\n    if (err || stats.hasErrors()) {\n      console.error(err || stats.toString())\n      return\n    }\n    // console.log(stats.toString())\n\n    await finishOutputFunc()\n  }\n}\n\nasync function build() {\n  await deleteOldDir()\n  if (isProduction && !isAnalyzing)\n    await runWebpack(\n      true,\n      generateWebpackCallback(() => finishOutput('-without-katex')),\n    )\n  await runWebpack(\n    false,\n    generateWebpackCallback(() => finishOutput('')),\n  )\n}\n\nbuild()\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chat-gpt-search-engine-extension\",\n  \"scripts\": {\n    \"build\": \"node build.mjs --production\",\n    \"build:safari\": \"bash ./safari/build.sh\",\n    \"dev\": \"node build.mjs --development\",\n    \"analyze\": \"node build.mjs --analyze\",\n    \"lint\": \"eslint --ext .js,.mjs,.jsx .\",\n    \"lint:fix\": \"eslint --ext .js,.mjs,.jsx . --fix\",\n    \"pretty\": \"prettier --write ./**/*.{js,mjs,jsx,json,css,scss}\",\n    \"stage\": \"run-script-os\",\n    \"stage:default\": \"git add $(git diff --name-only --cached --diff-filter=d)\",\n    \"stage:win32\": \"powershell git add $(git diff --name-only --cached --diff-filter=d)\",\n    \"verify\": \"node .github/workflows/scripts/verify-search-engine-configs.mjs\"\n  },\n  \"pre-commit\": [\n    \"pretty\",\n    \"stage\",\n    \"lint\"\n  ],\n  \"dependencies\": {\n    \"@nem035/gpt-3-encoder\": \"^1.1.7\",\n    \"@picocss/pico\": \"^1.5.6\",\n    \"@primer/octicons-react\": \"^17.11.1\",\n    \"countries-list\": \"^2.6.1\",\n    \"eventsource-parser\": \"^0.1.0\",\n    \"expiry-map\": \"^2.0.0\",\n    \"file-saver\": \"^2.0.5\",\n    \"github-markdown-css\": \"^5.2.0\",\n    \"gpt-3-encoder\": \"^1.1.4\",\n    \"katex\": \"^0.16.4\",\n    \"lodash-es\": \"^4.17.21\",\n    \"parse5\": \"^6.0.1\",\n    \"preact\": \"^10.11.3\",\n    \"prop-types\": \"^15.8.1\",\n    \"react\": \"npm:@preact/compat@^17.1.2\",\n    \"react-bootstrap-icons\": \"^1.10.2\",\n    \"react-dom\": \"npm:@preact/compat@^17.1.2\",\n    \"react-draggable\": \"^4.4.5\",\n    \"react-markdown\": \"^8.0.5\",\n    \"react-tabs\": \"^4.2.1\",\n    \"rehype-highlight\": \"^6.0.0\",\n    \"rehype-katex\": \"^6.0.2\",\n    \"rehype-raw\": \"^6.1.1\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-math\": \"^5.1.1\",\n    \"uuid\": \"^9.0.0\",\n    \"webextension-polyfill\": \"^0.10.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/plugin-transform-react-jsx\": \"^7.20.13\",\n    \"@babel/plugin-transform-runtime\": \"^7.19.6\",\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"@types/archiver\": \"^5.3.1\",\n    \"@types/fs-extra\": \"^11.0.1\",\n    \"@types/jsdom\": \"^21.1.0\",\n    \"@types/webextension-polyfill\": \"^0.10.0\",\n    \"archiver\": \"^5.3.1\",\n    \"babel-loader\": \"^9.1.2\",\n    \"css-loader\": \"^6.7.3\",\n    \"css-minimizer-webpack-plugin\": \"^4.2.2\",\n    \"eslint\": \"^8.34.0\",\n    \"eslint-plugin-react\": \"^7.32.2\",\n    \"fs-extra\": \"^11.1.0\",\n    \"jsdom\": \"^21.1.0\",\n    \"less-loader\": \"^11.1.0\",\n    \"mini-css-extract-plugin\": \"^2.7.2\",\n    \"node-fetch\": \"^3.3.0\",\n    \"pre-commit\": \"^1.2.2\",\n    \"prettier\": \"^2.8.4\",\n    \"progress-bar-webpack-plugin\": \"^2.1.0\",\n    \"run-script-os\": \"^1.1.6\",\n    \"sass\": \"^1.58.1\",\n    \"sass-loader\": \"^13.2.0\",\n    \"terser-webpack-plugin\": \"^5.3.6\",\n    \"webpack\": \"^5.75.0\",\n    \"webpack-bundle-analyzer\": \"^4.8.0\"\n  }\n}\n"
  },
  {
    "path": "safari/appdmg.json",
    "content": "{\n  \"title\": \"chatGPT for Search Engine\",\n  \"icon\": \"../src/logo.png\",\n  \"contents\": [\n    { \"x\": 448, \"y\": 344, \"type\": \"link\", \"path\": \"/Applications\" },\n    { \"x\": 192, \"y\": 344, \"type\": \"file\", \"path\": \"../build/chatGPT-for-Search-Engine.app\" }\n  ]\n}\n"
  },
  {
    "path": "safari/build.sh",
    "content": "xcrun safari-web-extension-converter ./build/firefox \\\n --project-location ./build/safari --app-name chatGPT-for-Search-Engine \\\n --bundle-identifier dev.josStorer.chatGPT-for-Search-Engine --force --no-prompt --no-open\ngit apply safari/project.patch\nxcodebuild archive -project ./build/safari/chatGPT-for-Search-Engine/chatGPT-for-Search-Engine.xcodeproj \\\n -scheme \"chatGPT-for-Search-Engine (macOS)\" -configuration Release -archivePath ./build/safari/chatGPT-for-Search-Engine.xcarchive\nxcodebuild -exportArchive -archivePath ./build/safari/chatGPT-for-Search-Engine.xcarchive \\\n -exportOptionsPlist ./safari/export-options.plist -exportPath ./build\nnpm install -D appdmg\nrm ./build/safari.dmg\nappdmg ./safari/appdmg.json ./build/safari.dmg"
  },
  {
    "path": "safari/export-options.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>method</key>\n\t<string>mac-application</string>\n</dict>\n</plist>"
  },
  {
    "path": "safari/project.patch",
    "content": "--- a/build/safari/chatGPT-for-Search-Engine/chatGPT-for-Search-Engine.xcodeproj/project.pbxproj\n+++ b/build/safari/chatGPT-for-Search-Engine/chatGPT-for-Search-Engine.xcodeproj/project.pbxproj\n@@ -825,7 +825,7 @@\n \t\t\t\t\t\"@executable_path/../../../../Frameworks\",\n \t\t\t\t);\n \t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n-\t\t\t\tMARKETING_VERSION = 1.0;\n+\t\t\t\tMARKETING_VERSION = 0.0.0;\n \t\t\t\tOTHER_LDFLAGS = (\n \t\t\t\t\t\"-framework\",\n \t\t\t\t\tSafariServices,\n@@ -878,6 +878,10 @@\n \t\t\tisa = XCBuildConfiguration;\n \t\t\tbuildSettings = {\n \t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n+\t\t\t\tARCHS = (\n+\t\t\t\t\tarm64,\n+\t\t\t\t\tx86_64,\n+\t\t\t\t);\n \t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n \t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n \t\t\t\tCODE_SIGN_ENTITLEMENTS = \"macOS (App)/chatGPT-for-Search-Engine.entitlements\";\n@@ -887,6 +891,7 @@\n \t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n \t\t\t\tINFOPLIST_FILE = \"macOS (App)/Info.plist\";\n \t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"chatGPT-for-Search-Engine\";\n+\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\n \t\t\t\tINFOPLIST_KEY_NSMainStoryboardFile = Main;\n \t\t\t\tINFOPLIST_KEY_NSPrincipalClass = NSApplication;\n \t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n@@ -894,7 +899,7 @@\n \t\t\t\t\t\"@executable_path/../Frameworks\",\n \t\t\t\t);\n \t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n-\t\t\t\tMARKETING_VERSION = 1.0;\n+\t\t\t\tMARKETING_VERSION = 0.0.0;\n \t\t\t\tOTHER_LDFLAGS = (\n \t\t\t\t\t\"-framework\",\n \t\t\t\t\tSafariServices,\n"
  },
  {
    "path": "screenshot/engines/README.md",
    "content": "The images below are for preview purposes only and this project actually supports style adaptation now\n\n![bing](bing.png)\n![duckduckgo](duckduckgo.png)\n![google](google.png)\n![kagi](kagi.png)\n![naver](naver.png)\n![startpage](startpage.png)\n![yahoojp](yahoo.jp.png)\n![yahoo](yahoo.png)\n![yandex](yandex.png)\n![baidu](baidu.png)\n![brave](brave.png)\n![searx](searx.png)\n![ecosia](ecosia.png)\n"
  },
  {
    "path": "src/background/apis/chatgpt-web.mjs",
    "content": "// web version\n\nimport { fetchSSE } from '../../utils/fetch-sse'\nimport { isEmpty } from 'lodash-es'\nimport { chatgptWebModelKeys, getUserConfig, Models } from '../../config'\n\nasync function request(token, method, path, data) {\n  const apiUrl = (await getUserConfig()).customChatGptWebApiUrl\n  const response = await fetch(`${apiUrl}/backend-api${path}`, {\n    method,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${token}`,\n    },\n    body: JSON.stringify(data),\n  })\n  const responseText = await response.text()\n  console.debug(`request: ${path}`, responseText)\n  return { response, responseText }\n}\n\nexport async function sendMessageFeedback(token, data) {\n  await request(token, 'POST', '/conversation/message_feedback', data)\n}\n\nexport async function setConversationProperty(token, conversationId, propertyObject) {\n  await request(token, 'PATCH', `/conversation/${conversationId}`, propertyObject)\n}\n\nexport async function sendModerations(token, question, conversationId, messageId) {\n  await request(token, 'POST', `/moderations`, {\n    conversation_id: conversationId,\n    input: question,\n    message_id: messageId,\n    model: 'text-moderation-playground',\n  })\n}\n\nexport async function getModels(token) {\n  const response = JSON.parse((await request(token, 'GET', '/models')).responseText)\n  return response.models\n}\n\n/**\n * @param {Runtime.Port} port\n * @param {string} question\n * @param {Session} session\n * @param {string} accessToken\n */\nexport async function generateAnswersWithChatgptWebApi(port, question, session, accessToken) {\n  const deleteConversation = () => {\n    setConversationProperty(accessToken, session.conversationId, { is_visible: false })\n  }\n\n  const controller = new AbortController()\n  port.onDisconnect.addListener(() => {\n    console.debug('port disconnected')\n    controller.abort()\n    deleteConversation()\n  })\n\n  const models = await getModels(accessToken).catch(() => {})\n  const config = await getUserConfig()\n\n  let answer = ''\n  await fetchSSE(`${config.customChatGptWebApiUrl}${config.customChatGptWebApiPath}`, {\n    method: 'POST',\n    signal: controller.signal,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${accessToken}`,\n    },\n    body: JSON.stringify({\n      action: 'next',\n      conversation_id: session.conversationId,\n      messages: [\n        {\n          id: session.messageId,\n          role: 'user',\n          content: {\n            content_type: 'text',\n            parts: [question],\n          },\n        },\n      ],\n      model: models ? models[0].slug : Models[chatgptWebModelKeys[0]].value,\n      parent_message_id: session.parentMessageId,\n    }),\n    onMessage(message) {\n      console.debug('sse message', message)\n      if (message === '[DONE]') {\n        session.conversationRecords.push({ question: question, answer: answer })\n        console.debug('conversation history', { content: session.conversationRecords })\n        port.postMessage({ answer: null, done: true, session: session })\n        return\n      }\n      let data\n      try {\n        data = JSON.parse(message)\n      } catch (error) {\n        console.debug('json error', error)\n        return\n      }\n      if (data.conversation_id) session.conversationId = data.conversation_id\n      if (data.message?.id) session.parentMessageId = data.message.id\n\n      answer = data.message?.content?.parts?.[0]\n      if (answer) {\n        port.postMessage({ answer: answer, done: false, session: session })\n      }\n    },\n    async onStart() {\n      // sendModerations(accessToken, question, session.conversationId, session.messageId)\n    },\n    async onEnd() {},\n    async onError(resp) {\n      if (resp.status === 403) {\n        throw new Error('CLOUDFLARE')\n      }\n      const error = await resp.json().catch(() => ({}))\n      throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)\n    },\n  })\n}\n"
  },
  {
    "path": "src/background/apis/openai-api.mjs",
    "content": "// api version\n\nimport { maxResponseTokenLength, Models, getUserConfig } from '../../config'\nimport { fetchSSE } from '../../utils/fetch-sse'\nimport { getConversationPairs } from '../../utils/get-conversation-pairs'\nimport { isEmpty } from 'lodash-es'\n\nconst getChatgptPromptBase = async () => {\n  return `You are a helpful, creative, clever, and very friendly assistant. You are familiar with various languages in the world.`\n}\n\nconst getGptPromptBase = async () => {\n  return (\n    `The following is a conversation with an AI assistant.` +\n    `The assistant is helpful, creative, clever, and very friendly. The assistant is familiar with various languages in the world.\\n\\n` +\n    `Human: Hello, who are you?\\n` +\n    `AI: I am an AI created by OpenAI. How can I help you today?\\n` +\n    `Human: 谢谢\\n` +\n    `AI: 不客气\\n`\n  )\n}\n\n/**\n * @param {Browser.Runtime.Port} port\n * @param {string} question\n * @param {Session} session\n * @param {string} apiKey\n * @param {string} modelName\n */\nexport async function generateAnswersWithGptCompletionApi(\n  port,\n  question,\n  session,\n  apiKey,\n  modelName,\n) {\n  const controller = new AbortController()\n  port.onDisconnect.addListener(() => {\n    console.debug('port disconnected')\n    controller.abort()\n  })\n\n  const prompt =\n    (await getGptPromptBase()) +\n    getConversationPairs(session.conversationRecords, false) +\n    `Human:${question}\\nAI:`\n  const apiUrl = (await getUserConfig()).customOpenAiApiUrl\n\n  let answer = ''\n  await fetchSSE(`${apiUrl}/v1/completions`, {\n    method: 'POST',\n    signal: controller.signal,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${apiKey}`,\n    },\n    body: JSON.stringify({\n      prompt: prompt,\n      model: Models[modelName].value,\n      stream: true,\n      max_tokens: maxResponseTokenLength,\n    }),\n    onMessage(message) {\n      console.debug('sse message', message)\n      if (message === '[DONE]') {\n        session.conversationRecords.push({ question: question, answer: answer })\n        console.debug('conversation history', { content: session.conversationRecords })\n        port.postMessage({ answer: null, done: true, session: session })\n        return\n      }\n      let data\n      try {\n        data = JSON.parse(message)\n      } catch (error) {\n        console.debug('json error', error)\n        return\n      }\n      answer += data.choices[0].text\n      port.postMessage({ answer: answer, done: false, session: null })\n    },\n    async onStart() {},\n    async onEnd() {},\n    async onError(resp) {\n      if (resp.status === 403) {\n        throw new Error('CLOUDFLARE')\n      }\n      const error = await resp.json().catch(() => ({}))\n      throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)\n    },\n  })\n}\n\n/**\n * @param {Browser.Runtime.Port} port\n * @param {string} question\n * @param {Session} session\n * @param {string} apiKey\n * @param {string} modelName\n */\nexport async function generateAnswersWithChatgptApi(port, question, session, apiKey, modelName) {\n  const controller = new AbortController()\n  port.onDisconnect.addListener(() => {\n    console.debug('port disconnected')\n    controller.abort()\n  })\n\n  const prompt = getConversationPairs(session.conversationRecords, true)\n  prompt.unshift({ role: 'system', content: await getChatgptPromptBase() })\n  prompt.push({ role: 'user', content: question })\n  const apiUrl = (await getUserConfig()).customOpenAiApiUrl\n\n  let answer = ''\n  await fetchSSE(`${apiUrl}/v1/chat/completions`, {\n    method: 'POST',\n    signal: controller.signal,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${apiKey}`,\n    },\n    body: JSON.stringify({\n      messages: prompt,\n      model: Models[modelName].value,\n      stream: true,\n      max_tokens: maxResponseTokenLength,\n    }),\n    onMessage(message) {\n      console.debug('sse message', message)\n      if (message === '[DONE]') {\n        session.conversationRecords.push({ question: question, answer: answer })\n        console.debug('conversation history', { content: session.conversationRecords })\n        port.postMessage({ answer: null, done: true, session: session })\n        return\n      }\n      let data\n      try {\n        data = JSON.parse(message)\n      } catch (error) {\n        console.debug('json error', error)\n        return\n      }\n      if ('content' in data.choices[0].delta) answer += data.choices[0].delta.content\n      port.postMessage({ answer: answer, done: false, session: null })\n    },\n    async onStart() {},\n    async onEnd() {},\n    async onError(resp) {\n      if (resp.status === 403) {\n        throw new Error('CLOUDFLARE')\n      }\n      const error = await resp.json().catch(() => ({}))\n      throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)\n    },\n  })\n}\n"
  },
  {
    "path": "src/background/index.mjs",
    "content": "import { v4 as uuidv4 } from 'uuid'\nimport Browser from 'webextension-polyfill'\nimport ExpiryMap from 'expiry-map'\nimport { generateAnswersWithChatgptWebApi, sendMessageFeedback } from './apis/chatgpt-web'\nimport {\n  generateAnswersWithChatgptApi,\n  generateAnswersWithGptCompletionApi,\n} from './apis/openai-api'\nimport {\n  chatgptApiModelKeys,\n  chatgptWebModelKeys,\n  getUserConfig,\n  gptApiModelKeys,\n  isUsingApiKey,\n} from '../config'\nimport { isSafari } from '../utils/is-safari'\nimport { config as toolsConfig } from '../content-script/selection-tools'\n\nconst KEY_ACCESS_TOKEN = 'accessToken'\nconst cache = new ExpiryMap(10 * 1000)\n\n/**\n * @returns {Promise<string>}\n */\nasync function getAccessToken() {\n  if (cache.get(KEY_ACCESS_TOKEN)) {\n    return cache.get(KEY_ACCESS_TOKEN)\n  }\n  if (isSafari()) {\n    const userConfig = await getUserConfig()\n    if (userConfig.accessToken) {\n      cache.set(KEY_ACCESS_TOKEN, userConfig.accessToken)\n    } else {\n      throw new Error('UNAUTHORIZED')\n    }\n  } else {\n    const resp = await fetch('https://chat.openai.com/api/auth/session')\n    if (resp.status === 403) {\n      throw new Error('CLOUDFLARE')\n    }\n    const data = await resp.json().catch(() => ({}))\n    if (!data.accessToken) {\n      throw new Error('UNAUTHORIZED')\n    }\n    cache.set(KEY_ACCESS_TOKEN, data.accessToken)\n  }\n  return cache.get(KEY_ACCESS_TOKEN)\n}\n\nBrowser.runtime.onConnect.addListener((port) => {\n  console.debug('connected')\n  port.onMessage.addListener(async (msg) => {\n    console.debug('received msg', msg)\n    const config = await getUserConfig()\n    const session = msg.session\n    if (session.useApiKey == null) {\n      session.useApiKey = isUsingApiKey(config)\n    }\n\n    try {\n      if (chatgptWebModelKeys.includes(config.modelName)) {\n        const accessToken = await getAccessToken()\n        session.messageId = uuidv4()\n        if (session.parentMessageId == null) {\n          session.parentMessageId = uuidv4()\n        }\n        await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)\n      } else if (gptApiModelKeys.includes(config.modelName)) {\n        await generateAnswersWithGptCompletionApi(\n          port,\n          session.question,\n          session,\n          config.apiKey,\n          config.modelName,\n        )\n      } else if (chatgptApiModelKeys.includes(config.modelName)) {\n        await generateAnswersWithChatgptApi(\n          port,\n          session.question,\n          session,\n          config.apiKey,\n          config.modelName,\n        )\n      }\n    } catch (err) {\n      console.error(err)\n      port.postMessage({ error: err.message })\n      cache.delete(KEY_ACCESS_TOKEN)\n    }\n  })\n})\n\nBrowser.runtime.onMessage.addListener(async (message) => {\n  if (message.type === 'FEEDBACK') {\n    const token = await getAccessToken()\n    await sendMessageFeedback(token, message.data)\n  }\n})\n\nBrowser.contextMenus.removeAll().then(() => {\n  const menuId = 'ChatGPTBox-Menu'\n  Browser.contextMenus.create({\n    id: menuId,\n    title: 'ChatGPTBox',\n    contexts: ['all'],\n  })\n\n  Browser.contextMenus.create({\n    id: menuId + 'new',\n    parentId: menuId,\n    title: 'New Chat',\n    contexts: ['selection'],\n  })\n  for (const key in toolsConfig) {\n    const toolConfig = toolsConfig[key]\n    Browser.contextMenus.create({\n      id: menuId + key,\n      parentId: menuId,\n      title: toolConfig.label,\n      contexts: ['selection'],\n    })\n  }\n\n  Browser.contextMenus.onClicked.addListener((info, tab) => {\n    const itemId = info.menuItemId === menuId ? 'new' : info.menuItemId.replace(menuId, '')\n    const message = {\n      itemId: itemId,\n      selectionText: info.selectionText,\n    }\n    console.debug('menu clicked', message)\n    Browser.tabs.sendMessage(tab.id, {\n      type: 'MENU',\n      data: message,\n    })\n  })\n})\n"
  },
  {
    "path": "src/components/ConversationCard/index.jsx",
    "content": "import { memo, useEffect, useState } from 'react'\nimport PropTypes from 'prop-types'\nimport Browser from 'webextension-polyfill'\nimport InputBox from '../InputBox'\nimport ConversationItem from '../ConversationItem'\nimport { createElementAtPosition, initSession, isSafari } from '../../utils'\nimport { DownloadIcon } from '@primer/octicons-react'\nimport { WindowDesktop, XLg } from 'react-bootstrap-icons'\nimport FileSaver from 'file-saver'\nimport { render } from 'preact'\nimport FloatingToolbar from '../FloatingToolbar'\n\nconst logo = Browser.runtime.getURL('logo.png')\n\nclass ConversationItemData extends Object {\n  /**\n   * @param {'question'|'answer'|'error'} type\n   * @param {string} content\n   * @param {object} session\n   * @param {bool} done\n   */\n  constructor(type, content, session = null, done = false) {\n    super()\n    this.type = type\n    this.content = content\n    this.session = session\n    this.done = done\n  }\n}\n\nfunction ConversationCard(props) {\n  const [isReady, setIsReady] = useState(!props.question)\n  const [port, setPort] = useState(() => Browser.runtime.connect())\n  const [session, setSession] = useState(props.session)\n  /**\n   * @type {[ConversationItemData[], (conversationItemData: ConversationItemData[]) => void]}\n   */\n  const [conversationItemData, setConversationItemData] = useState(\n    (() => {\n      if (props.session.conversationRecords.length === 0)\n        if (props.question)\n          return [\n            new ConversationItemData(\n              'answer',\n              '<p class=\"gpt-loading\">Waiting for response...</p>',\n            ),\n          ]\n        else return []\n      else {\n        const ret = []\n        for (const record of props.session.conversationRecords) {\n          ret.push(\n            new ConversationItemData('question', record.question + '\\n<hr/>', props.session, true),\n          )\n          ret.push(\n            new ConversationItemData('answer', record.answer + '\\n<hr/>', props.session, true),\n          )\n        }\n        return ret\n      }\n    })(),\n  )\n\n  useEffect(() => {\n    if (props.onUpdate) props.onUpdate()\n  })\n\n  useEffect(() => {\n    // when the page is responsive, session may accumulate redundant data and needs to be cleared after remounting and before making a new request\n    if (props.question) {\n      const newSession = initSession({ question: props.question })\n      setSession(newSession)\n      port.postMessage({ session: newSession })\n    }\n  }, [props.question]) // usually only triggered once\n\n  /**\n   * @param {string} value\n   * @param {boolean} appended\n   * @param {'question'|'answer'|'error'} newType\n   * @param {boolean} done\n   */\n  const UpdateAnswer = (value, appended, newType, done = false) => {\n    setConversationItemData((old) => {\n      const copy = [...old]\n      const index = copy.findLastIndex((v) => v.type === 'answer')\n      if (index === -1) return copy\n      copy[index] = new ConversationItemData(\n        newType,\n        appended ? copy[index].content + value : value,\n      )\n      copy[index].session = { ...session }\n      copy[index].done = done\n      return copy\n    })\n  }\n\n  useEffect(() => {\n    const listener = () => {\n      setPort(Browser.runtime.connect())\n    }\n    port.onDisconnect.addListener(listener)\n    return () => {\n      port.onDisconnect.removeListener(listener)\n    }\n  }, [port])\n  useEffect(() => {\n    const listener = (msg) => {\n      if (msg.answer) {\n        UpdateAnswer(msg.answer, false, 'answer')\n      }\n      if (msg.session) {\n        setSession(msg.session)\n      }\n      if (msg.done) {\n        UpdateAnswer('\\n<hr/>', true, 'answer', true)\n        setIsReady(true)\n      }\n      if (msg.error) {\n        switch (msg.error) {\n          case 'UNAUTHORIZED':\n            UpdateAnswer(\n              `UNAUTHORIZED<br>Please login at https://chat.openai.com first${\n                isSafari() ? '<br>Then open https://chat.openai.com/api/auth/session' : ''\n              }<br>And refresh this page or type you question again` +\n                `<br><br>Consider creating an api key at https://platform.openai.com/account/api-keys<hr>`,\n              false,\n              'error',\n            )\n            break\n          case 'CLOUDFLARE':\n            UpdateAnswer(\n              `OpenAI Security Check Required<br>Please open ${\n                isSafari() ? 'https://chat.openai.com/api/auth/session' : 'https://chat.openai.com'\n              }<br>And refresh this page or type you question again` +\n                `<br><br>Consider creating an api key at https://platform.openai.com/account/api-keys<hr>`,\n              false,\n              'error',\n            )\n            break\n          default:\n            setConversationItemData([\n              ...conversationItemData,\n              new ConversationItemData('error', msg.error + '\\n<hr/>'),\n            ])\n            break\n        }\n        setIsReady(true)\n      }\n    }\n    port.onMessage.addListener(listener)\n    return () => {\n      port.onMessage.removeListener(listener)\n    }\n  }, [conversationItemData])\n\n  return (\n    <div className=\"gpt-inner\">\n      <div className=\"gpt-header\">\n        {!props.closeable ? (\n          <img src={logo} width=\"20\" height=\"20\" style=\"margin:5px 15px 0px;user-select:none;\" />\n        ) : (\n          <XLg\n            className=\"gpt-util-icon\"\n            style=\"margin:5px 15px 0px;\"\n            title=\"Close the Window\"\n            size={16}\n            onClick={() => {\n              if (props.onClose) props.onClose()\n            }}\n          />\n        )}\n        {props.draggable ? (\n          <div className=\"dragbar\" />\n        ) : (\n          <WindowDesktop\n            className=\"gpt-util-icon\"\n            title=\"Float the Window\"\n            size={16}\n            onClick={() => {\n              const position = { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 }\n              const toolbarContainer = createElementAtPosition(position.x, position.y)\n              toolbarContainer.className = 'toolbar-container-not-queryable'\n              render(\n                <FloatingToolbar\n                  session={session}\n                  selection=\"\"\n                  position={position}\n                  container={toolbarContainer}\n                  closeable={true}\n                  triggered={true}\n                  onClose={() => toolbarContainer.remove()}\n                />,\n                toolbarContainer,\n              )\n            }}\n          />\n        )}\n        <span\n          title=\"Save Conversation\"\n          className=\"gpt-util-icon\"\n          style=\"margin:15px 15px 10px;\"\n          onClick={() => {\n            let output = ''\n            session.conversationRecords.forEach((data) => {\n              output += `Question:\\n\\n${data.question}\\n\\nAnswer:\\n\\n${data.answer}\\n\\n<hr/>\\n\\n`\n            })\n            const blob = new Blob([output], { type: 'text/plain;charset=utf-8' })\n            FileSaver.saveAs(blob, 'conversation.md')\n          }}\n        >\n          <DownloadIcon size={16} />\n        </span>\n      </div>\n      <hr />\n      <div className=\"markdown-body\">\n        {conversationItemData.map((data, idx) => (\n          <ConversationItem\n            content={data.content}\n            key={idx}\n            type={data.type}\n            session={data.session}\n            done={data.done}\n          />\n        ))}\n      </div>\n      <InputBox\n        enabled={isReady}\n        onSubmit={(question) => {\n          const newQuestion = new ConversationItemData('question', question + '\\n<hr/>')\n          const newAnswer = new ConversationItemData(\n            'answer',\n            '<p class=\"gpt-loading\">Waiting for response...</p>',\n          )\n          setConversationItemData([...conversationItemData, newQuestion, newAnswer])\n          setIsReady(false)\n\n          const newSession = { ...session, question }\n          setSession(newSession)\n          try {\n            port.postMessage({ session: newSession })\n          } catch (e) {\n            UpdateAnswer(e, false, 'error')\n          }\n        }}\n      />\n    </div>\n  )\n}\n\nConversationCard.propTypes = {\n  session: PropTypes.object.isRequired,\n  question: PropTypes.string.isRequired,\n  onUpdate: PropTypes.func,\n  draggable: PropTypes.bool,\n  closeable: PropTypes.bool,\n  onClose: PropTypes.func,\n}\n\nexport default memo(ConversationCard)\n"
  },
  {
    "path": "src/components/ConversationItem/index.jsx",
    "content": "import { useState } from 'react'\nimport FeedbackForChatGPTWeb from '../FeedbackForChatGPTWeb'\nimport { ChevronDownIcon, LinkExternalIcon, XCircleIcon } from '@primer/octicons-react'\nimport CopyButton from '../CopyButton'\nimport PropTypes from 'prop-types'\nimport MarkdownRender from '../MarkdownRender/markdown.jsx'\n\nexport function ConversationItem({ type, content, session, done }) {\n  const [collapsed, setCollapsed] = useState(false)\n\n  switch (type) {\n    case 'question':\n      return (\n        <div className={type} dir=\"auto\">\n          <div className=\"gpt-header\">\n            <p>You:</p>\n            <div style=\"display: flex; gap: 15px;\">\n              <CopyButton contentFn={() => content} size={14} />\n              {!collapsed ? (\n                <span title=\"Collapse\" className=\"gpt-util-icon\" onClick={() => setCollapsed(true)}>\n                  <XCircleIcon size={14} />\n                </span>\n              ) : (\n                <span title=\"Expand\" className=\"gpt-util-icon\" onClick={() => setCollapsed(false)}>\n                  <ChevronDownIcon size={14} />\n                </span>\n              )}\n            </div>\n          </div>\n          {!collapsed && <MarkdownRender>{content}</MarkdownRender>}\n        </div>\n      )\n    case 'answer':\n      return (\n        <div className={type} dir=\"auto\">\n          <div className=\"gpt-header\">\n            <p>{session ? 'ChatGPT:' : 'Loading...'}</p>\n            <div style=\"display: flex; gap: 15px;\">\n              {done && session && session.conversationId && (\n                <FeedbackForChatGPTWeb\n                  messageId={session.messageId}\n                  conversationId={session.conversationId}\n                />\n              )}\n              {session && session.conversationId && (\n                <a\n                  title=\"Continue on official website\"\n                  href={'https://chat.openai.com/chat/' + session.conversationId}\n                  target=\"_blank\"\n                  rel=\"nofollow noopener noreferrer\"\n                  style=\"color: inherit;\"\n                >\n                  <LinkExternalIcon size={14} />\n                </a>\n              )}\n              {session && <CopyButton contentFn={() => content} size={14} />}\n              {!collapsed ? (\n                <span title=\"Collapse\" className=\"gpt-util-icon\" onClick={() => setCollapsed(true)}>\n                  <XCircleIcon size={14} />\n                </span>\n              ) : (\n                <span title=\"Expand\" className=\"gpt-util-icon\" onClick={() => setCollapsed(false)}>\n                  <ChevronDownIcon size={14} />\n                </span>\n              )}\n            </div>\n          </div>\n          {!collapsed && <MarkdownRender>{content}</MarkdownRender>}\n        </div>\n      )\n    case 'error':\n      return (\n        <div className={type} dir=\"auto\">\n          <div className=\"gpt-header\">\n            <p>Error:</p>\n            <div style=\"display: flex; gap: 15px;\">\n              <CopyButton contentFn={() => content} size={14} />\n              {!collapsed ? (\n                <span title=\"Collapse\" className=\"gpt-util-icon\" onClick={() => setCollapsed(true)}>\n                  <XCircleIcon size={14} />\n                </span>\n              ) : (\n                <span title=\"Expand\" className=\"gpt-util-icon\" onClick={() => setCollapsed(false)}>\n                  <ChevronDownIcon size={14} />\n                </span>\n              )}\n            </div>\n          </div>\n          {!collapsed && <MarkdownRender>{content}</MarkdownRender>}\n        </div>\n      )\n  }\n}\n\nConversationItem.propTypes = {\n  type: PropTypes.oneOf(['question', 'answer', 'error']).isRequired,\n  content: PropTypes.string.isRequired,\n  session: PropTypes.object.isRequired,\n  done: PropTypes.bool.isRequired,\n}\n\nexport default ConversationItem\n"
  },
  {
    "path": "src/components/CopyButton/index.jsx",
    "content": "import { useState } from 'react'\nimport { CheckIcon, CopyIcon } from '@primer/octicons-react'\nimport PropTypes from 'prop-types'\n\nCopyButton.propTypes = {\n  contentFn: PropTypes.func.isRequired,\n  size: PropTypes.number.isRequired,\n  className: PropTypes.string,\n}\n\nfunction CopyButton({ className, contentFn, size }) {\n  const [copied, setCopied] = useState(false)\n\n  const onClick = () => {\n    navigator.clipboard\n      .writeText(contentFn())\n      .then(() => setCopied(true))\n      .then(() =>\n        setTimeout(() => {\n          setCopied(false)\n        }, 600),\n      )\n  }\n\n  return (\n    <span title=\"Copy\" className={`gpt-util-icon ${className ? className : ''}`} onClick={onClick}>\n      {copied ? <CheckIcon size={size} /> : <CopyIcon size={size} />}\n    </span>\n  )\n}\n\nexport default CopyButton\n"
  },
  {
    "path": "src/components/DecisionCard/index.jsx",
    "content": "import { LightBulbIcon, SearchIcon } from '@primer/octicons-react'\nimport { useState, useEffect } from 'react'\nimport PropTypes from 'prop-types'\nimport ConversationCard from '../ConversationCard'\nimport { defaultConfig, getUserConfig } from '../../config'\nimport Browser from 'webextension-polyfill'\nimport { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils'\n\nfunction DecisionCard(props) {\n  const [triggered, setTriggered] = useState(false)\n  const [config, setConfig] = useState(defaultConfig)\n  const [render, setRender] = useState(false)\n\n  const question = props.question\n\n  useEffect(() => {\n    getUserConfig()\n      .then(setConfig)\n      .then(() => setRender(true))\n  }, [])\n\n  useEffect(() => {\n    const listener = (changes) => {\n      const changedItems = Object.keys(changes)\n      let newConfig = {}\n      for (const key of changedItems) {\n        newConfig[key] = changes[key].newValue\n      }\n      setConfig({ ...config, ...newConfig })\n    }\n    Browser.storage.local.onChanged.addListener(listener)\n    return () => {\n      Browser.storage.local.onChanged.removeListener(listener)\n    }\n  }, [config])\n\n  const updatePosition = () => {\n    if (!render) return\n\n    const container = props.container\n    const siteConfig = props.siteConfig\n    container.classList.remove('sidebar-free')\n\n    if (config.appendQuery) {\n      const appendContainer = getPossibleElementByQuerySelector([config.appendQuery])\n      if (appendContainer) {\n        appendContainer.appendChild(container)\n        return\n      }\n    }\n\n    if (config.prependQuery) {\n      const prependContainer = getPossibleElementByQuerySelector([config.prependQuery])\n      if (prependContainer) {\n        prependContainer.prepend(container)\n        return\n      }\n    }\n\n    if (!siteConfig) return\n\n    if (config.insertAtTop) {\n      const resultsContainerQuery = getPossibleElementByQuerySelector(\n        siteConfig.resultsContainerQuery,\n      )\n      if (resultsContainerQuery) resultsContainerQuery.prepend(container)\n    } else {\n      const sidebarContainer = getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery)\n      if (sidebarContainer) {\n        sidebarContainer.prepend(container)\n      } else {\n        const appendContainer = getPossibleElementByQuerySelector(siteConfig.appendContainerQuery)\n        if (appendContainer) {\n          container.classList.add('sidebar-free')\n          appendContainer.appendChild(container)\n        } else {\n          const resultsContainerQuery = getPossibleElementByQuerySelector(\n            siteConfig.resultsContainerQuery,\n          )\n          if (resultsContainerQuery) resultsContainerQuery.prepend(container)\n        }\n      }\n    }\n  }\n\n  useEffect(() => updatePosition(), [config])\n\n  return (\n    render && (\n      <div data-theme={config.themeMode}>\n        {(() => {\n          if (question)\n            switch (config.triggerMode) {\n              case 'always':\n                return <ConversationCard session={props.session} question={question} />\n              case 'manually':\n                if (triggered) {\n                  return <ConversationCard session={props.session} question={question} />\n                }\n                return (\n                  <p\n                    className=\"gpt-inner manual-btn icon-and-text\"\n                    onClick={() => setTriggered(true)}\n                  >\n                    <SearchIcon size=\"small\" /> Ask ChatGPT\n                  </p>\n                )\n              case 'questionMark':\n                if (endsWithQuestionMark(question.trim())) {\n                  return <ConversationCard session={props.session} question={question} />\n                }\n                if (triggered) {\n                  return <ConversationCard session={props.session} question={question} />\n                }\n                return (\n                  <p\n                    className=\"gpt-inner manual-btn icon-and-text\"\n                    onClick={() => setTriggered(true)}\n                  >\n                    <SearchIcon size=\"small\" /> Ask ChatGPT\n                  </p>\n                )\n            }\n          else\n            return (\n              <p className=\"gpt-inner icon-and-text\">\n                <LightBulbIcon size=\"small\" /> No Input Found\n              </p>\n            )\n        })()}\n      </div>\n    )\n  )\n}\n\nDecisionCard.propTypes = {\n  session: PropTypes.object.isRequired,\n  question: PropTypes.string.isRequired,\n  siteConfig: PropTypes.object.isRequired,\n  container: PropTypes.object.isRequired,\n}\n\nexport default DecisionCard\n"
  },
  {
    "path": "src/components/FeedbackForChatGPTWeb/index.jsx",
    "content": "import PropTypes from 'prop-types'\nimport { memo, useCallback, useState } from 'react'\nimport { ThumbsupIcon, ThumbsdownIcon } from '@primer/octicons-react'\nimport Browser from 'webextension-polyfill'\n\nconst FeedbackForChatGPTWeb = (props) => {\n  const [action, setAction] = useState(null)\n\n  const clickThumbsUp = useCallback(async () => {\n    if (action) {\n      return\n    }\n    setAction('thumbsUp')\n    await Browser.runtime.sendMessage({\n      type: 'FEEDBACK',\n      data: {\n        conversation_id: props.conversationId,\n        message_id: props.messageId,\n        rating: 'thumbsUp',\n      },\n    })\n  }, [props, action])\n\n  const clickThumbsDown = useCallback(async () => {\n    if (action) {\n      return\n    }\n    setAction('thumbsDown')\n    await Browser.runtime.sendMessage({\n      type: 'FEEDBACK',\n      data: {\n        conversation_id: props.conversationId,\n        message_id: props.messageId,\n        rating: 'thumbsDown',\n        text: '',\n        tags: [],\n      },\n    })\n  }, [props, action])\n\n  return (\n    <div title=\"Feedback\" className=\"gpt-feedback\">\n      <span\n        onClick={clickThumbsUp}\n        className={action === 'thumbsUp' ? 'gpt-feedback-selected' : undefined}\n      >\n        <ThumbsupIcon size={14} />\n      </span>\n      <span\n        onClick={clickThumbsDown}\n        className={action === 'thumbsDown' ? 'gpt-feedback-selected' : undefined}\n      >\n        <ThumbsdownIcon size={14} />\n      </span>\n    </div>\n  )\n}\n\nFeedbackForChatGPTWeb.propTypes = {\n  messageId: PropTypes.string.isRequired,\n  conversationId: PropTypes.string.isRequired,\n}\n\nexport default memo(FeedbackForChatGPTWeb)\n"
  },
  {
    "path": "src/components/FloatingToolbar/index.jsx",
    "content": "import Browser from 'webextension-polyfill'\nimport { cloneElement, useEffect, useState } from 'react'\nimport ConversationCard from '../ConversationCard'\nimport PropTypes from 'prop-types'\nimport { defaultConfig, getUserConfig } from '../../config.mjs'\nimport { config as toolsConfig } from '../../content-script/selection-tools'\nimport { setElementPositionInViewport } from '../../utils'\nimport Draggable from 'react-draggable'\n\nconst logo = Browser.runtime.getURL('logo.png')\n\nfunction FloatingToolbar(props) {\n  const [prompt, setPrompt] = useState(props.prompt)\n  const [triggered, setTriggered] = useState(props.triggered)\n  const [config, setConfig] = useState(defaultConfig)\n  const [render, setRender] = useState(false)\n  const [position, setPosition] = useState(props.position)\n  const [virtualPosition, setVirtualPosition] = useState({ x: 0, y: 0 })\n\n  useEffect(() => {\n    getUserConfig()\n      .then(setConfig)\n      .then(() => setRender(true))\n  }, [])\n\n  useEffect(() => {\n    const listener = (changes) => {\n      const changedItems = Object.keys(changes)\n      let newConfig = {}\n      for (const key of changedItems) {\n        newConfig[key] = changes[key].newValue\n      }\n      setConfig({ ...config, ...newConfig })\n    }\n    Browser.storage.local.onChanged.addListener(listener)\n    return () => {\n      Browser.storage.local.onChanged.removeListener(listener)\n    }\n  }, [config])\n\n  if (!render) return <div />\n\n  if (triggered) {\n    const updatePosition = () => {\n      const newPosition = setElementPositionInViewport(props.container, position.x, position.y)\n      if (position.x !== newPosition.x || position.y !== newPosition.y) setPosition(newPosition) // clear extra virtual position offset\n    }\n\n    const dragEvent = {\n      onDrag: (e, ui) => {\n        setVirtualPosition({ x: virtualPosition.x + ui.deltaX, y: virtualPosition.y + ui.deltaY })\n      },\n      onStop: () => {\n        setPosition({ x: position.x + virtualPosition.x, y: position.y + virtualPosition.y })\n        setVirtualPosition({ x: 0, y: 0 })\n      },\n    }\n\n    if (virtualPosition.x === 0 && virtualPosition.y === 0) {\n      updatePosition() // avoid jitter\n    }\n\n    return (\n      <div data-theme={config.themeMode}>\n        <Draggable\n          handle=\".dragbar\"\n          onDrag={dragEvent.onDrag}\n          onStop={dragEvent.onStop}\n          position={virtualPosition}\n        >\n          <div className=\"gpt-selection-window\">\n            <div className=\"chat-gpt-container\">\n              <ConversationCard\n                session={props.session}\n                question={prompt}\n                draggable={true}\n                closeable={props.closeable}\n                onClose={props.onClose}\n                onUpdate={() => {\n                  updatePosition()\n                }}\n              />\n            </div>\n          </div>\n        </Draggable>\n      </div>\n    )\n  } else {\n    if (config.activeSelectionTools.length === 0) return <div />\n\n    const tools = []\n\n    for (const key in toolsConfig) {\n      if (config.activeSelectionTools.includes(key)) {\n        const toolConfig = toolsConfig[key]\n        tools.push(\n          cloneElement(toolConfig.icon, {\n            size: 20,\n            className: 'gpt-selection-toolbar-button',\n            title: toolConfig.label,\n            onClick: async () => {\n              setPrompt(await toolConfig.genPrompt(props.selection))\n              setTriggered(true)\n            },\n          }),\n        )\n      }\n    }\n\n    return (\n      <div data-theme={config.themeMode}>\n        <div className=\"gpt-selection-toolbar\">\n          <img src={logo} width=\"24\" height=\"24\" style=\"user-select:none;\" />\n          {tools}\n        </div>\n      </div>\n    )\n  }\n}\n\nFloatingToolbar.propTypes = {\n  session: PropTypes.object.isRequired,\n  selection: PropTypes.string.isRequired,\n  position: PropTypes.object.isRequired,\n  container: PropTypes.object.isRequired,\n  triggered: PropTypes.bool,\n  closeable: PropTypes.bool,\n  onClose: PropTypes.func,\n  prompt: PropTypes.string,\n}\n\nexport default FloatingToolbar\n"
  },
  {
    "path": "src/components/InputBox/index.jsx",
    "content": "import { useEffect, useRef, useState } from 'react'\nimport PropTypes from 'prop-types'\nimport { updateRefHeight } from '../../utils'\n\nexport function InputBox({ onSubmit, enabled }) {\n  const [value, setValue] = useState('')\n  const inputRef = useRef(null)\n\n  useEffect(() => {\n    updateRefHeight(inputRef)\n  })\n\n  const onKeyDown = (e) => {\n    if (e.keyCode === 13 && e.shiftKey === false) {\n      e.preventDefault()\n      if (!value) return\n      onSubmit(value)\n      setValue('')\n    }\n  }\n\n  return (\n    <textarea\n      ref={inputRef}\n      disabled={!enabled}\n      className=\"interact-input\"\n      placeholder={\n        enabled\n          ? 'Type your question here\\nEnter to send, shift + enter to break line'\n          : 'Wait for the answer to finish and then continue here'\n      }\n      value={value}\n      onChange={(e) => setValue(e.target.value)}\n      onKeyDown={onKeyDown}\n    />\n  )\n}\n\nInputBox.propTypes = {\n  onSubmit: PropTypes.func.isRequired,\n  enabled: PropTypes.bool,\n}\n\nexport default InputBox\n"
  },
  {
    "path": "src/components/MarkdownRender/markdown-without-katex.jsx",
    "content": "import ReactMarkdown from 'react-markdown'\nimport rehypeRaw from 'rehype-raw'\nimport rehypeHighlight from 'rehype-highlight'\nimport remarkGfm from 'remark-gfm'\nimport CopyButton from '../CopyButton'\nimport { useRef } from 'react'\nimport PropTypes from 'prop-types'\n\nfunction Pre({ className, children }) {\n  const preRef = useRef(null)\n  return (\n    <pre className={className} ref={preRef} style=\"position: relative;\">\n      <CopyButton\n        className=\"code-copy-btn\"\n        contentFn={() => preRef.current.textContent}\n        size={14}\n      />\n      {children}\n    </pre>\n  )\n}\n\nPre.propTypes = {\n  className: PropTypes.string.isRequired,\n  children: PropTypes.object.isRequired,\n}\n\nexport function MarkdownRender(props) {\n  const linkProperties = {\n    target: '_blank',\n    style: 'color: #8ab4f8;',\n    rel: 'nofollow noopener noreferrer',\n  }\n  return (\n    <ReactMarkdown\n      remarkPlugins={[remarkGfm]}\n      rehypePlugins={[\n        rehypeRaw,\n        [\n          rehypeHighlight,\n          {\n            detect: true,\n            ignoreMissing: true,\n          },\n        ],\n      ]}\n      components={{\n        a: (props) => (\n          <a href={props.href} {...linkProperties}>\n            {props.children}\n          </a>\n        ),\n        pre: Pre,\n      }}\n      {...props}\n    >\n      {props.children}\n    </ReactMarkdown>\n  )\n}\n\nMarkdownRender.propTypes = {\n  ...ReactMarkdown.propTypes,\n}\n\nexport default MarkdownRender\n"
  },
  {
    "path": "src/components/MarkdownRender/markdown.jsx",
    "content": "import 'katex/dist/katex.min.css'\nimport ReactMarkdown from 'react-markdown'\nimport rehypeRaw from 'rehype-raw'\nimport rehypeHighlight from 'rehype-highlight'\nimport rehypeKatex from 'rehype-katex'\nimport remarkMath from 'remark-math'\nimport remarkGfm from 'remark-gfm'\nimport CopyButton from '../CopyButton'\nimport { useRef } from 'react'\nimport PropTypes from 'prop-types'\n\nfunction Pre({ className, children }) {\n  const preRef = useRef(null)\n  return (\n    <pre className={className} ref={preRef} style=\"position: relative;\">\n      <CopyButton\n        className=\"code-copy-btn\"\n        contentFn={() => preRef.current.textContent}\n        size={14}\n      />\n      {children}\n    </pre>\n  )\n}\n\nPre.propTypes = {\n  className: PropTypes.string.isRequired,\n  children: PropTypes.object.isRequired,\n}\n\nexport function MarkdownRender(props) {\n  const linkProperties = {\n    target: '_blank',\n    style: 'color: #8ab4f8;',\n    rel: 'nofollow noopener noreferrer',\n  }\n  return (\n    <ReactMarkdown\n      remarkPlugins={[remarkMath, remarkGfm]}\n      rehypePlugins={[\n        rehypeKatex,\n        rehypeRaw,\n        [\n          rehypeHighlight,\n          {\n            detect: true,\n            ignoreMissing: true,\n          },\n        ],\n      ]}\n      components={{\n        a: (props) => (\n          <a href={props.href} {...linkProperties}>\n            {props.children}\n          </a>\n        ),\n        pre: Pre,\n      }}\n      {...props}\n    >\n      {props.children}\n    </ReactMarkdown>\n  )\n}\n\nMarkdownRender.propTypes = {\n  ...ReactMarkdown.propTypes,\n}\n\nexport default MarkdownRender\n"
  },
  {
    "path": "src/config.mjs",
    "content": "import { defaults } from 'lodash-es'\nimport Browser from 'webextension-polyfill'\nimport { isMobile } from './utils/is-mobile'\nimport { config as toolsConfig } from './content-script/selection-tools'\nimport { languages } from 'countries-list'\n\n/**\n * @typedef {object} Model\n * @property {string} value\n * @property {string} desc\n */\n/**\n * @type {Object.<string,Model>}\n */\nexport const Models = {\n  chatgptFree: { value: 'text-davinci-002-render-sha', desc: 'ChatGPT (Web)' },\n  chatgptApi: { value: 'gpt-3.5-turbo', desc: 'ChatGPT (GPT-3.5)' },\n  gptDavinci: { value: 'text-davinci-003', desc: 'GPT3' },\n}\n\nexport const chatgptWebModelKeys = ['chatgptFree']\nexport const gptApiModelKeys = ['gptDavinci']\nexport const chatgptApiModelKeys = ['chatgptApi']\n\nexport const TriggerMode = {\n  always: 'Always',\n  questionMark: 'When query ends with question mark (?)',\n  manually: 'Manually',\n}\n\nexport const ThemeMode = {\n  light: 'Light',\n  dark: 'Dark',\n  auto: 'Auto',\n}\n\nexport const languageList = { auto: { name: 'Auto', native: 'Auto' }, ...languages }\n\nexport const maxResponseTokenLength = 1000\n\n/**\n * @typedef {typeof defaultConfig} UserConfig\n */\nexport const defaultConfig = {\n  /** @type {keyof TriggerMode}*/\n  triggerMode: 'manually',\n  /** @type {keyof ThemeMode}*/\n  themeMode: 'auto',\n  /** @type {keyof Models}*/\n  modelName: 'chatgptFree',\n  apiKey: '',\n  insertAtTop: isMobile(),\n  siteRegex: 'match nothing',\n  userSiteRegexOnly: false,\n  inputQuery: '',\n  appendQuery: '',\n  prependQuery: '',\n  accessToken: '',\n  tokenSavedOn: 0,\n  preferredLanguage: navigator.language.substring(0, 2),\n  userLanguage: navigator.language.substring(0, 2), // unchangeable\n  customChatGptWebApiUrl: 'https://chat.openai.com',\n  customChatGptWebApiPath: '/backend-api/conversation',\n  customOpenAiApiUrl: 'https://api.openai.com',\n  selectionTools: Object.keys(toolsConfig),\n  activeSelectionTools: Object.keys(toolsConfig),\n  // importing configuration will result in gpt-3-encoder being packaged into the output file\n  siteAdapters: ['bilibili', 'github', 'gitlab', 'quora', 'reddit', 'youtube', 'zhihu'],\n  activeSiteAdapters: ['bilibili', 'github', 'gitlab', 'quora', 'reddit', 'youtube', 'zhihu'],\n}\n\nexport async function getUserLanguage() {\n  return languageList[defaultConfig.userLanguage].name\n}\n\nexport async function getUserLanguageNative() {\n  return languageList[defaultConfig.userLanguage].native\n}\n\nexport async function getPreferredLanguage() {\n  const config = await getUserConfig()\n  if (config.preferredLanguage === 'auto') return await getUserLanguage()\n  return languageList[config.preferredLanguage].name\n}\n\nexport async function getPreferredLanguageNative() {\n  const config = await getUserConfig()\n  if (config.preferredLanguage === 'auto') return await getUserLanguageNative()\n  return languageList[config.preferredLanguage].native\n}\n\nexport function isUsingApiKey(config) {\n  return (\n    gptApiModelKeys.includes(config.modelName) || chatgptApiModelKeys.includes(config.modelName)\n  )\n}\n\n/**\n * get user config from local storage\n * @returns {Promise<UserConfig>}\n */\nexport async function getUserConfig() {\n  const options = await Browser.storage.local.get(Object.keys(defaultConfig))\n  return defaults(options, defaultConfig)\n}\n\n/**\n * set user config to local storage\n * @param {Partial<UserConfig>} value\n */\nexport async function setUserConfig(value) {\n  await Browser.storage.local.set(value)\n}\n\nexport async function setAccessToken(accessToken) {\n  await setUserConfig({ accessToken, tokenSavedOn: Date.now() })\n}\n\nconst TOKEN_DURATION = 30 * 24 * 3600 * 1000\n\nexport async function clearOldAccessToken() {\n  const duration = Date.now() - (await getUserConfig()).tokenSavedOn\n  if (duration > TOKEN_DURATION) {\n    await setAccessToken('')\n  }\n}\n"
  },
  {
    "path": "src/content-script/index.jsx",
    "content": "import './styles.scss'\nimport { render } from 'preact'\nimport DecisionCard from '../components/DecisionCard'\nimport { config as siteConfig } from './site-adapters'\nimport { config as toolsConfig } from './selection-tools'\nimport { clearOldAccessToken, getUserConfig, setAccessToken, getPreferredLanguage } from '../config'\nimport {\n  createElementAtPosition,\n  getPossibleElementByQuerySelector,\n  initSession,\n  isSafari,\n} from '../utils'\nimport FloatingToolbar from '../components/FloatingToolbar'\nimport Browser from 'webextension-polyfill'\n\n/**\n * @param {SiteConfig} siteConfig\n * @param {UserConfig} userConfig\n */\nasync function mountComponent(siteConfig, userConfig) {\n  if (\n    !getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) &&\n    !getPossibleElementByQuerySelector(siteConfig.appendContainerQuery) &&\n    !getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery) &&\n    !getPossibleElementByQuerySelector([userConfig.prependQuery]) &&\n    !getPossibleElementByQuerySelector([userConfig.appendQuery])\n  )\n    return\n\n  document.querySelectorAll('.chat-gpt-container').forEach((e) => e.remove())\n\n  let question\n  if (userConfig.inputQuery) question = await getInput([userConfig.inputQuery])\n  if (!question && siteConfig) question = await getInput(siteConfig.inputQuery)\n\n  document.querySelectorAll('.chat-gpt-container').forEach((e) => e.remove())\n  const container = document.createElement('div')\n  container.className = 'chat-gpt-container'\n  render(\n    <DecisionCard\n      session={initSession()}\n      question={question}\n      siteConfig={siteConfig}\n      container={container}\n    />,\n    container,\n  )\n}\n\n/**\n * @param {string[]|function} inputQuery\n * @returns {Promise<string>}\n */\nasync function getInput(inputQuery) {\n  if (typeof inputQuery === 'function') {\n    const input = await inputQuery()\n    if (input) return `Reply in ${await getPreferredLanguage()}.\\n` + input\n    return input\n  }\n  const searchInput = getPossibleElementByQuerySelector(inputQuery)\n  if (searchInput && searchInput.value) {\n    return searchInput.value\n  }\n}\n\nasync function prepareForSafari() {\n  await clearOldAccessToken()\n\n  if (location.hostname !== 'chat.openai.com' || location.pathname !== '/api/auth/session') return\n\n  const response = document.querySelector('pre').textContent\n\n  let data\n  try {\n    data = JSON.parse(response)\n  } catch (error) {\n    console.error('json error', error)\n    return\n  }\n  if (data.accessToken) {\n    await setAccessToken(data.accessToken)\n  }\n}\n\nlet toolbarContainer\n\nasync function prepareForSelectionTools() {\n  document.addEventListener('mouseup', (e) => {\n    if (toolbarContainer && toolbarContainer.contains(e.target)) return\n    if (\n      toolbarContainer &&\n      window.getSelection()?.rangeCount > 0 &&\n      toolbarContainer.contains(window.getSelection()?.getRangeAt(0).endContainer.parentElement)\n    )\n      return\n\n    if (toolbarContainer) toolbarContainer.remove()\n    setTimeout(() => {\n      const selection = window.getSelection()?.toString()\n      if (selection) {\n        const position = { x: e.clientX + 15, y: e.clientY - 15 }\n        toolbarContainer = createElementAtPosition(position.x, position.y)\n        toolbarContainer.className = 'toolbar-container'\n        render(\n          <FloatingToolbar\n            session={initSession()}\n            selection={selection}\n            position={position}\n            container={toolbarContainer}\n          />,\n          toolbarContainer,\n        )\n      }\n    })\n  })\n  document.addEventListener('mousedown', (e) => {\n    if (toolbarContainer && toolbarContainer.contains(e.target)) return\n\n    document.querySelectorAll('.toolbar-container').forEach((e) => e.remove())\n  })\n  document.addEventListener('keydown', (e) => {\n    if (\n      toolbarContainer &&\n      !toolbarContainer.contains(e.target) &&\n      (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA')\n    ) {\n      setTimeout(() => {\n        if (!window.getSelection()?.toString()) toolbarContainer.remove()\n      })\n    }\n  })\n}\n\nlet menuX, menuY\n\nasync function prepareForRightClickMenu() {\n  document.addEventListener('contextmenu', (e) => {\n    menuX = e.clientX\n    menuY = e.clientY\n  })\n\n  Browser.runtime.onMessage.addListener(async (message) => {\n    if (message.type === 'MENU') {\n      const data = message.data\n      if (data.itemId === 'new') {\n        const position = { x: menuX, y: menuY }\n        const container = createElementAtPosition(position.x, position.y)\n        container.className = 'toolbar-container-not-queryable'\n        render(\n          <FloatingToolbar\n            session={initSession()}\n            selection=\"\"\n            position={position}\n            container={container}\n            triggered={true}\n            closeable={true}\n            onClose={() => container.remove()}\n          />,\n          container,\n        )\n      } else {\n        const position = { x: menuX, y: menuY }\n        const container = createElementAtPosition(position.x, position.y)\n        container.className = 'toolbar-container-not-queryable'\n        render(\n          <FloatingToolbar\n            session={initSession()}\n            selection={data.selectionText}\n            position={position}\n            container={container}\n            triggered={true}\n            closeable={true}\n            onClose={() => container.remove()}\n            prompt={await toolsConfig[data.itemId].genPrompt(data.selectionText)}\n          />,\n          container,\n        )\n      }\n    }\n  })\n}\n\nasync function prepareForStaticCard() {\n  let siteRegex\n  if (userConfig.userSiteRegexOnly) siteRegex = userConfig.siteRegex\n  else\n    siteRegex = new RegExp(\n      (userConfig.siteRegex && userConfig.siteRegex + '|') + Object.keys(siteConfig).join('|'),\n    )\n\n  const matches = location.hostname.match(siteRegex)\n  if (matches) {\n    const siteName = matches[0]\n    if (siteName in siteConfig) {\n      const siteAction = siteConfig[siteName].action\n      if (siteAction && siteAction.init) {\n        await siteAction.init(location.hostname, userConfig, getInput, mountComponent)\n      }\n    }\n    if (\n      userConfig.siteAdapters.includes(siteName) &&\n      !userConfig.activeSiteAdapters.includes(siteName)\n    )\n      return\n\n    mountComponent(siteConfig[siteName], userConfig)\n  }\n}\n\nlet userConfig\n\nasync function run() {\n  userConfig = await getUserConfig()\n  if (isSafari()) await prepareForSafari()\n  prepareForSelectionTools()\n  prepareForStaticCard()\n  prepareForRightClickMenu()\n}\n\nrun()\n"
  },
  {
    "path": "src/content-script/selection-tools/index.mjs",
    "content": "import {\n  CardHeading,\n  CardList,\n  EmojiSmile,\n  Palette,\n  QuestionCircle,\n  Translate,\n} from 'react-bootstrap-icons'\nimport { getPreferredLanguage } from '../../config.mjs'\n\nexport const config = {\n  translate: {\n    icon: <Translate />,\n    label: 'Translate',\n    genPrompt: async (selection) => {\n      const preferredLanguage = await getPreferredLanguage()\n      return (\n        `Translate the following into ${preferredLanguage} and only show me the translated content.` +\n        `If it is already in ${preferredLanguage},` +\n        `translate it into English and only show me the translated content:\\n\"${selection}\"`\n      )\n    },\n  },\n  summary: {\n    icon: <CardHeading />,\n    label: 'Summary',\n    genPrompt: async (selection) => {\n      const preferredLanguage = await getPreferredLanguage()\n      return `Reply in ${preferredLanguage}.Summarize the following as concisely as possible:\\n\"${selection}\"`\n    },\n  },\n  polish: {\n    icon: <Palette />,\n    label: 'Polish',\n    genPrompt: async (selection) =>\n      `Check the following content for possible diction and grammar problems,and polish it carefully:\\n\"${selection}\"`,\n  },\n  sentiment: {\n    icon: <EmojiSmile />,\n    label: 'Sentiment Analysis',\n    genPrompt: async (selection) => {\n      const preferredLanguage = await getPreferredLanguage()\n      return `Reply in ${preferredLanguage}.Analyze the sentiments expressed in the following content and make a brief summary of the sentiments:\\n\"${selection}\"`\n    },\n  },\n  divide: {\n    icon: <CardList />,\n    label: 'Divide Paragraphs',\n    genPrompt: async (selection) =>\n      `Divide the following into paragraphs that are easy to read and understand:\\n\"${selection}\"`,\n  },\n  ask: {\n    icon: <QuestionCircle />,\n    label: 'Ask',\n    genPrompt: async (selection) => {\n      const preferredLanguage = await getPreferredLanguage()\n      return `Reply in ${preferredLanguage}.Analyze the following content and express your opinion,or give your answer:\\n\"${selection}\"`\n    },\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/arxiv/index.mjs",
    "content": "//TODO\n"
  },
  {
    "path": "src/content-script/site-adapters/baidu/index.mjs",
    "content": "import { config } from '../index'\n\nexport default {\n  init: async (hostname, userConfig, getInput, mountComponent) => {\n    try {\n      const targetNode = document.getElementById('wrapper_wrapper')\n      const observer = new MutationObserver(async (records) => {\n        if (\n          records.some(\n            (record) =>\n              record.type === 'childList' &&\n              [...record.addedNodes].some((node) => node.id === 'container'),\n          )\n        ) {\n          const searchValue = await getInput(config.baidu.inputQuery)\n          if (searchValue) {\n            mountComponent(config.baidu, userConfig)\n          }\n        }\n      })\n      observer.observe(targetNode, { childList: true })\n    } catch (e) {\n      /* empty */\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/bilibili/index.mjs",
    "content": "import { cropText } from '../../../utils'\nimport { config } from '../index.mjs'\n\nexport default {\n  init: async (hostname, userConfig, getInput, mountComponent) => {\n    try {\n      let oldUrl = location.href\n      const checkUrlChange = async () => {\n        if (location.href !== oldUrl) {\n          oldUrl = location.href\n          mountComponent(config.bilibili, userConfig)\n        }\n      }\n      window.setInterval(checkUrlChange, 500)\n    } catch (e) {\n      /* empty */\n    }\n  },\n  inputQuery: async () => {\n    try {\n      const bvid = location.pathname.replace('video', '').replaceAll('/', '')\n      const p = Number(new URLSearchParams(location.search).get('p') || 1) - 1\n\n      const pagelistResponse = await fetch(\n        `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`,\n      )\n      const pagelistData = await pagelistResponse.json()\n      const videoList = pagelistData.data\n      const cid = videoList[p].cid\n      const title = videoList[p].part\n\n      const infoResponse = await fetch(\n        `https://api.bilibili.com/x/player/v2?bvid=${bvid}&cid=${cid}`,\n        {\n          credentials: 'include',\n        },\n      )\n      const infoData = await infoResponse.json()\n      const subtitleUrl = infoData.data.subtitle.subtitles[0].subtitle_url\n\n      const subtitleResponse = await fetch(subtitleUrl)\n      const subtitleData = await subtitleResponse.json()\n      const subtitles = subtitleData.body\n\n      let subtitleContent = ''\n      for (let i = 0; i < subtitles.length; i++) {\n        if (i === subtitles.length - 1) subtitleContent += subtitles[i].content\n        else subtitleContent += subtitles[i].content + ','\n      }\n\n      return cropText(\n        `用尽量简练的语言,联系视频标题,对视频进行内容摘要,视频标题为:\"${title}\",字幕内容为:\\n${subtitleContent}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/github/index.mjs",
    "content": "import { cropText, limitedFetch } from '../../../utils'\nimport { config } from '../index.mjs'\n\nconst getPatchUrl = async () => {\n  const patchUrl = location.origin + location.pathname + '.patch'\n  const response = await fetch(patchUrl, { method: 'HEAD' })\n  if (response.ok) return patchUrl\n  return ''\n}\n\nconst getPatchData = async (patchUrl) => {\n  if (!patchUrl) return\n\n  let patchData = await limitedFetch(patchUrl, 1024 * 40)\n  patchData = patchData.substring(patchData.indexOf('---'))\n  return patchData\n}\n\nexport default {\n  init: async (hostname, userConfig, getInput, mountComponent) => {\n    try {\n      const targetNode = document.querySelector('body')\n      const observer = new MutationObserver(async (records) => {\n        if (\n          records.some(\n            (record) =>\n              record.type === 'childList' &&\n              [...record.addedNodes].some((node) => node.classList.contains('page-responsive')),\n          )\n        ) {\n          const patchUrl = await getPatchUrl()\n          if (patchUrl) {\n            mountComponent(config.github, userConfig)\n          }\n        }\n      })\n      observer.observe(targetNode, { childList: true })\n    } catch (e) {\n      /* empty */\n    }\n  },\n  inputQuery: async () => {\n    try {\n      const patchUrl = await getPatchUrl()\n      const patchData = await getPatchData(patchUrl)\n      if (!patchData) return\n\n      return cropText(\n        `Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +\n          `The patch contents of this commit are as follows:\\n${patchData}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/gitlab/index.mjs",
    "content": "import { cropText, limitedFetch } from '../../../utils'\n\nconst getPatchUrl = async () => {\n  const patchUrl = location.origin + location.pathname + '.patch'\n  const response = await fetch(patchUrl, { method: 'HEAD' })\n  if (response.ok) return patchUrl\n  return ''\n}\n\nconst getPatchData = async (patchUrl) => {\n  if (!patchUrl) return\n\n  let patchData = await limitedFetch(patchUrl, 1024 * 40)\n  patchData = patchData.substring(patchData.indexOf('---'))\n  return patchData\n}\n\nexport default {\n  inputQuery: async () => {\n    try {\n      const patchUrl = await getPatchUrl()\n      const patchData = await getPatchData(patchUrl)\n      if (!patchData) return\n\n      return cropText(\n        `Analyze the contents of a git commit,provide a suitable commit message,and summarize the contents of the commit.` +\n          `The patch contents of this commit are as follows:\\n${patchData}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/index.mjs",
    "content": "import baidu from './baidu'\nimport bilibili from './bilibili'\nimport youtube from './youtube'\nimport github from './github'\nimport gitlab from './gitlab'\nimport zhihu from './zhihu'\nimport reddit from './reddit'\nimport quora from './quora'\n\n/**\n * @typedef {object} SiteConfigAction\n * @property {function} init\n */\n/**\n * @typedef {object} SiteConfig\n * @property {string[]|function} inputQuery - for search box\n * @property {string[]} sidebarContainerQuery - prepend child to\n * @property {string[]} appendContainerQuery - if sidebarContainer not exists, append child to\n * @property {string[]} resultsContainerQuery - prepend child to if insertAtTop is true\n * @property {SiteConfigAction} action\n */\n/**\n * @type {Object.<string,SiteConfig>}\n */\nexport const config = {\n  google: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#rhs'],\n    appendContainerQuery: ['#rcnt'],\n    resultsContainerQuery: ['#rso'],\n  },\n  bing: {\n    inputQuery: [\"[name='q']\"],\n    sidebarContainerQuery: ['#b_context'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#b_results'],\n  },\n  yahoo: {\n    inputQuery: [\"input[name='p']\"],\n    sidebarContainerQuery: ['#right', '.Contents__inner.Contents__inner--sub'],\n    appendContainerQuery: ['#cols', '#contents__wrap'],\n    resultsContainerQuery: [\n      '#main-algo',\n      '.searchCenterMiddle',\n      '.Contents__inner.Contents__inner--main',\n      '#contents',\n    ],\n  },\n  duckduckgo: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.results--sidebar.js-results-sidebar'],\n    appendContainerQuery: ['#links_wrapper'],\n    resultsContainerQuery: ['.results'],\n  },\n  startpage: {\n    inputQuery: [\"input[name='query']\"],\n    sidebarContainerQuery: ['.layout-web__sidebar.layout-web__sidebar--web'],\n    appendContainerQuery: ['.layout-web__body.layout-web__body--desktop'],\n    resultsContainerQuery: ['.mainline-results'],\n  },\n  baidu: {\n    inputQuery: [\"input[id='kw']\"],\n    sidebarContainerQuery: ['#content_right'],\n    appendContainerQuery: ['#container'],\n    resultsContainerQuery: ['#content_left', '#results'],\n    action: {\n      init: baidu.init,\n    },\n  },\n  kagi: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.right-content-box._0_right_sidebar'],\n    appendContainerQuery: ['#_0_app_content'],\n    resultsContainerQuery: ['#main', '#app'],\n  },\n  yandex: {\n    inputQuery: [\"input[name='text']\"],\n    sidebarContainerQuery: ['#search-result-aside'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#search-result'],\n  },\n  naver: {\n    inputQuery: [\"input[name='query']\"],\n    sidebarContainerQuery: ['#sub_pack'],\n    appendContainerQuery: ['#content'],\n    resultsContainerQuery: ['#main_pack', '#ct'],\n  },\n  brave: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#side-right'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#results'],\n  },\n  searx: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['#sidebar_results', '#sidebar'],\n    appendContainerQuery: [],\n    resultsContainerQuery: ['#urls', '#main_results', '#results'],\n  },\n  ecosia: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.sidebar.web__sidebar'],\n    appendContainerQuery: ['#main'],\n    resultsContainerQuery: ['.mainline'],\n  },\n  neeva: {\n    inputQuery: [\"input[name='q']\"],\n    sidebarContainerQuery: ['.result-group-layout__stickyContainer-iDIO8'],\n    appendContainerQuery: ['.search-index__searchHeaderContainer-2JD6q'],\n    resultsContainerQuery: ['.result-group-layout__component-1jzTe', '#search'],\n  },\n  bilibili: {\n    inputQuery: bilibili.inputQuery,\n    sidebarContainerQuery: ['#danmukuBox'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n    action: {\n      init: bilibili.init,\n    },\n  },\n  youtube: {\n    inputQuery: youtube.inputQuery,\n    sidebarContainerQuery: ['#secondary'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n    action: {\n      init: youtube.init,\n    },\n  },\n  github: {\n    inputQuery: github.inputQuery,\n    sidebarContainerQuery: ['#diff', '.commit'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n    action: {\n      init: github.init,\n    },\n  },\n  gitlab: {\n    inputQuery: gitlab.inputQuery,\n    sidebarContainerQuery: ['.js-commit-box-info'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n  },\n  zhihu: {\n    inputQuery: zhihu.inputQuery,\n    sidebarContainerQuery: ['.Question-sideColumn', '.Post-Header'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n  },\n  reddit: {\n    inputQuery: reddit.inputQuery,\n    sidebarContainerQuery: ['.side .spacer .linkinfo'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n  },\n  quora: {\n    inputQuery: quora.inputQuery,\n    sidebarContainerQuery: ['.q-box.PageContentsLayout___StyledBox-d2uxks-0'],\n    appendContainerQuery: [],\n    resultsContainerQuery: [],\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/quora/index.mjs",
    "content": "import { cropText } from '../../../utils'\n\nexport default {\n  inputQuery: async () => {\n    try {\n      if (location.pathname === '/') return\n\n      const texts = document.querySelectorAll('.q-box.qu-userSelect--text')\n      let title\n      if (texts.length > 0) title = texts[0].textContent\n      let answers = ''\n      if (texts.length > 1)\n        for (let i = 1; i < texts.length; i++) {\n          answers += `answer${i}:${texts[i].textContent}|`\n        }\n\n      return cropText(\n        `Below is the content from a question and answer platform,giving the corresponding summary and your opinion on it.` +\n          `The question is:'${title}',` +\n          `Some answers are as follows:\\n${answers}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/reddit/index.mjs",
    "content": "import { cropText } from '../../../utils'\n\nexport default {\n  inputQuery: async () => {\n    try {\n      const title = document.querySelector('.entry .title').textContent\n      const texts = document.querySelectorAll('.entry .usertext-body')\n      let description\n      if (texts.length > 0) description = texts[0].textContent\n      let answers = ''\n      if (texts.length > 1)\n        for (let i = 1; i < texts.length; i++) {\n          answers += `answer${i}:${texts[i].textContent}|`\n        }\n\n      return cropText(\n        `Below is the content from a social forum,giving the corresponding summary and your opinion on it.` +\n          `The title is:'${title}',and the further description of the title is:'${description}'.` +\n          `Some answers are as follows:\\n${answers}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/stackoverflow/index.mjs",
    "content": "//TODO\n"
  },
  {
    "path": "src/content-script/site-adapters/youtube/index.mjs",
    "content": "import { cropText } from '../../../utils'\nimport { config } from '../index.mjs'\n\nexport default {\n  init: async (hostname, userConfig, getInput, mountComponent) => {\n    try {\n      let oldUrl = location.href\n      const checkUrlChange = async () => {\n        if (location.href !== oldUrl) {\n          oldUrl = location.href\n          mountComponent(config.youtube, userConfig)\n        }\n      }\n      window.setInterval(checkUrlChange, 500)\n    } catch (e) {\n      /* empty */\n    }\n  },\n  inputQuery: async () => {\n    try {\n      const docText = await (\n        await fetch(location.href, {\n          credentials: 'include',\n        })\n      ).text()\n\n      const subtitleUrlStartAt = docText.indexOf('https://www.youtube.com/api/timedtext')\n      if (subtitleUrlStartAt === -1) return\n\n      let subtitleUrl = docText.substring(subtitleUrlStartAt)\n      subtitleUrl = subtitleUrl.substring(0, subtitleUrl.indexOf('\"'))\n      subtitleUrl = subtitleUrl.replaceAll('\\\\u0026', '&')\n\n      let title = docText.substring(docText.indexOf('\"title\":\"') + '\"title\":\"'.length)\n      title = title.substring(0, title.indexOf('\",\"'))\n\n      const subtitleResponse = await fetch(subtitleUrl)\n      if (!subtitleResponse.ok) return\n      let subtitleData = await subtitleResponse.text()\n\n      let subtitleContent = ''\n      while (subtitleData.indexOf('\">') !== -1) {\n        subtitleData = subtitleData.substring(subtitleData.indexOf('\">') + 2)\n        subtitleContent += subtitleData.substring(0, subtitleData.indexOf('<')) + ','\n      }\n\n      await new Promise((r) => setTimeout(r, 1000))\n\n      return cropText(\n        `Provide a brief summary of the video using concise language and incorporating the video title.` +\n          `The video title is:\"${title}\".The subtitle content is as follows:\\n${subtitleContent}`,\n      )\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/site-adapters/zhihu/index.mjs",
    "content": "import { cropText } from '../../../utils'\n\nexport default {\n  inputQuery: async () => {\n    try {\n      const title = document.querySelector('.QuestionHeader-title')?.textContent\n      if (title) {\n        const description = document.querySelector('.QuestionRichText')?.textContent\n        const answer = document.querySelector('.AnswerItem .RichText')?.textContent\n\n        return cropText(\n          `以下是一个问答平台的提问与回答内容,给出相应的摘要,以及你对此的看法.问题是:\"${title}\",问题的进一步描述是:\"${description}\".` +\n            `其中一个回答如下:\\n${answer}`,\n        )\n      } else {\n        const title = document.querySelector('.Post-Title')?.textContent\n        const description = document.querySelector('.Post-RichText')?.textContent\n\n        if (title) {\n          return cropText(\n            `以下是一篇文章,给出相应的摘要,以及你对此的看法.标题是:\"${title}\",内容是:\\n\"${description}\"`,\n          )\n        }\n      }\n    } catch (e) {\n      console.log(e)\n    }\n  },\n}\n"
  },
  {
    "path": "src/content-script/styles.scss",
    "content": "[data-theme='auto'] {\n  @import 'github-markdown-css/github-markdown.css';\n  @media screen and (prefers-color-scheme: dark) {\n    @import 'highlight.js/scss/github-dark.scss';\n    --font-color: #c9d1d9;\n    --theme-color: #202124;\n    --theme-border-color: #3c4043;\n    --dragbar-color: #3c4043;\n  }\n  @media screen and (prefers-color-scheme: light) {\n    @import 'highlight.js/scss/github.scss';\n    --font-color: #24292f;\n    --theme-color: #eaecf0;\n    --theme-border-color: #aeafb2;\n    --dragbar-color: #dfe0e1;\n  }\n}\n\n[data-theme='dark'] {\n  @import 'highlight.js/scss/github-dark.scss';\n  @import 'github-markdown-css/github-markdown-dark.css';\n\n  --font-color: #c9d1d9;\n  --theme-color: #202124;\n  --theme-border-color: #3c4043;\n  --dragbar-color: #3c4043;\n}\n\n[data-theme='light'] {\n  @import 'highlight.js/scss/github.scss';\n  @import 'github-markdown-css/github-markdown-light.css';\n\n  --font-color: #24292f;\n  --theme-color: #eaecf0;\n  --theme-border-color: #aeafb2;\n  --dragbar-color: #ccced0;\n}\n\n.sidebar-free {\n  margin-left: 60px;\n}\n\n.chat-gpt-container {\n  width: 100%;\n  flex-basis: 0;\n  flex-grow: 1;\n  margin-bottom: 20px;\n\n  .gpt-inner {\n    border-radius: 8px;\n    border: 1px solid;\n    overflow: hidden;\n    border-color: var(--theme-border-color);\n    background-color: var(--theme-color);\n    margin: 0;\n\n    hr {\n      height: 1px;\n      background-color: var(--theme-border-color);\n      border: none;\n    }\n  }\n\n  .markdown-body {\n    padding: 5px 15px 10px;\n    background-color: var(--theme-color);\n    color: var(--font-color);\n    max-height: 800px;\n    overflow-y: auto;\n\n    ul,\n    ol {\n      padding-left: 1.5em;\n    }\n\n    ol {\n      list-style: none;\n      counter-reset: item;\n\n      li {\n        counter-increment: item;\n\n        &:before {\n          content: counter(item) '. ';\n          margin-left: -0.75em;\n        }\n      }\n    }\n  }\n\n  .icon-and-text {\n    color: var(--font-color);\n    display: flex;\n    align-items: center;\n    padding: 15px;\n    gap: 6px;\n  }\n\n  .manual-btn {\n    cursor: pointer;\n  }\n\n  .gpt-loading {\n    color: var(--font-color);\n    animation: gpt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n  }\n\n  .code-copy-btn {\n    color: inherit;\n    position: absolute;\n    right: 10px;\n    top: 3px;\n    cursor: pointer;\n  }\n\n  :is(.answer, .question, .error) {\n    font-size: 15px;\n    line-height: 1.6;\n    border-radius: 8px;\n    word-break: break-all;\n\n    pre {\n      margin-top: 10px;\n    }\n\n    & > p {\n      margin-bottom: 10px;\n    }\n\n    code {\n      white-space: pre-wrap;\n      word-break: break-word;\n      border-radius: 8px;\n\n      .hljs {\n        padding: 0;\n      }\n    }\n\n    p {\n      margin: 0;\n    }\n  }\n\n  .gpt-header {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 5px;\n    color: var(--font-color);\n\n    p {\n      font-weight: bold;\n    }\n\n    .gpt-feedback {\n      display: flex;\n      gap: 6px;\n      cursor: pointer;\n    }\n\n    .gpt-feedback-selected {\n      color: #f08080;\n    }\n\n    .gpt-util-icon {\n      cursor: pointer;\n    }\n  }\n\n  .error {\n    color: #ec4336;\n  }\n\n  .interact-input {\n    box-sizing: border-box;\n    padding: 5px 15px;\n    border: 0;\n    border-top: 1px solid var(--theme-border-color);\n    width: 100%;\n    background-color: var(--theme-color);\n    color: var(--font-color);\n    resize: none;\n    max-height: 240px;\n  }\n\n  .dragbar {\n    cursor: move;\n    width: 250px;\n    height: 12px;\n    border-radius: 10px;\n    background-color: var(--dragbar-color);\n  }\n}\n\n@keyframes gpt-pulse {\n  0%,\n  100% {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0.5;\n  }\n}\n\n.gpt-selection-toolbar {\n  display: flex;\n  align-items: center;\n  border-radius: 15px;\n  padding: 2px;\n  background-color: #ffffff;\n  box-shadow: 4px 2px 4px rgba(0, 0, 0, 0.2);\n}\n\n.gpt-selection-toolbar-button {\n  margin-left: 2px;\n  padding: 2px;\n  border-radius: 30px;\n  background-color: #ffffff;\n  color: #24292f;\n  cursor: pointer;\n}\n\n.gpt-selection-toolbar-button:hover {\n  background-color: #d4d5da;\n}\n\n.gpt-selection-window {\n  width: 600px;\n  height: auto;\n  border-radius: 8px;\n  background-color: var(--theme-color);\n  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);\n}\n"
  },
  {
    "path": "src/manifest.json",
    "content": "{\n  \"name\": \"ChatGPT for Search Engine\",\n  \"description\": \"Display ChatGPT response alongside Search Engine results\",\n  \"version\": \"2.0.0\",\n  \"manifest_version\": 3,\n  \"icons\": {\n    \"16\": \"logo.png\",\n    \"32\": \"logo.png\",\n    \"48\": \"logo.png\",\n    \"128\": \"logo.png\"\n  },\n  \"host_permissions\": [\n    \"https://*.openai.com/\"\n  ],\n  \"permissions\": [\n    \"storage\",\n    \"contextMenus\"\n  ],\n  \"background\": {\n    \"service_worker\": \"background.js\"\n  },\n  \"action\": {\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"https://*/*\"\n      ],\n      \"js\": [\n        \"shared.js\",\n        \"content-script.js\"\n      ],\n      \"css\": [\n        \"content-script.css\"\n      ]\n    }\n  ],\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\n        \"*.png\"\n      ],\n      \"matches\": [\n        \"<all_urls>\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/manifest.v2.json",
    "content": "{\n  \"name\": \"ChatGPT for Search Engine\",\n  \"description\": \"Display ChatGPT response alongside Search Engine results\",\n  \"version\": \"2.0.0\",\n  \"manifest_version\": 2,\n  \"icons\": {\n    \"16\": \"logo.png\",\n    \"32\": \"logo.png\",\n    \"48\": \"logo.png\",\n    \"128\": \"logo.png\"\n  },\n  \"permissions\": [\n    \"storage\",\n    \"contextMenus\",\n    \"https://*.openai.com/\"\n  ],\n  \"background\": {\n    \"scripts\": [\n      \"background.js\"\n    ]\n  },\n  \"browser_action\": {\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"https://*/*\"\n      ],\n      \"js\": [\n        \"shared.js\",\n        \"content-script.js\"\n      ],\n      \"css\": [\n        \"content-script.css\"\n      ]\n    }\n  ],\n  \"web_accessible_resources\": [\n    \"*.png\"\n  ]\n}"
  },
  {
    "path": "src/popup/Popup.jsx",
    "content": "import '@picocss/pico'\nimport { useEffect, useState } from 'react'\nimport {\n  setUserConfig,\n  getUserConfig,\n  TriggerMode,\n  ThemeMode,\n  defaultConfig,\n  Models,\n  isUsingApiKey,\n  languageList,\n} from '../config'\nimport { Tab, Tabs, TabList, TabPanel } from 'react-tabs'\nimport 'react-tabs/style/react-tabs.css'\nimport './styles.scss'\nimport { MarkGithubIcon } from '@primer/octicons-react'\nimport Browser from 'webextension-polyfill'\nimport PropTypes from 'prop-types'\nimport { config as toolsConfig } from '../content-script/selection-tools'\nimport wechatpay from './donation/wechatpay.jpg'\n\nfunction GeneralPart({ config, updateConfig }) {\n  const [balance, setBalance] = useState(null)\n\n  const getBalance = async () => {\n    const response = await fetch('https://api.openai.com/dashboard/billing/credit_grants', {\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${config.apiKey}`,\n      },\n    })\n    if (response.ok) setBalance((await response.json()).total_available.toFixed(2))\n  }\n\n  return (\n    <>\n      <label>\n        <legend>Trigger Mode</legend>\n        <select\n          required\n          onChange={(e) => {\n            const mode = e.target.value\n            updateConfig({ triggerMode: mode })\n          }}\n        >\n          {Object.entries(TriggerMode).map(([key, desc]) => {\n            return (\n              <option value={key} key={key} selected={key === config.triggerMode}>\n                {desc}\n              </option>\n            )\n          })}\n        </select>\n      </label>\n      <label>\n        <legend>Theme Mode</legend>\n        <select\n          required\n          onChange={(e) => {\n            const mode = e.target.value\n            updateConfig({ themeMode: mode })\n          }}\n        >\n          {Object.entries(ThemeMode).map(([key, desc]) => {\n            return (\n              <option value={key} key={key} selected={key === config.themeMode}>\n                {desc}\n              </option>\n            )\n          })}\n        </select>\n      </label>\n      <label>\n        <legend>API Mode</legend>\n        <span style=\"display: flex; gap: 15px;\">\n          <select\n            style={isUsingApiKey(config) ? 'width: 50%;' : undefined}\n            required\n            onChange={(e) => {\n              const modelName = e.target.value\n              updateConfig({ modelName: modelName })\n            }}\n          >\n            {Object.entries(Models).map(([key, model]) => {\n              return (\n                <option value={key} key={key} selected={key === config.modelName}>\n                  {model.desc}\n                </option>\n              )\n            })}\n          </select>\n          {isUsingApiKey(config) && (\n            <span style=\"width: 50%; display: flex; gap: 5px;\">\n              <input\n                type=\"password\"\n                value={config.apiKey}\n                placeholder=\"API Key\"\n                onChange={(e) => {\n                  const apiKey = e.target.value\n                  updateConfig({ apiKey: apiKey })\n                }}\n              />\n              {config.apiKey.length === 0 ? (\n                <a\n                  href=\"https://platform.openai.com/account/api-keys\"\n                  target=\"_blank\"\n                  rel=\"nofollow noopener noreferrer\"\n                >\n                  <button type=\"button\">Get</button>\n                </a>\n              ) : balance ? (\n                <button type=\"button\" onClick={getBalance}>\n                  {balance}\n                </button>\n              ) : (\n                <button type=\"button\" onClick={getBalance}>\n                  Balance\n                </button>\n              )}\n            </span>\n          )}\n        </span>\n      </label>\n      <label>\n        <legend>Preferred Language</legend>\n        <span style=\"display: flex; gap: 15px;\">\n          <select\n            required\n            onChange={(e) => {\n              const preferredLanguageKey = e.target.value\n              updateConfig({ preferredLanguage: preferredLanguageKey })\n            }}\n          >\n            {Object.entries(languageList).map(([k, v]) => {\n              return (\n                <option value={k} key={k} selected={k === config.preferredLanguage}>\n                  {v.native}\n                </option>\n              )\n            })}\n          </select>\n        </span>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={config.insertAtTop}\n          onChange={(e) => {\n            const checked = e.target.checked\n            updateConfig({ insertAtTop: checked })\n          }}\n        />\n        Insert chatGPT at the top of search results\n      </label>\n    </>\n  )\n}\n\nGeneralPart.propTypes = {\n  config: PropTypes.object.isRequired,\n  updateConfig: PropTypes.func.isRequired,\n}\n\nfunction AdvancedPart({ config, updateConfig }) {\n  return (\n    <>\n      <label>\n        Custom ChatGPT Web API Url\n        <input\n          type=\"text\"\n          value={config.customChatGptWebApiUrl}\n          onChange={(e) => {\n            const value = e.target.value\n            updateConfig({ customChatGptWebApiUrl: value })\n          }}\n        />\n      </label>\n      <label>\n        Custom ChatGPT Web API Path\n        <input\n          type=\"text\"\n          value={config.customChatGptWebApiPath}\n          onChange={(e) => {\n            const value = e.target.value\n            updateConfig({ customChatGptWebApiPath: value })\n          }}\n        />\n      </label>\n      <label>\n        Custom OpenAI API Url\n        <input\n          type=\"text\"\n          value={config.customOpenAiApiUrl}\n          onChange={(e) => {\n            const value = e.target.value\n            updateConfig({ customOpenAiApiUrl: value })\n          }}\n        />\n      </label>\n      <label>\n        Custom Site Regex:\n        <input\n          type=\"text\"\n          value={config.siteRegex}\n          onChange={(e) => {\n            const regex = e.target.value\n            updateConfig({ siteRegex: regex })\n          }}\n        />\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={config.userSiteRegexOnly}\n          onChange={(e) => {\n            const checked = e.target.checked\n            updateConfig({ userSiteRegexOnly: checked })\n          }}\n        />\n        Only use Custom Site Regex for website matching, ignore built-in rules\n      </label>\n      <br />\n      <label>\n        Input Query:\n        <input\n          type=\"text\"\n          value={config.inputQuery}\n          onChange={(e) => {\n            const query = e.target.value\n            updateConfig({ inputQuery: query })\n          }}\n        />\n      </label>\n      <label>\n        Append Query:\n        <input\n          type=\"text\"\n          value={config.appendQuery}\n          onChange={(e) => {\n            const query = e.target.value\n            updateConfig({ appendQuery: query })\n          }}\n        />\n      </label>\n      <label>\n        Prepend Query:\n        <input\n          type=\"text\"\n          value={config.prependQuery}\n          onChange={(e) => {\n            const query = e.target.value\n            updateConfig({ prependQuery: query })\n          }}\n        />\n      </label>\n    </>\n  )\n}\n\nAdvancedPart.propTypes = {\n  config: PropTypes.object.isRequired,\n  updateConfig: PropTypes.func.isRequired,\n}\n\nfunction SelectionTools({ config, updateConfig }) {\n  return (\n    <>\n      {config.selectionTools.map((key) => (\n        <label key={key}>\n          <input\n            type=\"checkbox\"\n            checked={config.activeSelectionTools.includes(key)}\n            onChange={(e) => {\n              const checked = e.target.checked\n              const activeSelectionTools = config.activeSelectionTools.filter((i) => i !== key)\n              if (checked) activeSelectionTools.push(key)\n              updateConfig({ activeSelectionTools })\n            }}\n          />\n          {toolsConfig[key].label}\n        </label>\n      ))}\n    </>\n  )\n}\n\nSelectionTools.propTypes = {\n  config: PropTypes.object.isRequired,\n  updateConfig: PropTypes.func.isRequired,\n}\n\nfunction SiteAdapters({ config, updateConfig }) {\n  return (\n    <>\n      {config.siteAdapters.map((key) => (\n        <label key={key}>\n          <input\n            type=\"checkbox\"\n            checked={config.activeSiteAdapters.includes(key)}\n            onChange={(e) => {\n              const checked = e.target.checked\n              const activeSiteAdapters = config.activeSiteAdapters.filter((i) => i !== key)\n              if (checked) activeSiteAdapters.push(key)\n              updateConfig({ activeSiteAdapters })\n            }}\n          />\n          {key}\n        </label>\n      ))}\n    </>\n  )\n}\n\nSiteAdapters.propTypes = {\n  config: PropTypes.object.isRequired,\n  updateConfig: PropTypes.func.isRequired,\n}\n\nfunction Donation() {\n  return (\n    <div style=\"display:flex;flex-direction:column;align-items:center;\">\n      <a\n        href=\"https://www.buymeacoffee.com/josStorer\"\n        target=\"_blank\"\n        rel=\"nofollow noopener noreferrer\"\n      >\n        <img\n          align=\"center\"\n          alt=\"buymeacoffee\"\n          src=\"https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg\"\n        />\n      </a>\n      <hr />\n      <>\n        Wechat Pay\n        <img alt=\"wechatpay\" src={wechatpay} />\n      </>\n    </div>\n  )\n}\n\n// eslint-disable-next-line react/prop-types\nfunction Footer({ currentVersion, latestVersion }) {\n  return (\n    <div className=\"footer\">\n      <div>\n        Current Version: {currentVersion}{' '}\n        {currentVersion === latestVersion ? (\n          '(Latest)'\n        ) : (\n          <>\n            (Latest:{' '}\n            <a\n              href={'https://github.com/josStorer/chatGPTBox/releases/tag/v' + latestVersion}\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              {latestVersion}\n            </a>\n            )\n          </>\n        )}\n      </div>\n      <div>\n        <a\n          href=\"https://github.com/josStorer/chatGPTBox\"\n          target=\"_blank\"\n          rel=\"nofollow noopener noreferrer\"\n        >\n          <span>Help | Changelog </span>\n          <MarkGithubIcon />\n        </a>\n      </div>\n    </div>\n  )\n}\n\nfunction Popup() {\n  const [config, setConfig] = useState(defaultConfig)\n  const [currentVersion, setCurrentVersion] = useState('')\n  const [latestVersion, setLatestVersion] = useState('')\n\n  const updateConfig = (value) => {\n    setConfig({ ...config, ...value })\n    setUserConfig(value)\n  }\n\n  useEffect(() => {\n    getUserConfig().then((config) => {\n      setConfig(config)\n      setCurrentVersion(Browser.runtime.getManifest().version.replace('v', ''))\n      fetch('https://api.github.com/repos/josstorer/chatGPTBox/releases/latest').then((response) =>\n        response.json().then((data) => {\n          setLatestVersion(data.tag_name.replace('v', ''))\n        }),\n      )\n    })\n  }, [])\n\n  useEffect(() => {\n    document.documentElement.dataset.theme = config.themeMode\n  }, [config.themeMode])\n\n  return (\n    <div className=\"container\">\n      <form>\n        <Tabs selectedTabClassName=\"popup-tab--selected\">\n          <TabList>\n            <Tab className=\"popup-tab\">General</Tab>\n            <Tab className=\"popup-tab\">SelectionTools</Tab>\n            <Tab className=\"popup-tab\">SiteAdapters</Tab>\n            <Tab className=\"popup-tab\">Advanced</Tab>\n            <Tab className=\"popup-tab\">Donation</Tab>\n          </TabList>\n\n          <TabPanel>\n            <GeneralPart config={config} updateConfig={updateConfig} />\n          </TabPanel>\n          <TabPanel>\n            <SelectionTools config={config} updateConfig={updateConfig} />\n          </TabPanel>\n          <TabPanel>\n            <SiteAdapters config={config} updateConfig={updateConfig} />\n          </TabPanel>\n          <TabPanel>\n            <AdvancedPart config={config} updateConfig={updateConfig} />\n          </TabPanel>\n          <TabPanel>\n            <Donation />\n          </TabPanel>\n        </Tabs>\n      </form>\n      <hr />\n      <Footer currentVersion={currentVersion} latestVersion={latestVersion} />\n    </div>\n  )\n}\n\nexport default Popup\n"
  },
  {
    "path": "src/popup/index.html",
    "content": "<html>\n  <head>\n    <title>ChatGPT for Search Engine</title>\n    <link rel=\"stylesheet\" href=\"popup.css\" />\n    <meta charset=\"UTF-8\">\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"shared.js\"></script>\n    <script src=\"popup.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/popup/index.jsx",
    "content": "import { render } from 'preact'\nimport Popup from './Popup'\n\nrender(<Popup />, document.getElementById('app'))\n"
  },
  {
    "path": "src/popup/styles.scss",
    "content": "[data-theme='auto'] {\n  @import 'github-markdown-css/github-markdown.css';\n  @media screen and (prefers-color-scheme: dark) {\n    @import 'highlight.js/scss/github-dark.scss';\n    --font-color: #c9d1d9;\n    --theme-color: #202124;\n    --active-color: #3c4043;\n  }\n  @media screen and (prefers-color-scheme: light) {\n    @import 'highlight.js/scss/github.scss';\n    --font-color: #24292f;\n    --theme-color: #ffffff;\n    --active-color: #eaecf0;\n  }\n}\n\n[data-theme='dark'] {\n  @import 'highlight.js/scss/github-dark.scss';\n  @import 'github-markdown-css/github-markdown-dark.css';\n\n  --font-color: #c9d1d9;\n  --theme-color: #202124;\n  --active-color: #3c4043;\n}\n\n[data-theme='light'] {\n  @import 'highlight.js/scss/github.scss';\n  @import 'github-markdown-css/github-markdown-light.css';\n\n  --font-color: #24292f;\n  --theme-color: #ffffff;\n  --active-color: #eaecf0;\n}\n\n.container {\n  width: 440px;\n  height: 560px;\n  padding: 20px;\n  overflow-y: auto;\n}\n\n.container legend {\n  font-weight: bold;\n}\n\n.container form {\n  margin-bottom: 0;\n}\n\n.container fieldset {\n  margin-bottom: 0;\n}\n\n.footer {\n  width: 90%;\n  position: absolute;\n  bottom: 10px;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  align-items: center;\n  background-color: var(--active-color);\n  border-radius: 5px;\n  padding: 6px;\n}\n\n.popup-tab {\n  display: inline-block;\n  position: relative;\n  list-style: none;\n  padding: 6px 12px 0;\n  cursor: pointer;\n  border-radius: 5px;\n  margin-right: 2px;\n  background-color: var(--theme-color);\n  color: var(--font-color);\n\n  &--selected {\n    background: var(--active-color);\n  }\n}\n"
  },
  {
    "path": "src/utils/create-element-at-position.mjs",
    "content": "export function createElementAtPosition(x = 0, y = 0, zIndex = 2147483647) {\n  const element = document.createElement('div')\n  element.style.position = 'fixed'\n  element.style.zIndex = zIndex\n  element.style.left = x + 'px'\n  element.style.top = y + 'px'\n  document.documentElement.appendChild(element)\n  return element\n}\n"
  },
  {
    "path": "src/utils/crop-text.mjs",
    "content": "// MIT License\n//\n// Copyright (c) 2023 josStorer\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { maxResponseTokenLength } from '../config.mjs'\nimport { encode } from '@nem035/gpt-3-encoder'\n\n// TODO add model support\nexport function cropText(\n  text,\n  maxLength = 3900 - maxResponseTokenLength,\n  startLength = 400,\n  endLength = 300,\n  tiktoken = true,\n) {\n  const splits = text.split(/[,，。?？!！;；]/).map((s) => s.trim())\n  const splitsLength = splits.map((s) => (tiktoken ? encode(s).length : s.length))\n  const length = splitsLength.reduce((sum, length) => sum + length, 0)\n\n  const cropLength = length - startLength - endLength\n  const cropTargetLength = maxLength - startLength - endLength\n  const cropPercentage = cropTargetLength / cropLength\n  const cropStep = Math.max(0, 1 / cropPercentage - 1)\n\n  if (cropStep === 0) return text\n\n  let croppedText = ''\n  let currentLength = 0\n  let currentIndex = 0\n  let currentStep = 0\n\n  for (; currentIndex < splits.length; currentIndex++) {\n    if (currentLength + splitsLength[currentIndex] + 1 <= startLength) {\n      croppedText += splits[currentIndex] + ','\n      currentLength += splitsLength[currentIndex] + 1\n    } else if (currentLength + splitsLength[currentIndex] + 1 + endLength <= maxLength) {\n      if (currentStep < cropStep) {\n        currentStep++\n      } else {\n        croppedText += splits[currentIndex] + ','\n        currentLength += splitsLength[currentIndex] + 1\n        currentStep = currentStep - cropStep\n      }\n    } else {\n      break\n    }\n  }\n\n  let endPart = ''\n  let endPartLength = 0\n  for (let i = splits.length - 1; endPartLength + splitsLength[i] <= endLength; i--) {\n    endPart = splits[i] + ',' + endPart\n    endPartLength += splitsLength[i] + 1\n  }\n  currentLength += endPartLength\n  croppedText += endPart\n\n  console.log(\n    `maxLength: ${maxLength}\\n` +\n      // `croppedTextLength: ${tiktoken ? encode(croppedText).length : croppedText.length}\\n` +\n      `desiredLength: ${currentLength}\\n` +\n      `content: ${croppedText}`,\n  )\n  return croppedText\n}\n"
  },
  {
    "path": "src/utils/ends-with-question-mark.mjs",
    "content": "export function endsWithQuestionMark(question) {\n  return (\n    question.endsWith('?') || // ASCII\n    question.endsWith('？') || // Chinese/Japanese\n    question.endsWith('؟') || // Arabic\n    question.endsWith('⸮') // Arabic\n  )\n}\n"
  },
  {
    "path": "src/utils/fetch-sse.mjs",
    "content": "import { createParser } from 'eventsource-parser'\nimport { streamAsyncIterable } from './stream-async-iterable'\n\nexport async function fetchSSE(resource, options) {\n  const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options\n  const resp = await fetch(resource, fetchOptions)\n  if (!resp.ok) {\n    await onError(resp)\n  }\n  const parser = createParser((event) => {\n    if (event.type === 'event') {\n      onMessage(event.data)\n    }\n  })\n  let hasStarted = false\n  for await (const chunk of streamAsyncIterable(resp.body)) {\n    const str = new TextDecoder().decode(chunk)\n    parser.feed(str)\n\n    if (!hasStarted) {\n      hasStarted = true\n      await onStart(str)\n    }\n  }\n  await onEnd()\n}\n"
  },
  {
    "path": "src/utils/get-conversation-pairs.mjs",
    "content": "export function getConversationPairs(records, isChatgpt) {\n  let pairs\n  if (isChatgpt) {\n    pairs = []\n    for (const record of records) {\n      pairs.push({ role: 'user', content: record['question'] })\n      pairs.push({ role: 'assistant', content: record['answer'] })\n    }\n  } else {\n    pairs = ''\n    for (const record of records) {\n      pairs += 'Human:' + record.question + '\\nAI:' + record.answer + '\\n'\n    }\n  }\n\n  return pairs\n}\n"
  },
  {
    "path": "src/utils/get-possible-element-by-query-selector.mjs",
    "content": "export function getPossibleElementByQuerySelector(queryArray) {\n  for (const query of queryArray) {\n    if (query) {\n      try {\n        const element = document.querySelector(query)\n        if (element) {\n          return element\n        }\n      } catch (e) {\n        /* empty */\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/index.mjs",
    "content": "export * from './create-element-at-position'\nexport * from './crop-text'\nexport * from './ends-with-question-mark'\nexport * from './fetch-sse'\nexport * from './get-conversation-pairs'\nexport * from './get-possible-element-by-query-selector'\nexport * from './init-session'\nexport * from './is-mobile'\nexport * from './is-safari'\nexport * from './limited-fetch'\nexport * from './set-element-position-in-viewport'\nexport * from './stream-async-iterable'\nexport * from './update-ref-height'\n"
  },
  {
    "path": "src/utils/init-session.mjs",
    "content": "/**\n * @typedef {object} Session\n * @property {string|null} question\n * @property {string|null} conversationId - chatGPT web mode\n * @property {string|null} messageId - chatGPT web mode\n * @property {string|null} parentMessageId - chatGPT web mode\n * @property {Object[]|null} conversationRecords\n * @property {bool|null} useApiKey\n */\n/**\n * @param {Session} session\n * @returns {Session}\n */\nexport function initSession({\n  question = null,\n  conversationId = null,\n  messageId = null,\n  parentMessageId = null,\n  conversationRecords = [],\n  useApiKey = null,\n} = {}) {\n  return {\n    question,\n    conversationId,\n    messageId,\n    parentMessageId,\n    conversationRecords,\n    useApiKey,\n  }\n}\n"
  },
  {
    "path": "src/utils/is-mobile.mjs",
    "content": "// https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser\n\nexport async function isMobile() {\n  let check = false\n  ;(function (a) {\n    if (\n      /(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(\n        a,\n      ) ||\n      /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(\n        a.substr(0, 4),\n      )\n    )\n      check = true\n  })(navigator.userAgent || navigator.vendor || window.opera)\n  return check\n}\n"
  },
  {
    "path": "src/utils/is-safari.mjs",
    "content": "export function isSafari() {\n  return navigator.vendor === 'Apple Computer, Inc.'\n}\n"
  },
  {
    "path": "src/utils/limited-fetch.mjs",
    "content": "// https://stackoverflow.com/questions/64304365/stop-request-after-x-amount-is-fetched\n\nexport async function limitedFetch(url, maxBytes) {\n  return new Promise((resolve, reject) => {\n    try {\n      const xhr = new XMLHttpRequest()\n      xhr.onprogress = (ev) => {\n        if (ev.loaded < maxBytes) return\n        resolve(ev.target.responseText.substring(0, maxBytes))\n        xhr.abort()\n      }\n      xhr.onload = (ev) => {\n        resolve(ev.target.responseText.substring(0, maxBytes))\n      }\n      xhr.onerror = (ev) => {\n        reject(new Error(ev.target.status))\n      }\n\n      xhr.open('GET', url)\n      xhr.send()\n    } catch (err) {\n      reject(err)\n    }\n  })\n}\n"
  },
  {
    "path": "src/utils/set-element-position-in-viewport.mjs",
    "content": "export function setElementPositionInViewport(element, x = 0, y = 0) {\n  const retX = Math.min(window.innerWidth - element.offsetWidth, Math.max(0, x))\n  const retY = Math.min(window.innerHeight - element.offsetHeight, Math.max(0, y))\n  element.style.left = retX + 'px'\n  element.style.top = retY + 'px'\n  return { x: retX, y: retY }\n}\n"
  },
  {
    "path": "src/utils/stream-async-iterable.mjs",
    "content": "export async function* streamAsyncIterable(stream) {\n  const reader = stream.getReader()\n  try {\n    while (true) {\n      const { done, value } = await reader.read()\n      if (done) {\n        return\n      }\n      yield value\n    }\n  } finally {\n    reader.releaseLock()\n  }\n}\n"
  },
  {
    "path": "src/utils/update-ref-height.mjs",
    "content": "export function updateRefHeight(ref) {\n  ref.current.style.height = 'auto'\n  const computed = window.getComputedStyle(ref.current)\n  const height =\n    parseInt(computed.getPropertyValue('border-top-width'), 10) +\n    parseInt(computed.getPropertyValue('padding-top'), 10) +\n    ref.current.scrollHeight +\n    parseInt(computed.getPropertyValue('padding-bottom'), 10) +\n    parseInt(computed.getPropertyValue('border-bottom-width'), 10)\n  ref.current.style.height = `${height}px`\n}\n"
  }
]