import { useState, useEffect, useMemo } from "react";
import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import LoadingIcon from "../icons/three-dots.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
import ConfigIcon from "../icons/config.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ConnectionIcon from "../icons/connection.svg";
import CloudSuccessIcon from "../icons/cloud-success.svg";
import CloudFailIcon from "../icons/cloud-fail.svg";
import {
Input,
List,
ListItem,
Modal,
PasswordInput,
Popover,
Select,
showConfirm,
showToast,
} from "./ui-lib";
import { ModelConfigList } from "./model-config";
import { IconButton } from "./button";
import {
SubmitKey,
useChatStore,
Theme,
useUpdateStore,
useAccessStore,
useAppConfig,
} from "../store";
import Locale, {
AllLangs,
ALL_LANG_OPTIONS,
changeLang,
getLang,
} from "../locales";
import { copyToClipboard } from "../utils";
import Link from "next/link";
import {
Azure,
OPENAI_BASE_URL,
Path,
RELEASE_URL,
STORAGE_KEY,
ServiceProvider,
SlotID,
UPDATE_URL,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji";
import { getClientConfig } from "../config/client";
import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
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.coundSync();
}, [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.coundSync();
}, [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 = currentVersion !== remoteId;
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;
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(true)}
>
{checkingUpdate ? (
) : hasNewVersion ? (
{Locale.Settings.Update.GoToUpdate}
) : (
}
text={Locale.Settings.Update.CheckUpdate}
onClick={() => checkUpdate(true)}
/>
)}
updateConfig(
(config) =>
(config.fontSize = Number.parseInt(e.currentTarget.value)),
)
}
>
updateConfig(
(config) =>
(config.enableAutoGenerateTitle = e.currentTarget.checked),
)
}
>
updateConfig(
(config) =>
(config.sendPreviewBubble = 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)}
/>
{showAccessCode && (
{
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
);
}}
/>
)}
{!accessStore.hideUserApiKey && (
<>
{
// 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),
)
}
>
)
}
{accessStore.useCustomConfig && (
<>
{accessStore.provider === "OpenAI" ? (
<>
accessStore.update(
(access) =>
(access.openaiUrl = e.currentTarget.value),
)
}
>
{
accessStore.update(
(access) =>
(access.openaiApiKey = e.currentTarget.value),
);
}}
/>
>
) : (
<>
accessStore.update(
(access) =>
(access.azureUrl = e.currentTarget.value),
)
}
>
{
accessStore.update(
(access) =>
(access.azureApiKey = e.currentTarget.value),
);
}}
/>
accessStore.update(
(access) =>
(access.azureApiVersion =
e.currentTarget.value),
)
}
>
>
)}
>
)}
>
)}
{!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)} />
)}
);
}