├── .prettierignore
├── src
├── base.css
├── global.d.ts
├── logo.png
├── analytics.ts
├── options
│ ├── index.tsx
│ ├── index.html
│ ├── PromptCard.tsx
│ ├── AddNewPromptModal.tsx
│ ├── ProviderSelect.tsx
│ └── App.tsx
├── popup
│ ├── index.tsx
│ ├── index.html
│ └── App.tsx
├── messaging.ts
├── _locales
│ ├── ko
│ │ └── messages.json
│ └── en
│ │ └── messages.json
├── background
│ ├── stream-async-iterable.ts
│ ├── types.ts
│ ├── fetch-sse.ts
│ ├── index.ts
│ └── providers
│ │ └── upstage.ts
├── utils.ts
├── content-script
│ ├── ChatGPTCard.tsx
│ ├── ChatGPTContainer.tsx
│ ├── utils.ts
│ ├── search-engine-configs.ts
│ ├── Promotion.tsx
│ ├── styles.scss
│ ├── ChatGPTFeedback.tsx
│ ├── index.tsx
│ ├── ChatGPTQuery.tsx
│ ├── dark.scss
│ └── light.scss
├── api.ts
├── manifest.v2.json
├── manifest.json
└── config.ts
├── .gitignore
├── .husky
└── pre-commit
├── screenshots
├── brave.png
├── opera.png
└── extension.png
├── tailwind.config.cjs
├── .prettierrc
├── tsconfig.json
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── pre-release-build.yml
├── .eslintrc.json
├── README.md
├── package.json
└── LICENSE
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
--------------------------------------------------------------------------------
/src/base.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | background.js
4 | .DS_Store
5 | *.zip
6 | .env
7 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png' {
2 | const value: any
3 | export = value
4 | }
5 |
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkimForks/chatgpt-arxiv-extension/HEAD/src/logo.png
--------------------------------------------------------------------------------
/src/analytics.ts:
--------------------------------------------------------------------------------
1 | export function captureEvent(event: string, properties?: object) {
2 | // TODO
3 | }
4 |
--------------------------------------------------------------------------------
/screenshots/brave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkimForks/chatgpt-arxiv-extension/HEAD/screenshots/brave.png
--------------------------------------------------------------------------------
/screenshots/opera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkimForks/chatgpt-arxiv-extension/HEAD/screenshots/opera.png
--------------------------------------------------------------------------------
/screenshots/extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkimForks/chatgpt-arxiv-extension/HEAD/screenshots/extension.png
--------------------------------------------------------------------------------
/src/options/index.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import App from './App'
3 |
4 | render(, document.getElementById('app')!)
5 |
--------------------------------------------------------------------------------
/src/popup/index.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import App from './App'
3 |
4 | render(, document.getElementById('app')!)
5 |
--------------------------------------------------------------------------------
/src/messaging.ts:
--------------------------------------------------------------------------------
1 | export interface Answer {
2 | text: string
3 | messageId: string
4 | conversationId: string
5 | parentMessageId: string
6 | }
7 |
--------------------------------------------------------------------------------
/src/_locales/ko/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "Solar Arxiv"
4 | },
5 | "appDesc": {
6 | "message": "Arxiv 빠른 요약을 제공합니다."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "Solar Arxiv"
4 | },
5 | "appDesc": {
6 | "message": "Provide summary of arxiv papers"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | corePlugins: {
4 | preflight: false,
5 | },
6 | content: ['./src/**/*.tsx'],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPT for Google
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "bracketSpacing": true,
8 | "overrides": [
9 | {
10 | "files": ".prettierrc",
11 | "options": {
12 | "parser": "json"
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "module": "esnext",
5 | "target": "es2018",
6 | "allowJs": true,
7 | "esModuleInterop": true,
8 | "jsx": "react-jsx",
9 | "noEmit": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "moduleResolution": "node"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ChatGPT for Google
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Desktop (please complete the following information):**
14 | - OS: [e.g. Windows]
15 | - Browser [e.g. chrome, brave]
16 |
--------------------------------------------------------------------------------
/src/background/stream-async-iterable.ts:
--------------------------------------------------------------------------------
1 | export async function* streamAsyncIterable(stream: ReadableStream) {
2 | const reader = stream.getReader()
3 | try {
4 | while (true) {
5 | const { done, value } = await reader.read()
6 | if (done) {
7 | return
8 | }
9 | yield value
10 | }
11 | } finally {
12 | reader.releaseLock()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { Theme } from './config'
3 |
4 | export function detectSystemColorScheme() {
5 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
6 | return Theme.Dark
7 | }
8 | return Theme.Light
9 | }
10 |
11 | export function getExtensionVersion() {
12 | return Browser.runtime.getManifest().version
13 | }
14 |
--------------------------------------------------------------------------------
/src/background/types.ts:
--------------------------------------------------------------------------------
1 | import { Answer } from '../messaging'
2 |
3 | export type Event =
4 | | {
5 | type: 'answer'
6 | data: Answer
7 | }
8 | | {
9 | type: 'done'
10 | }
11 |
12 | export interface GenerateAnswerParams {
13 | prompt: string
14 | previousMessages: object[]
15 | onEvent: (event: Event) => void
16 | signal?: AbortSignal
17 | conversationId?: string
18 | parentMessageId?: string
19 | }
20 |
21 | export interface Provider {
22 | generateAnswer(params: GenerateAnswerParams): Promise<{ cleanup?: () => void }>
23 | }
24 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "env": {
4 | "browser": true
5 | },
6 | "plugins": ["@typescript-eslint"],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/recommended",
10 | "plugin:react/recommended",
11 | "plugin:react-hooks/recommended",
12 | "prettier"
13 | ],
14 | "overrides": [],
15 | "parserOptions": {
16 | "ecmaVersion": "latest",
17 | "sourceType": "module"
18 | },
19 | "rules": {
20 | "react/react-in-jsx-scope": "off"
21 | },
22 | "ignorePatterns": ["build/**"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/background/fetch-sse.ts:
--------------------------------------------------------------------------------
1 | import { createParser } from 'eventsource-parser'
2 | import { isEmpty } from 'lodash-es'
3 | import { streamAsyncIterable } from './stream-async-iterable.js'
4 |
5 | export async function fetchSSE(
6 | resource: string,
7 | options: RequestInit & { onMessage: (message: string) => void },
8 | ) {
9 | const { onMessage, ...fetchOptions } = options
10 | const resp = await fetch(resource, fetchOptions)
11 | if (!resp.ok) {
12 | const error = await resp.json().catch(() => ({}))
13 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
14 | }
15 | const parser = createParser((event) => {
16 | if (event.type === 'event') {
17 | onMessage(event.data)
18 | }
19 | })
20 | for await (const chunk of streamAsyncIterable(resp.body!)) {
21 | const str = new TextDecoder().decode(chunk)
22 | parser.feed(str)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release-build.yml:
--------------------------------------------------------------------------------
1 | name: Pre-release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | build:
8 | runs-on: ubuntu-22.04
9 |
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: actions/setup-node@v3
13 | with:
14 | node-version: 18
15 | - run: npm install
16 | - run: npm run build
17 |
18 | - uses: josStorer/get-current-time@v2.0.2
19 | id: current-time
20 | with:
21 | format: YY_MMDD_HH_mm
22 |
23 | - uses: actions/upload-artifact@v3
24 | with:
25 | name: Chromium_ChatGPT_Extension_Build_${{ steps.current-time.outputs.formattedTime }}
26 | path: build/chromium/*
27 |
28 | - uses: actions/upload-artifact@v3
29 | with:
30 | name: Firefox_ChatGPT_Extension_Build_${{ steps.current-time.outputs.formattedTime }}
31 | path: build/firefox/*
32 |
--------------------------------------------------------------------------------
/src/content-script/ChatGPTCard.tsx:
--------------------------------------------------------------------------------
1 | import { SearchIcon } from '@primer/octicons-react'
2 | import { useState } from 'preact/hooks'
3 | import { TriggerMode } from '../config'
4 | import ChatGPTQuery, { QueryStatus } from './ChatGPTQuery'
5 |
6 | interface Props {
7 | question: string
8 | promptSource: string
9 | triggerMode: TriggerMode
10 | onStatusChange?: (status: QueryStatus) => void
11 | }
12 |
13 | function ChatGPTCard(props: Props) {
14 | const [triggered, setTriggered] = useState(false)
15 |
16 | if (props.triggerMode === TriggerMode.Always) {
17 | return
18 | }
19 | if (triggered) {
20 | return
21 | }
22 | return (
23 | setTriggered(true)}>
24 | Ask arXivGPT to summarize
25 |
26 | )
27 | }
28 |
29 | export default ChatGPTCard
30 |
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import { getExtensionVersion } from './utils'
2 |
3 | const API_HOST = 'https://chatgpt4google.com'
4 | // const API_HOST = 'http://localhost:3000'
5 |
6 | export interface PromotionResponse {
7 | url: string
8 | title?: string
9 | text?: string
10 | image?: { url: string; size?: number }
11 | footer?: { text: string; url: string }
12 | label?: { text: string; url: string }
13 | }
14 |
15 | export async function fetchPromotion(): Promise {
16 | return fetch(`${API_HOST}/api/p`, {
17 | headers: {
18 | 'x-version': getExtensionVersion(),
19 | },
20 | }).then((r) => r.json())
21 | }
22 |
23 | export async function fetchExtensionConfigs(): Promise<{
24 | chatgpt_webapp_model_name: string
25 | openai_model_names: string[]
26 | }> {
27 | return fetch(`${API_HOST}/api/config`, {
28 | headers: {
29 | 'x-version': getExtensionVersion(),
30 | },
31 | }).then((r) => r.json())
32 | }
33 |
--------------------------------------------------------------------------------
/src/manifest.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_appName__",
3 | "description": "__MSG_appDesc__",
4 | "default_locale": "en",
5 | "version": "2024.07.02",
6 | "manifest_version": 2,
7 | "icons": {
8 | "16": "logo.png",
9 | "32": "logo.png",
10 | "48": "logo.png",
11 | "128": "logo.png"
12 | },
13 | "permissions": ["storage", "https://*.openai.com/"],
14 | "background": {
15 | "scripts": ["background.js"]
16 | },
17 | "browser_action": {
18 | "default_popup": "popup.html"
19 | },
20 | "options_ui": {
21 | "page": "options.html",
22 | "open_in_tab": true
23 | },
24 | "content_scripts": [
25 | {
26 | "matches": [
27 | "https://arxiv.org/*",
28 | "https://www.biorxiv.org/content/*",
29 | "https://pubmed.ncbi.nlm.nih.gov/*",
30 | "https://ieeexplore.ieee.org/document/*",
31 | "https://dl.acm.org/doi/*"
32 | ],
33 | "js": ["content-script.js"],
34 | "css": ["content-script.css"]
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_appName__",
3 | "description": "__MSG_appDesc__",
4 | "default_locale": "en",
5 | "version": "2024.07.02",
6 | "manifest_version": 3,
7 | "icons": {
8 | "16": "logo.png",
9 | "32": "logo.png",
10 | "48": "logo.png",
11 | "128": "logo.png"
12 | },
13 | "host_permissions": ["https://*.openai.com/"],
14 | "permissions": ["storage"],
15 | "background": {
16 | "service_worker": "background.js"
17 | },
18 | "action": {
19 | "default_popup": "popup.html"
20 | },
21 | "options_ui": {
22 | "page": "options.html",
23 | "open_in_tab": true
24 | },
25 | "content_scripts": [
26 | {
27 | "matches": [
28 | "https://arxiv.org/*",
29 | "https://www.biorxiv.org/content/*",
30 | "https://pubmed.ncbi.nlm.nih.gov/*",
31 | "https://ieeexplore.ieee.org/document/*",
32 | "https://dl.acm.org/doi/*"
33 | ],
34 | "js": ["content-script.js"],
35 | "css": ["content-script.css"]
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/content-script/ChatGPTContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import useSWRImmutable from 'swr/immutable'
3 | import { fetchPromotion } from '../api'
4 | import { TriggerMode } from '../config'
5 | import ChatGPTCard from './ChatGPTCard'
6 | import { QueryStatus } from './ChatGPTQuery'
7 |
8 | interface Props {
9 | question: string
10 | promptSource: string
11 | triggerMode: TriggerMode
12 | }
13 |
14 | function ChatGPTContainer(props: Props) {
15 | const [queryStatus, setQueryStatus] = useState()
16 | const query = useSWRImmutable(
17 | queryStatus === 'success' ? 'promotion' : undefined,
18 | fetchPromotion,
19 | { shouldRetryOnError: false },
20 | )
21 | return (
22 | <>
23 |
24 |
30 |
31 | >
32 | )
33 | }
34 |
35 | export default ChatGPTContainer
36 |
--------------------------------------------------------------------------------
/src/content-script/utils.ts:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 | export function getPossibleElementByQuerySelector(
4 | queryArray: string[],
5 | ): T | undefined {
6 | for (const query of queryArray) {
7 | const element = document.querySelector(query)
8 | if (element) {
9 | return element as T
10 | }
11 | }
12 | }
13 |
14 | export function endsWithQuestionMark(question: string) {
15 | return (
16 | question.endsWith('?') || // ASCII
17 | question.endsWith('?') || // Chinese/Japanese
18 | question.endsWith('؟') || // Arabic
19 | question.endsWith('⸮') // Arabic
20 | )
21 | }
22 |
23 | export function isBraveBrowser() {
24 | return (navigator as any).brave?.isBrave()
25 | }
26 |
27 | export async function shouldShowRatingTip() {
28 | const { ratingTipShowTimes = 0 } = await Browser.storage.local.get('ratingTipShowTimes')
29 | if (ratingTipShowTimes >= 5) {
30 | return false
31 | }
32 | await Browser.storage.local.set({ ratingTipShowTimes: ratingTipShowTimes + 1 })
33 | return ratingTipShowTimes >= 2
34 | }
35 |
36 | export function isValidHttpUrl(string: string) {
37 | let url
38 | try {
39 | url = new URL(string)
40 | } catch (_) {
41 | return false
42 | }
43 | return url.protocol === 'http:' || url.protocol === 'https:'
44 | }
45 |
--------------------------------------------------------------------------------
/src/content-script/search-engine-configs.ts:
--------------------------------------------------------------------------------
1 | export interface SearchEngine {
2 | inputQuery: string[]
3 | bodyQuery: string[]
4 | sidebarContainerQuery: string[]
5 | appendContainerQuery: string[]
6 | watchRouteChange?: (callback: () => void) => void
7 | }
8 |
9 | export const config: Record = {
10 | google: {
11 | inputQuery: ["input[name='q']"],
12 | bodyQuery: ['#place-'],
13 | sidebarContainerQuery: ['#rhs'],
14 | appendContainerQuery: ['#rcnt'],
15 | },
16 | arxiv: {
17 | inputQuery: ["input[name='query']"],
18 | bodyQuery: ['#abs'],
19 | sidebarContainerQuery: ['div[class="metatable"]'],
20 | appendContainerQuery: [],
21 | },
22 | biorxiv: {
23 | inputQuery: ["input[name='query']"],
24 | bodyQuery: ['div[class="inside"]'],
25 | sidebarContainerQuery: ['#panels-ajax-tab-container-highwire_article_tabs'],
26 | appendContainerQuery: [],
27 | },
28 | pubmed: {
29 | inputQuery: [],
30 | bodyQuery: ['#abstract'],
31 | sidebarContainerQuery: ['#copyright'],
32 | appendContainerQuery: [],
33 | },
34 | ieeexplore: {
35 | inputQuery: [],
36 | bodyQuery: ['div.abstract-text.row div.u-mb-1 div'],
37 | sidebarContainerQuery: ['div.u-pb-1.stats-document-abstract-publishedIn'],
38 | appendContainerQuery: [],
39 | },
40 | acm: {
41 | inputQuery: [],
42 | bodyQuery: ['div.abstractInFull > p'],
43 | sidebarContainerQuery: ['div.pb-dropzone[data-pb-dropzone="pubContentAccessDenialDropzone"]'],
44 | appendContainerQuery: [],
45 | },
46 | }
47 |
--------------------------------------------------------------------------------
/src/content-script/Promotion.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { captureEvent } from '../analytics'
3 | import type { PromotionResponse } from '../api'
4 |
5 | interface Props {
6 | data: PromotionResponse
7 | }
8 |
9 | function Promotion({ data }: Props) {
10 | const capturePromotionClick = useCallback(() => {
11 | captureEvent('click_promotion', { link: data.url })
12 | }, [data.url])
13 |
14 | return (
15 |
22 |
23 | {!!data.image && (
24 |

29 | )}
30 |
31 |
32 | {!!data.title &&
{data.title}
}
33 | {!!data.text &&
{data.text}
}
34 |
35 |
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default Promotion
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # arXivGPT
2 |
3 | [link-chrome]: https://chrome.google.com/webstore/detail/arxivgpt/fbbfpcjhnnklhmncjickdipdlhoddjoh?hl=en&authuser=0 'Chrome Web Store'
4 |
5 | [
][link-chrome]
6 |
7 | ## Screenshot
8 |
9 |
10 |
11 | ## Avaiable Sites (TBA or TBA as a configuration feature)
12 | * "https://arxiv.org/*",
13 | * "https://www.biorxiv.org/content/*",
14 | * "https://pubmed.ncbi.nlm.nih.gov/*",
15 | * "https://ieeexplore.ieee.org/document/*"
16 |
17 | ## Custom Prompt
18 | You can change the prompt.
19 |
20 |
21 | ## Troubleshooting
22 |
23 | ### How to make it work in Brave
24 |
25 | 
26 |
27 | Disable "Prevent sites from fingerprinting me based on my language preferences" in `brave://settings/shields`
28 |
29 | ### How to make it work in Opera
30 |
31 | 
32 |
33 | Enable "Allow access to search page results" in the extension management page
34 |
35 | ## Build from source
36 |
37 | 1. Clone the repo
38 | 2. Install dependencies with `npm`
39 | 3. `npm run build`
40 | 4. Load `build/chromium/` or `build/firefox/` directory to your browser
41 |
42 | ## Credit
43 |
44 | This project is inspired by [ZohaibAhmed/ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) and https://github.com/wong2/chatgpt-google-extension
45 | This project is inspired by [ZohaibAhmed/ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) and wong2/chatgpt-google-extension
46 |
--------------------------------------------------------------------------------
/src/content-script/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'light.scss';
2 | @import 'dark.scss';
3 |
4 | .chat-gpt-container {
5 | margin-bottom: 30px;
6 | flex-basis: 0;
7 | flex-grow: 1;
8 | z-index: 1;
9 |
10 | .chat-gpt-card {
11 | border: 1px solid;
12 | border-radius: 8px;
13 | padding: 15px;
14 | line-height: 1.5;
15 | }
16 |
17 | &.sidebar-free {
18 | margin-left: 30px;
19 | height: fit-content;
20 | }
21 |
22 | p {
23 | margin: 0;
24 | }
25 |
26 | a.gpt-promotion-link {
27 | &:hover {
28 | text-decoration: none;
29 | }
30 | a:visited {
31 | color: inherit;
32 | }
33 | }
34 |
35 | .icon-and-text {
36 | display: flex;
37 | align-items: center;
38 | gap: 6px;
39 | }
40 |
41 | #gpt-answer.markdown-body.gpt-markdown {
42 | font-size: 15px;
43 | line-height: 1.6;
44 |
45 | pre {
46 | margin-top: 10px;
47 | }
48 |
49 | & > p:not(:last-child) {
50 | margin-bottom: 10px;
51 | }
52 |
53 | pre code {
54 | white-space: pre-wrap;
55 | word-break: break-all;
56 | }
57 |
58 | pre code.hljs {
59 | padding: 0;
60 | background-color: transparent;
61 | }
62 |
63 | ol li {
64 | list-style: disc;
65 | }
66 |
67 | img {
68 | width: 100%;
69 | }
70 |
71 | a {
72 | text-decoration: underline;
73 | &:visited {
74 | color: unset;
75 | }
76 | }
77 | }
78 |
79 | .gpt-header {
80 | display: flex;
81 | flex-direction: row;
82 | justify-content: flex-start;
83 | align-items: center;
84 | margin-bottom: 10px;
85 | gap: 5px;
86 |
87 | .gpt-feedback {
88 | margin-left: auto;
89 | display: flex;
90 | gap: 6px;
91 | cursor: pointer;
92 | }
93 |
94 | .gpt-feedback-selected {
95 | color: #ff6347;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/background/index.ts:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 | import { getProviderConfigs, ProviderType } from '../config'
3 | import { UpstageProvider } from './providers/upstage'
4 |
5 | async function generateAnswers(
6 | port: Browser.Runtime.Port,
7 | question: string,
8 | conversationId: string | undefined,
9 | parentMessageId: string | undefined,
10 | previousMessages: object[],
11 | ) {
12 | const providerConfigs = await getProviderConfigs()
13 |
14 | const { apiKey, model } = providerConfigs.configs[ProviderType.GPT3]!
15 | const provider = new UpstageProvider(apiKey, model)
16 |
17 | const controller = new AbortController()
18 | port.onDisconnect.addListener(() => {
19 | controller.abort()
20 | cleanup?.()
21 | })
22 |
23 | const { cleanup } = await provider.generateAnswer({
24 | prompt: question,
25 | previousMessages: previousMessages,
26 | signal: controller.signal,
27 | onEvent(event) {
28 | if (event.type === 'done') {
29 | port.postMessage({ event: 'DONE' })
30 | return
31 | }
32 | port.postMessage(event.data)
33 | },
34 | conversationId: conversationId,
35 | parentMessageId: parentMessageId,
36 | })
37 | }
38 |
39 | Browser.runtime.onConnect.addListener((port) => {
40 | port.onMessage.addListener(async (msg) => {
41 | console.debug('received msg', msg)
42 | try {
43 | await generateAnswers(
44 | port,
45 | msg.question,
46 | msg.conversationId,
47 | msg.parentMessageId,
48 | msg.previousMessages,
49 | )
50 | } catch (err: any) {
51 | console.error(err)
52 | const error_msg = '\nPlease check your API key and model name in the extension options.'
53 | port.postMessage({ text: err.message + error_msg })
54 | }
55 | })
56 | })
57 |
58 | Browser.runtime.onInstalled.addListener((details) => {
59 | if (details.reason === 'install') {
60 | Browser.runtime.openOptionsPage()
61 | }
62 | })
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-gpt-google-extension",
3 | "author": "wong2",
4 | "scripts": {
5 | "build": "node build.mjs",
6 | "lint": "eslint --ext .js,.mjs,.jsx .",
7 | "lint:fix": "eslint --ext .js,.mjs,.jsx . --fix",
8 | "prepare": "husky install",
9 | "watch": "chokidar src -c 'npm run build'"
10 | },
11 | "dependencies": {
12 | "@geist-ui/core": "^2.3.8",
13 | "@geist-ui/icons": "^1.0.2",
14 | "@primer/octicons-react": "^17.9.0",
15 | "eventsource-parser": "^0.0.5",
16 | "expiry-map": "^2.0.0",
17 | "github-markdown-css": "^5.1.0",
18 | "inter-ui": "^3.19.3",
19 | "lodash-es": "^4.17.21",
20 | "preact": "^10.11.3",
21 | "prop-types": "^15.8.1",
22 | "punycode": "^2.1.1",
23 | "react": "npm:@preact/compat@^17.1.2",
24 | "react-dom": "npm:@preact/compat@^17.1.2",
25 | "react-markdown": "^8.0.4",
26 | "rehype-highlight": "^6.0.0",
27 | "swr": "^2.0.0",
28 | "uuid": "^9.0.0"
29 | },
30 | "devDependencies": {
31 | "@types/fs-extra": "^9.0.13",
32 | "@types/lodash-es": "^4.17.6",
33 | "@types/uuid": "^9.0.0",
34 | "@types/webextension-polyfill": "^0.9.2",
35 | "@typescript-eslint/eslint-plugin": "^5.47.0",
36 | "@typescript-eslint/parser": "^5.47.0",
37 | "archiver": "^5.3.1",
38 | "autoprefixer": "^10.4.13",
39 | "chokidar-cli": "^3.0.0",
40 | "dotenv": "^16.0.3",
41 | "esbuild": "^0.17.4",
42 | "esbuild-style-plugin": "^1.6.1",
43 | "eslint": "^8.30.0",
44 | "eslint-config-prettier": "^8.5.0",
45 | "eslint-plugin-react": "^7.31.11",
46 | "eslint-plugin-react-hooks": "^4.6.0",
47 | "fs-extra": "^11.1.0",
48 | "husky": "^8.0.0",
49 | "lint-staged": "^13.1.0",
50 | "postcss": "^8.4.20",
51 | "postcss-scss": "^4.0.6",
52 | "prettier": "^2.8.0",
53 | "prettier-plugin-organize-imports": "^3.2.1",
54 | "sass": "^1.57.1",
55 | "tailwindcss": "^3.2.4",
56 | "typescript": "^4.9.4",
57 | "webextension-polyfill": "^0.10.0"
58 | },
59 | "lint-staged": {
60 | "**/*.{js,jsx,ts,tsx,mjs}": [
61 | "npx prettier --write",
62 | "npx eslint --fix"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/background/providers/upstage.ts:
--------------------------------------------------------------------------------
1 | import { fetchSSE } from '../fetch-sse'
2 | import { GenerateAnswerParams, Provider } from '../types'
3 |
4 | export class UpstageProvider implements Provider {
5 | constructor(private token: string, private model: string) {
6 | this.token = token
7 | this.model = model
8 | }
9 |
10 | private buildMessages(params: GenerateAnswerParams): object[] {
11 | if (params.previousMessages === undefined) {
12 | params.previousMessages = []
13 | }
14 |
15 | console.log(params.previousMessages[0])
16 |
17 | const messsages = [
18 | {
19 | role: 'system',
20 | content: 'You are excellent researchers. Please privde information about research paper.',
21 | },
22 | ...params.previousMessages,
23 | {
24 | role: 'user',
25 | content: params.prompt,
26 | },
27 | ]
28 |
29 | console.log(messsages)
30 |
31 | return messsages
32 | }
33 |
34 | async generateAnswer(params: GenerateAnswerParams) {
35 | console.log(params)
36 |
37 | let result = ''
38 | await fetchSSE('https://api.upstage.ai/v1/solar/chat/completions', {
39 | method: 'POST',
40 | signal: params.signal,
41 | headers: {
42 | 'Content-Type': 'application/json',
43 | Authorization: `Bearer ${this.token}`,
44 | },
45 | body: JSON.stringify({
46 | model: this.model,
47 | messages: this.buildMessages(params),
48 | stream: true,
49 | max_tokens: 4096,
50 | }),
51 | onMessage(message) {
52 | console.debug('sse message', message)
53 | if (message === '[DONE]') {
54 | params.onEvent({ type: 'done' })
55 | return
56 | }
57 | let data
58 | try {
59 | data = JSON.parse(message)
60 | const text = data.choices[0]?.delta?.content
61 | if (text === '<|im_end|>' || text === '<|im_sep|>' || text === undefined) {
62 | params.onEvent({ type: 'done' })
63 | return
64 | }
65 | result += text
66 | params.onEvent({
67 | type: 'answer',
68 | data: {
69 | text: result + '✏',
70 | messageId: data.id,
71 | conversationId: data.id,
72 | },
73 | })
74 | } catch (err) {
75 | console.error(err)
76 | return
77 | }
78 | },
79 | })
80 | return {}
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/content-script/ChatGPTFeedback.tsx:
--------------------------------------------------------------------------------
1 | import { ThumbsdownIcon, ThumbsupIcon, CopyIcon, CheckIcon } from '@primer/octicons-react'
2 | import { memo, useCallback, useState } from 'react'
3 | import { useEffect } from 'preact/hooks'
4 | import Browser from 'webextension-polyfill'
5 |
6 | interface Props {
7 | messageId: string
8 | conversationId: string
9 | answerText: string
10 | }
11 |
12 | function ChatGPTFeedback(props: Props) {
13 | const [copied, setCopied] = useState(false)
14 | const [action, setAction] = useState<'thumbsUp' | 'thumbsDown' | null>(null)
15 |
16 | const clickThumbsUp = useCallback(async () => {
17 | if (action) {
18 | return
19 | }
20 | setAction('thumbsUp')
21 | await Browser.runtime.sendMessage({
22 | type: 'FEEDBACK',
23 | data: {
24 | conversation_id: props.conversationId,
25 | message_id: props.messageId,
26 | rating: 'thumbsUp',
27 | },
28 | })
29 | }, [action, props.conversationId, props.messageId])
30 |
31 | const clickThumbsDown = useCallback(async () => {
32 | if (action) {
33 | return
34 | }
35 | setAction('thumbsDown')
36 | await Browser.runtime.sendMessage({
37 | type: 'FEEDBACK',
38 | data: {
39 | conversation_id: props.conversationId,
40 | message_id: props.messageId,
41 | rating: 'thumbsDown',
42 | text: '',
43 | tags: [],
44 | },
45 | })
46 | }, [action, props.conversationId, props.messageId])
47 |
48 | const clickCopyToClipboard = useCallback(async () => {
49 | await navigator.clipboard.writeText(props.answerText)
50 | setCopied(true)
51 | }, [props.answerText])
52 |
53 | useEffect(() => {
54 | if (copied) {
55 | const timer = setTimeout(() => {
56 | setCopied(false)
57 | }, 500)
58 | return () => clearTimeout(timer)
59 | }
60 | }, [copied])
61 |
62 | return (
63 |
64 |
68 |
69 |
70 |
74 |
75 |
76 |
77 | {copied ? : }
78 |
79 |
80 | )
81 | }
82 |
83 | export default memo(ChatGPTFeedback)
84 |
--------------------------------------------------------------------------------
/src/options/PromptCard.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Divider, Grid, Text, Textarea, useToasts } from '@geist-ui/core'
2 | import Trash2 from '@geist-ui/icons/trash2'
3 | import { useCallback, useState } from 'preact/hooks'
4 |
5 | function PromptCard(props: {
6 | header: string
7 | prompt: string
8 | language: string
9 | onSave: (newPrompt: string) => Promise
10 | onDismiss?: () => Promise
11 | }) {
12 | const { header, prompt, language, onSave, onDismiss } = props
13 | const [value, setValue] = useState(prompt)
14 | const { setToast } = useToasts()
15 |
16 | const onClickSave = useCallback(
17 | (prompt: string) => {
18 | setValue(prompt)
19 | onSave(prompt)
20 | .then(() => {
21 | setToast({ text: 'Prompt changes saved', type: 'success' })
22 | })
23 | .catch(() => {
24 | setToast({ text: 'Failed to save prompt', type: 'error' })
25 | })
26 | },
27 | [onSave, setToast],
28 | )
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 | {header}
37 |
38 |
39 | {onDismiss && (
40 |
41 | }
44 | auto
45 | px={0.6}
46 | onClick={() =>
47 | onDismiss()
48 | .then(() => {
49 | setToast({ text: 'Prompt removed', type: 'success' })
50 | })
51 | .catch(() => {
52 | setToast({ text: 'Failed to remove prompt', type: 'error' })
53 | })
54 | }
55 | />
56 |
57 | )}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
73 |
74 |
75 |
84 |
85 |
86 | )
87 | }
88 |
89 | export default PromptCard
90 |
--------------------------------------------------------------------------------
/src/options/AddNewPromptModal.tsx:
--------------------------------------------------------------------------------
1 | import { Input, Modal, Text, Textarea, useToasts } from '@geist-ui/core'
2 | import { useState } from 'preact/hooks'
3 | import { isValidHttpUrl } from '../content-script/utils'
4 |
5 | function AddNewPromptModal(props: {
6 | visible: boolean
7 | onClose: () => void
8 | onSave: (newOverride: { site: string; prompt: string }) => Promise
9 | }) {
10 | const { visible, onClose, onSave } = props
11 | const [site, setSite] = useState('')
12 | const [siteError, setSiteError] = useState(false)
13 | const [prompt, setPrompt] = useState('')
14 | const [promptError, setPromptError] = useState(false)
15 | const { setToast } = useToasts()
16 |
17 | function validateInput() {
18 | const isSiteValid = isValidHttpUrl(site)
19 | setSiteError(!isSiteValid)
20 | if (!isSiteValid) {
21 | return false
22 | }
23 | const isPromptValid = prompt.trim().length > 0
24 | setPromptError(!isPromptValid)
25 | return isPromptValid
26 | }
27 |
28 | function close() {
29 | setSite('')
30 | setSiteError(false)
31 | setPrompt('')
32 | setPromptError(false)
33 | onClose()
34 | }
35 |
36 | return (
37 |
38 | Add New Prompt
39 |
40 | setSite(e.target.value)}
46 | >
47 | {siteError && (
48 |
49 | Site is not valid
50 |
51 | )}
52 |
53 | {promptError && (
54 |
55 |
56 | Prompt cannot be empty
57 |
58 |
59 | )}
60 |
69 | close()}>
70 | Cancel
71 |
72 | {
74 | if (!validateInput()) {
75 | return
76 | }
77 | onSave({ site, prompt })
78 | .then(() => {
79 | setToast({ text: 'New Prompt saved', type: 'success' })
80 | close()
81 | })
82 | .catch(() => {
83 | setToast({ text: 'Failed to save prompt', type: 'error' })
84 | })
85 | }}
86 | >
87 | Save
88 |
89 |
90 | )
91 | }
92 |
93 | export default AddNewPromptModal
94 |
--------------------------------------------------------------------------------
/src/popup/App.tsx:
--------------------------------------------------------------------------------
1 | import { GearIcon, GlobeIcon } from '@primer/octicons-react'
2 | import { useCallback } from 'react'
3 | import useSWR from 'swr'
4 | import Browser from 'webextension-polyfill'
5 | import '../base.css'
6 | import logo from '../logo.png'
7 |
8 | const isChrome = /chrome/i.test(navigator.userAgent)
9 |
10 | function App() {
11 | const accessTokenQuery = useSWR(
12 | 'accessToken',
13 | () => Browser.runtime.sendMessage({ type: 'GET_ACCESS_TOKEN' }),
14 | { shouldRetryOnError: false },
15 | )
16 | const hideShortcutsTipQuery = useSWR('hideShortcutsTip', async () => {
17 | const { hideShortcutsTip } = await Browser.storage.local.get('hideShortcutsTip')
18 | return !!hideShortcutsTip
19 | })
20 |
21 | const _openOptionsPage = useCallback(() => {
22 | Browser.runtime.sendMessage({ type: 'OPEN_OPTIONS_PAGE' })
23 | }, [])
24 |
25 | const openOptionsPage = useCallback(() => {
26 | if (chrome.runtime && chrome.runtime.openOptionsPage) {
27 | chrome.runtime.openOptionsPage()
28 | } else {
29 | window.open(chrome.runtime.getURL('options.html'))
30 | }
31 | }, [])
32 |
33 | const openShortcutsPage = useCallback(() => {
34 | Browser.storage.local.set({ hideShortcutsTip: true })
35 | Browser.tabs.create({ url: 'chrome://extensions/shortcuts' })
36 | }, [])
37 |
38 | return (
39 |
40 |
41 |

42 |
ArxivGPT
43 |
44 |
45 |
46 |
47 |
48 | {isChrome && !hideShortcutsTipQuery.isLoading && !hideShortcutsTipQuery.data && (
49 |
50 | Tip:{' '}
51 |
52 | setup shortcuts
53 | {' '}
54 | for faster access.
55 |
56 | )}
57 | {(() => {
58 | if (accessTokenQuery.isLoading) {
59 | return (
60 |
61 |
62 |
63 | )
64 | }
65 | if (accessTokenQuery.data) {
66 | return
67 | }
68 | return (
69 |
70 |
71 | Enjoy ArxivGPT in your browser. Click configure to change the settings.
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | )
80 | })()}
81 |
82 | )
83 | }
84 |
85 | export default App
86 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { defaults } from 'lodash-es'
2 | import Browser from 'webextension-polyfill'
3 |
4 | export enum TriggerMode {
5 | Always = 'always',
6 | Manually = 'manually',
7 | }
8 |
9 | export const TRIGGER_MODE_TEXT = {
10 | [TriggerMode.Always]: { title: 'Always', desc: 'ArxivGPT is queried on every search' },
11 | [TriggerMode.Manually]: {
12 | title: 'Manually',
13 | desc: 'ArxivGPT is queried when you manually click a button',
14 | },
15 | }
16 |
17 | export enum Theme {
18 | Auto = 'auto',
19 | Light = 'light',
20 | Dark = 'dark',
21 | }
22 |
23 | export enum Language {
24 | Auto = 'auto',
25 | English = 'english',
26 | Chinese = 'chinese',
27 | Spanish = 'spanish',
28 | French = 'french',
29 | Korean = 'korean',
30 | Japanese = 'japanese',
31 | German = 'german',
32 | Portuguese = 'portuguese',
33 | }
34 |
35 | export const Prompt =
36 | 'Please summarize the paper by author(s) in one concise sentence. \
37 | Then, list key insights and lessons learned from the paper.\
38 | Next, generate 3-5 questions that you would like to ask the authors about their work. \
39 | Finally, provide 3-5 suggestions for related topics or future research directions \
40 | based on the content of the paper. \
41 | If applicable, list at least 5 relevant references from the field of study of the paper. \
42 | '
43 |
44 | export interface SitePrompt {
45 | site: string
46 | prompt: string
47 | }
48 |
49 | const userConfigWithDefaultValue = {
50 | triggerMode: TriggerMode.Always,
51 | theme: Theme.Auto,
52 | language: Language.Auto,
53 | prompt: Prompt,
54 | promptOverrides: [] as SitePrompt[],
55 | }
56 |
57 | export type UserConfig = typeof userConfigWithDefaultValue
58 |
59 | export async function getUserConfig(): Promise {
60 | const result = await Browser.storage.local.get(Object.keys(userConfigWithDefaultValue))
61 | return defaults(result, userConfigWithDefaultValue)
62 | }
63 |
64 | export async function updateUserConfig(updates: Partial) {
65 | console.debug('update configs', updates)
66 | return Browser.storage.local.set(updates)
67 | }
68 |
69 | export enum ProviderType {
70 | GPT3 = 'gpt3',
71 | }
72 |
73 | interface GPT3ProviderConfig {
74 | model: string
75 | apiKey: string
76 | }
77 |
78 | export interface ProviderConfigs {
79 | provider: ProviderType
80 | configs: {
81 | [ProviderType.GPT3]: GPT3ProviderConfig | undefined
82 | }
83 | }
84 |
85 | export async function getProviderConfigs(): Promise {
86 | const { provider = ProviderType.GPT3 } = await Browser.storage.local.get('provider')
87 | const configKey = `provider:${ProviderType.GPT3}`
88 | const result = await Browser.storage.local.get(configKey)
89 | return {
90 | provider,
91 | configs: {
92 | [ProviderType.GPT3]: result[configKey],
93 | },
94 | }
95 | }
96 |
97 | export async function saveProviderConfigs(
98 | provider: ProviderType,
99 | configs: ProviderConfigs['configs'],
100 | ) {
101 | return Browser.storage.local.set({
102 | provider,
103 | [`provider:${ProviderType.GPT3}`]: configs[ProviderType.GPT3],
104 | })
105 | }
106 |
--------------------------------------------------------------------------------
/src/options/ProviderSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input, Select, Spinner, Tabs, useInput, useToasts } from '@geist-ui/core'
2 | import { FC, useCallback, useState } from 'react'
3 | import useSWR from 'swr'
4 | import { fetchExtensionConfigs } from '../api'
5 | import { getProviderConfigs, ProviderConfigs, ProviderType, saveProviderConfigs } from '../config'
6 |
7 | interface ConfigProps {
8 | config: ProviderConfigs
9 | models: string[]
10 | }
11 |
12 | async function loadModels(): Promise {
13 | const configs = await fetchExtensionConfigs()
14 | return ['solar-1-mini-chat']
15 | //return configs.openai_model_names
16 | }
17 |
18 | const ConfigPanel: FC = ({ config, models }) => {
19 | const [tab, setTab] = useState(config.provider)
20 | const { bindings: apiKeyBindings } = useInput(config.configs[ProviderType.GPT3]?.apiKey ?? '')
21 | const [model, setModel] = useState(config.configs[ProviderType.GPT3]?.model ?? models[0])
22 | const { setToast } = useToasts()
23 |
24 | const save = useCallback(async () => {
25 | if (tab === ProviderType.GPT3) {
26 | if (!apiKeyBindings.value) {
27 | alert('Please enter your Solar API key. Get from https://console.upstage.ai')
28 | return
29 | }
30 | if (!model || !models.includes(model)) {
31 | alert('Please select a valid model')
32 | return
33 | }
34 | }
35 | await saveProviderConfigs(tab, {
36 | [ProviderType.GPT3]: {
37 | model,
38 | apiKey: apiKeyBindings.value,
39 | },
40 | })
41 | setToast({ text: 'Changes saved', type: 'success' })
42 | }, [apiKeyBindings.value, model, models, setToast, tab])
43 |
44 | return (
45 |
46 |
setTab(v as ProviderType)}>
47 |
48 |
49 |
50 | Solar official API, more stable,{' '}
51 | charge by usage
52 |
53 |
54 |
66 |
67 |
68 |
69 | You can find or create your API key{' '}
70 |
71 | here
72 |
73 |
74 |
75 |
76 |
77 |
80 |
81 | )
82 | }
83 |
84 | function ProviderSelect() {
85 | const query = useSWR('provider-configs', async () => {
86 | const [config, models] = await Promise.all([getProviderConfigs(), loadModels()])
87 | return { config, models }
88 | })
89 | if (query.isLoading) {
90 | return
91 | }
92 | return
93 | }
94 |
95 | export default ProviderSelect
96 |
--------------------------------------------------------------------------------
/src/content-script/index.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import '../base.css'
3 | import { getUserConfig, Theme } from '../config'
4 | import { detectSystemColorScheme } from '../utils'
5 | import ChatGPTContainer from './ChatGPTContainer'
6 | import { config, SearchEngine } from './search-engine-configs'
7 | import './styles.scss'
8 | import { getPossibleElementByQuerySelector } from './utils'
9 |
10 | async function mount(question: string, promptSource: string, siteConfig: SearchEngine) {
11 | const container = document.createElement('div')
12 | container.className = 'chat-gpt-container'
13 |
14 | const userConfig = await getUserConfig()
15 | let theme: Theme
16 | if (userConfig.theme === Theme.Auto) {
17 | theme = detectSystemColorScheme()
18 | } else {
19 | theme = userConfig.theme
20 | }
21 | if (theme === Theme.Dark) {
22 | container.classList.add('gpt-dark')
23 | } else {
24 | container.classList.add('gpt-light')
25 | }
26 |
27 | const siderbarContainer = getPossibleElementByQuerySelector(siteConfig.sidebarContainerQuery)
28 | if (siderbarContainer) {
29 | siderbarContainer.prepend(container)
30 | } else {
31 | container.classList.add('sidebar-free')
32 | const appendContainer = getPossibleElementByQuerySelector(siteConfig.appendContainerQuery)
33 | if (appendContainer) {
34 | appendContainer.appendChild(container)
35 | }
36 | }
37 |
38 | render(
39 | ,
44 | container,
45 | )
46 | }
47 |
48 | /**
49 | * mount html elements when requestions triggered
50 | * @param question question string
51 | * @param index question index
52 | */
53 | export async function requeryMount(question: string, index: number) {
54 | const container = document.querySelector('.question-container')
55 | let theme: Theme
56 | const questionItem = document.createElement('div')
57 | questionItem.className = `question-${index}`
58 |
59 | const userConfig = await getUserConfig()
60 | if (userConfig.theme === Theme.Auto) {
61 | theme = detectSystemColorScheme()
62 | } else {
63 | theme = userConfig.theme
64 | }
65 | if (theme === Theme.Dark) {
66 | container?.classList.add('gpt-dark')
67 | questionItem.classList.add('gpt-dark')
68 | } else {
69 | container?.classList.add('gpt-light')
70 | questionItem.classList.add('gpt-light')
71 | }
72 | questionItem.innerText = `Q${index + 1} : ${question}`
73 | container?.appendChild(questionItem)
74 | }
75 |
76 | const siteRegex = new RegExp(Object.keys(config).join('|'))
77 | const siteName = location.hostname.match(siteRegex)![0]
78 | const siteConfig = config[siteName]
79 |
80 | async function run() {
81 | const searchInput = getPossibleElementByQuerySelector(siteConfig.inputQuery)
82 | console.log('Try to Mount ChatGPT on', siteName)
83 |
84 | if (siteConfig.bodyQuery) {
85 | const bodyElement = getPossibleElementByQuerySelector(siteConfig.bodyQuery)
86 | console.debug('bodyElement', bodyElement)
87 |
88 | if (bodyElement && bodyElement.textContent) {
89 | const bodyInnerText = bodyElement.textContent.trim().replace(/\s+/g, ' ').substring(0, 1500)
90 | console.log('Body: ' + bodyInnerText)
91 | const userConfig = await getUserConfig()
92 |
93 | const found = userConfig.promptOverrides.find(
94 | (override) => new URL(override.site).hostname === location.hostname,
95 | )
96 | const question = found?.prompt ?? userConfig.prompt
97 | const promptSource = found?.site ?? 'default'
98 |
99 | mount(question + bodyInnerText, promptSource, siteConfig)
100 | }
101 | }
102 |
103 | //const searchInput = getPossibleElementByQuerySelector(siteConfig.inputQuery)
104 | //if (searchInput && searchInput.value) {
105 | // console.debug('Mount ChatGPT on', siteName)
106 | // const userConfig = await getUserConfig()
107 | // const searchValueWithLanguageOption =
108 | // userConfig.language === Language.Auto
109 | // ? searchInput.value
110 | // : `${searchInput.value}(in ${userConfig.language})`
111 | // mount(searchValueWithLanguageOption, siteConfig)
112 | }
113 |
114 | run()
115 |
116 | if (siteConfig.watchRouteChange) {
117 | siteConfig.watchRouteChange(run)
118 | }
119 |
--------------------------------------------------------------------------------
/src/options/App.tsx:
--------------------------------------------------------------------------------
1 | import { Button, CssBaseline, GeistProvider, Radio, Text, Toggle, useToasts } from '@geist-ui/core'
2 | import { Plus } from '@geist-ui/icons'
3 | import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
4 | import '../base.css'
5 | import {
6 | getUserConfig,
7 | Language,
8 | Prompt,
9 | SitePrompt,
10 | Theme,
11 | TriggerMode,
12 | TRIGGER_MODE_TEXT,
13 | updateUserConfig,
14 | } from '../config'
15 | import logo from '../logo.png'
16 | import { detectSystemColorScheme, getExtensionVersion } from '../utils'
17 | import AddNewPromptModal from './AddNewPromptModal'
18 | import PromptCard from './PromptCard'
19 | import ProviderSelect from './ProviderSelect'
20 |
21 | function OptionsPage(props: { theme: Theme; onThemeChange: (theme: Theme) => void }) {
22 | const [triggerMode, setTriggerMode] = useState(TriggerMode.Always)
23 | const [language, setLanguage] = useState(Language.Auto)
24 | const [prompt, setPrompt] = useState(Prompt)
25 | const [promptOverrides, setPromptOverrides] = useState([])
26 | const [modalVisible, setModalVisible] = useState(false)
27 |
28 | const { setToast } = useToasts()
29 |
30 | useEffect(() => {
31 | getUserConfig().then((config) => {
32 | setTriggerMode(config.triggerMode)
33 | setLanguage(config.language)
34 | setPrompt(config.prompt)
35 | setPromptOverrides(config.promptOverrides)
36 | })
37 | }, [])
38 |
39 | const closeModalHandler = useCallback(() => {
40 | setModalVisible(false)
41 | }, [])
42 |
43 | const onTriggerModeChange = useCallback(
44 | (mode: TriggerMode) => {
45 | setTriggerMode(mode)
46 | updateUserConfig({ triggerMode: mode })
47 | setToast({ text: 'Changes saved', type: 'success' })
48 | },
49 | [setToast],
50 | )
51 |
52 | const onThemeChange = useCallback(
53 | (theme: Theme) => {
54 | updateUserConfig({ theme })
55 | props.onThemeChange(theme)
56 | setToast({ text: 'Changes saved', type: 'success' })
57 | },
58 | [props, setToast],
59 | )
60 |
61 | const onLanguageChange = useCallback(
62 | (language: Language) => {
63 | updateUserConfig({ language })
64 | setToast({ text: 'Changes saved', type: 'success' })
65 | },
66 | [setToast],
67 | )
68 |
69 | return (
70 |
71 |
93 |
94 | Options
95 |
96 | AI Provider (Now, we only support Solar LLM)
97 |
98 |
99 |
100 |
101 | Prompt
102 |
103 |
104 | updateUserConfig({ prompt })}
107 | prompt={prompt}
108 | />
109 |
110 | {promptOverrides.map((override, index) => {
111 | return (
112 |
113 |
{
117 | const newOverride: SitePrompt = {
118 | site: override.site,
119 | prompt: newPrompt,
120 | }
121 | const newOverrides = promptOverrides.filter((o) => o.site !== override.site)
122 | newOverrides.splice(index, 0, newOverride)
123 | setPromptOverrides(newOverrides)
124 | return updateUserConfig({ promptOverrides: newOverrides })
125 | }}
126 | onDismiss={() => {
127 | const newOverrides = promptOverrides.filter((_, i) => i !== index)
128 | setPromptOverrides(newOverrides)
129 | return updateUserConfig({ promptOverrides: newOverrides })
130 | }}
131 | />
132 |
133 | )
134 | })}
135 |
136 |
140 |
141 | {
145 | const newOverride: SitePrompt = {
146 | site,
147 | prompt,
148 | }
149 | const newOverrides = promptOverrides.concat([newOverride])
150 | setPromptOverrides(newOverrides)
151 | return updateUserConfig({ promptOverrides: newOverrides })
152 | }}
153 | />
154 |
155 |
156 | Trigger Mode
157 |
158 |
159 | onTriggerModeChange(val as TriggerMode)}
162 | >
163 | {Object.entries(TRIGGER_MODE_TEXT).map(([value, texts]) => {
164 | return (
165 |
166 | {texts.title}
167 | {texts.desc}
168 |
169 | )
170 | })}
171 |
172 |
173 | Theme
174 |
175 | onThemeChange(val as Theme)} useRow>
176 | {Object.entries(Theme).map(([k, v]) => {
177 | return (
178 |
179 | {k}
180 |
181 | )
182 | })}
183 |
184 |
185 | Misc
186 |
187 |
188 |
189 |
190 | Auto delete conversations generated by search
191 |
192 |
193 |
194 |
195 | )
196 | }
197 |
198 | function App() {
199 | const [theme, setTheme] = useState(Theme.Auto)
200 |
201 | const themeType = useMemo(() => {
202 | if (theme === Theme.Auto) {
203 | return detectSystemColorScheme()
204 | }
205 | return theme
206 | }, [theme])
207 |
208 | useEffect(() => {
209 | getUserConfig().then((config) => setTheme(config.theme))
210 | }, [])
211 |
212 | return (
213 |
214 |
215 |
216 |
217 | )
218 | }
219 |
220 | export default App
221 |
--------------------------------------------------------------------------------
/src/content-script/ChatGPTQuery.tsx:
--------------------------------------------------------------------------------
1 | import { GearIcon } from '@primer/octicons-react'
2 | import { useEffect, useState } from 'preact/hooks'
3 | import { memo, useCallback, useRef } from 'react'
4 | import ReactMarkdown from 'react-markdown'
5 | import rehypeHighlight from 'rehype-highlight'
6 | import Browser from 'webextension-polyfill'
7 | import { captureEvent } from '../analytics'
8 | import { Answer } from '../messaging'
9 | import ChatGPTFeedback from './ChatGPTFeedback'
10 | import { shouldShowRatingTip } from './utils.js'
11 |
12 | export type QueryStatus = 'success' | 'error' | undefined
13 |
14 | interface Props {
15 | question: string
16 | promptSource: string
17 | onStatusChange?: (status: QueryStatus) => void
18 | }
19 |
20 | interface Requestion {
21 | requestion: string
22 | index: number
23 | answer: Answer | null
24 | }
25 |
26 | interface ReQuestionAnswerProps {
27 | answerText: string | undefined
28 | }
29 |
30 | function ChatGPTQuery(props: Props) {
31 | const inputRef = useRef(null)
32 | const [answer, setAnswer] = useState(null)
33 | const [error, setError] = useState('')
34 | const [retry, setRetry] = useState(0)
35 | const [done, setDone] = useState(false)
36 | const [showTip, setShowTip] = useState(false)
37 | const [status, setStatus] = useState()
38 | const [reError, setReError] = useState('')
39 | const [reQuestionDone, setReQuestionDone] = useState(false)
40 | const [requestionList, setRequestionList] = useState([])
41 | const [questionIndex, setQuestionIndex] = useState(0)
42 | const [reQuestionAnswerText, setReQuestionAnswerText] = useState()
43 | const [previousMessages, setPreviousMeessages] = useState