123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- 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 LoadingIcon from "../icons/three-dots.svg";
- import EditIcon from "../icons/edit.svg";
- import EyeIcon from "../icons/eye.svg";
- import {
- Input,
- List,
- ListItem,
- Modal,
- PasswordInput,
- Popover,
- Select,
- } 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>
- );
- }
- function formatVersionDate(t: string) {
- const d = new Date(+t);
- const year = d.getUTCFullYear();
- const month = d.getUTCMonth() + 1;
- const day = d.getUTCDate();
- return [
- year.toString(),
- month.toString().padStart(2, "0"),
- day.toString().padStart(2, "0"),
- ].join("");
- }
- 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 = formatVersionDate(updateStore.version);
- const remoteId = formatVersionDate(updateStore.remoteVersion);
- const hasNewVersion = currentVersion !== remoteId;
- function checkUpdate(force = false) {
- setCheckingUpdate(true);
- updateStore.getLatestVersion(force).then(() => {
- setCheckingUpdate(false);
- });
- console.log(
- "[Update] local version ",
- new Date(+updateStore.version).toLocaleString(),
- );
- console.log(
- "[Update] remote version ",
- new Date(+updateStore.remoteVersion).toLocaleString(),
- );
- }
- 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(),
- // 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);
- }
- };
- document.addEventListener("keydown", keydownEvent);
- return () => {
- document.removeEventListener("keydown", keydownEvent);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- 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 ? (
- <LoadingIcon />
- ) : 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>
- ) : (
- <></>
- )}
- {!accessStore.hideUserApiKey ? (
- <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>
- ) : null}
- <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>
- );
- }
|