import type { Prompt } from '../store/prompt'; import { nanoid } from 'nanoid'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { getClientConfig } from '../config/client'; import { Alibaba, Anthropic, Azure, Baidu, ByteDance, ChatGLM, Google, GoogleSafetySettingsThreshold, Iflytek, Moonshot, OPENAI_BASE_URL, Path, RELEASE_URL, SAAS_CHAT_URL, ServiceProvider, SlotID, Stability, STORAGE_KEY, Tencent, UPDATE_URL, XAI, } from '../constant'; import AddIcon from '../icons/add.svg'; import ClearIcon from '../icons/clear.svg'; import CloseIcon from '../icons/close.svg'; import CloudFailIcon from '../icons/cloud-fail.svg'; import CloudSuccessIcon from '../icons/cloud-success.svg'; import ConfigIcon from '../icons/config.svg'; import ConfirmIcon from '../icons/confirm.svg'; import ConnectionIcon from '../icons/connection.svg'; import CopyIcon from '../icons/copy.svg'; import DownloadIcon from '../icons/download.svg'; import EditIcon from '../icons/edit.svg'; import EyeIcon from '../icons/eye.svg'; import FireIcon from '../icons/fire.svg'; import ResetIcon from '../icons/reload.svg'; import LoadingIcon from '../icons/three-dots.svg'; import UploadIcon from '../icons/upload.svg'; import Locale, { ALL_LANG_OPTIONS, AllLangs, changeLang, getLang, } from '../locales'; import { SubmitKey, Theme, useAccessStore, useAppConfig, useChatStore, useUpdateStore, } from '../store'; import { useMaskStore } from '../store/mask'; import { SearchService, usePromptStore } from '../store/prompt'; import { useSyncStore } from '../store/sync'; import { clientUpdate, copyToClipboard, semverCompare } from '../utils'; import { trackSettingsPageGuideToCPaymentClick } from '../utils/auth-settings-events'; import { ProviderType } from '../utils/cloud'; import { IconButton } from './button'; import { Avatar, AvatarPicker } from './emoji'; import { ErrorBoundary } from './error'; import { InputRange } from './input-range'; import { ModelConfigList } from './model-config'; import { RealtimeConfigList } from './realtime-chat/realtime-config'; import styles from './settings.module.scss'; import { TTSConfigList } from './tts-config'; import { Input, List, ListItem, Modal, PasswordInput, Popover, Select, showConfirm, showToast, } from './ui-lib'; function EditPromptModal(props: { id: string; onClose: () => void }) { const promptStore = usePromptStore(); const prompt = promptStore.get(props.id); return prompt ? (
, ]} >
promptStore.updatePrompt( props.id, prompt => (prompt.title = e.currentTarget.value), )} > promptStore.updatePrompt( props.id, prompt => (prompt.content = e.currentTarget.value), )} >
) : null; } function UserPromptModal(props: { onClose?: () => void }) { const promptStore = usePromptStore(); const userPrompts = promptStore.getUserPrompts(); const builtinPrompts = SearchService.builtinPrompts; const allPrompts = userPrompts.concat(builtinPrompts); const [searchInput, setSearchInput] = useState(''); const [searchPrompts, setSearchPrompts] = useState([]); const prompts = searchInput.length > 0 ? searchPrompts : allPrompts; const [editingPromptId, setEditingPromptId] = useState(); useEffect(() => { if (searchInput.length > 0) { const searchResult = SearchService.search(searchInput); setSearchPrompts(searchResult); } else { setSearchPrompts([]); } }, [searchInput]); return (
props.onClose?.()} actions={[ { const promptId = promptStore.add({ id: nanoid(), createdAt: Date.now(), title: 'Empty Prompt', content: 'Empty Prompt Content', }); setEditingPromptId(promptId); }} icon={} bordered text={Locale.Settings.Prompt.Modal.Add} />, ]} >
setSearchInput(e.currentTarget.value)} >
{prompts.map((v, _) => (
{v.title}
{v.content}
{v.isUser && ( } className={styles['user-prompt-button']} onClick={() => promptStore.remove(v.id!)} /> )} {v.isUser ? ( } className={styles['user-prompt-button']} onClick={() => setEditingPromptId(v.id)} /> ) : ( } className={styles['user-prompt-button']} onClick={() => setEditingPromptId(v.id)} /> )} } className={styles['user-prompt-button']} onClick={() => copyToClipboard(v.content)} />
))}
{editingPromptId !== undefined && ( setEditingPromptId(undefined)} /> )}
); } function DangerItems() { const chatStore = useChatStore(); const appConfig = useAppConfig(); return ( { if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) { appConfig.reset(); } }} type="danger" /> { if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) { chatStore.clearAllData(); } }} type="danger" /> ); } function CheckButton() { const syncStore = useSyncStore(); const couldCheck = useMemo(() => { return syncStore.cloudSync(); }, [syncStore]); const [checkState, setCheckState] = useState< 'none' | 'checking' | 'success' | 'failed' >('none'); async function check() { setCheckState('checking'); const valid = await syncStore.check(); setCheckState(valid ? 'success' : 'failed'); } if (!couldCheck) { return null; } return ( ) : checkState === 'checking' ? ( ) : checkState === 'success' ? ( ) : checkState === 'failed' ? ( ) : ( ) } > ); } function SyncConfigModal(props: { onClose?: () => void }) { const syncStore = useSyncStore(); return (
props.onClose?.()} actions={[ , } bordered text={Locale.UI.Confirm} />, ]} > { syncStore.update( config => (config.useProxy = e.currentTarget.checked), ); }} > {syncStore.useProxy ? ( { syncStore.update( config => (config.proxyUrl = e.currentTarget.value), ); }} > ) : null} {syncStore.provider === ProviderType.WebDAV && ( <> { syncStore.update( config => (config.webdav.endpoint = e.currentTarget.value), ); }} > { syncStore.update( config => (config.webdav.username = e.currentTarget.value), ); }} > { syncStore.update( config => (config.webdav.password = e.currentTarget.value), ); }} > )} {syncStore.provider === ProviderType.UpStash && ( { syncStore.update( config => (config.upstash.endpoint = e.currentTarget.value), ); }} > { syncStore.update( config => (config.upstash.username = e.currentTarget.value), ); }} > { syncStore.update( config => (config.upstash.apiKey = e.currentTarget.value), ); }} > )}
); } function SyncItems() { const syncStore = useSyncStore(); const chatStore = useChatStore(); const promptStore = usePromptStore(); const maskStore = useMaskStore(); const couldSync = useMemo(() => { return syncStore.cloudSync(); }, [syncStore]); const [showSyncConfigModal, setShowSyncConfigModal] = useState(false); const stateOverview = useMemo(() => { const sessions = chatStore.sessions; const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0); return { chat: sessions.length, message: messageCount, prompt: Object.keys(promptStore.prompts).length, mask: Object.keys(maskStore.masks).length, }; }, [chatStore.sessions, maskStore.masks, promptStore.prompts]); return ( <>
} text={Locale.UI.Config} onClick={() => { setShowSyncConfigModal(true); }} /> {couldSync && ( } text={Locale.UI.Sync} onClick={async () => { try { await syncStore.sync(); showToast(Locale.Settings.Sync.Success); } catch (e) { showToast(Locale.Settings.Sync.Fail); console.error('[Sync]', e); } }} /> )}
} text={Locale.UI.Export} onClick={() => { syncStore.export(); }} /> } text={Locale.UI.Import} onClick={() => { syncStore.import(); }} />
{showSyncConfigModal && ( setShowSyncConfigModal(false)} /> )} ); } export function Settings() { const navigate = useNavigate(); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const config = useAppConfig(); const updateConfig = config.update; const updateStore = useUpdateStore(); const [checkingUpdate, setCheckingUpdate] = useState(false); const currentVersion = updateStore.formatVersion(updateStore.version); const remoteId = updateStore.formatVersion(updateStore.remoteVersion); const hasNewVersion = semverCompare(currentVersion, remoteId) === -1; const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL; function checkUpdate(force = false) { setCheckingUpdate(true); updateStore.getLatestVersion(force).then(() => { setCheckingUpdate(false); }); console.log('[Update] local version ', updateStore.version); console.log('[Update] remote version ', updateStore.remoteVersion); } const accessStore = useAccessStore(); const shouldHideBalanceQuery = useMemo(() => { const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); return ( accessStore.hideBalanceQuery || isOpenAiUrl || accessStore.provider === ServiceProvider.Azure ); }, [ accessStore.hideBalanceQuery, accessStore.openaiUrl, accessStore.provider, ]); const usage = { used: updateStore.used, subscription: updateStore.subscription, }; const [loadingUsage, setLoadingUsage] = useState(false); function checkUsage(force = false) { if (shouldHideBalanceQuery) { return; } setLoadingUsage(true); updateStore.updateUsage(force).finally(() => { setLoadingUsage(false); }); } const enabledAccessControl = useMemo( () => accessStore.enabledAccessControl(), // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const promptStore = usePromptStore(); const builtinCount = SearchService.count.builtin; const customCount = promptStore.getUserPrompts().length ?? 0; const [shouldShowPromptModal, setShowPromptModal] = useState(false); const showUsage = accessStore.isAuthorized(); useEffect(() => { // checks per minutes checkUpdate(); showUsage && checkUsage(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const keydownEvent = (e: KeyboardEvent) => { if (e.key === 'Escape') { navigate(Path.Home); } }; if (clientConfig?.isApp) { // Force to set custom endpoint to true if it's app accessStore.update((state) => { state.useCustomConfig = true; }); } document.addEventListener('keydown', keydownEvent); return () => { document.removeEventListener('keydown', keydownEvent); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const clientConfig = useMemo(() => getClientConfig(), []); const showAccessCode = enabledAccessControl && !clientConfig?.isApp; const accessCodeComponent = showAccessCode && ( { accessStore.update( access => (access.accessCode = e.currentTarget.value), ); }} /> ); const saasStartComponent = ( } type="primary" text={Locale.Settings.Access.SaasStart.ChatNow} onClick={() => { trackSettingsPageGuideToCPaymentClick(); window.location.href = SAAS_CHAT_URL; }} /> ); const useCustomConfigComponent // Conditionally render the following ListItem based on clientConfig.isApp = !clientConfig?.isApp && ( // only show if isApp is false accessStore.update( access => (access.useCustomConfig = e.currentTarget.checked), )} > ); const openAIConfigComponent = accessStore.provider === ServiceProvider.OpenAI && ( <> accessStore.update( access => (access.openaiUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.openaiApiKey = e.currentTarget.value), ); }} /> ); const azureConfigComponent = accessStore.provider === ServiceProvider.Azure && ( <> accessStore.update( access => (access.azureUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.azureApiKey = e.currentTarget.value), ); }} /> accessStore.update( access => (access.azureApiVersion = e.currentTarget.value), )} > ); const googleConfigComponent = accessStore.provider === ServiceProvider.Google && ( <> accessStore.update( access => (access.googleUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.googleApiKey = e.currentTarget.value), ); }} /> accessStore.update( access => (access.googleApiVersion = e.currentTarget.value), )} > ); const anthropicConfigComponent = accessStore.provider === ServiceProvider.Anthropic && ( <> accessStore.update( access => (access.anthropicUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.anthropicApiKey = e.currentTarget.value), ); }} /> accessStore.update( access => (access.anthropicApiVersion = e.currentTarget.value), )} > ); const baiduConfigComponent = accessStore.provider === ServiceProvider.Baidu && ( <> accessStore.update( access => (access.baiduUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.baiduApiKey = e.currentTarget.value), ); }} /> { accessStore.update( access => (access.baiduSecretKey = e.currentTarget.value), ); }} /> ); const tencentConfigComponent = accessStore.provider === ServiceProvider.Tencent && ( <> accessStore.update( access => (access.tencentUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.tencentSecretId = e.currentTarget.value), ); }} /> { accessStore.update( access => (access.tencentSecretKey = e.currentTarget.value), ); }} /> ); const byteDanceConfigComponent = accessStore.provider === ServiceProvider.ByteDance && ( <> accessStore.update( access => (access.bytedanceUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.bytedanceApiKey = e.currentTarget.value), ); }} /> ); const alibabaConfigComponent = accessStore.provider === ServiceProvider.Alibaba && ( <> accessStore.update( access => (access.alibabaUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.alibabaApiKey = e.currentTarget.value), ); }} /> ); const moonshotConfigComponent = accessStore.provider === ServiceProvider.Moonshot && ( <> accessStore.update( access => (access.moonshotUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.moonshotApiKey = e.currentTarget.value), ); }} /> ); const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && ( <> accessStore.update( access => (access.xaiUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.xaiApiKey = e.currentTarget.value), ); }} /> ); const chatglmConfigComponent = accessStore.provider === ServiceProvider.ChatGLM && ( <> accessStore.update( access => (access.chatglmUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.chatglmApiKey = e.currentTarget.value), ); }} /> ); const stabilityConfigComponent = accessStore.provider === ServiceProvider.Stability && ( <> accessStore.update( access => (access.stabilityUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.stabilityApiKey = e.currentTarget.value), ); }} /> ); const lflytekConfigComponent = accessStore.provider === ServiceProvider.Iflytek && ( <> accessStore.update( access => (access.iflytekUrl = e.currentTarget.value), )} > { accessStore.update( access => (access.iflytekApiKey = e.currentTarget.value), ); }} /> { accessStore.update( access => (access.iflytekApiSecret = e.currentTarget.value), ); }} /> ); return (
{Locale.Settings.Title}
{Locale.Settings.SubTitle}
} onClick={() => navigate(Path.Home)} bordered />
setShowEmojiPicker(false)} content={( { updateConfig(config => (config.avatar = avatar)); setShowEmojiPicker(false); }} /> )} open={showEmojiPicker} >
{ setShowEmojiPicker(!showEmojiPicker); }} >
{checkingUpdate ? ( ) : hasNewVersion ? ( clientConfig?.isApp ? ( } text={Locale.Settings.Update.GoToUpdate} onClick={() => clientUpdate()} /> ) : ( {Locale.Settings.Update.GoToUpdate} ) ) : ( } text={Locale.Settings.Update.CheckUpdate} onClick={() => checkUpdate(true)} /> )} updateConfig( config => (config.fontSize = Number.parseInt(e.currentTarget.value)), )} > updateConfig( config => (config.fontFamily = e.currentTarget.value), )} > updateConfig( config => (config.enableAutoGenerateTitle = e.currentTarget.checked), )} > updateConfig( config => (config.sendPreviewBubble = e.currentTarget.checked), )} > updateConfig( config => (config.enableArtifacts = e.currentTarget.checked), )} > updateConfig( config => (config.enableCodeFold = e.currentTarget.checked), )} >
updateConfig( config => (config.dontShowMaskSplashScreen = !e.currentTarget.checked), )} > updateConfig( config => (config.hideBuiltinMasks = e.currentTarget.checked), )} > updateConfig( config => (config.disablePromptHint = e.currentTarget.checked), )} > } text={Locale.Settings.Prompt.Edit} onClick={() => setShowPromptModal(true)} /> {saasStartComponent} {accessCodeComponent} {!accessStore.hideUserApiKey && ( <> {useCustomConfigComponent} {accessStore.useCustomConfig && ( <> {openAIConfigComponent} {azureConfigComponent} {googleConfigComponent} {anthropicConfigComponent} {baiduConfigComponent} {byteDanceConfigComponent} {alibabaConfigComponent} {tencentConfigComponent} {moonshotConfigComponent} {stabilityConfigComponent} {lflytekConfigComponent} {XAIConfigComponent} {chatglmConfigComponent} )} )} {!shouldHideBalanceQuery && !clientConfig?.isApp ? ( {!showUsage || loadingUsage ? (
) : ( } text={Locale.Settings.Usage.Check} onClick={() => checkUsage(true)} /> )} ) : null} config.update( config => (config.customModels = e.currentTarget.value), )} > { const modelConfig = { ...config.modelConfig }; updater(modelConfig); config.update(config => (config.modelConfig = modelConfig)); }} /> {shouldShowPromptModal && ( setShowPromptModal(false)} /> )} { const realtimeConfig = { ...config.realtimeConfig }; updater(realtimeConfig); config.update( config => (config.realtimeConfig = realtimeConfig), ); }} /> { const ttsConfig = { ...config.ttsConfig }; updater(ttsConfig); config.update(config => (config.ttsConfig = ttsConfig)); }} />
); }