|
|
import { useState, useEffect, useMemo, HTMLProps, useRef } 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 EditIcon from "../icons/edit.svg"; |
|
|
import EyeIcon from "../icons/eye.svg"; |
|
|
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; |
|
|
import { ModelConfigList } from "./model-config"; |
|
|
|
|
|
import { IconButton } from "./button"; |
|
|
import { |
|
|
SubmitKey, |
|
|
useChatStore, |
|
|
Theme, |
|
|
useUpdateStore, |
|
|
useAccessStore, |
|
|
useAppConfig, |
|
|
} from "../store"; |
|
|
|
|
|
import Locale, { AllLangs, changeLang, getLang } from "../locales"; |
|
|
import { copyToClipboard } from "../utils"; |
|
|
import Link from "next/link"; |
|
|
import { Path, 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"; |
|
|
|
|
|
function EditPromptModal(props: { id: number; onClose: () => void }) { |
|
|
const promptStore = usePromptStore(); |
|
|
const prompt = promptStore.get(props.id); |
|
|
|
|
|
return prompt ? ( |
|
|
<div className="modal-mask"> |
|
|
<Modal |
|
|
title={Locale.Settings.Prompt.EditModal.Title} |
|
|
onClose={props.onClose} |
|
|
actions={[ |
|
|
<IconButton |
|
|
key="" |
|
|
onClick={props.onClose} |
|
|
text={Locale.UI.Confirm} |
|
|
bordered |
|
|
/>, |
|
|
]} |
|
|
> |
|
|
<div className={styles["edit-prompt-modal"]}> |
|
|
<input |
|
|
type="text" |
|
|
value={prompt.title} |
|
|
readOnly={!prompt.isUser} |
|
|
className={styles["edit-prompt-title"]} |
|
|
onInput={(e) => |
|
|
promptStore.update( |
|
|
props.id, |
|
|
(prompt) => (prompt.title = e.currentTarget.value), |
|
|
) |
|
|
} |
|
|
></input> |
|
|
<Input |
|
|
value={prompt.content} |
|
|
readOnly={!prompt.isUser} |
|
|
className={styles["edit-prompt-content"]} |
|
|
rows={10} |
|
|
onInput={(e) => |
|
|
promptStore.update( |
|
|
props.id, |
|
|
(prompt) => (prompt.content = e.currentTarget.value), |
|
|
) |
|
|
} |
|
|
></Input> |
|
|
</div> |
|
|
</Modal> |
|
|
</div> |
|
|
) : 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<Prompt[]>([]); |
|
|
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts; |
|
|
|
|
|
const [editingPromptId, setEditingPromptId] = useState<number>(); |
|
|
|
|
|
useEffect(() => { |
|
|
if (searchInput.length > 0) { |
|
|
const searchResult = SearchService.search(searchInput); |
|
|
setSearchPrompts(searchResult); |
|
|
} else { |
|
|
setSearchPrompts([]); |
|
|
} |
|
|
}, [searchInput]); |
|
|
|
|
|
return ( |
|
|
<div className="modal-mask"> |
|
|
<Modal |
|
|
title={Locale.Settings.Prompt.Modal.Title} |
|
|
onClose={() => props.onClose?.()} |
|
|
actions={[ |
|
|
<IconButton |
|
|
key="add" |
|
|
onClick={() => |
|
|
promptStore.add({ |
|
|
title: "Empty Prompt", |
|
|
content: "Empty Prompt Content", |
|
|
}) |
|
|
} |
|
|
icon={<AddIcon />} |
|
|
bordered |
|
|
text={Locale.Settings.Prompt.Modal.Add} |
|
|
/>, |
|
|
]} |
|
|
> |
|
|
<div className={styles["user-prompt-modal"]}> |
|
|
<input |
|
|
type="text" |
|
|
className={styles["user-prompt-search"]} |
|
|
placeholder={Locale.Settings.Prompt.Modal.Search} |
|
|
value={searchInput} |
|
|
onInput={(e) => setSearchInput(e.currentTarget.value)} |
|
|
></input> |
|
|
|
|
|
<div className={styles["user-prompt-list"]}> |
|
|
{prompts.map((v, _) => ( |
|
|
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}> |
|
|
<div className={styles["user-prompt-header"]}> |
|
|
<div className={styles["user-prompt-title"]}>{v.title}</div> |
|
|
<div className={styles["user-prompt-content"] + " one-line"}> |
|
|
{v.content} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className={styles["user-prompt-buttons"]}> |
|
|
{v.isUser && ( |
|
|
<IconButton |
|
|
icon={<ClearIcon />} |
|
|
className={styles["user-prompt-button"]} |
|
|
onClick={() => promptStore.remove(v.id!)} |
|
|
/> |
|
|
)} |
|
|
{v.isUser ? ( |
|
|
<IconButton |
|
|
icon={<EditIcon />} |
|
|
className={styles["user-prompt-button"]} |
|
|
onClick={() => setEditingPromptId(v.id)} |
|
|
/> |
|
|
) : ( |
|
|
<IconButton |
|
|
icon={<EyeIcon />} |
|
|
className={styles["user-prompt-button"]} |
|
|
onClick={() => setEditingPromptId(v.id)} |
|
|
/> |
|
|
)} |
|
|
<IconButton |
|
|
icon={<CopyIcon />} |
|
|
className={styles["user-prompt-button"]} |
|
|
onClick={() => copyToClipboard(v.content)} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</Modal> |
|
|
|
|
|
{editingPromptId !== undefined && ( |
|
|
<EditPromptModal |
|
|
id={editingPromptId!} |
|
|
onClose={() => setEditingPromptId(undefined)} |
|
|
/> |
|
|
)} |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function Settings() { |
|
|
const navigate = useNavigate(); |
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false); |
|
|
const config = useAppConfig(); |
|
|
const updateConfig = config.update; |
|
|
const resetConfig = config.reset; |
|
|
const chatStore = useChatStore(); |
|
|
|
|
|
const updateStore = useUpdateStore(); |
|
|
const [checkingUpdate, setCheckingUpdate] = useState(false); |
|
|
const currentVersion = updateStore.version; |
|
|
const remoteId = updateStore.remoteVersion; |
|
|
const hasNewVersion = currentVersion !== remoteId; |
|
|
|
|
|
function checkUpdate(force = false) { |
|
|
setCheckingUpdate(true); |
|
|
updateStore.getLatestVersion(force).then(() => { |
|
|
setCheckingUpdate(false); |
|
|
}); |
|
|
} |
|
|
|
|
|
const usage = { |
|
|
used: updateStore.used, |
|
|
subscription: updateStore.subscription, |
|
|
}; |
|
|
const [loadingUsage, setLoadingUsage] = useState(false); |
|
|
function checkUsage(force = false) { |
|
|
setLoadingUsage(true); |
|
|
updateStore.updateUsage(force).finally(() => { |
|
|
setLoadingUsage(false); |
|
|
}); |
|
|
} |
|
|
|
|
|
const accessStore = useAccessStore(); |
|
|
const enabledAccessControl = useMemo( |
|
|
() => accessStore.enabledAccessControl(), |
|
|
|
|
|
[], |
|
|
); |
|
|
|
|
|
const promptStore = usePromptStore(); |
|
|
const builtinCount = SearchService.count.builtin; |
|
|
const customCount = promptStore.getUserPrompts().length ?? 0; |
|
|
const [shouldShowPromptModal, setShowPromptModal] = useState(false); |
|
|
|
|
|
const showUsage = accessStore.isAuthorized(); |
|
|
useEffect(() => { |
|
|
|
|
|
checkUpdate(); |
|
|
showUsage && checkUsage(); |
|
|
|
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
const keydownEvent = (e: KeyboardEvent) => { |
|
|
if (e.key === "Escape") { |
|
|
navigate(Path.Home); |
|
|
} |
|
|
}; |
|
|
document.addEventListener("keydown", keydownEvent); |
|
|
return () => { |
|
|
document.removeEventListener("keydown", keydownEvent); |
|
|
}; |
|
|
|
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<ErrorBoundary> |
|
|
<div className="window-header"> |
|
|
<div className="window-header-title"> |
|
|
<div className="window-header-main-title"> |
|
|
{Locale.Settings.Title} |
|
|
</div> |
|
|
<div className="window-header-sub-title"> |
|
|
{Locale.Settings.SubTitle} |
|
|
</div> |
|
|
</div> |
|
|
<div className="window-actions"> |
|
|
<div className="window-action-button"> |
|
|
<IconButton |
|
|
icon={<ClearIcon />} |
|
|
onClick={() => { |
|
|
if (confirm(Locale.Settings.Actions.ConfirmClearAll)) { |
|
|
chatStore.clearAllData(); |
|
|
} |
|
|
}} |
|
|
bordered |
|
|
title={Locale.Settings.Actions.ClearAll} |
|
|
/> |
|
|
</div> |
|
|
<div className="window-action-button"> |
|
|
<IconButton |
|
|
icon={<ResetIcon />} |
|
|
onClick={() => { |
|
|
if (confirm(Locale.Settings.Actions.ConfirmResetAll)) { |
|
|
resetConfig(); |
|
|
} |
|
|
}} |
|
|
bordered |
|
|
title={Locale.Settings.Actions.ResetAll} |
|
|
/> |
|
|
</div> |
|
|
<div className="window-action-button"> |
|
|
<IconButton |
|
|
icon={<CloseIcon />} |
|
|
onClick={() => navigate(Path.Home)} |
|
|
bordered |
|
|
title={Locale.Settings.Actions.Close} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div className={styles["settings"]}> |
|
|
<List> |
|
|
<ListItem title={Locale.Settings.Avatar}> |
|
|
<Popover |
|
|
onClose={() => setShowEmojiPicker(false)} |
|
|
content={ |
|
|
<AvatarPicker |
|
|
onEmojiClick={(avatar: string) => { |
|
|
updateConfig((config) => (config.avatar = avatar)); |
|
|
setShowEmojiPicker(false); |
|
|
}} |
|
|
/> |
|
|
} |
|
|
open={showEmojiPicker} |
|
|
> |
|
|
<div |
|
|
className={styles.avatar} |
|
|
onClick={() => setShowEmojiPicker(true)} |
|
|
> |
|
|
<Avatar avatar={config.avatar} /> |
|
|
</div> |
|
|
</Popover> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")} |
|
|
subTitle={ |
|
|
checkingUpdate |
|
|
? Locale.Settings.Update.IsChecking |
|
|
: hasNewVersion |
|
|
? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR") |
|
|
: Locale.Settings.Update.IsLatest |
|
|
} |
|
|
> |
|
|
{checkingUpdate ? ( |
|
|
<div /> |
|
|
) : hasNewVersion ? ( |
|
|
<Link href={UPDATE_URL} target="_blank" className="link"> |
|
|
{Locale.Settings.Update.GoToUpdate} |
|
|
</Link> |
|
|
) : ( |
|
|
<IconButton |
|
|
icon={<ResetIcon></ResetIcon>} |
|
|
text={Locale.Settings.Update.CheckUpdate} |
|
|
onClick={() => checkUpdate(true)} |
|
|
/> |
|
|
)} |
|
|
</ListItem> |
|
|
|
|
|
<ListItem title={Locale.Settings.SendKey}> |
|
|
<select |
|
|
value={config.submitKey} |
|
|
onChange={(e) => { |
|
|
updateConfig( |
|
|
(config) => |
|
|
(config.submitKey = e.target.value as any as SubmitKey), |
|
|
); |
|
|
}} |
|
|
> |
|
|
{Object.values(SubmitKey).map((v) => ( |
|
|
<option value={v} key={v}> |
|
|
{v} |
|
|
</option> |
|
|
))} |
|
|
</select> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem title={Locale.Settings.Theme}> |
|
|
<select |
|
|
value={config.theme} |
|
|
onChange={(e) => { |
|
|
updateConfig( |
|
|
(config) => (config.theme = e.target.value as any as Theme), |
|
|
); |
|
|
}} |
|
|
> |
|
|
{Object.values(Theme).map((v) => ( |
|
|
<option value={v} key={v}> |
|
|
{v} |
|
|
</option> |
|
|
))} |
|
|
</select> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem title={Locale.Settings.Lang.Name}> |
|
|
<select |
|
|
value={getLang()} |
|
|
onChange={(e) => { |
|
|
changeLang(e.target.value as any); |
|
|
}} |
|
|
> |
|
|
{AllLangs.map((lang) => ( |
|
|
<option value={lang} key={lang}> |
|
|
{Locale.Settings.Lang.Options[lang]} |
|
|
</option> |
|
|
))} |
|
|
</select> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.FontSize.Title} |
|
|
subTitle={Locale.Settings.FontSize.SubTitle} |
|
|
> |
|
|
<InputRange |
|
|
title={`${config.fontSize ?? 14}px`} |
|
|
value={config.fontSize} |
|
|
min="12" |
|
|
max="18" |
|
|
step="1" |
|
|
onChange={(e) => |
|
|
updateConfig( |
|
|
(config) => |
|
|
(config.fontSize = Number.parseInt(e.currentTarget.value)), |
|
|
) |
|
|
} |
|
|
></InputRange> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.SendPreviewBubble.Title} |
|
|
subTitle={Locale.Settings.SendPreviewBubble.SubTitle} |
|
|
> |
|
|
<input |
|
|
type="checkbox" |
|
|
checked={config.sendPreviewBubble} |
|
|
onChange={(e) => |
|
|
updateConfig( |
|
|
(config) => |
|
|
(config.sendPreviewBubble = e.currentTarget.checked), |
|
|
) |
|
|
} |
|
|
></input> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.Mask.Title} |
|
|
subTitle={Locale.Settings.Mask.SubTitle} |
|
|
> |
|
|
<input |
|
|
type="checkbox" |
|
|
checked={!config.dontShowMaskSplashScreen} |
|
|
onChange={(e) => |
|
|
updateConfig( |
|
|
(config) => |
|
|
(config.dontShowMaskSplashScreen = |
|
|
!e.currentTarget.checked), |
|
|
) |
|
|
} |
|
|
></input> |
|
|
</ListItem> |
|
|
</List> |
|
|
|
|
|
<List> |
|
|
{enabledAccessControl ? ( |
|
|
<ListItem |
|
|
title={Locale.Settings.AccessCode.Title} |
|
|
subTitle={Locale.Settings.AccessCode.SubTitle} |
|
|
> |
|
|
<PasswordInput |
|
|
value={accessStore.accessCode} |
|
|
type="text" |
|
|
placeholder={Locale.Settings.AccessCode.Placeholder} |
|
|
onChange={(e) => { |
|
|
accessStore.updateCode(e.currentTarget.value); |
|
|
}} |
|
|
/> |
|
|
</ListItem> |
|
|
) : ( |
|
|
<></> |
|
|
)} |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.Token.Title} |
|
|
subTitle={Locale.Settings.Token.SubTitle} |
|
|
> |
|
|
<PasswordInput |
|
|
value={accessStore.token} |
|
|
type="text" |
|
|
placeholder={Locale.Settings.Token.Placeholder} |
|
|
onChange={(e) => { |
|
|
accessStore.updateToken(e.currentTarget.value); |
|
|
}} |
|
|
/> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.Usage.Title} |
|
|
subTitle={ |
|
|
showUsage |
|
|
? loadingUsage |
|
|
? Locale.Settings.Usage.IsChecking |
|
|
: Locale.Settings.Usage.SubTitle( |
|
|
usage?.used ?? "[?]", |
|
|
usage?.subscription ?? "[?]", |
|
|
) |
|
|
: Locale.Settings.Usage.NoAccess |
|
|
} |
|
|
> |
|
|
{!showUsage || loadingUsage ? ( |
|
|
<div /> |
|
|
) : ( |
|
|
<IconButton |
|
|
icon={<ResetIcon></ResetIcon>} |
|
|
text={Locale.Settings.Usage.Check} |
|
|
onClick={() => checkUsage(true)} |
|
|
/> |
|
|
)} |
|
|
</ListItem> |
|
|
</List> |
|
|
|
|
|
<List> |
|
|
<ListItem |
|
|
title={Locale.Settings.Prompt.Disable.Title} |
|
|
subTitle={Locale.Settings.Prompt.Disable.SubTitle} |
|
|
> |
|
|
<input |
|
|
type="checkbox" |
|
|
checked={config.disablePromptHint} |
|
|
onChange={(e) => |
|
|
updateConfig( |
|
|
(config) => |
|
|
(config.disablePromptHint = e.currentTarget.checked), |
|
|
) |
|
|
} |
|
|
></input> |
|
|
</ListItem> |
|
|
|
|
|
<ListItem |
|
|
title={Locale.Settings.Prompt.List} |
|
|
subTitle={Locale.Settings.Prompt.ListCount( |
|
|
builtinCount, |
|
|
customCount, |
|
|
)} |
|
|
> |
|
|
<IconButton |
|
|
icon={<EditIcon />} |
|
|
text={Locale.Settings.Prompt.Edit} |
|
|
onClick={() => setShowPromptModal(true)} |
|
|
/> |
|
|
</ListItem> |
|
|
</List> |
|
|
|
|
|
<List> |
|
|
<ModelConfigList |
|
|
modelConfig={config.modelConfig} |
|
|
updateConfig={(upater) => { |
|
|
const modelConfig = { ...config.modelConfig }; |
|
|
upater(modelConfig); |
|
|
config.update((config) => (config.modelConfig = modelConfig)); |
|
|
}} |
|
|
/> |
|
|
</List> |
|
|
|
|
|
{shouldShowPromptModal && ( |
|
|
<UserPromptModal onClose={() => setShowPromptModal(false)} /> |
|
|
)} |
|
|
</div> |
|
|
</ErrorBoundary> |
|
|
); |
|
|
} |
|
|
|