mirror of
https://github.com/wassname/chatGPTBox.git
synced 2026-07-09 23:09:25 +08:00
style: refactor option page sections
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "其他"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user