From 4c8ae1f270d2c4bc90cc2979fd89598631d86201 Mon Sep 17 00:00:00 2001 From: josc146 Date: Thu, 9 Mar 2023 23:01:44 +0800 Subject: [PATCH] refactor: adjust directory structure --- .../scripts/verify-search-engine-configs.mjs | 2 +- build.mjs | 15 +- src/background/{ => apis}/chatgpt-web.mjs | 4 +- src/background/{ => apis}/openai-api.mjs | 11 +- src/background/index.mjs | 16 +- .../ConversationCardForSearch/index.jsx | 155 ++++++++++ src/components/ConversationItem/index.jsx | 59 ++++ .../CopyButton/index.jsx} | 0 .../DecisionCardForSearch/index.jsx} | 18 +- .../FeedbackForChatGPTWeb/index.jsx} | 6 +- src/components/InputBox/index.jsx | 50 ++++ .../markdown-without-katex.jsx | 2 +- .../MarkdownRender}/markdown.jsx | 2 +- src/{config.js => config.mjs} | 0 src/content-script/ChatGPTQuery.jsx | 266 ------------------ src/content-script/index.jsx | 29 +- .../index.mjs} | 0 src/utils.mjs | 45 --- src/utils/endsWithQuestionMark.mjs | 8 + src/{background => utils}/fetch-sse.mjs | 2 +- src/utils/getConversationPairs.mjs | 17 ++ .../getPossibleElementByQuerySelector.mjs | 14 + src/utils/index.mjs | 6 + src/utils/isSafari.mjs | 3 + .../stream-async-iterable.mjs | 0 25 files changed, 380 insertions(+), 350 deletions(-) rename src/background/{ => apis}/chatgpt-web.mjs (97%) rename src/background/{ => apis}/openai-api.mjs (93%) create mode 100644 src/components/ConversationCardForSearch/index.jsx create mode 100644 src/components/ConversationItem/index.jsx rename src/{content-script/CopyButton.jsx => components/CopyButton/index.jsx} (100%) rename src/{content-script/ChatGPTCard.jsx => components/DecisionCardForSearch/index.jsx} (89%) rename src/{content-script/ChatGPTFeedback.jsx => components/FeedbackForChatGPTWeb/index.jsx} (92%) create mode 100644 src/components/InputBox/index.jsx rename src/{content-script => components/MarkdownRender}/markdown-without-katex.jsx (97%) rename src/{content-script => components/MarkdownRender}/markdown.jsx (97%) rename src/{config.js => config.mjs} (100%) delete mode 100644 src/content-script/ChatGPTQuery.jsx rename src/content-script/{search-engine-configs.mjs => site-adapters/index.mjs} (100%) delete mode 100644 src/utils.mjs create mode 100644 src/utils/endsWithQuestionMark.mjs rename src/{background => utils}/fetch-sse.mjs (90%) create mode 100644 src/utils/getConversationPairs.mjs create mode 100644 src/utils/getPossibleElementByQuerySelector.mjs create mode 100644 src/utils/index.mjs create mode 100644 src/utils/isSafari.mjs rename src/{background => utils}/stream-async-iterable.mjs (100%) diff --git a/.github/workflows/scripts/verify-search-engine-configs.mjs b/.github/workflows/scripts/verify-search-engine-configs.mjs index f8e7f2c..f915127 100644 --- a/.github/workflows/scripts/verify-search-engine-configs.mjs +++ b/.github/workflows/scripts/verify-search-engine-configs.mjs @@ -1,5 +1,5 @@ import { JSDOM } from 'jsdom' -import { config } from '../../../src/content-script/search-engine-configs.mjs' +import { config } from '../../../src/content-script/site-adapters' import fetch, { Headers } from 'node-fetch' const urls = { diff --git a/build.mjs b/build.mjs index 57d51f8..2e4779d 100644 --- a/build.mjs +++ b/build.mjs @@ -64,10 +64,14 @@ async function runWebpack(isWithoutKatex, callback) { }), ...(isWithoutKatex ? [ - new webpack.NormalModuleReplacementPlugin( - /markdown\.jsx/, - './markdown-without-katex.jsx', - ), + new webpack.NormalModuleReplacementPlugin(/markdown\.jsx/, (result) => { + if (result.request) { + result.request = result.request.replace( + 'markdown.jsx', + 'markdown-without-katex.jsx', + ) + } + }), ] : []), ], @@ -82,6 +86,9 @@ async function runWebpack(isWithoutKatex, callback) { { test: /\.m?jsx?$/, exclude: /(node_modules)/, + resolve: { + fullySpecified: false, + }, use: [ { loader: 'babel-loader', diff --git a/src/background/chatgpt-web.mjs b/src/background/apis/chatgpt-web.mjs similarity index 97% rename from src/background/chatgpt-web.mjs rename to src/background/apis/chatgpt-web.mjs index 6701b57..5ce9ea5 100644 --- a/src/background/chatgpt-web.mjs +++ b/src/background/apis/chatgpt-web.mjs @@ -1,8 +1,8 @@ // web version -import { fetchSSE } from './fetch-sse.mjs' +import { fetchSSE } from '../../utils' import { isEmpty } from 'lodash-es' -import { chatgptWebModelKeys, Models } from '../config.js' +import { chatgptWebModelKeys, Models } from '../../config' async function request(token, method, path, data) { const response = await fetch(`https://chat.openai.com/backend-api${path}`, { diff --git a/src/background/openai-api.mjs b/src/background/apis/openai-api.mjs similarity index 93% rename from src/background/openai-api.mjs rename to src/background/apis/openai-api.mjs index 2fc3dd4..87c5d23 100644 --- a/src/background/openai-api.mjs +++ b/src/background/apis/openai-api.mjs @@ -1,9 +1,8 @@ // api version -import { Models } from '../config.js' -import { fetchSSE } from './fetch-sse.mjs' +import { Models } from '../../config' +import { fetchSSE, getConversationPairs } from '../../utils' import { isEmpty } from 'lodash-es' -import { getChatPairs } from '../utils.mjs' const chatgptPromptBase = `You are a helpful, creative, clever, and very friendly assistant.` + @@ -37,7 +36,9 @@ export async function generateAnswersWithGptCompletionApi( }) const prompt = - gptPromptBase + getChatPairs(session.conversationRecords, false) + `Human:${question}\nAI:` + gptPromptBase + + getConversationPairs(session.conversationRecords, false) + + `Human:${question}\nAI:` let answer = '' await fetchSSE('https://api.openai.com/v1/completions', { @@ -97,7 +98,7 @@ export async function generateAnswersWithChatgptApi(port, question, session, api controller.abort() }) - const prompt = getChatPairs(session.conversationRecords, true) + const prompt = getConversationPairs(session.conversationRecords, true) prompt.unshift({ role: 'system', content: chatgptPromptBase }) prompt.push({ role: 'user', content: question }) diff --git a/src/background/index.mjs b/src/background/index.mjs index 9ee0f55..36a93bd 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -1,19 +1,19 @@ import { v4 as uuidv4 } from 'uuid' import Browser from 'webextension-polyfill' -import { generateAnswersWithChatgptWebApi, sendMessageFeedback } from './chatgpt-web.mjs' +import ExpiryMap from 'expiry-map' +import { generateAnswersWithChatgptWebApi, sendMessageFeedback } from './apis/chatgpt-web' +import { + generateAnswersWithChatgptApi, + generateAnswersWithGptCompletionApi, +} from './apis/openai-api' import { chatgptApiModelKeys, chatgptWebModelKeys, getUserConfig, gptApiModelKeys, isUsingApiKey, -} from '../config.js' -import { - generateAnswersWithChatgptApi, - generateAnswersWithGptCompletionApi, -} from './openai-api.mjs' -import ExpiryMap from 'expiry-map' -import { isSafari } from '../utils.mjs' +} from '../config' +import { isSafari } from '../utils' const KEY_ACCESS_TOKEN = 'accessToken' const cache = new ExpiryMap(10 * 1000) diff --git a/src/components/ConversationCardForSearch/index.jsx b/src/components/ConversationCardForSearch/index.jsx new file mode 100644 index 0000000..7e9eb93 --- /dev/null +++ b/src/components/ConversationCardForSearch/index.jsx @@ -0,0 +1,155 @@ +import { memo, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import Browser from 'webextension-polyfill' +import InputBox from '../InputBox' +import ConversationItem from '../ConversationItem' +import { isSafari } from '../../utils' + +class ConversationItemData extends Object { + /** + * @param {'question'|'answer'|'error'} type + * @param {string} content + */ + constructor(type, content) { + super() + this.type = type + this.content = content + this.session = null + this.done = false + } +} + +function ConversationCardForSearch(props) { + /** + * @type {[ConversationItemData[], (conversationItemData: ConversationItemData[]) => void]} + */ + const [conversationItemData, setConversationItemData] = useState([ + new ConversationItemData('answer', '

Waiting for response...

'), + ]) + const [isReady, setIsReady] = useState(false) + const [port, setPort] = useState(() => Browser.runtime.connect()) + + useEffect(() => { + window.session.question = props.question + port.postMessage({ session: window.session }) + }, [props.question]) // usually only triggered once + + /** + * @param {string} value + * @param {boolean} appended + * @param {'question'|'answer'|'error'} newType + * @param {boolean} done + */ + const UpdateAnswer = (value, appended, newType, done = false) => { + setConversationItemData((old) => { + const copy = [...old] + const index = copy.findLastIndex((v) => v.type === 'answer') + if (index === -1) return copy + copy[index] = new ConversationItemData( + newType, + appended ? copy[index].content + value : value, + ) + copy[index].session = { ...window.session } + copy[index].done = done + return copy + }) + } + + useEffect(() => { + const listener = () => { + setPort(Browser.runtime.connect()) + } + port.onDisconnect.addListener(listener) + return () => { + port.onDisconnect.removeListener(listener) + } + }, [port]) + useEffect(() => { + const listener = (msg) => { + if (msg.answer) { + UpdateAnswer(msg.answer, false, 'answer') + } + if (msg.session) { + window.session = msg.session + } + if (msg.done) { + UpdateAnswer('\n
', true, 'answer', true) + setIsReady(true) + } + if (msg.error) { + switch (msg.error) { + case 'UNAUTHORIZED': + UpdateAnswer( + `UNAUTHORIZED
Please login at https://chat.openai.com first${ + isSafari() ? '
Then open https://chat.openai.com/api/auth/session' : '' + }
And refresh this page or type you question again`, + false, + 'error', + ) + break + case 'CLOUDFLARE': + UpdateAnswer( + `OpenAI Security Check Required
Please open ${ + isSafari() ? 'https://chat.openai.com/api/auth/session' : 'https://chat.openai.com' + }
And refresh this page or type you question again`, + false, + 'error', + ) + break + default: + setConversationItemData([ + ...conversationItemData, + new ConversationItemData('error', msg.error + '\n
'), + ]) + break + } + setIsReady(true) + } + } + port.onMessage.addListener(listener) + return () => { + port.onMessage.removeListener(listener) + } + }, [conversationItemData]) + + return ( +
+
+ {conversationItemData.map((data, idx) => ( + + ))} +
+ { + const newQuestion = new ConversationItemData('question', '**You:**\n' + question) + const newAnswer = new ConversationItemData( + 'answer', + '

Waiting for response...

', + ) + setConversationItemData([...conversationItemData, newQuestion, newAnswer]) + setIsReady(false) + + window.session.question = question + try { + port.postMessage({ session: window.session }) + } catch (e) { + UpdateAnswer(e, false, 'error') + } + }} + /> +
+ ) +} + +ConversationCardForSearch.propTypes = { + question: PropTypes.string.isRequired, +} + +export default memo(ConversationCardForSearch) diff --git a/src/components/ConversationItem/index.jsx b/src/components/ConversationItem/index.jsx new file mode 100644 index 0000000..a6ecf03 --- /dev/null +++ b/src/components/ConversationItem/index.jsx @@ -0,0 +1,59 @@ +import { useState } from 'react' +import FeedbackForChatGPTWeb from '../FeedbackForChatGPTWeb' +import { ChevronDownIcon, LinkExternalIcon, XCircleIcon } from '@primer/octicons-react' +import CopyButton from '../CopyButton' +import PropTypes from 'prop-types' +import MarkdownRender from '../MarkdownRender/markdown.jsx' + +export function ConversationItem({ type, content, session, done }) { + const [collapsed, setCollapsed] = useState(false) + + return ( +
+ {type === 'answer' && ( +
+

{session ? 'ChatGPT:' : 'Loading...'}

+
+ {done && !session.useApiKey && ( + + )} + {session && session.conversationId && !session.useApiKey && ( + + + + )} + {session && content} size={14} />} + {!collapsed ? ( + setCollapsed(true)}> + + + ) : ( + setCollapsed(false)}> + + + )} +
+
+ )} + {!collapsed && {content}} +
+ ) +} + +ConversationItem.propTypes = { + type: PropTypes.oneOf(['question', 'answer', 'error']).isRequired, + content: PropTypes.string.isRequired, + session: PropTypes.object.isRequired, + done: PropTypes.bool.isRequired, +} + +export default ConversationItem diff --git a/src/content-script/CopyButton.jsx b/src/components/CopyButton/index.jsx similarity index 100% rename from src/content-script/CopyButton.jsx rename to src/components/CopyButton/index.jsx diff --git a/src/content-script/ChatGPTCard.jsx b/src/components/DecisionCardForSearch/index.jsx similarity index 89% rename from src/content-script/ChatGPTCard.jsx rename to src/components/DecisionCardForSearch/index.jsx index 7cb75d0..b194d85 100644 --- a/src/content-script/ChatGPTCard.jsx +++ b/src/components/DecisionCardForSearch/index.jsx @@ -1,12 +1,12 @@ import { LightBulbIcon, SearchIcon } from '@primer/octicons-react' import { useState, useEffect } from 'react' import PropTypes from 'prop-types' -import ChatGPTQuery from './ChatGPTQuery' -import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../utils.mjs' -import { defaultConfig, getUserConfig } from '../config' +import ConversationCardForSearch from '../ConversationCardForSearch' +import { defaultConfig, getUserConfig } from '../../config' import Browser from 'webextension-polyfill' +import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils' -function ChatGPTCard(props) { +function DecisionCardForSearch(props) { const [triggered, setTriggered] = useState(false) const [config, setConfig] = useState(defaultConfig) const [render, setRender] = useState(false) @@ -92,10 +92,10 @@ function ChatGPTCard(props) { if (question) switch (config.triggerMode) { case 'always': - return + return case 'manually': if (triggered) { - return + return } return (

+ return } return (

@@ -129,10 +129,10 @@ function ChatGPTCard(props) { ) } -ChatGPTCard.propTypes = { +DecisionCardForSearch.propTypes = { question: PropTypes.string.isRequired, siteConfig: PropTypes.object.isRequired, container: PropTypes.object.isRequired, } -export default ChatGPTCard +export default DecisionCardForSearch diff --git a/src/content-script/ChatGPTFeedback.jsx b/src/components/FeedbackForChatGPTWeb/index.jsx similarity index 92% rename from src/content-script/ChatGPTFeedback.jsx rename to src/components/FeedbackForChatGPTWeb/index.jsx index e7b3c7f..dd31c3e 100644 --- a/src/content-script/ChatGPTFeedback.jsx +++ b/src/components/FeedbackForChatGPTWeb/index.jsx @@ -3,7 +3,7 @@ import { memo, useCallback, useState } from 'react' import { ThumbsupIcon, ThumbsdownIcon } from '@primer/octicons-react' import Browser from 'webextension-polyfill' -const ChatGPTFeedback = (props) => { +const FeedbackForChatGPTWeb = (props) => { const [action, setAction] = useState(null) const clickThumbsUp = useCallback(async () => { @@ -56,9 +56,9 @@ const ChatGPTFeedback = (props) => { ) } -ChatGPTFeedback.propTypes = { +FeedbackForChatGPTWeb.propTypes = { messageId: PropTypes.string.isRequired, conversationId: PropTypes.string.isRequired, } -export default memo(ChatGPTFeedback) +export default memo(FeedbackForChatGPTWeb) diff --git a/src/components/InputBox/index.jsx b/src/components/InputBox/index.jsx new file mode 100644 index 0000000..e0af448 --- /dev/null +++ b/src/components/InputBox/index.jsx @@ -0,0 +1,50 @@ +import { useEffect, useRef, useState } from 'react' +import PropTypes from 'prop-types' + +export function InputBox({ onSubmit, enabled }) { + const [value, setValue] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + inputRef.current.style.height = 'auto' + const computed = window.getComputedStyle(inputRef.current) + const height = + parseInt(computed.getPropertyValue('border-top-width'), 10) + + parseInt(computed.getPropertyValue('padding-top'), 10) + + inputRef.current.scrollHeight + + parseInt(computed.getPropertyValue('padding-bottom'), 10) + + parseInt(computed.getPropertyValue('border-bottom-width'), 10) + + inputRef.current.style.height = `${height}px` + }) + + const onKeyDown = (e) => { + if (e.keyCode === 13 && e.shiftKey === false) { + e.preventDefault() + if (!value) return + onSubmit(value) + setValue('') + } + } + + return ( +