├── .prettierignore ├── src ├── base.css ├── global.d.ts ├── logo.png ├── analytics.ts ├── messaging.ts ├── options │ ├── index.tsx │ ├── index.html │ ├── PromptSelect.tsx │ ├── ProviderSelect.tsx │ └── App.tsx ├── popup │ ├── index.tsx │ ├── index.html │ └── App.tsx ├── _locales │ └── en │ │ └── messages.json ├── background │ ├── stream-async-iterable.ts │ ├── types.ts │ ├── fetch-sse.ts │ ├── providers │ │ ├── openai.ts │ │ └── chatgpt.ts │ └── index.ts ├── utils.ts ├── api_config.json ├── api.ts ├── content-script │ ├── utils.ts │ ├── ChatGPTContainer.tsx │ ├── ChatGPTCard.tsx │ ├── Promotion.tsx │ ├── styles.scss │ ├── ChatGPTFeedback.tsx │ ├── search-engine-configs.ts │ ├── index.tsx │ ├── ChatGPTQuery.tsx │ ├── dark.scss │ └── light.scss ├── config.ts ├── manifest.v2.json └── manifest.json ├── .gitignore ├── .husky └── pre-commit ├── screenshots ├── brave.png ├── opera.png └── extension.png ├── tailwind.config.cjs ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── pre-release-build.yml ├── tsconfig.json ├── .eslintrc.json ├── run_grammar.py ├── package.json ├── README.md └── 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/xiaofen9/chatgpt-writting-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/xiaofen9/chatgpt-writting-extension/HEAD/screenshots/brave.png -------------------------------------------------------------------------------- /screenshots/opera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaofen9/chatgpt-writting-extension/HEAD/screenshots/opera.png -------------------------------------------------------------------------------- /screenshots/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaofen9/chatgpt-writting-extension/HEAD/screenshots/extension.png -------------------------------------------------------------------------------- /src/messaging.ts: -------------------------------------------------------------------------------- 1 | export interface Answer { 2 | text: string 3 | messageId: string 4 | conversationId: string 5 | } 6 | -------------------------------------------------------------------------------- /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/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "GPT for Academic Writter" 4 | }, 5 | "appDesc": { 6 | "message": "Provide GPT-powered paper editing" 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 | GPT for Academic Writter 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "resolveJsonModule": true, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | onEvent: (event: Event) => void 15 | signal?: AbortSignal 16 | } 17 | 18 | export interface Provider { 19 | generateAnswer(params: GenerateAnswerParams): Promise<{ cleanup?: () => void }> 20 | } 21 | -------------------------------------------------------------------------------- /.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/api_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chatgpt_webapp_model_name": "text-davinci-002-render", 3 | "openai_model_names": [ 4 | "gpt-3.5-turbo-16k-0613", 5 | "gpt-3.5-turbo-16k", 6 | "gpt-4-1106-preview", 7 | "gpt-4", 8 | "gpt-3.5-turbo-1106", 9 | "gpt-3.5-turbo", 10 | "gpt-4-0613", 11 | "gpt-4-0314", 12 | "gpt-3.5-turbo-0613" 13 | ], 14 | "openai_chat_model_names": [ 15 | "gpt-3.5-turbo-16k-0613", 16 | "gpt-3.5-turbo-16k", 17 | "gpt-4-1106-preview", 18 | "gpt-4", 19 | "gpt-3.5-turbo-1106", 20 | "gpt-3.5-turbo", 21 | "gpt-4-0613", 22 | "gpt-4-0314", 23 | "gpt-3.5-turbo-0613" 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/api.ts: -------------------------------------------------------------------------------- 1 | import * as api_config from './api_config.json' 2 | import { getExtensionVersion } from './utils' 3 | 4 | const API_HOST = 'https://chatgpt4google.com' 5 | // const API_HOST = 'http://localhost:3000' 6 | 7 | export interface PromotionResponse { 8 | url: string 9 | title?: string 10 | text?: string 11 | image?: { url: string; size?: number } 12 | footer?: { text: string; url: string } 13 | label?: { text: string; url: string } 14 | } 15 | 16 | export async function fetchPromotion(): Promise { 17 | return fetch(`${API_HOST}/api/p`, { 18 | headers: { 19 | 'x-version': getExtensionVersion(), 20 | }, 21 | }).then((r) => r.json()) 22 | } 23 | 24 | // get config from './api_config.json' 25 | export async function fetchExtensionConfigs(): Promise<{ 26 | chatgpt_webapp_model_name: string 27 | openai_model_names: string[] 28 | }> { 29 | // async so Promise 30 | return Promise.resolve(api_config) 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import Promotion from './Promotion' 8 | 9 | interface Props { 10 | question: 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 | 29 |
30 | {query.data && } 31 | 32 | ) 33 | } 34 | 35 | export default ChatGPTContainer 36 | -------------------------------------------------------------------------------- /run_grammar.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pylatexenc.latexwalker import LatexWalker, LatexEnvironmentNode 3 | 4 | def gpt_query_paragraph(text): 5 | res = openai.ChatCompletion.create( 6 | model="gpt-3.5-turbo", 7 | temperature = 0.3, 8 | messages=[ 9 | {"role": "system", "content": "You are a helpful assistant that translates English to French."}, 10 | {"role": "user", "content": text}],) 11 | return res.choices[0].message 12 | 13 | def parse_one_file(file_name): 14 | # read all files from dict 15 | print("parsing "+ file_name) 16 | paragraphs = [] 17 | with open(file_name, "r") as file: 18 | content = file.read() 19 | # Remove comments from the content 20 | # content = re.sub(r'%.*$', '', content, flags=re.MULTILINE) 21 | 22 | walker = LatexWalker(content) 23 | nodes, pos, len_ = walker.get_latex_nodes(pos=0) 24 | 25 | paragraphs = [] 26 | 27 | for node in nodes: 28 | if isinstance(node, LatexEnvironmentNode) and node.environmentname == 'document': 29 | for child_node in node.nodelist: 30 | if child_node.isNodeType('text'): 31 | text_content = child_node.content.strip() 32 | if len(text_content) > 0: 33 | paragraphs.append(text_content) 34 | 35 | print(paragraphs) -------------------------------------------------------------------------------- /src/content-script/ChatGPTCard.tsx: -------------------------------------------------------------------------------- 1 | import { LightBulbIcon, SearchIcon } from '@primer/octicons-react' 2 | import { useState } from 'preact/hooks' 3 | import { TriggerMode } from '../config' 4 | import ChatGPTQuery, { QueryStatus } from './ChatGPTQuery' 5 | import { endsWithQuestionMark } from './utils.js' 6 | 7 | interface Props { 8 | question: 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 (props.triggerMode === TriggerMode.QuestionMark) { 20 | if (endsWithQuestionMark(props.question.trim())) { 21 | return 22 | } 23 | return ( 24 |

