settings.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. import { useState, useEffect, useMemo } from "react";
  2. import styles from "./settings.module.scss";
  3. import ResetIcon from "../icons/reload.svg";
  4. import AddIcon from "../icons/add.svg";
  5. import CloseIcon from "../icons/close.svg";
  6. import CopyIcon from "../icons/copy.svg";
  7. import ClearIcon from "../icons/clear.svg";
  8. import LoadingIcon from "../icons/three-dots.svg";
  9. import EditIcon from "../icons/edit.svg";
  10. import EyeIcon from "../icons/eye.svg";
  11. import DownloadIcon from "../icons/download.svg";
  12. import UploadIcon from "../icons/upload.svg";
  13. import ConfigIcon from "../icons/config.svg";
  14. import ConfirmIcon from "../icons/confirm.svg";
  15. import ConnectionIcon from "../icons/connection.svg";
  16. import CloudSuccessIcon from "../icons/cloud-success.svg";
  17. import CloudFailIcon from "../icons/cloud-fail.svg";
  18. import {
  19. Input,
  20. List,
  21. ListItem,
  22. Modal,
  23. PasswordInput,
  24. Popover,
  25. Select,
  26. showConfirm,
  27. showToast,
  28. } from "./ui-lib";
  29. import { ModelConfigList } from "./model-config";
  30. import { IconButton } from "./button";
  31. import {
  32. SubmitKey,
  33. useChatStore,
  34. Theme,
  35. useUpdateStore,
  36. useAccessStore,
  37. useAppConfig,
  38. } from "../store";
  39. import Locale, {
  40. AllLangs,
  41. ALL_LANG_OPTIONS,
  42. changeLang,
  43. getLang,
  44. } from "../locales";
  45. import { copyToClipboard } from "../utils";
  46. import Link from "next/link";
  47. import { Path, RELEASE_URL, UPDATE_URL } from "../constant";
  48. import { Prompt, SearchService, usePromptStore } from "../store/prompt";
  49. import { ErrorBoundary } from "./error";
  50. import { InputRange } from "./input-range";
  51. import { useNavigate } from "react-router-dom";
  52. import { Avatar, AvatarPicker } from "./emoji";
  53. import { getClientConfig } from "../config/client";
  54. import { useSyncStore } from "../store/sync";
  55. import { nanoid } from "nanoid";
  56. import { useMaskStore } from "../store/mask";
  57. import { ProviderType } from "../utils/cloud";
  58. function EditPromptModal(props: { id: string; onClose: () => void }) {
  59. const promptStore = usePromptStore();
  60. const prompt = promptStore.get(props.id);
  61. return prompt ? (
  62. <div className="modal-mask">
  63. <Modal
  64. title={Locale.Settings.Prompt.EditModal.Title}
  65. onClose={props.onClose}
  66. actions={[
  67. <IconButton
  68. key=""
  69. onClick={props.onClose}
  70. text={Locale.UI.Confirm}
  71. bordered
  72. />,
  73. ]}
  74. >
  75. <div className={styles["edit-prompt-modal"]}>
  76. <input
  77. type="text"
  78. value={prompt.title}
  79. readOnly={!prompt.isUser}
  80. className={styles["edit-prompt-title"]}
  81. onInput={(e) =>
  82. promptStore.updatePrompt(
  83. props.id,
  84. (prompt) => (prompt.title = e.currentTarget.value),
  85. )
  86. }
  87. ></input>
  88. <Input
  89. value={prompt.content}
  90. readOnly={!prompt.isUser}
  91. className={styles["edit-prompt-content"]}
  92. rows={10}
  93. onInput={(e) =>
  94. promptStore.updatePrompt(
  95. props.id,
  96. (prompt) => (prompt.content = e.currentTarget.value),
  97. )
  98. }
  99. ></Input>
  100. </div>
  101. </Modal>
  102. </div>
  103. ) : null;
  104. }
  105. function UserPromptModal(props: { onClose?: () => void }) {
  106. const promptStore = usePromptStore();
  107. const userPrompts = promptStore.getUserPrompts();
  108. const builtinPrompts = SearchService.builtinPrompts;
  109. const allPrompts = userPrompts.concat(builtinPrompts);
  110. const [searchInput, setSearchInput] = useState("");
  111. const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
  112. const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
  113. const [editingPromptId, setEditingPromptId] = useState<string>();
  114. useEffect(() => {
  115. if (searchInput.length > 0) {
  116. const searchResult = SearchService.search(searchInput);
  117. setSearchPrompts(searchResult);
  118. } else {
  119. setSearchPrompts([]);
  120. }
  121. }, [searchInput]);
  122. return (
  123. <div className="modal-mask">
  124. <Modal
  125. title={Locale.Settings.Prompt.Modal.Title}
  126. onClose={() => props.onClose?.()}
  127. actions={[
  128. <IconButton
  129. key="add"
  130. onClick={() => {
  131. const promptId = promptStore.add({
  132. id: nanoid(),
  133. createdAt: Date.now(),
  134. title: "Empty Prompt",
  135. content: "Empty Prompt Content",
  136. });
  137. setEditingPromptId(promptId);
  138. }}
  139. icon={<AddIcon />}
  140. bordered
  141. text={Locale.Settings.Prompt.Modal.Add}
  142. />,
  143. ]}
  144. >
  145. <div className={styles["user-prompt-modal"]}>
  146. <input
  147. type="text"
  148. className={styles["user-prompt-search"]}
  149. placeholder={Locale.Settings.Prompt.Modal.Search}
  150. value={searchInput}
  151. onInput={(e) => setSearchInput(e.currentTarget.value)}
  152. ></input>
  153. <div className={styles["user-prompt-list"]}>
  154. {prompts.map((v, _) => (
  155. <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
  156. <div className={styles["user-prompt-header"]}>
  157. <div className={styles["user-prompt-title"]}>{v.title}</div>
  158. <div className={styles["user-prompt-content"] + " one-line"}>
  159. {v.content}
  160. </div>
  161. </div>
  162. <div className={styles["user-prompt-buttons"]}>
  163. {v.isUser && (
  164. <IconButton
  165. icon={<ClearIcon />}
  166. className={styles["user-prompt-button"]}
  167. onClick={() => promptStore.remove(v.id!)}
  168. />
  169. )}
  170. {v.isUser ? (
  171. <IconButton
  172. icon={<EditIcon />}
  173. className={styles["user-prompt-button"]}
  174. onClick={() => setEditingPromptId(v.id)}
  175. />
  176. ) : (
  177. <IconButton
  178. icon={<EyeIcon />}
  179. className={styles["user-prompt-button"]}
  180. onClick={() => setEditingPromptId(v.id)}
  181. />
  182. )}
  183. <IconButton
  184. icon={<CopyIcon />}
  185. className={styles["user-prompt-button"]}
  186. onClick={() => copyToClipboard(v.content)}
  187. />
  188. </div>
  189. </div>
  190. ))}
  191. </div>
  192. </div>
  193. </Modal>
  194. {editingPromptId !== undefined && (
  195. <EditPromptModal
  196. id={editingPromptId!}
  197. onClose={() => setEditingPromptId(undefined)}
  198. />
  199. )}
  200. </div>
  201. );
  202. }
  203. function DangerItems() {
  204. const chatStore = useChatStore();
  205. const appConfig = useAppConfig();
  206. return (
  207. <List>
  208. <ListItem
  209. title={Locale.Settings.Danger.Reset.Title}
  210. subTitle={Locale.Settings.Danger.Reset.SubTitle}
  211. >
  212. <IconButton
  213. text={Locale.Settings.Danger.Reset.Action}
  214. onClick={async () => {
  215. if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
  216. appConfig.reset();
  217. }
  218. }}
  219. type="danger"
  220. />
  221. </ListItem>
  222. <ListItem
  223. title={Locale.Settings.Danger.Clear.Title}
  224. subTitle={Locale.Settings.Danger.Clear.SubTitle}
  225. >
  226. <IconButton
  227. text={Locale.Settings.Danger.Clear.Action}
  228. onClick={async () => {
  229. if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
  230. chatStore.clearAllData();
  231. }
  232. }}
  233. type="danger"
  234. />
  235. </ListItem>
  236. </List>
  237. );
  238. }
  239. function CheckButton() {
  240. const syncStore = useSyncStore();
  241. const couldCheck = useMemo(() => {
  242. return syncStore.coundSync();
  243. }, [syncStore]);
  244. const [checkState, setCheckState] = useState<
  245. "none" | "checking" | "success" | "failed"
  246. >("none");
  247. async function check() {
  248. setCheckState("checking");
  249. const valid = await syncStore.check();
  250. setCheckState(valid ? "success" : "failed");
  251. }
  252. if (!couldCheck) return null;
  253. return (
  254. <IconButton
  255. text="检查可用性"
  256. bordered
  257. onClick={check}
  258. icon={
  259. checkState === "none" ? (
  260. <ConnectionIcon />
  261. ) : checkState === "checking" ? (
  262. <LoadingIcon />
  263. ) : checkState === "success" ? (
  264. <CloudSuccessIcon />
  265. ) : checkState === "failed" ? (
  266. <CloudFailIcon />
  267. ) : (
  268. <ConnectionIcon />
  269. )
  270. }
  271. ></IconButton>
  272. );
  273. }
  274. function SyncConfigModal(props: { onClose?: () => void }) {
  275. const syncStore = useSyncStore();
  276. return (
  277. <div className="modal-mask">
  278. <Modal
  279. title={Locale.Settings.Sync.Config.Modal.Title}
  280. onClose={() => props.onClose?.()}
  281. actions={[
  282. <CheckButton key="check" />,
  283. <IconButton
  284. key="confirm"
  285. onClick={props.onClose}
  286. icon={<ConfirmIcon />}
  287. bordered
  288. text={Locale.UI.Confirm}
  289. />,
  290. ]}
  291. >
  292. <List>
  293. <ListItem
  294. title={Locale.Settings.Sync.Config.SyncType.Title}
  295. subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
  296. >
  297. <select
  298. value={syncStore.provider}
  299. onChange={(e) => {
  300. syncStore.update(
  301. (config) =>
  302. (config.provider = e.target.value as ProviderType),
  303. );
  304. }}
  305. >
  306. {Object.entries(ProviderType).map(([k, v]) => (
  307. <option value={v} key={k}>
  308. {k}
  309. </option>
  310. ))}
  311. </select>
  312. </ListItem>
  313. <ListItem
  314. title={Locale.Settings.Sync.Config.Proxy.Title}
  315. subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
  316. >
  317. <input
  318. type="checkbox"
  319. checked={syncStore.useProxy}
  320. onChange={(e) => {
  321. syncStore.update(
  322. (config) => (config.useProxy = e.currentTarget.checked),
  323. );
  324. }}
  325. ></input>
  326. </ListItem>
  327. {syncStore.useProxy ? (
  328. <ListItem
  329. title={Locale.Settings.Sync.Config.ProxyUrl.Title}
  330. subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
  331. >
  332. <input
  333. type="text"
  334. value={syncStore.proxyUrl}
  335. onChange={(e) => {
  336. syncStore.update(
  337. (config) => (config.proxyUrl = e.currentTarget.value),
  338. );
  339. }}
  340. ></input>
  341. </ListItem>
  342. ) : null}
  343. </List>
  344. {syncStore.provider === ProviderType.WebDAV && (
  345. <>
  346. <List>
  347. <ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
  348. <input
  349. type="text"
  350. value={syncStore.webdav.endpoint}
  351. onChange={(e) => {
  352. syncStore.update(
  353. (config) =>
  354. (config.webdav.endpoint = e.currentTarget.value),
  355. );
  356. }}
  357. ></input>
  358. </ListItem>
  359. <ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
  360. <input
  361. type="text"
  362. value={syncStore.webdav.username}
  363. onChange={(e) => {
  364. syncStore.update(
  365. (config) =>
  366. (config.webdav.username = e.currentTarget.value),
  367. );
  368. }}
  369. ></input>
  370. </ListItem>
  371. <ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
  372. <PasswordInput
  373. value={syncStore.webdav.password}
  374. onChange={(e) => {
  375. syncStore.update(
  376. (config) =>
  377. (config.webdav.password = e.currentTarget.value),
  378. );
  379. }}
  380. ></PasswordInput>
  381. </ListItem>
  382. </List>
  383. </>
  384. )}
  385. {syncStore.provider === ProviderType.UpStash && (
  386. <List>
  387. <ListItem title={Locale.WIP}></ListItem>
  388. </List>
  389. )}
  390. </Modal>
  391. </div>
  392. );
  393. }
  394. function SyncItems() {
  395. const syncStore = useSyncStore();
  396. const chatStore = useChatStore();
  397. const promptStore = usePromptStore();
  398. const maskStore = useMaskStore();
  399. const couldSync = useMemo(() => {
  400. return syncStore.coundSync();
  401. }, [syncStore]);
  402. const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
  403. const stateOverview = useMemo(() => {
  404. const sessions = chatStore.sessions;
  405. const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
  406. return {
  407. chat: sessions.length,
  408. message: messageCount,
  409. prompt: Object.keys(promptStore.prompts).length,
  410. mask: Object.keys(maskStore.masks).length,
  411. };
  412. }, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
  413. return (
  414. <>
  415. <List>
  416. <ListItem
  417. title={Locale.Settings.Sync.CloudState}
  418. subTitle={
  419. syncStore.lastProvider
  420. ? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
  421. syncStore.lastProvider
  422. }]`
  423. : Locale.Settings.Sync.NotSyncYet
  424. }
  425. >
  426. <div style={{ display: "flex" }}>
  427. <IconButton
  428. icon={<ConfigIcon />}
  429. text={Locale.UI.Config}
  430. onClick={() => {
  431. setShowSyncConfigModal(true);
  432. }}
  433. />
  434. {couldSync && (
  435. <IconButton
  436. icon={<ResetIcon />}
  437. text={Locale.UI.Sync}
  438. onClick={async () => {
  439. try {
  440. await syncStore.sync();
  441. showToast(Locale.Settings.Sync.Success);
  442. } catch (e) {
  443. showToast(Locale.Settings.Sync.Fail);
  444. console.error("[Sync]", e);
  445. }
  446. }}
  447. />
  448. )}
  449. </div>
  450. </ListItem>
  451. <ListItem
  452. title={Locale.Settings.Sync.LocalState}
  453. subTitle={Locale.Settings.Sync.Overview(stateOverview)}
  454. >
  455. <div style={{ display: "flex" }}>
  456. <IconButton
  457. icon={<UploadIcon />}
  458. text={Locale.UI.Export}
  459. onClick={() => {
  460. syncStore.export();
  461. }}
  462. />
  463. <IconButton
  464. icon={<DownloadIcon />}
  465. text={Locale.UI.Import}
  466. onClick={() => {
  467. syncStore.import();
  468. }}
  469. />
  470. </div>
  471. </ListItem>
  472. </List>
  473. {showSyncConfigModal && (
  474. <SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
  475. )}
  476. </>
  477. );
  478. }
  479. export function Settings() {
  480. const navigate = useNavigate();
  481. const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  482. const config = useAppConfig();
  483. const updateConfig = config.update;
  484. const updateStore = useUpdateStore();
  485. const [checkingUpdate, setCheckingUpdate] = useState(false);
  486. const currentVersion = updateStore.formatVersion(updateStore.version);
  487. const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
  488. const hasNewVersion = currentVersion !== remoteId;
  489. const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
  490. function checkUpdate(force = false) {
  491. setCheckingUpdate(true);
  492. updateStore.getLatestVersion(force).then(() => {
  493. setCheckingUpdate(false);
  494. });
  495. console.log("[Update] local version ", updateStore.version);
  496. console.log("[Update] remote version ", updateStore.remoteVersion);
  497. }
  498. const usage = {
  499. used: updateStore.used,
  500. subscription: updateStore.subscription,
  501. };
  502. const [loadingUsage, setLoadingUsage] = useState(false);
  503. function checkUsage(force = false) {
  504. if (accessStore.hideBalanceQuery) {
  505. return;
  506. }
  507. setLoadingUsage(true);
  508. updateStore.updateUsage(force).finally(() => {
  509. setLoadingUsage(false);
  510. });
  511. }
  512. const accessStore = useAccessStore();
  513. const enabledAccessControl = useMemo(
  514. () => accessStore.enabledAccessControl(),
  515. // eslint-disable-next-line react-hooks/exhaustive-deps
  516. [],
  517. );
  518. const promptStore = usePromptStore();
  519. const builtinCount = SearchService.count.builtin;
  520. const customCount = promptStore.getUserPrompts().length ?? 0;
  521. const [shouldShowPromptModal, setShowPromptModal] = useState(false);
  522. const showUsage = accessStore.isAuthorized();
  523. useEffect(() => {
  524. // checks per minutes
  525. checkUpdate();
  526. showUsage && checkUsage();
  527. // eslint-disable-next-line react-hooks/exhaustive-deps
  528. }, []);
  529. useEffect(() => {
  530. const keydownEvent = (e: KeyboardEvent) => {
  531. if (e.key === "Escape") {
  532. navigate(Path.Home);
  533. }
  534. };
  535. document.addEventListener("keydown", keydownEvent);
  536. return () => {
  537. document.removeEventListener("keydown", keydownEvent);
  538. };
  539. // eslint-disable-next-line react-hooks/exhaustive-deps
  540. }, []);
  541. const clientConfig = useMemo(() => getClientConfig(), []);
  542. const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
  543. return (
  544. <ErrorBoundary>
  545. <div className="window-header" data-tauri-drag-region>
  546. <div className="window-header-title">
  547. <div className="window-header-main-title">
  548. {Locale.Settings.Title}
  549. </div>
  550. <div className="window-header-sub-title">
  551. {Locale.Settings.SubTitle}
  552. </div>
  553. </div>
  554. <div className="window-actions">
  555. <div className="window-action-button"></div>
  556. <div className="window-action-button"></div>
  557. <div className="window-action-button">
  558. <IconButton
  559. icon={<CloseIcon />}
  560. onClick={() => navigate(Path.Home)}
  561. bordered
  562. />
  563. </div>
  564. </div>
  565. </div>
  566. <div className={styles["settings"]}>
  567. <List>
  568. <ListItem title={Locale.Settings.Avatar}>
  569. <Popover
  570. onClose={() => setShowEmojiPicker(false)}
  571. content={
  572. <AvatarPicker
  573. onEmojiClick={(avatar: string) => {
  574. updateConfig((config) => (config.avatar = avatar));
  575. setShowEmojiPicker(false);
  576. }}
  577. />
  578. }
  579. open={showEmojiPicker}
  580. >
  581. <div
  582. className={styles.avatar}
  583. onClick={() => setShowEmojiPicker(true)}
  584. >
  585. <Avatar avatar={config.avatar} />
  586. </div>
  587. </Popover>
  588. </ListItem>
  589. <ListItem
  590. title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
  591. subTitle={
  592. checkingUpdate
  593. ? Locale.Settings.Update.IsChecking
  594. : hasNewVersion
  595. ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
  596. : Locale.Settings.Update.IsLatest
  597. }
  598. >
  599. {checkingUpdate ? (
  600. <LoadingIcon />
  601. ) : hasNewVersion ? (
  602. <Link href={updateUrl} target="_blank" className="link">
  603. {Locale.Settings.Update.GoToUpdate}
  604. </Link>
  605. ) : (
  606. <IconButton
  607. icon={<ResetIcon></ResetIcon>}
  608. text={Locale.Settings.Update.CheckUpdate}
  609. onClick={() => checkUpdate(true)}
  610. />
  611. )}
  612. </ListItem>
  613. <ListItem title={Locale.Settings.SendKey}>
  614. <Select
  615. value={config.submitKey}
  616. onChange={(e) => {
  617. updateConfig(
  618. (config) =>
  619. (config.submitKey = e.target.value as any as SubmitKey),
  620. );
  621. }}
  622. >
  623. {Object.values(SubmitKey).map((v) => (
  624. <option value={v} key={v}>
  625. {v}
  626. </option>
  627. ))}
  628. </Select>
  629. </ListItem>
  630. <ListItem title={Locale.Settings.Theme}>
  631. <Select
  632. value={config.theme}
  633. onChange={(e) => {
  634. updateConfig(
  635. (config) => (config.theme = e.target.value as any as Theme),
  636. );
  637. }}
  638. >
  639. {Object.values(Theme).map((v) => (
  640. <option value={v} key={v}>
  641. {v}
  642. </option>
  643. ))}
  644. </Select>
  645. </ListItem>
  646. <ListItem title={Locale.Settings.Lang.Name}>
  647. <Select
  648. value={getLang()}
  649. onChange={(e) => {
  650. changeLang(e.target.value as any);
  651. }}
  652. >
  653. {AllLangs.map((lang) => (
  654. <option value={lang} key={lang}>
  655. {ALL_LANG_OPTIONS[lang]}
  656. </option>
  657. ))}
  658. </Select>
  659. </ListItem>
  660. <ListItem
  661. title={Locale.Settings.FontSize.Title}
  662. subTitle={Locale.Settings.FontSize.SubTitle}
  663. >
  664. <InputRange
  665. title={`${config.fontSize ?? 14}px`}
  666. value={config.fontSize}
  667. min="12"
  668. max="18"
  669. step="1"
  670. onChange={(e) =>
  671. updateConfig(
  672. (config) =>
  673. (config.fontSize = Number.parseInt(e.currentTarget.value)),
  674. )
  675. }
  676. ></InputRange>
  677. </ListItem>
  678. <ListItem
  679. title={Locale.Settings.AutoGenerateTitle.Title}
  680. subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
  681. >
  682. <input
  683. type="checkbox"
  684. checked={config.enableAutoGenerateTitle}
  685. onChange={(e) =>
  686. updateConfig(
  687. (config) =>
  688. (config.enableAutoGenerateTitle = e.currentTarget.checked),
  689. )
  690. }
  691. ></input>
  692. </ListItem>
  693. <ListItem
  694. title={Locale.Settings.SendPreviewBubble.Title}
  695. subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
  696. >
  697. <input
  698. type="checkbox"
  699. checked={config.sendPreviewBubble}
  700. onChange={(e) =>
  701. updateConfig(
  702. (config) =>
  703. (config.sendPreviewBubble = e.currentTarget.checked),
  704. )
  705. }
  706. ></input>
  707. </ListItem>
  708. </List>
  709. <SyncItems />
  710. <List>
  711. <ListItem
  712. title={Locale.Settings.Mask.Splash.Title}
  713. subTitle={Locale.Settings.Mask.Splash.SubTitle}
  714. >
  715. <input
  716. type="checkbox"
  717. checked={!config.dontShowMaskSplashScreen}
  718. onChange={(e) =>
  719. updateConfig(
  720. (config) =>
  721. (config.dontShowMaskSplashScreen =
  722. !e.currentTarget.checked),
  723. )
  724. }
  725. ></input>
  726. </ListItem>
  727. <ListItem
  728. title={Locale.Settings.Mask.Builtin.Title}
  729. subTitle={Locale.Settings.Mask.Builtin.SubTitle}
  730. >
  731. <input
  732. type="checkbox"
  733. checked={config.hideBuiltinMasks}
  734. onChange={(e) =>
  735. updateConfig(
  736. (config) =>
  737. (config.hideBuiltinMasks = e.currentTarget.checked),
  738. )
  739. }
  740. ></input>
  741. </ListItem>
  742. </List>
  743. <List>
  744. <ListItem
  745. title={Locale.Settings.Prompt.Disable.Title}
  746. subTitle={Locale.Settings.Prompt.Disable.SubTitle}
  747. >
  748. <input
  749. type="checkbox"
  750. checked={config.disablePromptHint}
  751. onChange={(e) =>
  752. updateConfig(
  753. (config) =>
  754. (config.disablePromptHint = e.currentTarget.checked),
  755. )
  756. }
  757. ></input>
  758. </ListItem>
  759. <ListItem
  760. title={Locale.Settings.Prompt.List}
  761. subTitle={Locale.Settings.Prompt.ListCount(
  762. builtinCount,
  763. customCount,
  764. )}
  765. >
  766. <IconButton
  767. icon={<EditIcon />}
  768. text={Locale.Settings.Prompt.Edit}
  769. onClick={() => setShowPromptModal(true)}
  770. />
  771. </ListItem>
  772. </List>
  773. <List>
  774. {showAccessCode ? (
  775. <ListItem
  776. title={Locale.Settings.AccessCode.Title}
  777. subTitle={Locale.Settings.AccessCode.SubTitle}
  778. >
  779. <PasswordInput
  780. value={accessStore.accessCode}
  781. type="text"
  782. placeholder={Locale.Settings.AccessCode.Placeholder}
  783. onChange={(e) => {
  784. accessStore.updateCode(e.currentTarget.value);
  785. }}
  786. />
  787. </ListItem>
  788. ) : (
  789. <></>
  790. )}
  791. {!accessStore.hideUserApiKey ? (
  792. <>
  793. <ListItem
  794. title={Locale.Settings.Endpoint.Title}
  795. subTitle={Locale.Settings.Endpoint.SubTitle}
  796. >
  797. <input
  798. type="text"
  799. value={accessStore.openaiUrl}
  800. placeholder="https://api.openai.com/"
  801. onChange={(e) =>
  802. accessStore.updateOpenAiUrl(e.currentTarget.value)
  803. }
  804. ></input>
  805. </ListItem>
  806. <ListItem
  807. title={Locale.Settings.Token.Title}
  808. subTitle={Locale.Settings.Token.SubTitle}
  809. >
  810. <PasswordInput
  811. value={accessStore.token}
  812. type="text"
  813. placeholder={Locale.Settings.Token.Placeholder}
  814. onChange={(e) => {
  815. accessStore.updateToken(e.currentTarget.value);
  816. }}
  817. />
  818. </ListItem>
  819. </>
  820. ) : null}
  821. {!accessStore.hideBalanceQuery ? (
  822. <ListItem
  823. title={Locale.Settings.Usage.Title}
  824. subTitle={
  825. showUsage
  826. ? loadingUsage
  827. ? Locale.Settings.Usage.IsChecking
  828. : Locale.Settings.Usage.SubTitle(
  829. usage?.used ?? "[?]",
  830. usage?.subscription ?? "[?]",
  831. )
  832. : Locale.Settings.Usage.NoAccess
  833. }
  834. >
  835. {!showUsage || loadingUsage ? (
  836. <div />
  837. ) : (
  838. <IconButton
  839. icon={<ResetIcon></ResetIcon>}
  840. text={Locale.Settings.Usage.Check}
  841. onClick={() => checkUsage(true)}
  842. />
  843. )}
  844. </ListItem>
  845. ) : null}
  846. <ListItem
  847. title={Locale.Settings.CustomModel.Title}
  848. subTitle={Locale.Settings.CustomModel.SubTitle}
  849. >
  850. <input
  851. type="text"
  852. value={config.customModels}
  853. placeholder="model1,model2,model3"
  854. onChange={(e) =>
  855. config.update(
  856. (config) => (config.customModels = e.currentTarget.value),
  857. )
  858. }
  859. ></input>
  860. </ListItem>
  861. </List>
  862. <List>
  863. <ModelConfigList
  864. modelConfig={config.modelConfig}
  865. updateConfig={(updater) => {
  866. const modelConfig = { ...config.modelConfig };
  867. updater(modelConfig);
  868. config.update((config) => (config.modelConfig = modelConfig));
  869. }}
  870. />
  871. </List>
  872. {shouldShowPromptModal && (
  873. <UserPromptModal onClose={() => setShowPromptModal(false)} />
  874. )}
  875. <DangerItems />
  876. </div>
  877. </ErrorBoundary>
  878. );
  879. }