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
? (
)
: 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.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));
}}
/>
);
}