├── .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 | 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 | [Chrome][link-chrome] 6 | 7 | ## Screenshot 8 | 9 | image 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 | image 20 | 21 | ## Troubleshooting 22 | 23 | ### How to make it work in Brave 24 | 25 | ![Screenshot](screenshots/brave.png?raw=true) 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 | ![Screenshot](screenshots/opera.png?raw=true) 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 | 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 |