style: refactor option page sections

This commit is contained in:
josc146
2023-04-29 13:39:32 +08:00
parent f331d95907
commit 4ba1a8d124
11 changed files with 736 additions and 623 deletions
+5 -1
View File
@@ -106,5 +106,9 @@
"Export": "Export",
"Always Create New Conversation Window": "Always Create New Conversation Window",
"Please keep this tab open. You can now use the web mode of ChatGPTBox": "Please keep this tab open. You can now use the web mode of ChatGPTBox",
"Go Back": "Go Back"
"Go Back": "Go Back",
"Modules": "Modules",
"API Params": "API Params",
"API Url": "API Url",
"Others": "Others"
}
+5 -1
View File
@@ -106,5 +106,9 @@
"Export": "导出",
"Always Create New Conversation Window": "总是创建新的对话窗口",
"Please keep this tab open. You can now use the web mode of ChatGPTBox": "请保持这个页面打开, 现在你可以使用ChatGPTBox的网页版模式",
"Go Back": "返回"
"Go Back": "返回",
"Modules": "模块",
"API Params": "API参数",
"API Url": "API地址",
"Others": "其他"
}
+5 -1
View File
@@ -106,5 +106,9 @@
"Export": "匯出",
"Always Create New Conversation Window": "總是建立新的對話視窗",
"Please keep this tab open. You can now use the web mode of ChatGPTBox": "請保持這個頁面開啟, 現在妳可以使用ChatGPTBox的網頁版模式",
"Go Back": "返回"
"Go Back": "返回",
"Modules": "模組",
"API Params": "API參數",
"API Url": "API網址",
"Others": "其他"
}
+7 -618
View File
@@ -4,627 +4,20 @@ import {
defaultConfig,
getPreferredLanguageKey,
getUserConfig,
isUsingApiKey,
isUsingAzureOpenAi,
isUsingCustomModel,
isUsingCustomNameOnlyModel,
isUsingGithubThirdPartyApi,
isUsingMultiModeModel,
ModelMode,
Models,
setUserConfig,
ThemeMode,
TriggerMode,
} from '../config/index.mjs'
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
import 'react-tabs/style/react-tabs.css'
import './styles.scss'
import { MarkGithubIcon } from '@primer/octicons-react'
import Browser from 'webextension-polyfill'
import PropTypes from 'prop-types'
import { config as toolsConfig } from '../content-script/selection-tools'
import { useWindowTheme } from '../hooks/use-window-theme.mjs'
import { languageList } from '../config/language.mjs'
import {
isEdge,
isFirefox,
isMobile,
isSafari,
openUrl,
parseFloatWithClamp,
parseIntWithClamp,
} from '../utils/index.mjs'
import { isMobile } from '../utils/index.mjs'
import { useTranslation } from 'react-i18next'
function GeneralPart({ config, updateConfig }) {
const { t, i18n } = useTranslation()
const [balance, setBalance] = useState(null)
const getBalance = async () => {
const response = await fetch(`${config.customOpenAiApiUrl}/dashboard/billing/credit_grants`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
})
if (response.ok) setBalance((await response.json()).total_available.toFixed(2))
else openUrl('https://platform.openai.com/account/usage')
}
return (
<>
<label>
<legend>{t('Triggers')}</legend>
<select
required
onChange={(e) => {
const mode = e.target.value
updateConfig({ triggerMode: mode })
}}
>
{Object.entries(TriggerMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.triggerMode}>
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>{t('Theme')}</legend>
<select
required
onChange={(e) => {
const mode = e.target.value
updateConfig({ themeMode: mode })
}}
>
{Object.entries(ThemeMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.themeMode}>
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>{t('API Mode')}</legend>
<span style="display: flex; gap: 15px;">
<select
style={
isUsingApiKey(config) ||
isUsingMultiModeModel(config) ||
isUsingCustomModel(config) ||
isUsingAzureOpenAi(config) ||
isUsingCustomNameOnlyModel(config)
? 'width: 50%;'
: undefined
}
required
onChange={(e) => {
const modelName = e.target.value
updateConfig({ modelName: modelName })
}}
>
{Object.entries(Models).map(([key, model]) => {
return (
<option value={key} key={key} selected={key === config.modelName}>
{t(model.desc)}
</option>
)
})}
</select>
{isUsingMultiModeModel(config) && (
<select
style="width: 50%;"
required
onChange={(e) => {
const modelMode = e.target.value
updateConfig({ modelMode: modelMode })
}}
>
{Object.entries(ModelMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.modelMode}>
{t(desc)}
</option>
)
})}
</select>
)}
{isUsingApiKey(config) && (
<span style="width: 50%; display: flex; gap: 5px;">
<input
type="password"
value={config.apiKey}
placeholder={t('API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ apiKey: apiKey })
}}
/>
{config.apiKey.length === 0 ? (
<a
href="https://platform.openai.com/account/api-keys"
target="_blank"
rel="nofollow noopener noreferrer"
>
<button style="white-space: nowrap;" type="button">
{t('Get')}
</button>
</a>
) : balance ? (
<button type="button" onClick={getBalance}>
{balance}
</button>
) : (
<button type="button" onClick={getBalance}>
{t('Balance')}
</button>
)}
</span>
)}
{isUsingCustomModel(config) && (
<input
style="width: 50%;"
type="text"
value={config.customModelName}
placeholder={t('Model Name')}
onChange={(e) => {
const customModelName = e.target.value
updateConfig({ customModelName: customModelName })
}}
/>
)}
{isUsingCustomNameOnlyModel(config) && (
<input
style="width: 50%;"
type="text"
value={config.poeCustomBotName}
placeholder={t('Bot Name')}
onChange={(e) => {
const customName = e.target.value
updateConfig({ poeCustomBotName: customName })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="password"
style="width: 50%;"
value={config.azureApiKey}
placeholder={t('Azure API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ azureApiKey: apiKey })
}}
/>
)}
</span>
{isUsingCustomModel(config) && (
<input
type="text"
value={config.customModelApiUrl}
placeholder={t('Custom Model API Url')}
onChange={(e) => {
const value = e.target.value
updateConfig({ customModelApiUrl: value })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="password"
value={config.azureEndpoint}
placeholder={t('Azure Endpoint')}
onChange={(e) => {
const endpoint = e.target.value
updateConfig({ azureEndpoint: endpoint })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="text"
value={config.azureDeploymentName}
placeholder={t('Azure Deployment Name')}
onChange={(e) => {
const deploymentName = e.target.value
updateConfig({ azureDeploymentName: deploymentName })
}}
/>
)}
{isUsingGithubThirdPartyApi(config) && (
<input
type="text"
value={config.githubThirdPartyUrl}
placeholder={t('API Url')}
onChange={(e) => {
const url = e.target.value
updateConfig({ githubThirdPartyUrl: url })
}}
/>
)}
</label>
<label>
<legend>{t('Preferred Language')}</legend>
<select
required
onChange={(e) => {
const preferredLanguageKey = e.target.value
updateConfig({ preferredLanguage: preferredLanguageKey })
let lang
if (preferredLanguageKey === 'auto') lang = config.userLanguage
else lang = preferredLanguageKey
i18n.changeLanguage(lang)
Browser.tabs.query({}).then((tabs) => {
tabs.forEach((tab) => {
Browser.tabs
.sendMessage(tab.id, {
type: 'CHANGE_LANG',
data: {
lang,
},
})
.catch(() => {})
})
})
}}
>
{Object.entries(languageList).map(([k, v]) => {
return (
<option value={k} key={k} selected={k === config.preferredLanguage}>
{v.native}
</option>
)
})}
</select>
</label>
<label>
<input
type="checkbox"
checked={config.insertAtTop}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ insertAtTop: checked })
}}
/>
{t('Insert ChatGPT at the top of search results')}
</label>
<label>
<input
type="checkbox"
checked={config.lockWhenAnswer}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ lockWhenAnswer: checked })
}}
/>
{t('Lock scrollbar while answering')}
</label>
<label>
<input
type="checkbox"
checked={config.autoRegenAfterSwitchModel}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ autoRegenAfterSwitchModel: checked })
}}
/>
{t('Regenerate the answer after switching model')}
</label>
<label>
<input
type="checkbox"
checked={config.alwaysPinWindow}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ alwaysPinWindow: checked })
}}
/>
{t('Always pin the floating window')}
</label>
<br />
</>
)
}
GeneralPart.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function FeaturePages({ config, updateConfig }) {
const { t } = useTranslation()
const [backgroundPermission, setBackgroundPermission] = useState(false)
if (!isMobile() && !isFirefox() && !isSafari())
Browser.permissions.contains({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
return (
<div style="display:flex;flex-direction:column;align-items:left;">
{!isMobile() && !isFirefox() && !isSafari() && (
<button
type="button"
onClick={() => {
if (isEdge()) openUrl('edge://extensions/shortcuts')
else openUrl('chrome://extensions/shortcuts')
}}
>
{t('Keyboard Shortcuts')}
</button>
)}
<button
type="button"
onClick={() => {
openUrl(Browser.runtime.getURL('IndependentPanel.html'))
}}
>
{t('Open Conversation Page')}
</button>
{!isMobile() && (
<button
type="button"
onClick={() => {
Browser.windows.create({
url: Browser.runtime.getURL('IndependentPanel.html'),
type: 'popup',
width: 500,
height: 650,
})
}}
>
{t('Open Conversation Window')}
</button>
)}
{!isMobile() && !isFirefox() && !isSafari() && (
<label>
<input
type="checkbox"
checked={backgroundPermission}
onChange={(e) => {
const checked = e.target.checked
if (checked)
Browser.permissions.request({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
else
Browser.permissions.remove({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
}}
/>
{t('Keep Conversation Window in Background')}
</label>
)}
{!isMobile() && (
<label>
<input
type="checkbox"
checked={config.alwaysCreateNewConversationWindow}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ alwaysCreateNewConversationWindow: checked })
}}
/>
{t('Always Create New Conversation Window')}
</label>
)}
</div>
)
}
FeaturePages.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function AdvancedPart({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<label>
{t('Max Response Token Length') + `: ${config.maxResponseTokenLength}`}
<input
type="range"
min="100"
max="40000"
step="100"
value={config.maxResponseTokenLength}
onChange={(e) => {
const value = parseIntWithClamp(e.target.value, 1000, 100, 40000)
updateConfig({ maxResponseTokenLength: value })
}}
/>
</label>
<label>
{t('Max Conversation Length') + `: ${config.maxConversationContextLength}`}
<input
type="range"
min="0"
max="100"
step="1"
value={config.maxConversationContextLength}
onChange={(e) => {
const value = parseIntWithClamp(e.target.value, 9, 0, 100)
updateConfig({ maxConversationContextLength: value })
}}
/>
</label>
<label>
{t('Temperature') + `: ${config.temperature}`}
<input
type="range"
min="0"
max="2"
step="0.1"
value={config.temperature}
onChange={(e) => {
const value = parseFloatWithClamp(e.target.value, 1, 0, 2)
updateConfig({ temperature: value })
}}
/>
</label>
<label>
{t('Custom ChatGPT Web API Url')}
<input
type="text"
value={config.customChatGptWebApiUrl}
onChange={(e) => {
const value = e.target.value
updateConfig({ customChatGptWebApiUrl: value })
}}
/>
</label>
<label>
{t('Custom ChatGPT Web API Path')}
<input
type="text"
value={config.customChatGptWebApiPath}
onChange={(e) => {
const value = e.target.value
updateConfig({ customChatGptWebApiPath: value })
}}
/>
</label>
<label>
{t('Custom OpenAI API Url')}
<input
type="text"
value={config.customOpenAiApiUrl}
onChange={(e) => {
const value = e.target.value
updateConfig({ customOpenAiApiUrl: value })
}}
/>
</label>
<label>
{t('Custom Site Regex')}
<input
type="text"
value={config.siteRegex}
onChange={(e) => {
const regex = e.target.value
updateConfig({ siteRegex: regex })
}}
/>
</label>
<label>
<input
type="checkbox"
checked={config.useSiteRegexOnly}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ useSiteRegexOnly: checked })
}}
/>
{t('Exclusively use Custom Site Regex for website matching, ignoring built-in rules')}
</label>
<br />
<label>
{t('Input Query')}
<input
type="text"
value={config.inputQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ inputQuery: query })
}}
/>
</label>
<label>
{t('Append Query')}
<input
type="text"
value={config.appendQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ appendQuery: query })
}}
/>
</label>
<label>
{t('Prepend Query')}
<input
type="text"
value={config.prependQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ prependQuery: query })
}}
/>
</label>
</>
)
}
AdvancedPart.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function SelectionTools({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
{config.selectionTools.map((key) => (
<label key={key}>
<input
type="checkbox"
checked={config.activeSelectionTools.includes(key)}
onChange={(e) => {
const checked = e.target.checked
const activeSelectionTools = config.activeSelectionTools.filter((i) => i !== key)
if (checked) activeSelectionTools.push(key)
updateConfig({ activeSelectionTools })
}}
/>
{t(toolsConfig[key].label)}
</label>
))}
</>
)
}
SelectionTools.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function SiteAdapters({ config, updateConfig }) {
return (
<>
{config.siteAdapters.map((key) => (
<label key={key}>
<input
type="checkbox"
checked={config.activeSiteAdapters.includes(key)}
onChange={(e) => {
const checked = e.target.checked
const activeSiteAdapters = config.activeSiteAdapters.filter((i) => i !== key)
if (checked) activeSiteAdapters.push(key)
updateConfig({ activeSiteAdapters })
}}
/>
{key}
</label>
))}
</>
)
}
SiteAdapters.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
import { GeneralPart } from './sections/GeneralPart'
import { FeaturePages } from './sections/FeaturePages'
import { AdvancedPart } from './sections/AdvancedPart'
import { ModulesPart } from './sections/ModulesPart'
// eslint-disable-next-line react/prop-types
function Footer({ currentVersion, latestVersion }) {
@@ -705,8 +98,7 @@ function Popup() {
<TabList>
<Tab className="popup-tab">{t('General')}</Tab>
<Tab className="popup-tab">{t('Feature Pages')}</Tab>
<Tab className="popup-tab">{t('Selection Tools')}</Tab>
<Tab className="popup-tab">{t('Sites')}</Tab>
<Tab className="popup-tab">{t('Modules')}</Tab>
<Tab className="popup-tab">{t('Advanced')}</Tab>
</TabList>
@@ -717,10 +109,7 @@ function Popup() {
<FeaturePages config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<SelectionTools config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<SiteAdapters config={config} updateConfig={updateConfig} />
<ModulesPart config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<AdvancedPart config={config} updateConfig={updateConfig} />
+208
View File
@@ -0,0 +1,208 @@
import { useTranslation } from 'react-i18next'
import { parseFloatWithClamp, parseIntWithClamp } from '../../utils/index.mjs'
import PropTypes from 'prop-types'
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
ApiParams.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function ApiParams({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<label>
{t('Max Response Token Length') + `: ${config.maxResponseTokenLength}`}
<input
type="range"
min="100"
max="40000"
step="100"
value={config.maxResponseTokenLength}
onChange={(e) => {
const value = parseIntWithClamp(e.target.value, 1000, 100, 40000)
updateConfig({ maxResponseTokenLength: value })
}}
/>
</label>
<label>
{t('Max Conversation Length') + `: ${config.maxConversationContextLength}`}
<input
type="range"
min="0"
max="100"
step="1"
value={config.maxConversationContextLength}
onChange={(e) => {
const value = parseIntWithClamp(e.target.value, 9, 0, 100)
updateConfig({ maxConversationContextLength: value })
}}
/>
</label>
<label>
{t('Temperature') + `: ${config.temperature}`}
<input
type="range"
min="0"
max="2"
step="0.1"
value={config.temperature}
onChange={(e) => {
const value = parseFloatWithClamp(e.target.value, 1, 0, 2)
updateConfig({ temperature: value })
}}
/>
</label>
</>
)
}
ApiUrl.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function ApiUrl({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<label>
{t('Custom ChatGPT Web API Url')}
<input
type="text"
value={config.customChatGptWebApiUrl}
onChange={(e) => {
const value = e.target.value
updateConfig({ customChatGptWebApiUrl: value })
}}
/>
</label>
<label>
{t('Custom ChatGPT Web API Path')}
<input
type="text"
value={config.customChatGptWebApiPath}
onChange={(e) => {
const value = e.target.value
updateConfig({ customChatGptWebApiPath: value })
}}
/>
</label>
<label>
{t('Custom OpenAI API Url')}
<input
type="text"
value={config.customOpenAiApiUrl}
onChange={(e) => {
const value = e.target.value
updateConfig({ customOpenAiApiUrl: value })
}}
/>
</label>
</>
)
}
Others.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
function Others({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<label>
{t('Custom Site Regex')}
<input
type="text"
value={config.siteRegex}
onChange={(e) => {
const regex = e.target.value
updateConfig({ siteRegex: regex })
}}
/>
</label>
<label>
<input
type="checkbox"
checked={config.useSiteRegexOnly}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ useSiteRegexOnly: checked })
}}
/>
{t('Exclusively use Custom Site Regex for website matching, ignoring built-in rules')}
</label>
<br />
<label>
{t('Input Query')}
<input
type="text"
value={config.inputQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ inputQuery: query })
}}
/>
</label>
<label>
{t('Append Query')}
<input
type="text"
value={config.appendQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ appendQuery: query })
}}
/>
</label>
<label>
{t('Prepend Query')}
<input
type="text"
value={config.prependQuery}
onChange={(e) => {
const query = e.target.value
updateConfig({ prependQuery: query })
}}
/>
</label>
</>
)
}
AdvancedPart.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function AdvancedPart({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<Tabs selectedTabClassName="popup-tab--selected">
<TabList>
<Tab className="popup-tab">{t('API Params')}</Tab>
<Tab className="popup-tab">{t('API Url')}</Tab>
<Tab className="popup-tab">{t('Others')}</Tab>
</TabList>
<TabPanel>
<ApiParams config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<ApiUrl config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<Others config={config} updateConfig={updateConfig} />
</TabPanel>
</Tabs>
</>
)
}
+92
View File
@@ -0,0 +1,92 @@
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { isEdge, isFirefox, isMobile, isSafari, openUrl } from '../../utils/index.mjs'
import Browser from 'webextension-polyfill'
import PropTypes from 'prop-types'
FeaturePages.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function FeaturePages({ config, updateConfig }) {
const { t } = useTranslation()
const [backgroundPermission, setBackgroundPermission] = useState(false)
if (!isMobile() && !isFirefox() && !isSafari())
Browser.permissions.contains({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
return (
<div style="display:flex;flex-direction:column;align-items:left;">
{!isMobile() && !isFirefox() && !isSafari() && (
<button
type="button"
onClick={() => {
if (isEdge()) openUrl('edge://extensions/shortcuts')
else openUrl('chrome://extensions/shortcuts')
}}
>
{t('Keyboard Shortcuts')}
</button>
)}
<button
type="button"
onClick={() => {
openUrl(Browser.runtime.getURL('IndependentPanel.html'))
}}
>
{t('Open Conversation Page')}
</button>
{!isMobile() && (
<button
type="button"
onClick={() => {
Browser.windows.create({
url: Browser.runtime.getURL('IndependentPanel.html'),
type: 'popup',
width: 500,
height: 650,
})
}}
>
{t('Open Conversation Window')}
</button>
)}
{!isMobile() && !isFirefox() && !isSafari() && (
<label>
<input
type="checkbox"
checked={backgroundPermission}
onChange={(e) => {
const checked = e.target.checked
if (checked)
Browser.permissions.request({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
else
Browser.permissions.remove({ permissions: ['background'] }).then((result) => {
setBackgroundPermission(result)
})
}}
/>
{t('Keep Conversation Window in Background')}
</label>
)}
{!isMobile() && (
<label>
<input
type="checkbox"
checked={config.alwaysCreateNewConversationWindow}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ alwaysCreateNewConversationWindow: checked })
}}
/>
{t('Always Create New Conversation Window')}
</label>
)}
</div>
)
}
+320
View File
@@ -0,0 +1,320 @@
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { openUrl } from '../../utils/index.mjs'
import {
isUsingApiKey,
isUsingAzureOpenAi,
isUsingCustomModel,
isUsingCustomNameOnlyModel,
isUsingGithubThirdPartyApi,
isUsingMultiModeModel,
ModelMode,
Models,
ThemeMode,
TriggerMode,
} from '../../config/index.mjs'
import Browser from 'webextension-polyfill'
import { languageList } from '../../config/language.mjs'
import PropTypes from 'prop-types'
GeneralPart.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function GeneralPart({ config, updateConfig }) {
const { t, i18n } = useTranslation()
const [balance, setBalance] = useState(null)
const getBalance = async () => {
const response = await fetch(`${config.customOpenAiApiUrl}/dashboard/billing/credit_grants`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
})
if (response.ok) setBalance((await response.json()).total_available.toFixed(2))
else openUrl('https://platform.openai.com/account/usage')
}
return (
<>
<label>
<legend>{t('Triggers')}</legend>
<select
required
onChange={(e) => {
const mode = e.target.value
updateConfig({ triggerMode: mode })
}}
>
{Object.entries(TriggerMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.triggerMode}>
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>{t('Theme')}</legend>
<select
required
onChange={(e) => {
const mode = e.target.value
updateConfig({ themeMode: mode })
}}
>
{Object.entries(ThemeMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.themeMode}>
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>{t('API Mode')}</legend>
<span style="display: flex; gap: 15px;">
<select
style={
isUsingApiKey(config) ||
isUsingMultiModeModel(config) ||
isUsingCustomModel(config) ||
isUsingAzureOpenAi(config) ||
isUsingCustomNameOnlyModel(config)
? 'width: 50%;'
: undefined
}
required
onChange={(e) => {
const modelName = e.target.value
updateConfig({ modelName: modelName })
}}
>
{Object.entries(Models).map(([key, model]) => {
return (
<option value={key} key={key} selected={key === config.modelName}>
{t(model.desc)}
</option>
)
})}
</select>
{isUsingMultiModeModel(config) && (
<select
style="width: 50%;"
required
onChange={(e) => {
const modelMode = e.target.value
updateConfig({ modelMode: modelMode })
}}
>
{Object.entries(ModelMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.modelMode}>
{t(desc)}
</option>
)
})}
</select>
)}
{isUsingApiKey(config) && (
<span style="width: 50%; display: flex; gap: 5px;">
<input
type="password"
value={config.apiKey}
placeholder={t('API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ apiKey: apiKey })
}}
/>
{config.apiKey.length === 0 ? (
<a
href="https://platform.openai.com/account/api-keys"
target="_blank"
rel="nofollow noopener noreferrer"
>
<button style="white-space: nowrap;" type="button">
{t('Get')}
</button>
</a>
) : balance ? (
<button type="button" onClick={getBalance}>
{balance}
</button>
) : (
<button type="button" onClick={getBalance}>
{t('Balance')}
</button>
)}
</span>
)}
{isUsingCustomModel(config) && (
<input
style="width: 50%;"
type="text"
value={config.customModelName}
placeholder={t('Model Name')}
onChange={(e) => {
const customModelName = e.target.value
updateConfig({ customModelName: customModelName })
}}
/>
)}
{isUsingCustomNameOnlyModel(config) && (
<input
style="width: 50%;"
type="text"
value={config.poeCustomBotName}
placeholder={t('Bot Name')}
onChange={(e) => {
const customName = e.target.value
updateConfig({ poeCustomBotName: customName })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="password"
style="width: 50%;"
value={config.azureApiKey}
placeholder={t('Azure API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ azureApiKey: apiKey })
}}
/>
)}
</span>
{isUsingCustomModel(config) && (
<input
type="text"
value={config.customModelApiUrl}
placeholder={t('Custom Model API Url')}
onChange={(e) => {
const value = e.target.value
updateConfig({ customModelApiUrl: value })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="password"
value={config.azureEndpoint}
placeholder={t('Azure Endpoint')}
onChange={(e) => {
const endpoint = e.target.value
updateConfig({ azureEndpoint: endpoint })
}}
/>
)}
{isUsingAzureOpenAi(config) && (
<input
type="text"
value={config.azureDeploymentName}
placeholder={t('Azure Deployment Name')}
onChange={(e) => {
const deploymentName = e.target.value
updateConfig({ azureDeploymentName: deploymentName })
}}
/>
)}
{isUsingGithubThirdPartyApi(config) && (
<input
type="text"
value={config.githubThirdPartyUrl}
placeholder={t('API Url')}
onChange={(e) => {
const url = e.target.value
updateConfig({ githubThirdPartyUrl: url })
}}
/>
)}
</label>
<label>
<legend>{t('Preferred Language')}</legend>
<select
required
onChange={(e) => {
const preferredLanguageKey = e.target.value
updateConfig({ preferredLanguage: preferredLanguageKey })
let lang
if (preferredLanguageKey === 'auto') lang = config.userLanguage
else lang = preferredLanguageKey
i18n.changeLanguage(lang)
Browser.tabs.query({}).then((tabs) => {
tabs.forEach((tab) => {
Browser.tabs
.sendMessage(tab.id, {
type: 'CHANGE_LANG',
data: {
lang,
},
})
.catch(() => {})
})
})
}}
>
{Object.entries(languageList).map(([k, v]) => {
return (
<option value={k} key={k} selected={k === config.preferredLanguage}>
{v.native}
</option>
)
})}
</select>
</label>
<label>
<input
type="checkbox"
checked={config.insertAtTop}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ insertAtTop: checked })
}}
/>
{t('Insert ChatGPT at the top of search results')}
</label>
<label>
<input
type="checkbox"
checked={config.lockWhenAnswer}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ lockWhenAnswer: checked })
}}
/>
{t('Lock scrollbar while answering')}
</label>
<label>
<input
type="checkbox"
checked={config.autoRegenAfterSwitchModel}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ autoRegenAfterSwitchModel: checked })
}}
/>
{t('Regenerate the answer after switching model')}
</label>
<label>
<input
type="checkbox"
checked={config.alwaysPinWindow}
onChange={(e) => {
const checked = e.target.checked
updateConfig({ alwaysPinWindow: checked })
}}
/>
{t('Always pin the floating window')}
</label>
<br />
</>
)
}
+32
View File
@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
import { SelectionTools } from './SelectionTools'
import { SiteAdapters } from './SiteAdapters'
ModulesPart.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function ModulesPart({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<Tabs selectedTabClassName="popup-tab--selected">
<TabList>
<Tab className="popup-tab">{t('Selection Tools')}</Tab>
<Tab className="popup-tab">{t('Sites')}</Tab>
</TabList>
<TabPanel>
<SelectionTools config={config} updateConfig={updateConfig} />
</TabPanel>
<TabPanel>
<SiteAdapters config={config} updateConfig={updateConfig} />
</TabPanel>
</Tabs>
</>
)
}
+32
View File
@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next'
import { config as toolsConfig } from '../../content-script/selection-tools/index.mjs'
import PropTypes from 'prop-types'
SelectionTools.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function SelectionTools({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
{config.selectionTools.map((key) => (
<label key={key}>
<input
type="checkbox"
checked={config.activeSelectionTools.includes(key)}
onChange={(e) => {
const checked = e.target.checked
const activeSelectionTools = config.activeSelectionTools.filter((i) => i !== key)
if (checked) activeSelectionTools.push(key)
updateConfig({ activeSelectionTools })
}}
/>
{t(toolsConfig[key].label)}
</label>
))}
</>
)
}
+28
View File
@@ -0,0 +1,28 @@
import PropTypes from 'prop-types'
SiteAdapters.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
export function SiteAdapters({ config, updateConfig }) {
return (
<>
{config.siteAdapters.map((key) => (
<label key={key}>
<input
type="checkbox"
checked={config.activeSiteAdapters.includes(key)}
onChange={(e) => {
const checked = e.target.checked
const activeSiteAdapters = config.activeSiteAdapters.filter((i) => i !== key)
if (checked) activeSiteAdapters.push(key)
updateConfig({ activeSiteAdapters })
}}
/>
{key}
</label>
))}
</>
)
}
+2 -2
View File
@@ -27,7 +27,7 @@
display: flex;
flex-direction: column;
align-items: center;
min-width: 440px;
min-width: 460px;
min-height: 560px;
width: 100%;
height: 100%;
@@ -39,7 +39,7 @@
display: flex;
flex-direction: column;
align-items: center;
width: 440px;
width: 460px;
height: 560px;
padding: 20px;
overflow-y: auto;