mirror of
				https://github.com/Yidadaa/ChatGPT-Next-Web.git
				synced 2025-10-31 21:59:19 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			1258 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1258 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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 {
 | |
|   Anthropic,
 | |
|   Azure,
 | |
|   Google,
 | |
|   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 ? (
 | |
|     <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.updatePrompt(
 | |
|                 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.updatePrompt(
 | |
|                 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<string>();
 | |
| 
 | |
|   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={() => {
 | |
|               const promptId = promptStore.add({
 | |
|                 id: nanoid(),
 | |
|                 createdAt: Date.now(),
 | |
|                 title: "Empty Prompt",
 | |
|                 content: "Empty Prompt Content",
 | |
|               });
 | |
|               setEditingPromptId(promptId);
 | |
|             }}
 | |
|             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 DangerItems() {
 | |
|   const chatStore = useChatStore();
 | |
|   const appConfig = useAppConfig();
 | |
| 
 | |
|   return (
 | |
|     <List>
 | |
|       <ListItem
 | |
|         title={Locale.Settings.Danger.Reset.Title}
 | |
|         subTitle={Locale.Settings.Danger.Reset.SubTitle}
 | |
|       >
 | |
|         <IconButton
 | |
|           text={Locale.Settings.Danger.Reset.Action}
 | |
|           onClick={async () => {
 | |
|             if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
 | |
|               appConfig.reset();
 | |
|             }
 | |
|           }}
 | |
|           type="danger"
 | |
|         />
 | |
|       </ListItem>
 | |
|       <ListItem
 | |
|         title={Locale.Settings.Danger.Clear.Title}
 | |
|         subTitle={Locale.Settings.Danger.Clear.SubTitle}
 | |
|       >
 | |
|         <IconButton
 | |
|           text={Locale.Settings.Danger.Clear.Action}
 | |
|           onClick={async () => {
 | |
|             if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
 | |
|               chatStore.clearAllData();
 | |
|             }
 | |
|           }}
 | |
|           type="danger"
 | |
|         />
 | |
|       </ListItem>
 | |
|     </List>
 | |
|   );
 | |
| }
 | |
| 
 | |
| 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 (
 | |
|     <IconButton
 | |
|       text={Locale.Settings.Sync.Config.Modal.Check}
 | |
|       bordered
 | |
|       onClick={check}
 | |
|       icon={
 | |
|         checkState === "none" ? (
 | |
|           <ConnectionIcon />
 | |
|         ) : checkState === "checking" ? (
 | |
|           <LoadingIcon />
 | |
|         ) : checkState === "success" ? (
 | |
|           <CloudSuccessIcon />
 | |
|         ) : checkState === "failed" ? (
 | |
|           <CloudFailIcon />
 | |
|         ) : (
 | |
|           <ConnectionIcon />
 | |
|         )
 | |
|       }
 | |
|     ></IconButton>
 | |
|   );
 | |
| }
 | |
| 
 | |
| function SyncConfigModal(props: { onClose?: () => void }) {
 | |
|   const syncStore = useSyncStore();
 | |
| 
 | |
|   return (
 | |
|     <div className="modal-mask">
 | |
|       <Modal
 | |
|         title={Locale.Settings.Sync.Config.Modal.Title}
 | |
|         onClose={() => props.onClose?.()}
 | |
|         actions={[
 | |
|           <CheckButton key="check" />,
 | |
|           <IconButton
 | |
|             key="confirm"
 | |
|             onClick={props.onClose}
 | |
|             icon={<ConfirmIcon />}
 | |
|             bordered
 | |
|             text={Locale.UI.Confirm}
 | |
|           />,
 | |
|         ]}
 | |
|       >
 | |
|         <List>
 | |
|           <ListItem
 | |
|             title={Locale.Settings.Sync.Config.SyncType.Title}
 | |
|             subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
 | |
|           >
 | |
|             <select
 | |
|               value={syncStore.provider}
 | |
|               onChange={(e) => {
 | |
|                 syncStore.update(
 | |
|                   (config) =>
 | |
|                     (config.provider = e.target.value as ProviderType),
 | |
|                 );
 | |
|               }}
 | |
|             >
 | |
|               {Object.entries(ProviderType).map(([k, v]) => (
 | |
|                 <option value={v} key={k}>
 | |
|                   {k}
 | |
|                 </option>
 | |
|               ))}
 | |
|             </select>
 | |
|           </ListItem>
 | |
| 
 | |
|           <ListItem
 | |
|             title={Locale.Settings.Sync.Config.Proxy.Title}
 | |
|             subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
 | |
|           >
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               checked={syncStore.useProxy}
 | |
|               onChange={(e) => {
 | |
|                 syncStore.update(
 | |
|                   (config) => (config.useProxy = e.currentTarget.checked),
 | |
|                 );
 | |
|               }}
 | |
|             ></input>
 | |
|           </ListItem>
 | |
|           {syncStore.useProxy ? (
 | |
|             <ListItem
 | |
|               title={Locale.Settings.Sync.Config.ProxyUrl.Title}
 | |
|               subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
 | |
|             >
 | |
|               <input
 | |
|                 type="text"
 | |
|                 value={syncStore.proxyUrl}
 | |
|                 onChange={(e) => {
 | |
|                   syncStore.update(
 | |
|                     (config) => (config.proxyUrl = e.currentTarget.value),
 | |
|                   );
 | |
|                 }}
 | |
|               ></input>
 | |
|             </ListItem>
 | |
|           ) : null}
 | |
|         </List>
 | |
| 
 | |
|         {syncStore.provider === ProviderType.WebDAV && (
 | |
|           <>
 | |
|             <List>
 | |
|               <ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
 | |
|                 <input
 | |
|                   type="text"
 | |
|                   value={syncStore.webdav.endpoint}
 | |
|                   onChange={(e) => {
 | |
|                     syncStore.update(
 | |
|                       (config) =>
 | |
|                         (config.webdav.endpoint = e.currentTarget.value),
 | |
|                     );
 | |
|                   }}
 | |
|                 ></input>
 | |
|               </ListItem>
 | |
| 
 | |
|               <ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
 | |
|                 <input
 | |
|                   type="text"
 | |
|                   value={syncStore.webdav.username}
 | |
|                   onChange={(e) => {
 | |
|                     syncStore.update(
 | |
|                       (config) =>
 | |
|                         (config.webdav.username = e.currentTarget.value),
 | |
|                     );
 | |
|                   }}
 | |
|                 ></input>
 | |
|               </ListItem>
 | |
|               <ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
 | |
|                 <PasswordInput
 | |
|                   value={syncStore.webdav.password}
 | |
|                   onChange={(e) => {
 | |
|                     syncStore.update(
 | |
|                       (config) =>
 | |
|                         (config.webdav.password = e.currentTarget.value),
 | |
|                     );
 | |
|                   }}
 | |
|                 ></PasswordInput>
 | |
|               </ListItem>
 | |
|             </List>
 | |
|           </>
 | |
|         )}
 | |
| 
 | |
|         {syncStore.provider === ProviderType.UpStash && (
 | |
|           <List>
 | |
|             <ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
 | |
|               <input
 | |
|                 type="text"
 | |
|                 value={syncStore.upstash.endpoint}
 | |
|                 onChange={(e) => {
 | |
|                   syncStore.update(
 | |
|                     (config) =>
 | |
|                       (config.upstash.endpoint = e.currentTarget.value),
 | |
|                   );
 | |
|                 }}
 | |
|               ></input>
 | |
|             </ListItem>
 | |
| 
 | |
|             <ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
 | |
|               <input
 | |
|                 type="text"
 | |
|                 value={syncStore.upstash.username}
 | |
|                 placeholder={STORAGE_KEY}
 | |
|                 onChange={(e) => {
 | |
|                   syncStore.update(
 | |
|                     (config) =>
 | |
|                       (config.upstash.username = e.currentTarget.value),
 | |
|                   );
 | |
|                 }}
 | |
|               ></input>
 | |
|             </ListItem>
 | |
|             <ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
 | |
|               <PasswordInput
 | |
|                 value={syncStore.upstash.apiKey}
 | |
|                 onChange={(e) => {
 | |
|                   syncStore.update(
 | |
|                     (config) => (config.upstash.apiKey = e.currentTarget.value),
 | |
|                   );
 | |
|                 }}
 | |
|               ></PasswordInput>
 | |
|             </ListItem>
 | |
|           </List>
 | |
|         )}
 | |
|       </Modal>
 | |
|     </div>
 | |
|   );
 | |
| }
 | |
| 
 | |
| 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 (
 | |
|     <>
 | |
|       <List>
 | |
|         <ListItem
 | |
|           title={Locale.Settings.Sync.CloudState}
 | |
|           subTitle={
 | |
|             syncStore.lastProvider
 | |
|               ? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
 | |
|                   syncStore.lastProvider
 | |
|                 }]`
 | |
|               : Locale.Settings.Sync.NotSyncYet
 | |
|           }
 | |
|         >
 | |
|           <div style={{ display: "flex" }}>
 | |
|             <IconButton
 | |
|               icon={<ConfigIcon />}
 | |
|               text={Locale.UI.Config}
 | |
|               onClick={() => {
 | |
|                 setShowSyncConfigModal(true);
 | |
|               }}
 | |
|             />
 | |
|             {couldSync && (
 | |
|               <IconButton
 | |
|                 icon={<ResetIcon />}
 | |
|                 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);
 | |
|                   }
 | |
|                 }}
 | |
|               />
 | |
|             )}
 | |
|           </div>
 | |
|         </ListItem>
 | |
| 
 | |
|         <ListItem
 | |
|           title={Locale.Settings.Sync.LocalState}
 | |
|           subTitle={Locale.Settings.Sync.Overview(stateOverview)}
 | |
|         >
 | |
|           <div style={{ display: "flex" }}>
 | |
|             <IconButton
 | |
|               icon={<UploadIcon />}
 | |
|               text={Locale.UI.Export}
 | |
|               onClick={() => {
 | |
|                 syncStore.export();
 | |
|               }}
 | |
|             />
 | |
|             <IconButton
 | |
|               icon={<DownloadIcon />}
 | |
|               text={Locale.UI.Import}
 | |
|               onClick={() => {
 | |
|                 syncStore.import();
 | |
|               }}
 | |
|             />
 | |
|           </div>
 | |
|         </ListItem>
 | |
|       </List>
 | |
| 
 | |
|       {showSyncConfigModal && (
 | |
|         <SyncConfigModal onClose={() => 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 (
 | |
|     <ErrorBoundary>
 | |
|       <div className="window-header" data-tauri-drag-region>
 | |
|         <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"></div>
 | |
|           <div className="window-action-button"></div>
 | |
|           <div className="window-action-button">
 | |
|             <IconButton
 | |
|               icon={<CloseIcon />}
 | |
|               onClick={() => navigate(Path.Home)}
 | |
|               bordered
 | |
|             />
 | |
|           </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(!showEmojiPicker);
 | |
|                 }}
 | |
|               >
 | |
|                 <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={updateUrl} 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}>
 | |
|                   {ALL_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="40"
 | |
|               step="1"
 | |
|               onChange={(e) =>
 | |
|                 updateConfig(
 | |
|                   (config) =>
 | |
|                     (config.fontSize = Number.parseInt(e.currentTarget.value)),
 | |
|                 )
 | |
|               }
 | |
|             ></InputRange>
 | |
|           </ListItem>
 | |
| 
 | |
|           <ListItem
 | |
|             title={Locale.Settings.AutoGenerateTitle.Title}
 | |
|             subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
 | |
|           >
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               checked={config.enableAutoGenerateTitle}
 | |
|               onChange={(e) =>
 | |
|                 updateConfig(
 | |
|                   (config) =>
 | |
|                     (config.enableAutoGenerateTitle = e.currentTarget.checked),
 | |
|                 )
 | |
|               }
 | |
|             ></input>
 | |
|           </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>
 | |
|         </List>
 | |
| 
 | |
|         <SyncItems />
 | |
| 
 | |
|         <List>
 | |
|           <ListItem
 | |
|             title={Locale.Settings.Mask.Splash.Title}
 | |
|             subTitle={Locale.Settings.Mask.Splash.SubTitle}
 | |
|           >
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               checked={!config.dontShowMaskSplashScreen}
 | |
|               onChange={(e) =>
 | |
|                 updateConfig(
 | |
|                   (config) =>
 | |
|                     (config.dontShowMaskSplashScreen =
 | |
|                       !e.currentTarget.checked),
 | |
|                 )
 | |
|               }
 | |
|             ></input>
 | |
|           </ListItem>
 | |
| 
 | |
|           <ListItem
 | |
|             title={Locale.Settings.Mask.Builtin.Title}
 | |
|             subTitle={Locale.Settings.Mask.Builtin.SubTitle}
 | |
|           >
 | |
|             <input
 | |
|               type="checkbox"
 | |
|               checked={config.hideBuiltinMasks}
 | |
|               onChange={(e) =>
 | |
|                 updateConfig(
 | |
|                   (config) =>
 | |
|                     (config.hideBuiltinMasks = e.currentTarget.checked),
 | |
|                 )
 | |
|               }
 | |
|             ></input>
 | |
|           </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 id={SlotID.CustomModel}>
 | |
|           {showAccessCode && (
 | |
|             <ListItem
 | |
|               title={Locale.Settings.Access.AccessCode.Title}
 | |
|               subTitle={Locale.Settings.Access.AccessCode.SubTitle}
 | |
|             >
 | |
|               <PasswordInput
 | |
|                 value={accessStore.accessCode}
 | |
|                 type="text"
 | |
|                 placeholder={Locale.Settings.Access.AccessCode.Placeholder}
 | |
|                 onChange={(e) => {
 | |
|                   accessStore.update(
 | |
|                     (access) => (access.accessCode = e.currentTarget.value),
 | |
|                   );
 | |
|                 }}
 | |
|               />
 | |
|             </ListItem>
 | |
|           )}
 | |
| 
 | |
|           {!accessStore.hideUserApiKey && (
 | |
|             <>
 | |
|               {
 | |
|                 // Conditionally render the following ListItem based on clientConfig.isApp
 | |
|                 !clientConfig?.isApp && ( // only show if isApp is false
 | |
|                   <ListItem
 | |
|                     title={Locale.Settings.Access.CustomEndpoint.Title}
 | |
|                     subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
 | |
|                   >
 | |
|                     <input
 | |
|                       type="checkbox"
 | |
|                       checked={accessStore.useCustomConfig}
 | |
|                       onChange={(e) =>
 | |
|                         accessStore.update(
 | |
|                           (access) =>
 | |
|                             (access.useCustomConfig = e.currentTarget.checked),
 | |
|                         )
 | |
|                       }
 | |
|                     ></input>
 | |
|                   </ListItem>
 | |
|                 )
 | |
|               }
 | |
|               {accessStore.useCustomConfig && (
 | |
|                 <>
 | |
|                   <ListItem
 | |
|                     title={Locale.Settings.Access.Provider.Title}
 | |
|                     subTitle={Locale.Settings.Access.Provider.SubTitle}
 | |
|                   >
 | |
|                     <Select
 | |
|                       value={accessStore.provider}
 | |
|                       onChange={(e) => {
 | |
|                         accessStore.update(
 | |
|                           (access) =>
 | |
|                             (access.provider = e.target
 | |
|                               .value as ServiceProvider),
 | |
|                         );
 | |
|                       }}
 | |
|                     >
 | |
|                       {Object.entries(ServiceProvider).map(([k, v]) => (
 | |
|                         <option value={v} key={k}>
 | |
|                           {k}
 | |
|                         </option>
 | |
|                       ))}
 | |
|                     </Select>
 | |
|                   </ListItem>
 | |
| 
 | |
|                   {accessStore.provider === ServiceProvider.OpenAI && (
 | |
|                     <>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.OpenAI.Endpoint.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.OpenAI.Endpoint.SubTitle
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.openaiUrl}
 | |
|                           placeholder={OPENAI_BASE_URL}
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.openaiUrl = e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.OpenAI.ApiKey.Title}
 | |
|                         subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
 | |
|                       >
 | |
|                         <PasswordInput
 | |
|                           value={accessStore.openaiApiKey}
 | |
|                           type="text"
 | |
|                           placeholder={
 | |
|                             Locale.Settings.Access.OpenAI.ApiKey.Placeholder
 | |
|                           }
 | |
|                           onChange={(e) => {
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.openaiApiKey = e.currentTarget.value),
 | |
|                             );
 | |
|                           }}
 | |
|                         />
 | |
|                       </ListItem>
 | |
|                     </>
 | |
|                   )}
 | |
|                   {accessStore.provider === ServiceProvider.Azure && (
 | |
|                     <>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Azure.Endpoint.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Azure.Endpoint.SubTitle +
 | |
|                           Azure.ExampleEndpoint
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.azureUrl}
 | |
|                           placeholder={Azure.ExampleEndpoint}
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.azureUrl = e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Azure.ApiKey.Title}
 | |
|                         subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
 | |
|                       >
 | |
|                         <PasswordInput
 | |
|                           value={accessStore.azureApiKey}
 | |
|                           type="text"
 | |
|                           placeholder={
 | |
|                             Locale.Settings.Access.Azure.ApiKey.Placeholder
 | |
|                           }
 | |
|                           onChange={(e) => {
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.azureApiKey = e.currentTarget.value),
 | |
|                             );
 | |
|                           }}
 | |
|                         />
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Azure.ApiVerion.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Azure.ApiVerion.SubTitle
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.azureApiVersion}
 | |
|                           placeholder="2023-08-01-preview"
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.azureApiVersion =
 | |
|                                   e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                     </>
 | |
|                   )}
 | |
|                   {accessStore.provider === ServiceProvider.Google && (
 | |
|                     <>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Google.Endpoint.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Google.Endpoint.SubTitle +
 | |
|                           Google.ExampleEndpoint
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.googleUrl}
 | |
|                           placeholder={Google.ExampleEndpoint}
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.googleUrl = e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Google.ApiKey.Title}
 | |
|                         subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
 | |
|                       >
 | |
|                         <PasswordInput
 | |
|                           value={accessStore.googleApiKey}
 | |
|                           type="text"
 | |
|                           placeholder={
 | |
|                             Locale.Settings.Access.Google.ApiKey.Placeholder
 | |
|                           }
 | |
|                           onChange={(e) => {
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.googleApiKey = e.currentTarget.value),
 | |
|                             );
 | |
|                           }}
 | |
|                         />
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Google.ApiVersion.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Google.ApiVersion.SubTitle
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.googleApiVersion}
 | |
|                           placeholder="2023-08-01-preview"
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.googleApiVersion =
 | |
|                                   e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                     </>
 | |
|                   )}
 | |
|                   {accessStore.provider === ServiceProvider.Anthropic && (
 | |
|                     <>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Anthropic.Endpoint.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
 | |
|                           Anthropic.ExampleEndpoint
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.anthropicUrl}
 | |
|                           placeholder={Anthropic.ExampleEndpoint}
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.anthropicUrl = e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Anthropic.ApiKey.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Anthropic.ApiKey.SubTitle
 | |
|                         }
 | |
|                       >
 | |
|                         <PasswordInput
 | |
|                           value={accessStore.anthropicApiKey}
 | |
|                           type="text"
 | |
|                           placeholder={
 | |
|                             Locale.Settings.Access.Anthropic.ApiKey.Placeholder
 | |
|                           }
 | |
|                           onChange={(e) => {
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.anthropicApiKey =
 | |
|                                   e.currentTarget.value),
 | |
|                             );
 | |
|                           }}
 | |
|                         />
 | |
|                       </ListItem>
 | |
|                       <ListItem
 | |
|                         title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
 | |
|                         subTitle={
 | |
|                           Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
 | |
|                         }
 | |
|                       >
 | |
|                         <input
 | |
|                           type="text"
 | |
|                           value={accessStore.anthropicApiVersion}
 | |
|                           placeholder={Anthropic.Vision}
 | |
|                           onChange={(e) =>
 | |
|                             accessStore.update(
 | |
|                               (access) =>
 | |
|                                 (access.anthropicApiVersion =
 | |
|                                   e.currentTarget.value),
 | |
|                             )
 | |
|                           }
 | |
|                         ></input>
 | |
|                       </ListItem>
 | |
|                     </>
 | |
|                   )}
 | |
|                 </>
 | |
|               )}
 | |
|             </>
 | |
|           )}
 | |
| 
 | |
|           {!shouldHideBalanceQuery && !clientConfig?.isApp ? (
 | |
|             <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>
 | |
|           ) : null}
 | |
| 
 | |
|           <ListItem
 | |
|             title={Locale.Settings.Access.CustomModel.Title}
 | |
|             subTitle={Locale.Settings.Access.CustomModel.SubTitle}
 | |
|           >
 | |
|             <input
 | |
|               type="text"
 | |
|               value={config.customModels}
 | |
|               placeholder="model1,model2,model3"
 | |
|               onChange={(e) =>
 | |
|                 config.update(
 | |
|                   (config) => (config.customModels = e.currentTarget.value),
 | |
|                 )
 | |
|               }
 | |
|             ></input>
 | |
|           </ListItem>
 | |
|         </List>
 | |
| 
 | |
|         <List>
 | |
|           <ModelConfigList
 | |
|             modelConfig={config.modelConfig}
 | |
|             updateConfig={(updater) => {
 | |
|               const modelConfig = { ...config.modelConfig };
 | |
|               updater(modelConfig);
 | |
|               config.update((config) => (config.modelConfig = modelConfig));
 | |
|             }}
 | |
|           />
 | |
|         </List>
 | |
| 
 | |
|         {shouldShowPromptModal && (
 | |
|           <UserPromptModal onClose={() => setShowPromptModal(false)} />
 | |
|         )}
 | |
| 
 | |
|         <DangerItems />
 | |
|       </div>
 | |
|     </ErrorBoundary>
 | |
|   );
 | |
| }
 |