settings.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  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, STORAGE_KEY, 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={Locale.Settings.Sync.Config.Modal.Check}
  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.Settings.Sync.Config.UpStash.Endpoint}>
  388. <input
  389. type="text"
  390. value={syncStore.upstash.endpoint}
  391. onChange={(e) => {
  392. syncStore.update(
  393. (config) =>
  394. (config.upstash.endpoint = e.currentTarget.value),
  395. );
  396. }}
  397. ></input>
  398. </ListItem>
  399. <ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
  400. <input
  401. type="text"
  402. value={syncStore.upstash.username}
  403. placeholder={STORAGE_KEY}
  404. onChange={(e) => {
  405. syncStore.update(
  406. (config) =>
  407. (config.upstash.username = e.currentTarget.value),
  408. );
  409. }}
  410. ></input>
  411. </ListItem>
  412. <ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
  413. <PasswordInput
  414. value={syncStore.upstash.apiKey}
  415. onChange={(e) => {
  416. syncStore.update(
  417. (config) => (config.upstash.apiKey = e.currentTarget.value),
  418. );
  419. }}
  420. ></PasswordInput>
  421. </ListItem>
  422. </List>
  423. )}
  424. </Modal>
  425. </div>
  426. );
  427. }
  428. function SyncItems() {
  429. const syncStore = useSyncStore();
  430. const chatStore = useChatStore();
  431. const promptStore = usePromptStore();
  432. const maskStore = useMaskStore();
  433. const couldSync = useMemo(() => {
  434. return syncStore.coundSync();
  435. }, [syncStore]);
  436. const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
  437. const stateOverview = useMemo(() => {
  438. const sessions = chatStore.sessions;
  439. const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
  440. return {
  441. chat: sessions.length,
  442. message: messageCount,
  443. prompt: Object.keys(promptStore.prompts).length,
  444. mask: Object.keys(maskStore.masks).length,
  445. };
  446. }, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
  447. return (
  448. <>
  449. <List>
  450. <ListItem
  451. title={Locale.Settings.Sync.CloudState}
  452. subTitle={
  453. syncStore.lastProvider
  454. ? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
  455. syncStore.lastProvider
  456. }]`
  457. : Locale.Settings.Sync.NotSyncYet
  458. }
  459. >
  460. <div style={{ display: "flex" }}>
  461. <IconButton
  462. icon={<ConfigIcon />}
  463. text={Locale.UI.Config}
  464. onClick={() => {
  465. setShowSyncConfigModal(true);
  466. }}
  467. />
  468. {couldSync && (
  469. <IconButton
  470. icon={<ResetIcon />}
  471. text={Locale.UI.Sync}
  472. onClick={async () => {
  473. try {
  474. await syncStore.sync();
  475. showToast(Locale.Settings.Sync.Success);
  476. } catch (e) {
  477. showToast(Locale.Settings.Sync.Fail);
  478. console.error("[Sync]", e);
  479. }
  480. }}
  481. />
  482. )}
  483. </div>
  484. </ListItem>
  485. <ListItem
  486. title={Locale.Settings.Sync.LocalState}
  487. subTitle={Locale.Settings.Sync.Overview(stateOverview)}
  488. >
  489. <div style={{ display: "flex" }}>
  490. <IconButton
  491. icon={<UploadIcon />}
  492. text={Locale.UI.Export}
  493. onClick={() => {
  494. syncStore.export();
  495. }}
  496. />
  497. <IconButton
  498. icon={<DownloadIcon />}
  499. text={Locale.UI.Import}
  500. onClick={() => {
  501. syncStore.import();
  502. }}
  503. />
  504. </div>
  505. </ListItem>
  506. </List>
  507. {showSyncConfigModal && (
  508. <SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
  509. )}
  510. </>
  511. );
  512. }
  513. export function Settings() {
  514. const navigate = useNavigate();
  515. const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  516. const config = useAppConfig();
  517. const updateConfig = config.update;
  518. const updateStore = useUpdateStore();
  519. const [checkingUpdate, setCheckingUpdate] = useState(false);
  520. const currentVersion = updateStore.formatVersion(updateStore.version);
  521. const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
  522. const hasNewVersion = currentVersion !== remoteId;
  523. const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
  524. function checkUpdate(force = false) {
  525. setCheckingUpdate(true);
  526. updateStore.getLatestVersion(force).then(() => {
  527. setCheckingUpdate(false);
  528. });
  529. console.log("[Update] local version ", updateStore.version);
  530. console.log("[Update] remote version ", updateStore.remoteVersion);
  531. }
  532. const usage = {
  533. used: updateStore.used,
  534. subscription: updateStore.subscription,
  535. };
  536. const [loadingUsage, setLoadingUsage] = useState(false);
  537. function checkUsage(force = false) {
  538. if (accessStore.hideBalanceQuery) {
  539. return;
  540. }
  541. setLoadingUsage(true);
  542. updateStore.updateUsage(force).finally(() => {
  543. setLoadingUsage(false);
  544. });
  545. }
  546. const accessStore = useAccessStore();
  547. const enabledAccessControl = useMemo(
  548. () => accessStore.enabledAccessControl(),
  549. // eslint-disable-next-line react-hooks/exhaustive-deps
  550. [],
  551. );
  552. const promptStore = usePromptStore();
  553. const builtinCount = SearchService.count.builtin;
  554. const customCount = promptStore.getUserPrompts().length ?? 0;
  555. const [shouldShowPromptModal, setShowPromptModal] = useState(false);
  556. const showUsage = accessStore.isAuthorized();
  557. useEffect(() => {
  558. // checks per minutes
  559. checkUpdate();
  560. showUsage && checkUsage();
  561. // eslint-disable-next-line react-hooks/exhaustive-deps
  562. }, []);
  563. useEffect(() => {
  564. const keydownEvent = (e: KeyboardEvent) => {
  565. if (e.key === "Escape") {
  566. navigate(Path.Home);
  567. }
  568. };
  569. document.addEventListener("keydown", keydownEvent);
  570. return () => {
  571. document.removeEventListener("keydown", keydownEvent);
  572. };
  573. // eslint-disable-next-line react-hooks/exhaustive-deps
  574. }, []);
  575. const clientConfig = useMemo(() => getClientConfig(), []);
  576. const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
  577. return (
  578. <ErrorBoundary>
  579. <div className="window-header" data-tauri-drag-region>
  580. <div className="window-header-title">
  581. <div className="window-header-main-title">
  582. {Locale.Settings.Title}
  583. </div>
  584. <div className="window-header-sub-title">
  585. {Locale.Settings.SubTitle}
  586. </div>
  587. </div>
  588. <div className="window-actions">
  589. <div className="window-action-button"></div>
  590. <div className="window-action-button"></div>
  591. <div className="window-action-button">
  592. <IconButton
  593. icon={<CloseIcon />}
  594. onClick={() => navigate(Path.Home)}
  595. bordered
  596. />
  597. </div>
  598. </div>
  599. </div>
  600. <div className={styles["settings"]}>
  601. <List>
  602. <ListItem title={Locale.Settings.Avatar}>
  603. <Popover
  604. onClose={() => setShowEmojiPicker(false)}
  605. content={
  606. <AvatarPicker
  607. onEmojiClick={(avatar: string) => {
  608. updateConfig((config) => (config.avatar = avatar));
  609. setShowEmojiPicker(false);
  610. }}
  611. />
  612. }
  613. open={showEmojiPicker}
  614. >
  615. <div
  616. className={styles.avatar}
  617. onClick={() => setShowEmojiPicker(true)}
  618. >
  619. <Avatar avatar={config.avatar} />
  620. </div>
  621. </Popover>
  622. </ListItem>
  623. <ListItem
  624. title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
  625. subTitle={
  626. checkingUpdate
  627. ? Locale.Settings.Update.IsChecking
  628. : hasNewVersion
  629. ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
  630. : Locale.Settings.Update.IsLatest
  631. }
  632. >
  633. {checkingUpdate ? (
  634. <LoadingIcon />
  635. ) : hasNewVersion ? (
  636. <Link href={updateUrl} target="_blank" className="link">
  637. {Locale.Settings.Update.GoToUpdate}
  638. </Link>
  639. ) : (
  640. <IconButton
  641. icon={<ResetIcon></ResetIcon>}
  642. text={Locale.Settings.Update.CheckUpdate}
  643. onClick={() => checkUpdate(true)}
  644. />
  645. )}
  646. </ListItem>
  647. <ListItem title={Locale.Settings.SendKey}>
  648. <Select
  649. value={config.submitKey}
  650. onChange={(e) => {
  651. updateConfig(
  652. (config) =>
  653. (config.submitKey = e.target.value as any as SubmitKey),
  654. );
  655. }}
  656. >
  657. {Object.values(SubmitKey).map((v) => (
  658. <option value={v} key={v}>
  659. {v}
  660. </option>
  661. ))}
  662. </Select>
  663. </ListItem>
  664. <ListItem title={Locale.Settings.Theme}>
  665. <Select
  666. value={config.theme}
  667. onChange={(e) => {
  668. updateConfig(
  669. (config) => (config.theme = e.target.value as any as Theme),
  670. );
  671. }}
  672. >
  673. {Object.values(Theme).map((v) => (
  674. <option value={v} key={v}>
  675. {v}
  676. </option>
  677. ))}
  678. </Select>
  679. </ListItem>
  680. <ListItem title={Locale.Settings.Lang.Name}>
  681. <Select
  682. value={getLang()}
  683. onChange={(e) => {
  684. changeLang(e.target.value as any);
  685. }}
  686. >
  687. {AllLangs.map((lang) => (
  688. <option value={lang} key={lang}>
  689. {ALL_LANG_OPTIONS[lang]}
  690. </option>
  691. ))}
  692. </Select>
  693. </ListItem>
  694. <ListItem
  695. title={Locale.Settings.FontSize.Title}
  696. subTitle={Locale.Settings.FontSize.SubTitle}
  697. >
  698. <InputRange
  699. title={`${config.fontSize ?? 14}px`}
  700. value={config.fontSize}
  701. min="12"
  702. max="40"
  703. step="1"
  704. onChange={(e) =>
  705. updateConfig(
  706. (config) =>
  707. (config.fontSize = Number.parseInt(e.currentTarget.value)),
  708. )
  709. }
  710. ></InputRange>
  711. </ListItem>
  712. <ListItem
  713. title={Locale.Settings.AutoGenerateTitle.Title}
  714. subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
  715. >
  716. <input
  717. type="checkbox"
  718. checked={config.enableAutoGenerateTitle}
  719. onChange={(e) =>
  720. updateConfig(
  721. (config) =>
  722. (config.enableAutoGenerateTitle = e.currentTarget.checked),
  723. )
  724. }
  725. ></input>
  726. </ListItem>
  727. <ListItem
  728. title={Locale.Settings.SendPreviewBubble.Title}
  729. subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
  730. >
  731. <input
  732. type="checkbox"
  733. checked={config.sendPreviewBubble}
  734. onChange={(e) =>
  735. updateConfig(
  736. (config) =>
  737. (config.sendPreviewBubble = e.currentTarget.checked),
  738. )
  739. }
  740. ></input>
  741. </ListItem>
  742. </List>
  743. <SyncItems />
  744. <List>
  745. <ListItem
  746. title={Locale.Settings.Mask.Splash.Title}
  747. subTitle={Locale.Settings.Mask.Splash.SubTitle}
  748. >
  749. <input
  750. type="checkbox"
  751. checked={!config.dontShowMaskSplashScreen}
  752. onChange={(e) =>
  753. updateConfig(
  754. (config) =>
  755. (config.dontShowMaskSplashScreen =
  756. !e.currentTarget.checked),
  757. )
  758. }
  759. ></input>
  760. </ListItem>
  761. <ListItem
  762. title={Locale.Settings.Mask.Builtin.Title}
  763. subTitle={Locale.Settings.Mask.Builtin.SubTitle}
  764. >
  765. <input
  766. type="checkbox"
  767. checked={config.hideBuiltinMasks}
  768. onChange={(e) =>
  769. updateConfig(
  770. (config) =>
  771. (config.hideBuiltinMasks = e.currentTarget.checked),
  772. )
  773. }
  774. ></input>
  775. </ListItem>
  776. </List>
  777. <List>
  778. <ListItem
  779. title={Locale.Settings.Prompt.Disable.Title}
  780. subTitle={Locale.Settings.Prompt.Disable.SubTitle}
  781. >
  782. <input
  783. type="checkbox"
  784. checked={config.disablePromptHint}
  785. onChange={(e) =>
  786. updateConfig(
  787. (config) =>
  788. (config.disablePromptHint = e.currentTarget.checked),
  789. )
  790. }
  791. ></input>
  792. </ListItem>
  793. <ListItem
  794. title={Locale.Settings.Prompt.List}
  795. subTitle={Locale.Settings.Prompt.ListCount(
  796. builtinCount,
  797. customCount,
  798. )}
  799. >
  800. <IconButton
  801. icon={<EditIcon />}
  802. text={Locale.Settings.Prompt.Edit}
  803. onClick={() => setShowPromptModal(true)}
  804. />
  805. </ListItem>
  806. </List>
  807. <List>
  808. {showAccessCode ? (
  809. <ListItem
  810. title={Locale.Settings.AccessCode.Title}
  811. subTitle={Locale.Settings.AccessCode.SubTitle}
  812. >
  813. <PasswordInput
  814. value={accessStore.accessCode}
  815. type="text"
  816. placeholder={Locale.Settings.AccessCode.Placeholder}
  817. onChange={(e) => {
  818. accessStore.updateCode(e.currentTarget.value);
  819. }}
  820. />
  821. </ListItem>
  822. ) : (
  823. <></>
  824. )}
  825. {!accessStore.hideUserApiKey ? (
  826. <>
  827. <ListItem
  828. title={Locale.Settings.Endpoint.Title}
  829. subTitle={Locale.Settings.Endpoint.SubTitle}
  830. >
  831. <input
  832. type="text"
  833. value={accessStore.openaiUrl}
  834. placeholder="https://api.openai.com/"
  835. onChange={(e) =>
  836. accessStore.updateOpenAiUrl(e.currentTarget.value)
  837. }
  838. ></input>
  839. </ListItem>
  840. <ListItem
  841. title={Locale.Settings.Token.Title}
  842. subTitle={Locale.Settings.Token.SubTitle}
  843. >
  844. <PasswordInput
  845. value={accessStore.token}
  846. type="text"
  847. placeholder={Locale.Settings.Token.Placeholder}
  848. onChange={(e) => {
  849. accessStore.updateToken(e.currentTarget.value);
  850. }}
  851. />
  852. </ListItem>
  853. </>
  854. ) : null}
  855. {!accessStore.hideBalanceQuery ? (
  856. <ListItem
  857. title={Locale.Settings.Usage.Title}
  858. subTitle={
  859. showUsage
  860. ? loadingUsage
  861. ? Locale.Settings.Usage.IsChecking
  862. : Locale.Settings.Usage.SubTitle(
  863. usage?.used ?? "[?]",
  864. usage?.subscription ?? "[?]",
  865. )
  866. : Locale.Settings.Usage.NoAccess
  867. }
  868. >
  869. {!showUsage || loadingUsage ? (
  870. <div />
  871. ) : (
  872. <IconButton
  873. icon={<ResetIcon></ResetIcon>}
  874. text={Locale.Settings.Usage.Check}
  875. onClick={() => checkUsage(true)}
  876. />
  877. )}
  878. </ListItem>
  879. ) : null}
  880. <ListItem
  881. title={Locale.Settings.CustomModel.Title}
  882. subTitle={Locale.Settings.CustomModel.SubTitle}
  883. >
  884. <input
  885. type="text"
  886. value={config.customModels}
  887. placeholder="model1,model2,model3"
  888. onChange={(e) =>
  889. config.update(
  890. (config) => (config.customModels = e.currentTarget.value),
  891. )
  892. }
  893. ></input>
  894. </ListItem>
  895. </List>
  896. <List>
  897. <ModelConfigList
  898. modelConfig={config.modelConfig}
  899. updateConfig={(updater) => {
  900. const modelConfig = { ...config.modelConfig };
  901. updater(modelConfig);
  902. config.update((config) => (config.modelConfig = modelConfig));
  903. }}
  904. />
  905. </List>
  906. {shouldShowPromptModal && (
  907. <UserPromptModal onClose={() => setShowPromptModal(false)} />
  908. )}
  909. <DangerItems />
  910. </div>
  911. </ErrorBoundary>
  912. );
  913. }