From 9b37f358d44ac472def1a22b3d5ea9dbd2d3d6ad Mon Sep 17 00:00:00 2001 From: josc146 Date: Fri, 31 Mar 2023 20:04:38 +0800 Subject: [PATCH] feat: option to regenerate the answer after switching model (#98) --- src/_locales/en/main.json | 3 +- src/_locales/zh-hans/main.json | 3 +- src/_locales/zh-hant/main.json | 3 +- src/components/ConversationCard/index.jsx | 48 ++++++++++------------- src/components/DecisionCard/index.jsx | 29 ++------------ src/components/FloatingToolbar/index.jsx | 31 +++------------ src/config/index.mjs | 1 + src/hooks/use-config.mjs | 28 +++++++++++++ src/popup/Popup.jsx | 11 ++++++ 9 files changed, 75 insertions(+), 82 deletions(-) create mode 100644 src/hooks/use-config.mjs diff --git a/src/_locales/en/main.json b/src/_locales/en/main.json index 9f7dae1..143b66f 100644 --- a/src/_locales/en/main.json +++ b/src/_locales/en/main.json @@ -86,5 +86,6 @@ "Confirm": "Confirm", "Clear Conversation": "Clear Conversation", "Retry": "Retry", - "Exceeded maximum context length": "Exceeded maximum context length, please clear the conversation and try again" + "Exceeded maximum context length": "Exceeded maximum context length, please clear the conversation and try again", + "Regenerate the answer after switching model": "Regenerate the answer after switching model" } diff --git a/src/_locales/zh-hans/main.json b/src/_locales/zh-hans/main.json index fb91500..1da8975 100644 --- a/src/_locales/zh-hans/main.json +++ b/src/_locales/zh-hans/main.json @@ -86,5 +86,6 @@ "Confirm": "确认", "Clear Conversation": "清理对话", "Retry": "重试", - "Exceeded maximum context length": "超出最大上下文长度, 请清理对话并重试" + "Exceeded maximum context length": "超出最大上下文长度, 请清理对话并重试", + "Regenerate the answer after switching model": "快捷切换模型时自动重新生成回答" } diff --git a/src/_locales/zh-hant/main.json b/src/_locales/zh-hant/main.json index e48ca9a..53d1cf4 100644 --- a/src/_locales/zh-hant/main.json +++ b/src/_locales/zh-hant/main.json @@ -86,5 +86,6 @@ "Confirm": "確認", "Clear Conversation": "清理對話", "Retry": "重試", - "Exceeded maximum context length": "超出最大上下文長度, 請清理對話並重試" + "Exceeded maximum context length": "超出最大上下文長度, 請清理對話並重試", + "Regenerate the answer after switching model": "快捷切換模型時自動重新生成回答" } diff --git a/src/components/ConversationCard/index.jsx b/src/components/ConversationCard/index.jsx index bd8f14e..8839e84 100644 --- a/src/components/ConversationCard/index.jsx +++ b/src/components/ConversationCard/index.jsx @@ -10,9 +10,10 @@ import FileSaver from 'file-saver' import { render } from 'preact' import FloatingToolbar from '../FloatingToolbar' import { useClampWindowSize } from '../../hooks/use-clamp-window-size' -import { defaultConfig, getUserConfig, Models } from '../../config/index.mjs' +import { Models } from '../../config/index.mjs' import { useTranslation } from 'react-i18next' import DeleteButton from '../DeleteButton' +import { useConfig } from '../../hooks/use-config.mjs' const logo = Browser.runtime.getURL('logo.png') @@ -61,11 +62,7 @@ function ConversationCard(props) { } })(), ) - const [config, setConfig] = useState(defaultConfig) - - useEffect(() => { - getUserConfig().then(setConfig) - }, []) + const config = useConfig() useEffect(() => { if (props.onUpdate) props.onUpdate() @@ -172,6 +169,20 @@ function ConversationCard(props) { } }, [conversationItemData]) + const getRetryFn = (session) => () => { + updateAnswer(`

${t('Waiting for response...')}

`, false, 'answer') + setIsReady(false) + + const newSession = { ...session, isRetry: true } + setSession(newSession) + try { + port.postMessage({ stop: true }) + port.postMessage({ session: newSession }) + } catch (e) { + updateAnswer(e, false, 'error') + } + } + return (
@@ -203,7 +214,8 @@ function ConversationCard(props) { required onChange={(e) => { const modelName = e.target.value - setSession({ ...session, modelName, aiName: t(Models[modelName].desc) }) + if (config.autoRegenAfterSwitchModel) + getRetryFn({ ...session, modelName, aiName: t(Models[modelName].desc) })() }} > {Object.entries(Models).map(([key, model]) => { @@ -298,27 +310,7 @@ function ConversationCard(props) { session={session} done={data.done} port={port} - onRetry={ - idx === conversationItemData.length - 1 - ? () => { - updateAnswer( - `

${t('Waiting for response...')}

`, - false, - 'answer', - ) - setIsReady(false) - - const newSession = { ...session, isRetry: true } - setSession(newSession) - try { - port.postMessage({ stop: true }) - port.postMessage({ session: newSession }) - } catch (e) { - updateAnswer(e, false, 'error') - } - } - : null - } + onRetry={idx === conversationItemData.length - 1 ? getRetryFn(session) : null} /> ))}
diff --git a/src/components/DecisionCard/index.jsx b/src/components/DecisionCard/index.jsx index 07f520d..1b83d19 100644 --- a/src/components/DecisionCard/index.jsx +++ b/src/components/DecisionCard/index.jsx @@ -2,41 +2,20 @@ import { LightBulbIcon, SearchIcon } from '@primer/octicons-react' import { useState, useEffect } from 'react' import PropTypes from 'prop-types' import ConversationCard from '../ConversationCard' -import { defaultConfig, getUserConfig } from '../../config/index.mjs' -import Browser from 'webextension-polyfill' import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils' import { useTranslation } from 'react-i18next' +import { useConfig } from '../../hooks/use-config.mjs' function DecisionCard(props) { const { t } = useTranslation() const [triggered, setTriggered] = useState(false) - const [config, setConfig] = useState(defaultConfig) const [render, setRender] = useState(false) + const config = useConfig(() => { + setRender(true) + }) const question = props.question - useEffect(() => { - getUserConfig().then((config) => { - setConfig(config) - setRender(true) - }) - }, []) - - useEffect(() => { - const listener = (changes) => { - const changedItems = Object.keys(changes) - let newConfig = {} - for (const key of changedItems) { - newConfig[key] = changes[key].newValue - } - setConfig({ ...config, ...newConfig }) - } - Browser.storage.local.onChanged.addListener(listener) - return () => { - Browser.storage.local.onChanged.removeListener(listener) - } - }, [config]) - const updatePosition = () => { if (!render) return diff --git a/src/components/FloatingToolbar/index.jsx b/src/components/FloatingToolbar/index.jsx index 0928afd..45d1541 100644 --- a/src/components/FloatingToolbar/index.jsx +++ b/src/components/FloatingToolbar/index.jsx @@ -2,12 +2,12 @@ import Browser from 'webextension-polyfill' import { cloneElement, useEffect, useState } from 'react' import ConversationCard from '../ConversationCard' import PropTypes from 'prop-types' -import { defaultConfig, getUserConfig } from '../../config/index.mjs' import { config as toolsConfig } from '../../content-script/selection-tools' import { getClientPosition, isMobile, setElementPositionInViewport } from '../../utils' import Draggable from 'react-draggable' import { useClampWindowSize } from '../../hooks/use-clamp-window-size' import { useTranslation } from 'react-i18next' +import { useConfig } from '../../hooks/use-config.mjs' const logo = Browser.runtime.getURL('logo.png') @@ -16,36 +16,15 @@ function FloatingToolbar(props) { const [selection, setSelection] = useState(props.selection) const [prompt, setPrompt] = useState(props.prompt) const [triggered, setTriggered] = useState(props.triggered) - const [config, setConfig] = useState(defaultConfig) const [render, setRender] = useState(false) const [closeable, setCloseable] = useState(props.closeable) const [position, setPosition] = useState(getClientPosition(props.container)) const [virtualPosition, setVirtualPosition] = useState({ x: 0, y: 0 }) const windowSize = useClampWindowSize([750, 1500], [0, Infinity]) - - useEffect(() => { - getUserConfig().then((config) => { - setConfig(config) - setRender(true) - - if (!triggered) props.container.style.position = 'absolute' - }) - }, []) - - useEffect(() => { - const listener = (changes) => { - const changedItems = Object.keys(changes) - let newConfig = {} - for (const key of changedItems) { - newConfig[key] = changes[key].newValue - } - setConfig({ ...config, ...newConfig }) - } - Browser.storage.local.onChanged.addListener(listener) - return () => { - Browser.storage.local.onChanged.removeListener(listener) - } - }, [config]) + const config = useConfig(() => { + setRender(true) + if (!triggered) props.container.style.position = 'absolute' + }) useEffect(() => { if (isMobile()) { diff --git a/src/config/index.mjs b/src/config/index.mjs index 576090e..94fce5b 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -66,6 +66,7 @@ export const defaultConfig = { preferredLanguage: getNavigatorLanguage(), insertAtTop: isMobile(), lockWhenAnswer: false, + autoRegenAfterSwitchModel: false, customModelApiUrl: 'http://localhost:8000/chat/completions', customModelName: 'chatglm-6b-int4', diff --git a/src/hooks/use-config.mjs b/src/hooks/use-config.mjs new file mode 100644 index 0000000..13998be --- /dev/null +++ b/src/hooks/use-config.mjs @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react' +import { defaultConfig, getUserConfig } from '../config/index.mjs' +import Browser from 'webextension-polyfill' + +export function useConfig(initFn) { + const [config, setConfig] = useState(defaultConfig) + useEffect(() => { + getUserConfig().then((config) => { + setConfig(config) + if (initFn) initFn() + }) + }, []) + useEffect(() => { + const listener = (changes) => { + const changedItems = Object.keys(changes) + let newConfig = {} + for (const key of changedItems) { + newConfig[key] = changes[key].newValue + } + setConfig({ ...config, ...newConfig }) + } + Browser.storage.local.onChanged.addListener(listener) + return () => { + Browser.storage.local.onChanged.removeListener(listener) + } + }, [config]) + return config +} diff --git a/src/popup/Popup.jsx b/src/popup/Popup.jsx index d0e32f7..ea823d4 100644 --- a/src/popup/Popup.jsx +++ b/src/popup/Popup.jsx @@ -235,6 +235,17 @@ function GeneralPart({ config, updateConfig }) { /> {t('Lock scrollbar while answering')} +
)