25 | Trigger ChatGPT by appending a question mark after your query 26 |

27 | ) 28 | } 29 | if (triggered) { 30 | return 31 | } 32 | return ( 33 |

setTriggered(true)}> 34 | Ask ChatGPT for this query 35 |

36 | ) 37 | } 38 | 39 | export default ChatGPTCard 40 | -------------------------------------------------------------------------------- /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 | console.log(event) 17 | console.log('4444') 18 | if (event.type === 'event') { 19 | console.log(event.data) 20 | console.log('23333') 21 | onMessage(event.data) 22 | } 23 | }) 24 | for await (const chunk of streamAsyncIterable(resp.body!)) { 25 | const str = new TextDecoder().decode(chunk) 26 | console.log(str) 27 | console.log('=======') 28 | parser.feed(str) 29 | } 30 | } 31 | 32 | export async function fetchDirect( 33 | resource: string, 34 | options: RequestInit & { onMessage: (message: string) => void }, 35 | ) { 36 | const { onMessage, ...fetchOptions } = options 37 | const resp = await fetch(resource, fetchOptions) 38 | if (!resp.ok) { 39 | const error = await resp.json().catch(() => ({})) 40 | throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`) 41 | } 42 | 43 | for await (const chunk of streamAsyncIterable(resp.body!)) { 44 | const str = new TextDecoder().decode(chunk) 45 | onMessage(str) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/providers/openai.ts: -------------------------------------------------------------------------------- 1 | import { fetchDirect } from '../fetch-sse' 2 | import { GenerateAnswerParams, Provider } from '../types' 3 | 4 | export class OpenAIProvider implements Provider { 5 | constructor(private token: string, private model: string) { 6 | this.token = token 7 | this.model = model 8 | } 9 | 10 | private _generatePrompt(prompt: string): object[] { 11 | const arrayPrompt = prompt.split('---------WRITTING-GPT----------') 12 | const res = [ 13 | { role: 'system', content: arrayPrompt[0] }, 14 | { role: 'user', content: arrayPrompt[1] }, 15 | ] 16 | return res 17 | } 18 | 19 | async generateAnswer(params: GenerateAnswerParams) { 20 | let result = '' 21 | // params.prompt = "you are an helpful assistant. ---------WRITTING-GPT---------- 树上 10 只鸟,打掉 1 只,还剩几只?" 22 | await fetchDirect('https://api.openai.com/v1/chat/completions', { 23 | method: 'POST', 24 | signal: params.signal, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | Authorization: `Bearer ${this.token}`, 28 | }, 29 | body: JSON.stringify({ 30 | model: this.model, 31 | messages: this._generatePrompt(params.prompt), 32 | temperature: 0.4, 33 | }), 34 | onMessage(message) { 35 | console.debug('sse message', message) 36 | let data 37 | try { 38 | data = JSON.parse(message) 39 | const text = data.choices[0].message.content 40 | result += text 41 | params.onEvent({ 42 | type: 'answer', 43 | data: { 44 | text: result, 45 | messageId: data.id, 46 | conversationId: data.id, 47 | }, 48 | }) 49 | } catch (err) { 50 | console.error(err) 51 | return 52 | } 53 | 54 | params.onEvent({ type: 'done' }) 55 | return 56 | }, 57 | }) 58 | return {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | "@primer/octicons-react": "^17.9.0", 14 | "eventsource-parser": "^0.0.5", 15 | "expiry-map": "^2.0.0", 16 | "github-markdown-css": "^5.1.0", 17 | "inter-ui": "^3.19.3", 18 | "lodash-es": "^4.17.21", 19 | "preact": "^10.11.3", 20 | "prop-types": "^15.8.1", 21 | "punycode": "^2.1.1", 22 | "react": "npm:@preact/compat@^17.1.2", 23 | "react-dom": "npm:@preact/compat@^17.1.2", 24 | "react-markdown": "^8.0.4", 25 | "rehype-highlight": "^6.0.0", 26 | "swr": "^2.0.0", 27 | "uuid": "^9.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/fs-extra": "^9.0.13", 31 | "@types/lodash-es": "^4.17.6", 32 | "@types/uuid": "^9.0.0", 33 | "@types/webextension-polyfill": "^0.9.2", 34 | "@typescript-eslint/eslint-plugin": "^5.47.0", 35 | "@typescript-eslint/parser": "^5.47.0", 36 | "archiver": "^5.3.1", 37 | "autoprefixer": "^10.4.13", 38 | "chokidar-cli": "^3.0.0", 39 | "dotenv": "^16.0.3", 40 | "esbuild": "^0.17.4", 41 | "esbuild-style-plugin": "^1.6.1", 42 | "eslint": "^8.30.0", 43 | "eslint-config-prettier": "^8.5.0", 44 | "eslint-plugin-react": "^7.31.11", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "fs-extra": "^11.1.0", 47 | "husky": "^8.0.0", 48 | "lint-staged": "^13.1.0", 49 | "postcss": "^8.4.20", 50 | "postcss-scss": "^4.0.6", 51 | "prettier": "^2.8.0", 52 | "prettier-plugin-organize-imports": "^3.2.1", 53 | "sass": "^1.57.1", 54 | "tailwindcss": "^3.2.4", 55 | "typescript": "^4.9.4", 56 | "webextension-polyfill": "^0.10.0" 57 | }, 58 | "lint-staged": { 59 | "**/*.{js,jsx,ts,tsx,mjs}": [ 60 | "npx prettier --write", 61 | "npx eslint --fix" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT for Academic Writters 2 | This repo is based on https://github.com/wong2/chatgpt-google-extension 3 | 4 | ## Motivation 5 | GPT (Generative Pre-trained Transformer) is a powerful language model that can be used to assist with editing works. One way that GPT can help with editing is by organizing the major logic points in a piece of writing. 6 | 7 | To do this, a writer just need to input their major logic points into GPT and allow the model to generate an outline or structure for the piece. GPT can then suggest different ways of organizing the points to create a coherent and logical flow. 8 | 9 | In addition, GPT can also be used to suggest different ways of phrasing sentences or paragraphs to improve the overall readability and clarity of the writing. The model can identify common grammar and syntax errors and make suggestions for corrections. 10 | 11 | Another way that GPT can be useful for editing is by providing alternative phrasings or word choices to help avoid repetition and improve the variety of language used in the writing. 12 | 13 | ## Supported Platforms 14 | 15 | Overleaf, HackMd 16 | 17 | ## Installation 18 | 1. Download the [build.zip]([https://github.com/xiaofen9/chatgpt-writting-extension/blob/main/gptwritter.crx](https://github.com/xiaofen9/chatgpt-writting-extension/blob/main/build.zip)) and unzip it 19 | 2. Open `chrome://extensions` in your chrome, and click `load unpacked` 20 | 3. Load `build/chromium/` or `build/firefox/` directory to your browser 21 | 22 | 23 | Or build from source by yourself. 24 | 25 | 1. Clone the repo 26 | 2. Install dependencies with `npm` 27 | 3. `npm run build` 28 | 4. Load `build/chromium/` or `build/firefox/` directory to your browser 29 | 30 | 31 | ## How to use 32 | Intuitive workflow: 33 | - First, you select the text 34 | - Second, you click btns to let chatgpt to rewrite or concise your text. 35 | ![image](https://user-images.githubusercontent.com/20917869/221438513-3ac5bfb4-3d73-4fae-97a5-1c14622d96af.png) 36 | 37 | ## Features 38 | - Supoorts `rewrite for clarity` and `concise` feature 39 | - Supports the official OpenAI API 40 | - Supports ChatGPT Plus 41 | - Markdown rendering 42 | - Code highlights 43 | - Dark mode 44 | - Provide feedback to improve ChatGPT 45 | - Copy to clipboard 46 | - Custom trigger mode 47 | - Switch languages 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import Browser from 'webextension-polyfill' 2 | import { getProviderConfigs, ProviderType } from '../config' 3 | import { ChatGPTProvider, getChatGPTAccessToken, sendMessageFeedback } from './providers/chatgpt' 4 | import { OpenAIProvider } from './providers/openai' 5 | import { Provider } from './types' 6 | 7 | async function generateAnswers(port: Browser.Runtime.Port, question: string) { 8 | const providerConfigs = await getProviderConfigs() 9 | 10 | let provider: Provider 11 | if (providerConfigs.provider === ProviderType.ChatGPT) { 12 | const token = await getChatGPTAccessToken() 13 | provider = new ChatGPTProvider(token) 14 | } else if (providerConfigs.provider === ProviderType.GPT3) { 15 | const { apiKey, model } = providerConfigs.configs[ProviderType.GPT3]! 16 | provider = new OpenAIProvider(apiKey, model) 17 | } else { 18 | throw new Error(`Unknown provider ${providerConfigs.provider}`) 19 | } 20 | 21 | const controller = new AbortController() 22 | port.onDisconnect.addListener(() => { 23 | controller.abort() 24 | cleanup?.() 25 | }) 26 | 27 | const { cleanup } = await provider.generateAnswer({ 28 | prompt: question, 29 | signal: controller.signal, 30 | onEvent(event) { 31 | if (event.type === 'done') { 32 | port.postMessage({ event: 'DONE' }) 33 | return 34 | } 35 | port.postMessage(event.data) 36 | }, 37 | }) 38 | } 39 | 40 | Browser.runtime.onConnect.addListener((port) => { 41 | port.onMessage.addListener(async (msg) => { 42 | console.debug('received msg', msg) 43 | try { 44 | await generateAnswers(port, msg.question) 45 | } catch (err: any) { 46 | console.error(err) 47 | port.postMessage({ error: err.message }) 48 | } 49 | }) 50 | }) 51 | 52 | Browser.runtime.onMessage.addListener(async (message) => { 53 | if (message.type === 'FEEDBACK') { 54 | const token = await getChatGPTAccessToken() 55 | await sendMessageFeedback(token, message.data) 56 | } else if (message.type === 'OPEN_OPTIONS_PAGE') { 57 | Browser.runtime.openOptionsPage() 58 | } else if (message.type === 'GET_ACCESS_TOKEN') { 59 | return getChatGPTAccessToken() 60 | } 61 | }) 62 | 63 | Browser.runtime.onInstalled.addListener((details) => { 64 | if (details.reason === 'install') { 65 | Browser.runtime.openOptionsPage() 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /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/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 openShortcutsPage = useCallback(() => { 26 | Browser.storage.local.set({ hideShortcutsTip: true }) 27 | Browser.tabs.create({ url: 'chrome://extensions/shortcuts' }) 28 | }, []) 29 | 30 | return ( 31 |
32 |
33 | 34 |

ChatGPT for Google

35 |
36 | 37 | 38 | 39 |
40 | {isChrome && !hideShortcutsTipQuery.isLoading && !hideShortcutsTipQuery.data && ( 41 |

42 | Tip:{' '} 43 | 44 | setup shortcuts 45 | {' '} 46 | for faster access. 47 |

48 | )} 49 | {(() => { 50 | if (accessTokenQuery.isLoading) { 51 | return ( 52 |
53 | 54 |
55 | ) 56 | } 57 | if (accessTokenQuery.data) { 58 | return