├── .gitignore
├── LICENSE
├── README.md
├── build.mjs
├── googlechatgpt-current-chrome.zip
├── package-lock.json
├── package.json
├── src
├── _locales
│ ├── de
│ │ └── messages.json
│ ├── en
│ │ └── messages.json
│ ├── es
│ │ └── messages.json
│ ├── fr
│ │ └── messages.json
│ ├── it
│ │ └── messages.json
│ ├── ja
│ │ └── messages.json
│ ├── ko
│ │ └── messages.json
│ ├── pt_BR
│ │ └── messages.json
│ └── zh_CN
│ │ └── messages.json
├── assets
│ └── icons
│ │ ├── icon128.png
│ │ ├── icon16.png
│ │ └── icon48.png
├── background
│ └── bg.ts
├── components
│ ├── dropdown.tsx
│ ├── errorMessage.tsx
│ ├── footer.tsx
│ ├── navBar.tsx
│ ├── promptEditor.tsx
│ ├── socialIconButton.tsx
│ ├── toolbar.tsx
│ └── tooltipWrapper.tsx
├── content-scripts
│ ├── api.ts
│ └── mainUI.tsx
├── declaration.d.ts
├── manifest.json
├── manifest.v2.json
├── options
│ ├── options.html
│ └── options.tsx
├── style
│ └── base.css
└── util
│ ├── createShadowRoot.ts
│ ├── elementFinder.ts
│ ├── icons.tsx
│ ├── localization.ts
│ ├── localizedStrings.json
│ ├── promptManager.ts
│ ├── regionOptions.json
│ ├── timePeriodOptions.json
│ └── userConfig.ts
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /build/*
3 | !build/webchatgpt*.zip
4 | /*.log
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 qunash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [link-chrome]: https://chrome.google.com/webstore/detail/googlechatgpt-chatgpt-wit/mdonbhpnpdajiekihkjeneenjhmeipam?hl=en&authuser=0 'Chrome Web Store'
2 |
3 | [
][link-chrome]
4 | # GoogleChatGPT
5 |
6 |
7 |
8 |
9 |
10 | GoogleChatGPT is an innovative and user-friendly chrome extension that combines the power of Google web search with the knowledge of ChatGPT. The extension provides up-to-date information on a variety of topics, making it the perfect tool for anyone seeking quick and accurate information.
11 |
12 | Inspired by qunash/chatgpt-advanced, GoogleChatGPT offers a unique and interactive experience, allowing users to engage in a dialogue with the extension to find the information they need. This dialogue format makes it possible for GoogleChatGPT to answer follow-up questions, admit its mistakes, challenge incorrect premises, and reject inappropriate requests.
13 |
14 | To get started with GoogleChatGPT, simply install the extension in your Chrome browser and start searching. Whether you're looking for information on a specific topic, need help with a project, or just want to learn something new, GoogleChatGPT has got you covered.
15 |
16 | So why wait? Try GoogleChatGPT today and experience the power of combined knowledge!
17 |
18 | ## Key Features
19 | * Combines the power of Google web search with the knowledge of ChatGPT
20 | * Provides up-to-date information on a variety of topics
21 | * Easy to use, simply install and start searching
22 | * Inspired by qunash/chatgpt-advanced
23 |
24 |
25 | ## Chrome Installation
26 | 1. Download prebuilt chrome zip file: `googlechatgpt-current-chrome.zip`
27 | 2. Unzip the file.
28 | 3. Open `chrome://extensions` in Chrome.
29 | 4. Enable developer mode (top right corner).
30 | 5. Click on `Load unpacked` and select the unzipped folder.
31 | 6. Go to [ChatGPT](https://chat.openai.com/chat/) and enjoy!
32 |
33 | ## Build from source
34 |
35 | 1. `git clone https://github.com/hunkim/chatgpt-with-google.git`
36 | 2. `npm install`
37 | 3. `npm run build-prod`
38 | 4. Open `chrome://extensions` in Chrome.
39 | 5. Enable developer mode (top right corner).
40 | 6. Click on `Load unpacked` and select the `build` folder.
41 |
42 |
43 | ## Contributing
44 |
45 | Contributions are welcome! Please submit pull requests.
46 |
47 | ## Credit
48 | This project is forked from qunash/chatgpt-advanced.
49 |
--------------------------------------------------------------------------------
/build.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import archiver from "archiver";
3 | import fs from "fs-extra";
4 | import tailwindcss from "tailwindcss";
5 | import autoprefixer from "autoprefixer";
6 | import postcssPlugin from "esbuild-style-plugin";
7 | import copyStaticFilesPlugin from "esbuild-copy-files-plugin";
8 | import path from 'path';
9 |
10 |
11 | const buildDir = "build";
12 | const minify = process.argv.includes("--minify");
13 |
14 | async function createBuildDir() {
15 | if(!fs.existsSync(buildDir)) {
16 | fs.mkdirSync(buildDir);
17 | }
18 | }
19 |
20 | async function cleanBuildDir() {
21 | const entries = await fs.readdir(buildDir);
22 | for (const entry of entries) {
23 | if (path.extname(entry) === ".zip") continue;
24 | await fs.remove(`${buildDir}/${entry}`);
25 | }
26 | }
27 |
28 | async function runEsbuild() {
29 | await esbuild.build({
30 | entryPoints: [
31 | "src/content-scripts/mainUI.tsx",
32 | "src/background/bg.ts",
33 | "src/options/options.tsx",
34 | ],
35 | outdir: buildDir,
36 | bundle: true,
37 | minify: minify,
38 | treeShaking: true,
39 | define: {
40 | "process.env.NODE_ENV": '"production"',
41 | },
42 | jsxFactory: "h",
43 | jsxFragment: "Fragment",
44 | jsx: "automatic",
45 | loader: {
46 | ".png": "dataurl",
47 | },
48 | plugins: [
49 | postcssPlugin({
50 | postcss: {
51 | plugins: [tailwindcss, autoprefixer],
52 | },
53 | }),
54 | copyStaticFilesPlugin({
55 | source: ["src/manifest.json", "src/assets/"],
56 | target: buildDir,
57 | copyWithFolder: false,
58 | }),
59 | copyStaticFilesPlugin({
60 | source: ["src/options/options.html"],
61 | target: buildDir + "/options",
62 | copyWithFolder: false,
63 | }),
64 | copyStaticFilesPlugin({
65 | source: ["src/_locales/"],
66 | target: buildDir,
67 | copyWithFolder: true,
68 | }),
69 | ],
70 | });
71 | }
72 |
73 | async function zipExtensionForBrowser(browser) {
74 | const manifest = await fs.readJson(`${buildDir}/manifest.json`);
75 | const version = manifest.version;
76 | let archiveName = `build/googlechatgpt-${version}-${browser}.zip`;
77 |
78 | const archive = archiver("zip", { zlib: { level: 9 } });
79 | const stream = fs.createWriteStream(archiveName);
80 |
81 | archive.pipe(stream);
82 |
83 | await addFilesToZip(archive, browser);
84 |
85 | console.log(`Creating ${archiveName}…`);
86 | archive.finalize();
87 | }
88 |
89 | async function addFilesToZip(archive, browser) {
90 | const entries = await fs.readdir("build");
91 | for (const entry of entries) {
92 | const entryStat = await fs.stat(`build/${entry}`);
93 |
94 | if (entryStat.isDirectory()) {
95 | archive.directory(`build/${entry}`, entry);
96 | } else {
97 | if (path.extname(entry) === ".zip") continue;
98 | if (entry === "manifest.json") continue;
99 | archive.file(`build/${entry}`, { name: entry });
100 | }
101 | }
102 | if (browser === "firefox") {
103 | archive.file("src/manifest.v2.json", { name: "manifest.json" });
104 | } else if (browser === "chrome") {
105 | archive.file("build/manifest.json", { name: "manifest.json" });
106 | }
107 | }
108 |
109 | async function build() {
110 | await createBuildDir();
111 | await cleanBuildDir();
112 | await runEsbuild();
113 |
114 | const createZips = process.argv.includes("--create-zips");
115 | if (createZips) {
116 | try {
117 | await zipExtensionForBrowser("chrome");
118 | await zipExtensionForBrowser("firefox");
119 | } catch (error) {
120 | console.error(error);
121 | }
122 | }
123 |
124 | console.log("Build complete");
125 | }
126 |
127 | build();
128 |
--------------------------------------------------------------------------------
/googlechatgpt-current-chrome.zip:
--------------------------------------------------------------------------------
1 | build/googlechatgpt-2023.02.04-chrome.zip
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "webchatgpt",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "scripts": {
7 | "build-dev": "node build.mjs",
8 | "build-prod": "node build.mjs --create-zips",
9 | "build-prod-min": "node build.mjs --create-zips --minify",
10 | "watch": "chokidar src -c \"npm run build-dev\""
11 | },
12 | "eslintConfig": {
13 | "parser": "@typescript-eslint/parser",
14 | "extends": [
15 | "preact",
16 | "plugin:@typescript-eslint/recommended"
17 | ],
18 | "ignorePatterns": [
19 | "build/"
20 | ]
21 | },
22 | "dependencies": {
23 | "lodash-es": "^4.17.21",
24 | "preact": "^10.10.0",
25 | "uuid": "^9.0.0"
26 | },
27 | "devDependencies": {
28 | "@types/lodash-es": "^4.17.6",
29 | "@types/uuid": "^9.0.0",
30 | "@types/webextension-polyfill": "^0.10.0",
31 | "@typescript-eslint/eslint-plugin": "^5.30.6",
32 | "@typescript-eslint/parser": "^5.30.6",
33 | "archiver": "^5.3.1",
34 | "autoprefixer": "^10.4.13",
35 | "chokidar-cli": "^3.0.0",
36 | "daisyui": "^2.47.0",
37 | "esbuild": "^0.16.17",
38 | "esbuild-copy-files-plugin": "^1.1.0",
39 | "esbuild-style-plugin": "^1.6.1",
40 | "eslint": "^8.20.0",
41 | "eslint-config-preact": "^1.3.0",
42 | "fs-extra": "^11.1.0",
43 | "preact-cli": "^3.4.0",
44 | "tailwindcss": "^3.2.4",
45 | "typescript": "^4.5.2",
46 | "webextension-polyfill": "^0.10.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "GoogleChatGPT: ChatGPT mit Internetzugang"
4 | },
5 | "appDesc": {
6 | "message": "Erweitern Sie Ihre ChatGPT-Prompts mit relevanten Ergebnissen aus dem Web."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "GoogleChatGPT: ChatGPT with Google"
4 | },
5 | "appDesc": {
6 | "message": "Augment your ChatGPT prompts with relevant results from Google."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT: ChatGPT con acceso a internet"
4 | },
5 | "appDesc": {
6 | "message": "Aumente sus prompts ChatGPT con resultados relevantes de la web."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT: ChatGPT avec accès à Internet"
4 | },
5 | "appDesc": {
6 | "message": "Augmentez vos prompts ChatGPT avec des résultats pertinents provenant du Web."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT: ChatGPT con accesso a Internet"
4 | },
5 | "appDesc": {
6 | "message": "Migliora i tuoi prompt di ChatGPT con risultati pertinenti dal web."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT: インターネットにアクセスできる ChatGPT"
4 | },
5 | "appDesc": {
6 | "message": "Webから関連する結果を使用して、ChatGPTのプロンプトを拡張します。"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/ko/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "GoogleChatGPT: ChatGPT와 구글을 함께 사용"
4 | },
5 | "appDesc": {
6 | "message": "웹에서 관련 결과를 사용하여 ChatGPT 프롬프트를 향상시킵니다."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT: ChatGPT com acesso à internet"
4 | },
5 | "appDesc": {
6 | "message": "Aumente seus prompts do ChatGPT com resultados relevantes da web."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "WebChatGPT:可访问互联网的 ChatGPT"
4 | },
5 | "appDesc": {
6 | "message": "使用来自网络的相关结果增强您的 ChatGPT 提示。"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkim/chatgpt-with-google/9485748cebecd43c49229c93ae2b7b0387aaa2a7/src/assets/icons/icon128.png
--------------------------------------------------------------------------------
/src/assets/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkim/chatgpt-with-google/9485748cebecd43c49229c93ae2b7b0387aaa2a7/src/assets/icons/icon16.png
--------------------------------------------------------------------------------
/src/assets/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunkim/chatgpt-with-google/9485748cebecd43c49229c93ae2b7b0387aaa2a7/src/assets/icons/icon48.png
--------------------------------------------------------------------------------
/src/background/bg.ts:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill'
2 |
3 |
4 | const manifest_version = Browser.runtime.getManifest().manifest_version
5 |
6 |
7 | Browser.runtime.onInstalled.addListener(async () => openChatGPTWebpage())
8 |
9 | function openChatGPTWebpage() {
10 | Browser.tabs.create({
11 | url: "https://chat.openai.com/chat",
12 | })
13 | }
14 |
15 | // open chatgpt webpage when extension icon is clicked
16 | if (manifest_version == 2) {
17 | Browser.browserAction.onClicked.addListener(openChatGPTWebpage)
18 | } else {
19 | Browser.action.onClicked.addListener(openChatGPTWebpage)
20 | }
21 |
22 |
23 | Browser.runtime.onMessage.addListener((request) => {
24 | if (request === "show_options") {
25 | Browser.runtime.openOptionsPage()
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/src/components/dropdown.tsx:
--------------------------------------------------------------------------------
1 | import { h, JSX } from "preact"
2 |
3 | function Dropdown(props: {
4 | value: string | number
5 | onChange: (e: any) => void
6 | options: Array<{ value: any; label: string }>
7 | onClick?: (e: any) => void
8 | }): JSX.Element {
9 |
10 | return (
11 |
20 | )
21 | }
22 |
23 | export default Dropdown
24 |
--------------------------------------------------------------------------------
/src/components/errorMessage.tsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useEffect, useState } from 'preact/hooks'
3 |
4 |
5 | function ErrorMessage({ message }) {
6 | const [show, setShow] = useState(true)
7 |
8 | useEffect(() => {
9 | const timer = setTimeout(() => {
10 | setShow(false)
11 | }, 10000)
12 | return () => clearTimeout(timer)
13 | }, [])
14 |
15 | return show && (
16 | //
17 |
18 | An error occurred
19 | {message}
20 | Check the console for more details. (Ctrl+Shift+J)
21 |
22 | )
23 | }
24 |
25 | export default ErrorMessage
26 |
--------------------------------------------------------------------------------
/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import { h, Component } from 'preact'
2 | import Browser from 'webextension-polyfill'
3 |
4 | function Footer() {
5 | const extension_version = Browser.runtime.getManifest().version
6 |
7 | return (
8 |
14 | )
15 | }
16 |
17 | export default Footer
18 |
--------------------------------------------------------------------------------
/src/components/navBar.tsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useCallback, useEffect, useState } from 'preact/hooks'
3 | import { icons } from 'src/util/icons'
4 | import { getTranslation, Languages, localizationKeys } from 'src/util/localization'
5 | import Browser from 'webextension-polyfill'
6 | import IconButton from './socialIconButton'
7 | import TooltipWrapper from './tooltipWrapper'
8 |
9 |
10 | const NavBar = (
11 | props: {
12 | language: string,
13 | onLanguageChange: (language: string) => void,
14 | }
15 | ) => {
16 |
17 | const version = Browser.runtime.getManifest().version
18 |
19 | return (
20 |
21 |

22 |
GoogleChatGPT
23 |
{version}
24 |
25 |
29 |
)
30 | }
31 |
32 | //
33 | //
34 | //
35 | export default NavBar
36 |
--------------------------------------------------------------------------------
/src/components/promptEditor.tsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useState, useEffect, useRef, useLayoutEffect } from 'preact/hooks'
3 | import { getTranslation, localizationKeys } from 'src/util/localization'
4 | import { deletePrompt, getDefaultPrompt, getSavedPrompts, Prompt, savePrompt } from 'src/util/promptManager'
5 | import TooltipWrapper from './tooltipWrapper'
6 |
7 | const PromptEditor = (
8 | props: {
9 | language: string
10 | }
11 | ) => {
12 | const [savedPrompts, setSavedPrompts] = useState
([])
13 | const [prompt, setPrompt] = useState(getDefaultPrompt())
14 | const [hasWebResultsPlaceholder, setHasWebResultsPlaceholder] = useState(false)
15 | const [hasQueryPlaceholder, setHasQueryPlaceholder] = useState(false)
16 | const [deleteBtnText, setDeleteBtnText] = useState("delete")
17 |
18 | const [showErrors, setShowErrors] = useState(false)
19 | const [nameError, setNameError] = useState(false)
20 | const [textError, setTextError] = useState(false)
21 | const [webResultsError, setWebResultsError] = useState(false)
22 | const [queryError, setQueryError] = useState(false)
23 |
24 | useLayoutEffect(() => {
25 | updateSavedPrompts()
26 | }, [])
27 |
28 | const updateSavedPrompts = async () => {
29 | let prompts = await getSavedPrompts()
30 | setSavedPrompts(prompts)
31 | if (prompt.uuid === 'default') {
32 | setPrompt(prompts[0])
33 | }
34 | }
35 |
36 | useEffect(() => {
37 | updateSavedPrompts()
38 | }, [props.language])
39 |
40 | useEffect(() => {
41 | updatePlaceholderButtons(prompt.text)
42 | }, [prompt])
43 |
44 | useEffect(() => {
45 | setNameError(prompt.name.trim() === '')
46 | setTextError(prompt.text.trim() === '')
47 | setWebResultsError(!prompt.text.includes('{web_results}'))
48 | setQueryError(!prompt.text.includes('{query}'))
49 | }, [prompt])
50 |
51 | async function updateList() {
52 | getSavedPrompts().then(sp => {
53 | setSavedPrompts(sp)
54 | })
55 | }
56 |
57 | const handleSelect = (prompt: Prompt) => {
58 | setShowErrors(false)
59 | setPrompt(prompt)
60 | setDeleteBtnText("delete")
61 | }
62 |
63 |
64 | const handleAdd = () => {
65 | setShowErrors(false)
66 | setPrompt({ name: '', text: '' })
67 | setDeleteBtnText("delete")
68 | if (nameInputRef.current) {
69 | nameInputRef.current.focus()
70 | }
71 | }
72 |
73 | const handleSave = async () => {
74 | setShowErrors(true)
75 | if (nameError || textError || webResultsError || queryError) {
76 | return
77 | }
78 |
79 | await savePrompt(prompt)
80 | await updateList()
81 | }
82 |
83 | const handleDeleteBtnClick = () => {
84 | if (deleteBtnText === "delete") {
85 | setDeleteBtnText("check");
86 | } else {
87 | handleDelete();
88 | }
89 | }
90 |
91 | const handleDelete = async () => {
92 | await deletePrompt(prompt)
93 | updateList()
94 | handleAdd()
95 | }
96 |
97 |
98 | const nameInputRef = useRef(null)
99 | const textareaRef = useRef(null)
100 |
101 | const handleInsertText = (text: string) => {
102 | if (textareaRef.current) {
103 | const start = textareaRef.current.selectionStart
104 | const end = textareaRef.current.selectionEnd
105 | const currentText = textareaRef.current.value
106 | const newText = currentText.substring(0, start) + text + currentText.substring(end, currentText.length)
107 | textareaRef.current.setSelectionRange(start + text.length, start + text.length)
108 | textareaRef.current.focus()
109 |
110 | setPrompt({ ...prompt, text: newText })
111 | }
112 | }
113 |
114 | const handleTextareaChange = (e: Event) => {
115 | let text = (e.target as HTMLTextAreaElement).value
116 | if (text !== prompt.text) {
117 | setTextError(false)
118 | setPrompt({ ...prompt, text: text })
119 | }
120 | }
121 |
122 | const updatePlaceholderButtons = (text: string) => {
123 | setHasWebResultsPlaceholder(text.includes("{web_results}"))
124 | setHasQueryPlaceholder(text.includes("{query}"))
125 | }
126 |
127 | const actionToolbar = (
128 |
131 |
132 |
133 |
144 |
145 |
146 |
157 |
158 |
159 |
165 |
166 |
167 |
168 |
174 |
175 | )
176 |
177 | const PromptList = (
178 |
179 |
182 | {savedPrompts.map((prmpt: Prompt) => (
183 | - handleSelect(prmpt)}
186 | >
187 |
188 | 📝 {prmpt.name}
189 |
190 |
191 | ))}
192 |
193 |
194 | )
195 |
196 | const nameInput = (
197 | {
205 | setNameError(false)
206 | setPrompt({ ...prompt, name: (e.target as HTMLInputElement).value })
207 | }}
208 | disabled={prompt.uuid === 'default' || prompt.uuid === 'default_en'}
209 | />
210 | )
211 |
212 | const btnDelete = (
213 |
223 | )
224 |
225 | const textArea = (
226 |
235 | )
236 |
237 | return (
238 |
239 |
240 | {PromptList}
241 |
242 |
243 |
244 |
245 | {nameInput}
246 | {btnDelete}
247 |
248 | {textArea}
249 |
250 | {actionToolbar}
251 |
252 |
253 | )
254 | }
255 |
256 | export default PromptEditor
257 |
--------------------------------------------------------------------------------
/src/components/socialIconButton.tsx:
--------------------------------------------------------------------------------
1 | import { h, JSX } from "preact";
2 | import TooltipWrapper from "./tooltipWrapper";
3 |
4 | function IconButton(props: { url: string, tip: string, icon: JSX.Element }) {
5 | return (
6 |
7 |
8 |
9 | {props.icon}
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default IconButton
17 |
--------------------------------------------------------------------------------
/src/components/toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { h, options } from 'preact'
2 | import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
3 | import { icons } from 'src/util/icons'
4 | import { getSavedPrompts, Prompt } from 'src/util/promptManager'
5 | import { getUserConfig, updateUserConfig } from 'src/util/userConfig'
6 | import timePeriodOptions from 'src/util/timePeriodOptions.json'
7 | import regionOptions from 'src/util/regionOptions.json'
8 | import Browser from 'webextension-polyfill'
9 | import Dropdown from './dropdown'
10 | import { getTranslation, localizationKeys, setLocaleLanguage } from 'src/util/localization'
11 |
12 |
13 | const numResultsOptions = Array.from({ length: 10 }, (_, i) => i + 1).map((num) => ({
14 | value: num,
15 | label: `${num} result${num === 1 ? '' : 's'}`
16 | }))
17 |
18 | function Toolbar() {
19 | const [webAccess, setWebAccess] = useState(true)
20 | const [numResults, setNumResults] = useState(3)
21 | const [timePeriod, setTimePeriod] = useState('')
22 | const [region, setRegion] = useState('wt-wt')
23 | const [promptUUID, setPromptUUID] = useState('')
24 | const [prompts, setPrompts] = useState([])
25 |
26 | useEffect(() => {
27 | getUserConfig().then((userConfig) => {
28 | setWebAccess(userConfig.webAccess)
29 | setNumResults(userConfig.numWebResults)
30 | setTimePeriod(userConfig.timePeriod)
31 | setRegion(userConfig.region)
32 | setPromptUUID(userConfig.promptUUID)
33 |
34 | setLocaleLanguage(userConfig.language)
35 | })
36 | updatePrompts()
37 | }, [])
38 |
39 | const handlePromptClick = () => {
40 | updatePrompts()
41 | }
42 |
43 | const updatePrompts = () => {
44 | getSavedPrompts().then((savedPrompts) => {
45 | setPrompts(savedPrompts)
46 | })
47 | }
48 |
49 | const handleWebAccessToggle = useCallback(() => {
50 | setWebAccess(!webAccess)
51 | updateUserConfig({ webAccess: !webAccess })
52 | }, [webAccess])
53 |
54 | const handleNumResultsChange = useCallback((e: { target: { value: string } }) => {
55 | const value = parseInt(e.target.value)
56 | setNumResults(value)
57 | updateUserConfig({ numWebResults: value })
58 | }, [numResults])
59 |
60 | const handleTimePeriodChange = useCallback((e: { target: { value: string } }) => {
61 | setTimePeriod(e.target.value)
62 | updateUserConfig({ timePeriod: e.target.value })
63 | }, [timePeriod])
64 |
65 | const handleRegionChange = useCallback((e: { target: { value: string } }) => {
66 | setRegion(e.target.value)
67 | updateUserConfig({ region: e.target.value })
68 | }, [region])
69 |
70 | const handlePromptChange = (uuid: string) => {
71 | removeFocusFromCurrentElement()
72 |
73 | setPromptUUID(uuid)
74 | updateUserConfig({ promptUUID: uuid })
75 | }
76 |
77 | const removeFocusFromCurrentElement = () => (document.activeElement as HTMLElement)?.blur()
78 |
79 |
80 | const webAccessToggle =
85 |
86 | return (
87 |
88 |
Browser.runtime.sendMessage("show_options")}
90 | >
91 | {icons.tune}
92 |
93 | {webAccessToggle}
94 |
95 | )
96 | }
97 |
98 | export default Toolbar
99 |
--------------------------------------------------------------------------------
/src/components/tooltipWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { h, JSX } from "preact";
2 |
3 | export const tooltipPositions = {
4 | top: "wcg-tooltip-top",
5 | bottom: "wcg-tooltip-bottom"
6 | }
7 |
8 |
9 | function TooltipWrapper(props: { tip: string, children: JSX.Element, position?: string }) {
10 | if (!props.tip) {
11 | return props.children
12 | } else {
13 | return (
14 |
19 | {props.children}
20 |
21 | )
22 | }
23 | }
24 |
25 | export default TooltipWrapper
26 |
--------------------------------------------------------------------------------
/src/content-scripts/api.ts:
--------------------------------------------------------------------------------
1 | export interface SearchResult {
2 | body: string
3 | href: string
4 | title: string
5 | }
6 |
7 | export async function apiSearch(query: string, numResults: number, timePeriod: string, region: string): Promise {
8 | const url = `https://ddg-webapp-aagd.vercel.app/search?`
9 | + `max_results=${numResults}`
10 | + `&q=${query}`
11 | + (timePeriod ? `&time=${timePeriod}` : "")
12 | + (region ? `®ion=${region}` : "")
13 |
14 | const response = await fetch(url)
15 | const results = await response.json()
16 | return results.map((result: any) => {
17 | return {
18 | body: result.body,
19 | href: result.href,
20 | title: result.title
21 | }
22 | })
23 | }
--------------------------------------------------------------------------------
/src/content-scripts/mainUI.tsx:
--------------------------------------------------------------------------------
1 | import '../style/base.css'
2 | import { h, render } from 'preact'
3 | import { getTextArea, getFooter, getRootElement, getSubmitButton, getWebChatGPTToolbar } from '../util/elementFinder'
4 | import Toolbar from 'src/components/toolbar'
5 | import Footer from 'src/components/footer'
6 | import ErrorMessage from 'src/components/errorMessage'
7 | import { getUserConfig } from 'src/util/userConfig'
8 | import { apiSearch, SearchResult } from './api'
9 | import createShadowRoot from 'src/util/createShadowRoot'
10 | import { compilePrompt } from 'src/util/promptManager'
11 |
12 | var isProcessing = false
13 |
14 | var btnSubmit: HTMLButtonElement
15 | var textarea: HTMLTextAreaElement
16 | var footer: HTMLDivElement
17 |
18 | async function onSubmit(event: any) {
19 |
20 | if (event.shiftKey && event.key === 'Enter')
21 | return
22 |
23 | if ((event.type === "click" || event.key === 'Enter') && !isProcessing) {
24 |
25 | let query = textarea.value.trim()
26 |
27 | if (query === "") return
28 |
29 | textarea.value = ""
30 |
31 | const userConfig = await getUserConfig()
32 |
33 | isProcessing = true
34 |
35 | if (!userConfig.webAccess) {
36 | textarea.value = query
37 | pressEnter()
38 | isProcessing = false
39 | return
40 | }
41 |
42 | textarea.value = ""
43 |
44 | try {
45 | const results = await apiSearch(query, userConfig.numWebResults, userConfig.timePeriod, userConfig.region)
46 | await pasteWebResultsToTextArea(results, query)
47 | pressEnter()
48 | isProcessing = false
49 |
50 | } catch (error) {
51 | isProcessing = false
52 | showErrorMessage(error)
53 | }
54 | }
55 | }
56 |
57 | async function pasteWebResultsToTextArea(results: SearchResult[], query: string) {
58 |
59 | textarea.value = await compilePrompt(results, query)
60 | }
61 |
62 | function pressEnter() {
63 | textarea.focus()
64 | const enterEvent = new KeyboardEvent('keydown', {
65 | bubbles: true,
66 | cancelable: true,
67 | key: 'Enter',
68 | code: 'Enter'
69 | })
70 | textarea.dispatchEvent(enterEvent)
71 | }
72 |
73 | function showErrorMessage(error: Error) {
74 | console.log("GoogleChatGPT error --> API error: ", error)
75 | let div = document.createElement('div')
76 | document.body.appendChild(div)
77 | render(, div)
78 | }
79 |
80 |
81 | async function updateUI() {
82 |
83 | if (getWebChatGPTToolbar()) return
84 |
85 | btnSubmit = getSubmitButton()
86 | textarea = getTextArea()
87 | footer = getFooter()
88 |
89 | if (textarea && btnSubmit) {
90 |
91 | textarea.addEventListener("keydown", onSubmit)
92 | btnSubmit.addEventListener("click", onSubmit)
93 |
94 | const textareaParent = textarea.parentElement.parentElement
95 | textareaParent.style.flexDirection = 'column'
96 |
97 | const { shadowRootDiv, shadowRoot } = await createShadowRoot('content-scripts/mainUI.css')
98 | textareaParent.appendChild(shadowRootDiv)
99 | render(, shadowRoot)
100 | }
101 |
102 | if (footer) {
103 | let div = document.createElement('div')
104 | footer.lastElementChild.appendChild(div)
105 | render(, div)
106 | }
107 | }
108 |
109 | const rootEl = getRootElement()
110 | window.onload = function () {
111 | updateUI()
112 |
113 | try {
114 | new MutationObserver(() => {
115 | updateUI()
116 | }).observe(rootEl, { childList: true })
117 | } catch (e) {
118 | console.info("WebChatGPT error --> Could not update UI:\n", e.stack)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/declaration.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css' {
2 | const mapping: Record
3 | export default mapping
4 | }
5 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_appName__",
4 | "description": "__MSG_appDesc__",
5 | "default_locale": "en",
6 | "version": "2023.02.04",
7 | "icons": {
8 | "16": "icons/icon16.png",
9 | "48": "icons/icon48.png",
10 | "128": "icons/icon128.png"
11 | },
12 | "permissions": ["storage"],
13 | "host_permissions": ["https://ddg-webapp-aagd.vercel.app/*"],
14 | "background": {
15 | "service_worker": "background/bg.js"
16 | },
17 | "action": {},
18 | "content_scripts": [
19 | {
20 | "matches": ["https://chat.openai.com/chat*"],
21 | "js": ["content-scripts/mainUI.js"]
22 | }
23 | ],
24 | "options_ui": {
25 | "page": "options/options.html",
26 | "open_in_tab": true,
27 | "css": ["options/options.css"]
28 | },
29 | "web_accessible_resources": [
30 | {
31 | "resources": [
32 | "content-scripts/mainUI.css",
33 | "icons/icon48.png"
34 | ],
35 | "matches": ["https://chat.openai.com/*"]
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/manifest.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_appName__",
4 | "description": "__MSG_appDesc__",
5 | "default_locale": "en",
6 | "version": "2023.02.04",
7 | "icons": {
8 | "16": "icons/icon16.png",
9 | "48": "icons/icon48.png",
10 | "128": "icons/icon128.png"
11 | },
12 | "permissions": [
13 | "storage",
14 | "webRequest",
15 | "https://ddg-webapp-aagd.vercel.app/*"
16 | ],
17 | "background": {
18 | "scripts": ["background/bg.js"]
19 | },
20 | "browser_action": {},
21 | "browser_specific_settings": {
22 | "gecko": {
23 | "id": "{b13d04e3-41db-48b3-842c-8079df93c7ad}"
24 | }
25 | },
26 | "content_scripts": [
27 | {
28 | "matches": ["https://chat.openai.com/*"],
29 | "js": ["content-scripts/mainUI.js"]
30 | }
31 | ],
32 | "options_ui": {
33 | "page": "options/options.html",
34 | "open_in_tab": true,
35 | "chrome_style": true
36 | },
37 | "web_accessible_resources": ["content-scripts/mainUI.css", "icons/icon48.png"]
38 | }
39 |
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebChatpGPT Options
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/options/options.tsx:
--------------------------------------------------------------------------------
1 | import "../style/base.css"
2 | import { h, JSX, render } from "preact"
3 | import { getUserConfig, updateUserConfig } from "src/util/userConfig"
4 | import { useLayoutEffect, useState } from "preact/hooks"
5 | import PromptEditor from "src/components/promptEditor"
6 | import { getTranslation, localizationKeys, setLocaleLanguage } from "src/util/localization"
7 | import NavBar from "src/components/navBar"
8 | import { icons } from "src/util/icons"
9 |
10 | const Footer = (props: { language: string; }) => (
11 |
12 |
13 | {getTranslation(localizationKeys.UI.supportMe)}
14 |
15 |
16 |
17 |
18 |
19 | )
20 |
21 | const SocialCard = ({ icon, text }: { icon: JSX.Element, text: string }) => (
22 |
23 | {icon}
24 |
{text}
25 |
26 | )
27 |
28 |
29 | export default function OptionsPage() {
30 |
31 | const [language, setLanguage] = useState(null)
32 |
33 |
34 | useLayoutEffect(() => {
35 | getUserConfig().then(config => {
36 | setLanguage(config.language)
37 | setLocaleLanguage(config.language)
38 | })
39 | }, [])
40 |
41 | const onLanguageChange = (language: string) => {
42 | setLanguage(language)
43 | updateUserConfig({ language })
44 | setLocaleLanguage(language)
45 | }
46 |
47 | if (!language) {
48 | return
49 | }
50 |
51 | return (
52 |
53 |
54 |
58 |
59 |
62 |
63 |
64 |
65 | {/*
66 |
67 |
68 |
69 |
*/}
70 |
71 |
72 |
73 | )
74 | }
75 |
76 |
77 | render(, document.getElementById("options"))
78 |
--------------------------------------------------------------------------------
/src/style/base.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/util/createShadowRoot.ts:
--------------------------------------------------------------------------------
1 | import Browser from "webextension-polyfill"
2 |
3 | async function createShadowRoot(pathToCSS: string) {
4 | let shadowRootDiv = document.createElement('div')
5 | const shadowRoot = shadowRootDiv.attachShadow({ mode: 'open' })
6 | const style = document.createElement('style')
7 | style.textContent = await fetch(Browser.runtime.getURL(pathToCSS)).then(response => response.text())
8 | shadowRoot.append(style)
9 | return { shadowRootDiv, shadowRoot }
10 | }
11 |
12 | export default createShadowRoot
13 |
--------------------------------------------------------------------------------
/src/util/elementFinder.ts:
--------------------------------------------------------------------------------
1 | export function getTextArea(): HTMLTextAreaElement {
2 | return document.querySelector('textarea')
3 | }
4 |
5 | export function getFooter(): HTMLDivElement {
6 | return document.querySelector("div[class*='absolute bottom-0']")
7 | }
8 |
9 | export function getRootElement(): HTMLDivElement {
10 | return document.querySelector('div[id="__next"]')
11 | }
12 |
13 | export function getWebChatGPTToolbar(): HTMLElement {
14 | return document.querySelector("div[class*='wcg-toolbar']")
15 | }
16 |
17 | export function getSubmitButton(): HTMLButtonElement {
18 | const textarea = getTextArea()
19 | if (!textarea) {
20 | return null
21 | }
22 | return textarea.parentNode.querySelector("button")
23 | }
24 |
--------------------------------------------------------------------------------
/src/util/icons.tsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | export const twitterIcon = (
4 |
7 | )
8 |
9 | export const discordIcon = (
10 |
13 | )
14 |
15 | export const githubIcon = (
16 |
19 | )
20 |
21 | export const tuneIcon = (
22 |
25 | )
26 |
27 | export const Language = (
28 |
31 | )
32 |
33 | export const Expand = (
34 |
37 | )
38 |
39 | export const icons = {
40 | twitter: twitterIcon,
41 | discord: discordIcon,
42 | github: githubIcon,
43 | tune: tuneIcon,
44 | language: Language,
45 | expand: Expand,
46 | }
47 |
--------------------------------------------------------------------------------
/src/util/localization.ts:
--------------------------------------------------------------------------------
1 | import Browser from "webextension-polyfill"
2 | import * as localizedStrings from './localizedStrings.json'
3 |
4 | export const getSystemLanguage = () => Browser.i18n.getUILanguage().split("-")[0]
5 |
6 | export const Languages = {
7 | "auto": "Auto",
8 | "en": "English",
9 | "de": "Deutsch",
10 | "es": "Español",
11 | "fr": "Français",
12 | "it": "Italiano",
13 | "ja": "日本語",
14 | "ko": "한국어",
15 | "pt": "Português",
16 | "zh": "中文"
17 | }
18 |
19 | const DEFAULT_LANGUAGE = 'en'
20 |
21 |
22 | let language = getSystemLanguage()
23 |
24 | export const getLocaleLanguage = () => language
25 |
26 | export const getCurrentLanguageName = () => language === Languages.auto ? Languages.en : Languages[language]
27 |
28 | export const setLocaleLanguage = (newLanguage: string) => {
29 | language = newLanguage === 'auto' ? getSystemLanguage() : newLanguage
30 | console.debug(`Language set to ${language}`)
31 | }
32 |
33 | export const getTranslation = (key: string, lang? : string) => {
34 | if(lang) {
35 | return localizedStrings[key][lang]
36 | }
37 | if (language in localizedStrings[key]) {
38 | return localizedStrings[key][language]
39 | }
40 | return localizedStrings[key][DEFAULT_LANGUAGE]
41 | }
42 |
43 |
44 | export const localizationKeys = {
45 | defaultPrompt: 'default_prompt',
46 | UI: {
47 | language: 'language',
48 | supportThisProject: 'support_this_project',
49 | supportMe: 'support_me',
50 | chooseLanguage: 'choose_language',
51 | },
52 | placeholders: {
53 | namePlaceholder: 'name_placeholder',
54 | },
55 | buttons: {
56 | save: 'save',
57 | newPrompt: 'new_prompt',
58 | },
59 | placeHolderTips: {
60 | currentDate: 'current_date_placeholder_tip',
61 | webResults: 'web_results_placeholder_tip',
62 | query: 'query_placeholder_tip',
63 | },
64 | socialButtonTips: {
65 | twitter: 'twitter_button_tip',
66 | github: 'github_button_tip',
67 | discord: 'discord_button_tip',
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/util/localizedStrings.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_prompt": {
3 | "en": "Web search results:\n{web_results}\nInstructions: Please provide a concise and informative response to the user's query based on the information available in your training data and current knowledge. If necessary, you may use web search results from {current_date} to supplement your answer, but please clearly cite your sources using [[number](URL)].\n\nUser Query: {query}",
4 | "pt": "Resultados de pesquisa na web:\n\n{web_results}\nData atual: {current_date}\n\nInstruções: Usando os resultados de pesquisa na web fornecidos, escreva uma resposta abrangente para a consulta dada. Certifique-se de citar os resultados usando a notação [[número](URL)] após a referência. Se os resultados de pesquisa fornecidos se referem a múltiplos assuntos com o mesmo nome, escreva respostas separadas para cada assunto.\nConsulta: {query}",
5 | "es": "Resultados de búsqueda en la web:\n\n{web_results}\nFecha actual: {current_date}\n\nInstrucciones: Utilizando los resultados de búsqueda en la web proporcionados, escriba una respuesta completa a la consulta dada. Asegúrese de citar los resultados utilizando la notación [[número](URL)] después de la referencia. Si los resultados de búsqueda proporcionados se refieren a varios temas con el mismo nombre, escriba respuestas separadas para cada tema.\nConsulta: {query}",
6 | "fr": "Résultats de recherche sur le web:\n\n{web_results}\nDate actuelle: {current_date}\n\nInstructions: En utilisant les résultats de recherche sur le web fournis, écrivez une réponse complète à la question posée. Assurez-vous de citer les résultats en utilisant la notation [[numéro](URL)] après la référence. Si les résultats de recherche fournis se réfèrent à plusieurs sujets ayant le même nom, écrivez des réponses séparées pour chaque sujet.\nRequête: {query}",
7 | "de": "Web-Suchergebnisse:\n\n{web_results}\nAktuelles Datum: {current_date}\n\nAnweisungen: Verwenden Sie die bereitgestellten Web-Suchergebnisse, um eine umfassende Antwort auf die gegebene Anfrage zu geben. Stellen Sie sicher, dass Sie die Ergebnisse mit der Notation [[Zahl](URL)] nach der Referenz zitieren. Wenn die bereitgestellten Suchergebnisse auf mehrere Themen mit demselben Namen verweisen, schreiben Sie separate Antworten für jedes Thema.\nAnfrage: {query}",
8 | "it": "Risultati della ricerca Web:\n\n{web_results}\nData corrente: {current_date}\n\nIstruzioni: utilizzando i risultati della ricerca Web forniti, scrivere una risposta completa alla query specificata. Assicurati di citare i risultati utilizzando la notazione [[numero](URL)] dopo il riferimento. Se i risultati di ricerca forniti si riferiscono a più argomenti con lo stesso nome, scrivi risposte separate per ogni argomento.\nQuery: {query}",
9 | "zh": "网络搜索结果:\n\n{web_results}\n当前日期: {current_date}\n\n说明: 使用提供的网络搜索结果,对给定的查询进行综合回复。 确保在参考文献后使用 [[数字](URL)] 符号来引用结果。 如果提供的搜索结果涉及同名的多个主题,请为每个主题分别写下答案。\n查询: {query}",
10 | "ja": "Web 検索結果:\n\n{web_results}\n現在の日付: {current_date}\n\n指示: 提供された Web 検索結果を使用して、指定されたクエリに対する包括的な回答を作成します。 参考文献の後に必ず[[数字](URL)]表記で結果を引用してください。 提供された検索結果が同じ名前の複数の件名を参照している場合は、件名ごとに個別の回答を記述してください。\nクエリ: {query}",
11 | "ko": "웹 검색 결과:\n\n{web_results}\n현재 날짜: {current_date}\n\n지침: 제공된 웹 검색 결과를 사용하여 주어진 쿼리에 대한 포괄적인 회신을 작성합니다. 반드시 [[숫자](URL)] 표기를 사용하여 결과를 인용한다. 제공된 검색 결과가 동일한 이름을 가진 여러 주제를 참조하는 경우 각 주제에 대해 별도의 답변을 작성하십시오.\n쿼리: {query}"
12 | },
13 | "language": {
14 | "en": "Language",
15 | "pt": "Idioma",
16 | "es": "Idioma",
17 | "fr": "Langue",
18 | "de": "Sprache",
19 | "it": "Lingua",
20 | "zh": "语言",
21 | "ja": "言語",
22 | "ko": "언어"
23 | },
24 | "choose_language": {
25 | "en": "Choose language",
26 | "pt": "Escolha o idioma",
27 | "es": "Elegir idioma",
28 | "fr": "Choisir la langue",
29 | "de": "Sprache auswählen",
30 | "it": "Scegli la lingua",
31 | "zh": "选择语言",
32 | "ja": "言語を選択",
33 | "ko": "언어 선택"
34 | },
35 | "support_this_project": {
36 | "en": "Support this project",
37 | "pt": "Apoie este projeto",
38 | "es": "Apoya este proyecto",
39 | "fr": "Soutenez ce projet",
40 | "de": "Unterstützen Sie dieses Projekt",
41 | "it": "Sostieni questo progetto",
42 | "zh": "支持此项目",
43 | "ja": "このプロジェクトを支援",
44 | "ko": "이 프로젝트 지원"
45 | },
46 | "support_me": {
47 | "en": "This extension is free! 🥳 But the server it runs on is not. 😬\nPlease help me keep it alive! ⤵️",
48 | "pt": "Esta extensão é gratuita! 🥳 Mas o servidor em que ela é executada não é. 😬\nAjude-me a mantê-lo vivo! ⤵️",
49 | "es": "¡Esta extensión es gratuita! 🥳 Pero el servidor en el que se ejecuta no lo es. 😬\n¡Ayúdame a mantenerla viva! ⤵️",
50 | "fr": "Cette extension est gratuite ! 🥳 Mais le serveur sur lequel elle fonctionne ne l'est pas. 😬\nVeuillez m'aider à la maintenir en vie ! ⤵️",
51 | "de": "Diese Erweiterung ist kostenlos! 🥳 Aber der Server, auf dem sie läuft, ist es nicht. 😬\nBitte hilf mir, sie am Leben zu erhalten! ⤵️",
52 | "it": "Questa estensione è gratuita! 🥳 Ma il server su cui viene eseguita non lo è. 😬\nPer favore aiutami a tenerlo vivo! ⤵️",
53 | "zh": "此扩展程序是免费的!🥳 但是它运行的服务器不是。😬\n帮助这个项目继续下去!⤵️",
54 | "ja": "この拡張機能は無料です!🥳 しかし、それが実行されているサーバーは無料ではありません。😬\nこのプロジェクトを維持するのを手伝ってください!⤵️",
55 | "ko": "이 확장 프로그램은 무료입니다! 🥳 그러나 그것이 실행되는 서버는 아닙니다. 😬\n이 프로젝트를 계속 유지하도록 도와주세요! ⤵️"
56 | },
57 | "save": {
58 | "en": "Save",
59 | "pt": "Salvar",
60 | "es": "Guardar",
61 | "fr": "Enregistrer",
62 | "de": "Speichern",
63 | "it": "Salva",
64 | "zh": "保存",
65 | "ja": "保存",
66 | "ko": "저장"
67 | },
68 | "new_prompt": {
69 | "en": "New prompt",
70 | "pt": "Novo prompt",
71 | "es": "Nuevo prompt",
72 | "fr": "Nouveau prompt",
73 | "de": "Neues Prompt",
74 | "it": "Nuovo prompt",
75 | "zh": "新提示",
76 | "ja": "新しいプロンプト",
77 | "ko": "새로운 프롬프트"
78 | },
79 | "name_placeholder": {
80 | "en": "Name",
81 | "pt": "Nome",
82 | "es": "Nombre",
83 | "fr": "Nom",
84 | "de": "Name",
85 | "it": "Nome",
86 | "zh": "名称",
87 | "ja": "名前",
88 | "ko": "이름"
89 | },
90 | "current_date_placeholder_tip": {
91 | "en": "Insert placeholder for the current date (optional)",
92 | "pt": "Insira o espaço reservado para a data atual (opcional)",
93 | "es": "Ingrese un marcador de posición para la fecha actual (opcional)",
94 | "fr": "Insérer un marqueur de place pour la date actuelle (facultatif)",
95 | "de": "Platzhalter für das aktuelle Datum einfügen (optional)",
96 | "it": "Inserisci il segnaposto per la data attuale (opzionale)",
97 | "zh": "插入当前日期的占位符(可选)",
98 | "ja": "現在の日付のプレースホルダーを挿入(任意)",
99 | "ko": "현재 날짜의 자리 표시자를 삽입 (선택 사항)"
100 | },
101 | "web_results_placeholder_tip": {
102 | "en": "Insert placeholder for the web results (required)",
103 | "pt": "Insira o espaço reservado para os resultados da pesquisa na web (obrigatório)",
104 | "es": "Ingrese un marcador de posición para los resultados de búsqueda web (requerido)",
105 | "fr": "Insérer un marqueur de place pour les résultats de recherche web (requis)",
106 | "de": "Platzhalter für die Web-Ergebnisse einfügen (erforderlich)",
107 | "it": "Inserisci il segnaposto per i risultati web (richiesto)",
108 | "zh": "插入网络搜索结果的占位符(必需)",
109 | "ja": "Web結果のプレースホルダーを挿入(必須)",
110 | "ko": "웹 검색 결과의 자리 표시자를 삽입 (필수)"
111 | },
112 | "query_placeholder_tip": {
113 | "en": "Insert placeholder for the initial query (required)",
114 | "pt": "Insira o espaço reservado para a pergunta inicial (obrigatório)",
115 | "es": "Ingrese un marcador de posición para la consulta inicial (requerido)",
116 | "fr": "Insérer un marqueur de place pour la requête initiale (requis)",
117 | "de": "Platzhalter für die ursprüngliche Anfrage einfügen (erforderlich)",
118 | "it": "Inserisci il segnaposto per la query iniziale (richiesto)",
119 | "zh": "插入初始查询的占位符(必需)",
120 | "ja": "初期クエリのプレースホルダーを挿入(必須)",
121 | "ko": "초기 쿼리의 자리 표시자를 삽입 (필수)"
122 | },
123 | "twitter_button_tip": {
124 | "en": "Follow me on Twitter",
125 | "pt": "Siga-me no Twitter",
126 | "es": "Sígueme en Twitter",
127 | "fr": "Me suivre sur Twitter",
128 | "de": "Folgen Sie mir auf Twitter",
129 | "it": "Seguimi su Twitter",
130 | "zh": "在 Twitter 上关注我",
131 | "ja": "Twitterで私をフォロー",
132 | "ko": "Twitter에서 나를 팔로우하세요"
133 | },
134 | "github_button_tip": {
135 | "en": "View the source code on GitHub",
136 | "pt": "Veja o código fonte no GitHub",
137 | "es": "Ver el código fuente en GitHub",
138 | "fr": "Voir le code source sur GitHub",
139 | "de": "Quellcode auf GitHub anzeigen",
140 | "it": "Visualizza il codice sorgente su GitHub",
141 | "zh": "在 GitHub 上查看源代码",
142 | "ja": "GitHub上のソースコードを見る",
143 | "ko": "GitHub에서 소스 코드 보기"
144 | },
145 | "discord_button_tip": {
146 | "en": "Join our Discord community",
147 | "pt": "Junte-se à nossa comunidade no Discord",
148 | "es": "Únete a nuestra comunidad de Discord",
149 | "fr": "Rejoignez notre communauté Discord",
150 | "de": "Treten Sie unserer Discord-Community bei",
151 | "it": "Unisciti alla nostra comunità su Discord",
152 | "zh": "加入我们的 Discord 社区",
153 | "ja": "私たちのDiscordコミュニティに参加してください",
154 | "ko": "우리의 Discord 커뮤니티에 가입하세요"
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/util/promptManager.ts:
--------------------------------------------------------------------------------
1 | import { SearchResult } from "src/content-scripts/api"
2 | import Browser from "webextension-polyfill"
3 | import { v4 as uuidv4 } from 'uuid'
4 | import { getCurrentLanguageName, getLocaleLanguage, getTranslation, localizationKeys } from "./localization"
5 | import { getUserConfig } from "./userConfig"
6 |
7 | export const DEFAULT_PROMPT_KEY = 'default_prompt'
8 | export const CURRENT_PROMPT_UUID_KEY = 'promptUUID'
9 | export const SAVED_PROMPTS_KEY = 'saved_prompts'
10 |
11 | export interface Prompt {
12 | uuid?: string,
13 | name: string,
14 | text: string
15 | }
16 |
17 | export const compilePrompt = async (results: SearchResult[], query: string) => {
18 | const currentPrompt = await getCurrentPrompt()
19 | const formattedResults = formatWebResults(results)
20 | const currentDate = new Date().toLocaleDateString()
21 | const prompt = replaceVariables(currentPrompt.text, {
22 | '{web_results}': formattedResults,
23 | '{query}': query,
24 | '{current_date}': currentDate
25 | })
26 | return prompt
27 | }
28 |
29 | const formatWebResults = (results: SearchResult[]) => {
30 | let counter = 1
31 | return results.reduce((acc, result): string => acc += `[${counter++}] "${result.body}"\nURL: ${result.href}\n\n`, "")
32 | }
33 |
34 | const replaceVariables = (prompt: string, variables: { [key: string]: string }) => {
35 | let newPrompt = prompt
36 | for (const key in variables) {
37 | try {
38 | newPrompt = newPrompt.replaceAll(key, variables[key])
39 | } catch (error) {
40 | console.log("WebChatGPT error --> API error: ", error)
41 | }
42 | }
43 | return newPrompt
44 | }
45 |
46 | export const getDefaultPrompt = () => {
47 | return {
48 | name: 'Default prompt',
49 | // text: getTranslation(localizationKeys.defaultPrompt),
50 | text: getTranslation(localizationKeys.defaultPrompt, 'en') + (getLocaleLanguage() !== 'en' ? '\nReply in ' + getCurrentLanguageName() : ''),
51 | uuid: 'default'
52 | }
53 | }
54 |
55 | const getDefaultEnglishPrompt = () => {
56 | return { name: 'Default English', text: getTranslation(localizationKeys.defaultPrompt, 'en'), uuid: 'default_en' }
57 | }
58 |
59 | export const getCurrentPrompt = async () => {
60 | const userConfig = await getUserConfig()
61 | const currentPromptUuid = userConfig.promptUUID
62 | const savedPrompts = await getSavedPrompts()
63 | return savedPrompts.find((i: Prompt) => i.uuid === currentPromptUuid) || getDefaultPrompt()
64 | }
65 |
66 | export const getSavedPrompts = async (addDefaults: boolean = true) => {
67 | const data = await Browser.storage.sync.get([SAVED_PROMPTS_KEY])
68 | const savedPrompts = data[SAVED_PROMPTS_KEY] || []
69 |
70 | if (addDefaults)
71 | return addDefaultPrompts(savedPrompts)
72 |
73 | return savedPrompts
74 | }
75 |
76 | function addDefaultPrompts(prompts: Prompt[]) {
77 |
78 | if (getLocaleLanguage() !== 'en') {
79 | addPrompt(prompts, getDefaultEnglishPrompt())
80 | }
81 | addPrompt(prompts, getDefaultPrompt())
82 | return prompts
83 |
84 | function addPrompt(prompts: Prompt[], prompt: Prompt) {
85 | const index = prompts.findIndex((i: Prompt) => i.uuid === prompt.uuid)
86 | if (index >= 0) {
87 | prompts[index] = prompt
88 | } else {
89 | prompts.unshift(prompt)
90 | }
91 | }
92 | }
93 |
94 | export const savePrompt = async (prompt: Prompt) => {
95 | let savedPrompts = await getSavedPrompts(false)
96 | const index = savedPrompts.findIndex((i: Prompt) => i.uuid === prompt.uuid)
97 | if (index >= 0) {
98 | savedPrompts[index] = prompt
99 | } else {
100 | prompt.uuid = uuidv4()
101 | savedPrompts.push(prompt)
102 | }
103 |
104 | await Browser.storage.sync.set({ [SAVED_PROMPTS_KEY]: savedPrompts })
105 | }
106 |
107 | export const deletePrompt = async (prompt: Prompt) => {
108 | let savedPrompts = await getSavedPrompts()
109 | savedPrompts = savedPrompts.filter((i: Prompt) => i.uuid !== prompt.uuid)
110 | await Browser.storage.sync.set({ [SAVED_PROMPTS_KEY]: savedPrompts })
111 | }
112 |
--------------------------------------------------------------------------------
/src/util/regionOptions.json:
--------------------------------------------------------------------------------
1 | [
2 | { "value": "wt-wt", "label": "Any region" },
3 | { "value": "xa-ar", "label": "Saudi Arabia" },
4 | { "value": "xa-en", "label": "Saudi Arabia (en)" },
5 | { "value": "ar-es", "label": "Argentina" },
6 | { "value": "au-en", "label": "Australia" },
7 | { "value": "at-de", "label": "Austria" },
8 | { "value": "be-fr", "label": "Belgium (fr)" },
9 | { "value": "be-nl", "label": "Belgium (nl)" },
10 | { "value": "br-pt", "label": "Brazil" },
11 | { "value": "bg-bg", "label": "Bulgaria" },
12 | { "value": "ca-en", "label": "Canada" },
13 | { "value": "ca-fr", "label": "Canada (fr)" },
14 | { "value": "ct-ca", "label": "Catalan" },
15 | { "value": "cl-es", "label": "Chile" },
16 | { "value": "cn-zh", "label": "China" },
17 | { "value": "co-es", "label": "Colombia" },
18 | { "value": "hr-hr", "label": "Croatia" },
19 | { "value": "cz-cs", "label": "Czech Republic" },
20 | { "value": "dk-da", "label": "Denmark" },
21 | { "value": "ee-et", "label": "Estonia" },
22 | { "value": "fi-fi", "label": "Finland" },
23 | { "value": "fr-fr", "label": "France" },
24 | { "value": "de-de", "label": "Germany" },
25 | { "value": "gr-el", "label": "Greece" },
26 | { "value": "hk-tzh", "label": "Hong Kong" },
27 | { "value": "hu-hu", "label": "Hungary" },
28 | { "value": "in-en", "label": "India" },
29 | { "value": "id-id", "label": "Indonesia" },
30 | { "value": "id-en", "label": "Indonesia (en)" },
31 | { "value": "ie-en", "label": "Ireland" },
32 | { "value": "il-he", "label": "Israel" },
33 | { "value": "it-it", "label": "Italy" },
34 | { "value": "jp-jp", "label": "Japan" },
35 | { "value": "kr-kr", "label": "Korea" },
36 | { "value": "lv-lv", "label": "Latvia" },
37 | { "value": "lt-lt", "label": "Lithuania" },
38 | { "value": "xl-es", "label": "Latin America" },
39 | { "value": "my-ms", "label": "Malaysia" },
40 | { "value": "my-en", "label": "Malaysia (en)" },
41 | { "value": "mx-es", "label": "Mexico" },
42 | { "value": "nl-nl", "label": "Netherlands" },
43 | { "value": "nz-en", "label": "New Zealand" },
44 | { "value": "no-no", "label": "Norway" },
45 | { "value": "pe-es", "label": "Peru" },
46 | { "value": "ph-en", "label": "Philippines" },
47 | { "value": "ph-tl", "label": "Philippines (tl)" },
48 | { "value": "pl-pl", "label": "Poland" },
49 | { "value": "pt-pt", "label": "Portugal" },
50 | { "value": "ro-ro", "label": "Romania" },
51 | { "value": "ru-ru", "label": "Russia" },
52 | { "value": "sg-en", "label": "Singapore" },
53 | { "value": "sk-sk", "label": "Slovak Republic" },
54 | { "value": "sl-sl", "label": "Slovenia" },
55 | { "value": "za-en", "label": "South Africa" },
56 | { "value": "es-es", "label": "Spain" },
57 | { "value": "se-sv", "label": "Sweden" },
58 | { "value": "ch-de", "label": "Switzerland (de)" },
59 | { "value": "ch-fr", "label": "Switzerland (fr)" },
60 | { "value": "ch-it", "label": "Switzerland (it)" },
61 | { "value": "tw-tzh", "label": "Taiwan" },
62 | { "value": "th-th", "label": "Thailand" },
63 | { "value": "tr-tr", "label": "Turkey" },
64 | { "value": "ua-uk", "label": "Ukraine" },
65 | { "value": "uk-en", "label": "United Kingdom" },
66 | { "value": "us-en", "label": "United States" },
67 | { "value": "ue-es", "label": "United States (es)" },
68 | { "value": "ve-es", "label": "Venezuela" },
69 | { "value": "vn-vi", "label": "Vietnam" }
70 | ]
--------------------------------------------------------------------------------
/src/util/timePeriodOptions.json:
--------------------------------------------------------------------------------
1 | [
2 | { "value": "", "label": "Any time" },
3 | { "value": "d", "label": "Past day" },
4 | { "value": "w", "label": "Past week" },
5 | { "value": "m", "label": "Past month" },
6 | { "value": "y", "label": "Past year" }
7 | ]
8 |
--------------------------------------------------------------------------------
/src/util/userConfig.ts:
--------------------------------------------------------------------------------
1 | import { defaults } from 'lodash-es'
2 | import Browser from 'webextension-polyfill'
3 | import { getSystemLanguage } from './localization'
4 |
5 |
6 | const defaultConfig = {
7 | numWebResults: 1,
8 | webAccess: true,
9 | region: 'wt-wt',
10 | timePeriod: '',
11 | language: getSystemLanguage(),
12 | promptUUID: 'default',
13 | }
14 |
15 | export type UserConfig = typeof defaultConfig
16 |
17 | export async function getUserConfig(): Promise {
18 | const config = await Browser.storage.sync.get(defaultConfig)
19 | return defaults(config, defaultConfig)
20 | }
21 |
22 | export async function updateUserConfig(config: Partial): Promise {
23 | await Browser.storage.sync.set(config)
24 | }
25 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | corePlugins: {
4 | preflight: false,
5 | },
6 | content: ["./src/**/*.{html,tsx}"],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [require("daisyui")],
11 | daisyui: {
12 | themes: ["dark"],
13 | },
14 | prefix: "wcg-",
15 | };
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "esModuleInterop": true,
7 | "jsx": "react",
8 | "jsxFactory": "h",
9 | "jsxFragmentFactory": "Fragment",
10 | "noEmit": true,
11 | "allowJs": true,
12 | "checkJs": true,
13 | "skipLibCheck": true,
14 | "baseUrl": "./",
15 | "paths": {
16 | "react": ["./node_modules/preact/compat"],
17 | "react-dom": ["./node_modules/preact/compat"]
18 | },
19 | "resolveJsonModule": true
20 | },
21 | "include": ["src/**/*"]
22 | }
23 |
--------------------------------------------------------------------------------