settings.tsx 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  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 {
  48. Anthropic,
  49. Azure,
  50. Baidu,
  51. ByteDance,
  52. Google,
  53. OPENAI_BASE_URL,
  54. Path,
  55. RELEASE_URL,
  56. STORAGE_KEY,
  57. ServiceProvider,
  58. SlotID,
  59. UPDATE_URL,
  60. } from "../constant";
  61. import { Prompt, SearchService, usePromptStore } from "../store/prompt";
  62. import { ErrorBoundary } from "./error";
  63. import { InputRange } from "./input-range";
  64. import { useNavigate } from "react-router-dom";
  65. import { Avatar, AvatarPicker } from "./emoji";
  66. import { getClientConfig } from "../config/client";
  67. import { useSyncStore } from "../store/sync";
  68. import { nanoid } from "nanoid";
  69. import { useMaskStore } from "../store/mask";
  70. import { ProviderType } from "../utils/cloud";
  71. function EditPromptModal(props: { id: string; onClose: () => void }) {
  72. const promptStore = usePromptStore();
  73. const prompt = promptStore.get(props.id);
  74. return prompt ? (
  75. <div className="modal-mask">
  76. <Modal
  77. title={Locale.Settings.Prompt.EditModal.Title}
  78. onClose={props.onClose}
  79. actions={[
  80. <IconButton
  81. key=""
  82. onClick={props.onClose}
  83. text={Locale.UI.Confirm}
  84. bordered
  85. />,
  86. ]}
  87. >
  88. <div className={styles["edit-prompt-modal"]}>
  89. <input
  90. type="text"
  91. value={prompt.title}
  92. readOnly={!prompt.isUser}
  93. className={styles["edit-prompt-title"]}
  94. onInput={(e) =>
  95. promptStore.updatePrompt(
  96. props.id,
  97. (prompt) => (prompt.title = e.currentTarget.value),
  98. )
  99. }
  100. ></input>
  101. <Input
  102. value={prompt.content}
  103. readOnly={!prompt.isUser}
  104. className={styles["edit-prompt-content"]}
  105. rows={10}
  106. onInput={(e) =>
  107. promptStore.updatePrompt(
  108. props.id,
  109. (prompt) => (prompt.content = e.currentTarget.value),
  110. )
  111. }
  112. ></Input>
  113. </div>
  114. </Modal>
  115. </div>
  116. ) : null;
  117. }
  118. function UserPromptModal(props: { onClose?: () => void }) {
  119. const promptStore = usePromptStore();
  120. const userPrompts = promptStore.getUserPrompts();
  121. const builtinPrompts = SearchService.builtinPrompts;
  122. const allPrompts = userPrompts.concat(builtinPrompts);
  123. const [searchInput, setSearchInput] = useState("");
  124. const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
  125. const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
  126. const [editingPromptId, setEditingPromptId] = useState<string>();
  127. useEffect(() => {
  128. if (searchInput.length > 0) {
  129. const searchResult = SearchService.search(searchInput);
  130. setSearchPrompts(searchResult);
  131. } else {
  132. setSearchPrompts([]);
  133. }
  134. }, [searchInput]);
  135. return (
  136. <div className="modal-mask">
  137. <Modal
  138. title={Locale.Settings.Prompt.Modal.Title}
  139. onClose={() => props.onClose?.()}
  140. actions={[
  141. <IconButton
  142. key="add"
  143. onClick={() => {
  144. const promptId = promptStore.add({
  145. id: nanoid(),
  146. createdAt: Date.now(),
  147. title: "Empty Prompt",
  148. content: "Empty Prompt Content",
  149. });
  150. setEditingPromptId(promptId);
  151. }}
  152. icon={<AddIcon />}
  153. bordered
  154. text={Locale.Settings.Prompt.Modal.Add}
  155. />,
  156. ]}
  157. >
  158. <div className={styles["user-prompt-modal"]}>
  159. <input
  160. type="text"
  161. className={styles["user-prompt-search"]}
  162. placeholder={Locale.Settings.Prompt.Modal.Search}
  163. value={searchInput}
  164. onInput={(e) => setSearchInput(e.currentTarget.value)}
  165. ></input>
  166. <div className={styles["user-prompt-list"]}>
  167. {prompts.map((v, _) => (
  168. <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
  169. <div className={styles["user-prompt-header"]}>
  170. <div className={styles["user-prompt-title"]}>{v.title}</div>
  171. <div className={styles["user-prompt-content"] + " one-line"}>
  172. {v.content}
  173. </div>
  174. </div>
  175. <div className={styles["user-prompt-buttons"]}>
  176. {v.isUser && (
  177. <IconButton
  178. icon={<ClearIcon />}
  179. className={styles["user-prompt-button"]}
  180. onClick={() => promptStore.remove(v.id!)}
  181. />
  182. )}
  183. {v.isUser ? (
  184. <IconButton
  185. icon={<EditIcon />}
  186. className={styles["user-prompt-button"]}
  187. onClick={() => setEditingPromptId(v.id)}
  188. />
  189. ) : (
  190. <IconButton
  191. icon={<EyeIcon />}
  192. className={styles["user-prompt-button"]}
  193. onClick={() => setEditingPromptId(v.id)}
  194. />
  195. )}
  196. <IconButton
  197. icon={<CopyIcon />}
  198. className={styles["user-prompt-button"]}
  199. onClick={() => copyToClipboard(v.content)}
  200. />
  201. </div>
  202. </div>
  203. ))}
  204. </div>
  205. </div>
  206. </Modal>
  207. {editingPromptId !== undefined && (
  208. <EditPromptModal
  209. id={editingPromptId!}
  210. onClose={() => setEditingPromptId(undefined)}
  211. />
  212. )}
  213. </div>
  214. );
  215. }
  216. function DangerItems() {
  217. const chatStore = useChatStore();
  218. const appConfig = useAppConfig();
  219. return (
  220. <List>
  221. <ListItem
  222. title={Locale.Settings.Danger.Reset.Title}
  223. subTitle={Locale.Settings.Danger.Reset.SubTitle}
  224. >
  225. <IconButton
  226. text={Locale.Settings.Danger.Reset.Action}
  227. onClick={async () => {
  228. if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
  229. appConfig.reset();
  230. }
  231. }}
  232. type="danger"
  233. />
  234. </ListItem>
  235. <ListItem
  236. title={Locale.Settings.Danger.Clear.Title}
  237. subTitle={Locale.Settings.Danger.Clear.SubTitle}
  238. >
  239. <IconButton
  240. text={Locale.Settings.Danger.Clear.Action}
  241. onClick={async () => {
  242. if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
  243. chatStore.clearAllData();
  244. }
  245. }}
  246. type="danger"
  247. />
  248. </ListItem>
  249. </List>
  250. );
  251. }
  252. function CheckButton() {
  253. const syncStore = useSyncStore();
  254. const couldCheck = useMemo(() => {
  255. return syncStore.cloudSync();
  256. }, [syncStore]);
  257. const [checkState, setCheckState] = useState<
  258. "none" | "checking" | "success" | "failed"
  259. >("none");
  260. async function check() {
  261. setCheckState("checking");
  262. const valid = await syncStore.check();
  263. setCheckState(valid ? "success" : "failed");
  264. }
  265. if (!couldCheck) return null;
  266. return (
  267. <IconButton
  268. text={Locale.Settings.Sync.Config.Modal.Check}
  269. bordered
  270. onClick={check}
  271. icon={
  272. checkState === "none" ? (
  273. <ConnectionIcon />
  274. ) : checkState === "checking" ? (
  275. <LoadingIcon />
  276. ) : checkState === "success" ? (
  277. <CloudSuccessIcon />
  278. ) : checkState === "failed" ? (
  279. <CloudFailIcon />
  280. ) : (
  281. <ConnectionIcon />
  282. )
  283. }
  284. ></IconButton>
  285. );
  286. }
  287. function SyncConfigModal(props: { onClose?: () => void }) {
  288. const syncStore = useSyncStore();
  289. return (
  290. <div className="modal-mask">
  291. <Modal
  292. title={Locale.Settings.Sync.Config.Modal.Title}
  293. onClose={() => props.onClose?.()}
  294. actions={[
  295. <CheckButton key="check" />,
  296. <IconButton
  297. key="confirm"
  298. onClick={props.onClose}
  299. icon={<ConfirmIcon />}
  300. bordered
  301. text={Locale.UI.Confirm}
  302. />,
  303. ]}
  304. >
  305. <List>
  306. <ListItem
  307. title={Locale.Settings.Sync.Config.SyncType.Title}
  308. subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
  309. >
  310. <select
  311. value={syncStore.provider}
  312. onChange={(e) => {
  313. syncStore.update(
  314. (config) =>
  315. (config.provider = e.target.value as ProviderType),
  316. );
  317. }}
  318. >
  319. {Object.entries(ProviderType).map(([k, v]) => (
  320. <option value={v} key={k}>
  321. {k}
  322. </option>
  323. ))}
  324. </select>
  325. </ListItem>
  326. <ListItem
  327. title={Locale.Settings.Sync.Config.Proxy.Title}
  328. subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
  329. >
  330. <input
  331. type="checkbox"
  332. checked={syncStore.useProxy}
  333. onChange={(e) => {
  334. syncStore.update(
  335. (config) => (config.useProxy = e.currentTarget.checked),
  336. );
  337. }}
  338. ></input>
  339. </ListItem>
  340. {syncStore.useProxy ? (
  341. <ListItem
  342. title={Locale.Settings.Sync.Config.ProxyUrl.Title}
  343. subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
  344. >
  345. <input
  346. type="text"
  347. value={syncStore.proxyUrl}
  348. onChange={(e) => {
  349. syncStore.update(
  350. (config) => (config.proxyUrl = e.currentTarget.value),
  351. );
  352. }}
  353. ></input>
  354. </ListItem>
  355. ) : null}
  356. </List>
  357. {syncStore.provider === ProviderType.WebDAV && (
  358. <>
  359. <List>
  360. <ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
  361. <input
  362. type="text"
  363. value={syncStore.webdav.endpoint}
  364. onChange={(e) => {
  365. syncStore.update(
  366. (config) =>
  367. (config.webdav.endpoint = e.currentTarget.value),
  368. );
  369. }}
  370. ></input>
  371. </ListItem>
  372. <ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
  373. <input
  374. type="text"
  375. value={syncStore.webdav.username}
  376. onChange={(e) => {
  377. syncStore.update(
  378. (config) =>
  379. (config.webdav.username = e.currentTarget.value),
  380. );
  381. }}
  382. ></input>
  383. </ListItem>
  384. <ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
  385. <PasswordInput
  386. value={syncStore.webdav.password}
  387. onChange={(e) => {
  388. syncStore.update(
  389. (config) =>
  390. (config.webdav.password = e.currentTarget.value),
  391. );
  392. }}
  393. ></PasswordInput>
  394. </ListItem>
  395. </List>
  396. </>
  397. )}
  398. {syncStore.provider === ProviderType.UpStash && (
  399. <List>
  400. <ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
  401. <input
  402. type="text"
  403. value={syncStore.upstash.endpoint}
  404. onChange={(e) => {
  405. syncStore.update(
  406. (config) =>
  407. (config.upstash.endpoint = e.currentTarget.value),
  408. );
  409. }}
  410. ></input>
  411. </ListItem>
  412. <ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
  413. <input
  414. type="text"
  415. value={syncStore.upstash.username}
  416. placeholder={STORAGE_KEY}
  417. onChange={(e) => {
  418. syncStore.update(
  419. (config) =>
  420. (config.upstash.username = e.currentTarget.value),
  421. );
  422. }}
  423. ></input>
  424. </ListItem>
  425. <ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
  426. <PasswordInput
  427. value={syncStore.upstash.apiKey}
  428. onChange={(e) => {
  429. syncStore.update(
  430. (config) => (config.upstash.apiKey = e.currentTarget.value),
  431. );
  432. }}
  433. ></PasswordInput>
  434. </ListItem>
  435. </List>
  436. )}
  437. </Modal>
  438. </div>
  439. );
  440. }
  441. function SyncItems() {
  442. const syncStore = useSyncStore();
  443. const chatStore = useChatStore();
  444. const promptStore = usePromptStore();
  445. const maskStore = useMaskStore();
  446. const couldSync = useMemo(() => {
  447. return syncStore.cloudSync();
  448. }, [syncStore]);
  449. const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
  450. const stateOverview = useMemo(() => {
  451. const sessions = chatStore.sessions;
  452. const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
  453. return {
  454. chat: sessions.length,
  455. message: messageCount,
  456. prompt: Object.keys(promptStore.prompts).length,
  457. mask: Object.keys(maskStore.masks).length,
  458. };
  459. }, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
  460. return (
  461. <>
  462. <List>
  463. <ListItem
  464. title={Locale.Settings.Sync.CloudState}
  465. subTitle={
  466. syncStore.lastProvider
  467. ? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
  468. syncStore.lastProvider
  469. }]`
  470. : Locale.Settings.Sync.NotSyncYet
  471. }
  472. >
  473. <div style={{ display: "flex" }}>
  474. <IconButton
  475. icon={<ConfigIcon />}
  476. text={Locale.UI.Config}
  477. onClick={() => {
  478. setShowSyncConfigModal(true);
  479. }}
  480. />
  481. {couldSync && (
  482. <IconButton
  483. icon={<ResetIcon />}
  484. text={Locale.UI.Sync}
  485. onClick={async () => {
  486. try {
  487. await syncStore.sync();
  488. showToast(Locale.Settings.Sync.Success);
  489. } catch (e) {
  490. showToast(Locale.Settings.Sync.Fail);
  491. console.error("[Sync]", e);
  492. }
  493. }}
  494. />
  495. )}
  496. </div>
  497. </ListItem>
  498. <ListItem
  499. title={Locale.Settings.Sync.LocalState}
  500. subTitle={Locale.Settings.Sync.Overview(stateOverview)}
  501. >
  502. <div style={{ display: "flex" }}>
  503. <IconButton
  504. icon={<UploadIcon />}
  505. text={Locale.UI.Export}
  506. onClick={() => {
  507. syncStore.export();
  508. }}
  509. />
  510. <IconButton
  511. icon={<DownloadIcon />}
  512. text={Locale.UI.Import}
  513. onClick={() => {
  514. syncStore.import();
  515. }}
  516. />
  517. </div>
  518. </ListItem>
  519. </List>
  520. {showSyncConfigModal && (
  521. <SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
  522. )}
  523. </>
  524. );
  525. }
  526. export function Settings() {
  527. const navigate = useNavigate();
  528. const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  529. const config = useAppConfig();
  530. const updateConfig = config.update;
  531. const updateStore = useUpdateStore();
  532. const [checkingUpdate, setCheckingUpdate] = useState(false);
  533. const currentVersion = updateStore.formatVersion(updateStore.version);
  534. const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
  535. const hasNewVersion = currentVersion !== remoteId;
  536. const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
  537. function checkUpdate(force = false) {
  538. setCheckingUpdate(true);
  539. updateStore.getLatestVersion(force).then(() => {
  540. setCheckingUpdate(false);
  541. });
  542. console.log("[Update] local version ", updateStore.version);
  543. console.log("[Update] remote version ", updateStore.remoteVersion);
  544. }
  545. const accessStore = useAccessStore();
  546. const shouldHideBalanceQuery = useMemo(() => {
  547. const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
  548. return (
  549. accessStore.hideBalanceQuery ||
  550. isOpenAiUrl ||
  551. accessStore.provider === ServiceProvider.Azure
  552. );
  553. }, [
  554. accessStore.hideBalanceQuery,
  555. accessStore.openaiUrl,
  556. accessStore.provider,
  557. ]);
  558. const usage = {
  559. used: updateStore.used,
  560. subscription: updateStore.subscription,
  561. };
  562. const [loadingUsage, setLoadingUsage] = useState(false);
  563. function checkUsage(force = false) {
  564. if (shouldHideBalanceQuery) {
  565. return;
  566. }
  567. setLoadingUsage(true);
  568. updateStore.updateUsage(force).finally(() => {
  569. setLoadingUsage(false);
  570. });
  571. }
  572. const enabledAccessControl = useMemo(
  573. () => accessStore.enabledAccessControl(),
  574. // eslint-disable-next-line react-hooks/exhaustive-deps
  575. [],
  576. );
  577. const promptStore = usePromptStore();
  578. const builtinCount = SearchService.count.builtin;
  579. const customCount = promptStore.getUserPrompts().length ?? 0;
  580. const [shouldShowPromptModal, setShowPromptModal] = useState(false);
  581. const showUsage = accessStore.isAuthorized();
  582. useEffect(() => {
  583. // checks per minutes
  584. checkUpdate();
  585. showUsage && checkUsage();
  586. // eslint-disable-next-line react-hooks/exhaustive-deps
  587. }, []);
  588. useEffect(() => {
  589. const keydownEvent = (e: KeyboardEvent) => {
  590. if (e.key === "Escape") {
  591. navigate(Path.Home);
  592. }
  593. };
  594. if (clientConfig?.isApp) {
  595. // Force to set custom endpoint to true if it's app
  596. accessStore.update((state) => {
  597. state.useCustomConfig = true;
  598. });
  599. }
  600. document.addEventListener("keydown", keydownEvent);
  601. return () => {
  602. document.removeEventListener("keydown", keydownEvent);
  603. };
  604. // eslint-disable-next-line react-hooks/exhaustive-deps
  605. }, []);
  606. const clientConfig = useMemo(() => getClientConfig(), []);
  607. const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
  608. return (
  609. <ErrorBoundary>
  610. <div className="window-header" data-tauri-drag-region>
  611. <div className="window-header-title">
  612. <div className="window-header-main-title">
  613. {Locale.Settings.Title}
  614. </div>
  615. <div className="window-header-sub-title">
  616. {Locale.Settings.SubTitle}
  617. </div>
  618. </div>
  619. <div className="window-actions">
  620. <div className="window-action-button"></div>
  621. <div className="window-action-button"></div>
  622. <div className="window-action-button">
  623. <IconButton
  624. icon={<CloseIcon />}
  625. onClick={() => navigate(Path.Home)}
  626. bordered
  627. />
  628. </div>
  629. </div>
  630. </div>
  631. <div className={styles["settings"]}>
  632. <List>
  633. <ListItem title={Locale.Settings.Avatar}>
  634. <Popover
  635. onClose={() => setShowEmojiPicker(false)}
  636. content={
  637. <AvatarPicker
  638. onEmojiClick={(avatar: string) => {
  639. updateConfig((config) => (config.avatar = avatar));
  640. setShowEmojiPicker(false);
  641. }}
  642. />
  643. }
  644. open={showEmojiPicker}
  645. >
  646. <div
  647. className={styles.avatar}
  648. onClick={() => {
  649. setShowEmojiPicker(!showEmojiPicker);
  650. }}
  651. >
  652. <Avatar avatar={config.avatar} />
  653. </div>
  654. </Popover>
  655. </ListItem>
  656. <ListItem
  657. title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
  658. subTitle={
  659. checkingUpdate
  660. ? Locale.Settings.Update.IsChecking
  661. : hasNewVersion
  662. ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
  663. : Locale.Settings.Update.IsLatest
  664. }
  665. >
  666. {checkingUpdate ? (
  667. <LoadingIcon />
  668. ) : hasNewVersion ? (
  669. <Link href={updateUrl} target="_blank" className="link">
  670. {Locale.Settings.Update.GoToUpdate}
  671. </Link>
  672. ) : (
  673. <IconButton
  674. icon={<ResetIcon></ResetIcon>}
  675. text={Locale.Settings.Update.CheckUpdate}
  676. onClick={() => checkUpdate(true)}
  677. />
  678. )}
  679. </ListItem>
  680. <ListItem title={Locale.Settings.SendKey}>
  681. <Select
  682. value={config.submitKey}
  683. onChange={(e) => {
  684. updateConfig(
  685. (config) =>
  686. (config.submitKey = e.target.value as any as SubmitKey),
  687. );
  688. }}
  689. >
  690. {Object.values(SubmitKey).map((v) => (
  691. <option value={v} key={v}>
  692. {v}
  693. </option>
  694. ))}
  695. </Select>
  696. </ListItem>
  697. <ListItem title={Locale.Settings.Theme}>
  698. <Select
  699. value={config.theme}
  700. onChange={(e) => {
  701. updateConfig(
  702. (config) => (config.theme = e.target.value as any as Theme),
  703. );
  704. }}
  705. >
  706. {Object.values(Theme).map((v) => (
  707. <option value={v} key={v}>
  708. {v}
  709. </option>
  710. ))}
  711. </Select>
  712. </ListItem>
  713. <ListItem title={Locale.Settings.Lang.Name}>
  714. <Select
  715. value={getLang()}
  716. onChange={(e) => {
  717. changeLang(e.target.value as any);
  718. }}
  719. >
  720. {AllLangs.map((lang) => (
  721. <option value={lang} key={lang}>
  722. {ALL_LANG_OPTIONS[lang]}
  723. </option>
  724. ))}
  725. </Select>
  726. </ListItem>
  727. <ListItem
  728. title={Locale.Settings.FontSize.Title}
  729. subTitle={Locale.Settings.FontSize.SubTitle}
  730. >
  731. <InputRange
  732. title={`${config.fontSize ?? 14}px`}
  733. value={config.fontSize}
  734. min="12"
  735. max="40"
  736. step="1"
  737. onChange={(e) =>
  738. updateConfig(
  739. (config) =>
  740. (config.fontSize = Number.parseInt(e.currentTarget.value)),
  741. )
  742. }
  743. ></InputRange>
  744. </ListItem>
  745. <ListItem
  746. title={Locale.Settings.AutoGenerateTitle.Title}
  747. subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
  748. >
  749. <input
  750. type="checkbox"
  751. checked={config.enableAutoGenerateTitle}
  752. onChange={(e) =>
  753. updateConfig(
  754. (config) =>
  755. (config.enableAutoGenerateTitle = e.currentTarget.checked),
  756. )
  757. }
  758. ></input>
  759. </ListItem>
  760. <ListItem
  761. title={Locale.Settings.SendPreviewBubble.Title}
  762. subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
  763. >
  764. <input
  765. type="checkbox"
  766. checked={config.sendPreviewBubble}
  767. onChange={(e) =>
  768. updateConfig(
  769. (config) =>
  770. (config.sendPreviewBubble = e.currentTarget.checked),
  771. )
  772. }
  773. ></input>
  774. </ListItem>
  775. </List>
  776. <SyncItems />
  777. <List>
  778. <ListItem
  779. title={Locale.Settings.Mask.Splash.Title}
  780. subTitle={Locale.Settings.Mask.Splash.SubTitle}
  781. >
  782. <input
  783. type="checkbox"
  784. checked={!config.dontShowMaskSplashScreen}
  785. onChange={(e) =>
  786. updateConfig(
  787. (config) =>
  788. (config.dontShowMaskSplashScreen =
  789. !e.currentTarget.checked),
  790. )
  791. }
  792. ></input>
  793. </ListItem>
  794. <ListItem
  795. title={Locale.Settings.Mask.Builtin.Title}
  796. subTitle={Locale.Settings.Mask.Builtin.SubTitle}
  797. >
  798. <input
  799. type="checkbox"
  800. checked={config.hideBuiltinMasks}
  801. onChange={(e) =>
  802. updateConfig(
  803. (config) =>
  804. (config.hideBuiltinMasks = e.currentTarget.checked),
  805. )
  806. }
  807. ></input>
  808. </ListItem>
  809. </List>
  810. <List>
  811. <ListItem
  812. title={Locale.Settings.Prompt.Disable.Title}
  813. subTitle={Locale.Settings.Prompt.Disable.SubTitle}
  814. >
  815. <input
  816. type="checkbox"
  817. checked={config.disablePromptHint}
  818. onChange={(e) =>
  819. updateConfig(
  820. (config) =>
  821. (config.disablePromptHint = e.currentTarget.checked),
  822. )
  823. }
  824. ></input>
  825. </ListItem>
  826. <ListItem
  827. title={Locale.Settings.Prompt.List}
  828. subTitle={Locale.Settings.Prompt.ListCount(
  829. builtinCount,
  830. customCount,
  831. )}
  832. >
  833. <IconButton
  834. icon={<EditIcon />}
  835. text={Locale.Settings.Prompt.Edit}
  836. onClick={() => setShowPromptModal(true)}
  837. />
  838. </ListItem>
  839. </List>
  840. <List id={SlotID.CustomModel}>
  841. {showAccessCode && (
  842. <ListItem
  843. title={Locale.Settings.Access.AccessCode.Title}
  844. subTitle={Locale.Settings.Access.AccessCode.SubTitle}
  845. >
  846. <PasswordInput
  847. value={accessStore.accessCode}
  848. type="text"
  849. placeholder={Locale.Settings.Access.AccessCode.Placeholder}
  850. onChange={(e) => {
  851. accessStore.update(
  852. (access) => (access.accessCode = e.currentTarget.value),
  853. );
  854. }}
  855. />
  856. </ListItem>
  857. )}
  858. {!accessStore.hideUserApiKey && (
  859. <>
  860. {
  861. // Conditionally render the following ListItem based on clientConfig.isApp
  862. !clientConfig?.isApp && ( // only show if isApp is false
  863. <ListItem
  864. title={Locale.Settings.Access.CustomEndpoint.Title}
  865. subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
  866. >
  867. <input
  868. type="checkbox"
  869. checked={accessStore.useCustomConfig}
  870. onChange={(e) =>
  871. accessStore.update(
  872. (access) =>
  873. (access.useCustomConfig = e.currentTarget.checked),
  874. )
  875. }
  876. ></input>
  877. </ListItem>
  878. )
  879. }
  880. {accessStore.useCustomConfig && (
  881. <>
  882. <ListItem
  883. title={Locale.Settings.Access.Provider.Title}
  884. subTitle={Locale.Settings.Access.Provider.SubTitle}
  885. >
  886. <Select
  887. value={accessStore.provider}
  888. onChange={(e) => {
  889. accessStore.update(
  890. (access) =>
  891. (access.provider = e.target
  892. .value as ServiceProvider),
  893. );
  894. }}
  895. >
  896. {Object.entries(ServiceProvider).map(([k, v]) => (
  897. <option value={v} key={k}>
  898. {k}
  899. </option>
  900. ))}
  901. </Select>
  902. </ListItem>
  903. {accessStore.provider === ServiceProvider.OpenAI && (
  904. <>
  905. <ListItem
  906. title={Locale.Settings.Access.OpenAI.Endpoint.Title}
  907. subTitle={
  908. Locale.Settings.Access.OpenAI.Endpoint.SubTitle
  909. }
  910. >
  911. <input
  912. type="text"
  913. value={accessStore.openaiUrl}
  914. placeholder={OPENAI_BASE_URL}
  915. onChange={(e) =>
  916. accessStore.update(
  917. (access) =>
  918. (access.openaiUrl = e.currentTarget.value),
  919. )
  920. }
  921. ></input>
  922. </ListItem>
  923. <ListItem
  924. title={Locale.Settings.Access.OpenAI.ApiKey.Title}
  925. subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
  926. >
  927. <PasswordInput
  928. value={accessStore.openaiApiKey}
  929. type="text"
  930. placeholder={
  931. Locale.Settings.Access.OpenAI.ApiKey.Placeholder
  932. }
  933. onChange={(e) => {
  934. accessStore.update(
  935. (access) =>
  936. (access.openaiApiKey = e.currentTarget.value),
  937. );
  938. }}
  939. />
  940. </ListItem>
  941. </>
  942. )}
  943. {accessStore.provider === ServiceProvider.Azure && (
  944. <>
  945. <ListItem
  946. title={Locale.Settings.Access.Azure.Endpoint.Title}
  947. subTitle={
  948. Locale.Settings.Access.Azure.Endpoint.SubTitle +
  949. Azure.ExampleEndpoint
  950. }
  951. >
  952. <input
  953. type="text"
  954. value={accessStore.azureUrl}
  955. placeholder={Azure.ExampleEndpoint}
  956. onChange={(e) =>
  957. accessStore.update(
  958. (access) =>
  959. (access.azureUrl = e.currentTarget.value),
  960. )
  961. }
  962. ></input>
  963. </ListItem>
  964. <ListItem
  965. title={Locale.Settings.Access.Azure.ApiKey.Title}
  966. subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
  967. >
  968. <PasswordInput
  969. value={accessStore.azureApiKey}
  970. type="text"
  971. placeholder={
  972. Locale.Settings.Access.Azure.ApiKey.Placeholder
  973. }
  974. onChange={(e) => {
  975. accessStore.update(
  976. (access) =>
  977. (access.azureApiKey = e.currentTarget.value),
  978. );
  979. }}
  980. />
  981. </ListItem>
  982. <ListItem
  983. title={Locale.Settings.Access.Azure.ApiVerion.Title}
  984. subTitle={
  985. Locale.Settings.Access.Azure.ApiVerion.SubTitle
  986. }
  987. >
  988. <input
  989. type="text"
  990. value={accessStore.azureApiVersion}
  991. placeholder="2023-08-01-preview"
  992. onChange={(e) =>
  993. accessStore.update(
  994. (access) =>
  995. (access.azureApiVersion =
  996. e.currentTarget.value),
  997. )
  998. }
  999. ></input>
  1000. </ListItem>
  1001. </>
  1002. )}
  1003. {accessStore.provider === ServiceProvider.Google && (
  1004. <>
  1005. <ListItem
  1006. title={Locale.Settings.Access.Google.Endpoint.Title}
  1007. subTitle={
  1008. Locale.Settings.Access.Google.Endpoint.SubTitle +
  1009. Google.ExampleEndpoint
  1010. }
  1011. >
  1012. <input
  1013. type="text"
  1014. value={accessStore.googleUrl}
  1015. placeholder={Google.ExampleEndpoint}
  1016. onChange={(e) =>
  1017. accessStore.update(
  1018. (access) =>
  1019. (access.googleUrl = e.currentTarget.value),
  1020. )
  1021. }
  1022. ></input>
  1023. </ListItem>
  1024. <ListItem
  1025. title={Locale.Settings.Access.Google.ApiKey.Title}
  1026. subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
  1027. >
  1028. <PasswordInput
  1029. value={accessStore.googleApiKey}
  1030. type="text"
  1031. placeholder={
  1032. Locale.Settings.Access.Google.ApiKey.Placeholder
  1033. }
  1034. onChange={(e) => {
  1035. accessStore.update(
  1036. (access) =>
  1037. (access.googleApiKey = e.currentTarget.value),
  1038. );
  1039. }}
  1040. />
  1041. </ListItem>
  1042. <ListItem
  1043. title={Locale.Settings.Access.Google.ApiVersion.Title}
  1044. subTitle={
  1045. Locale.Settings.Access.Google.ApiVersion.SubTitle
  1046. }
  1047. >
  1048. <input
  1049. type="text"
  1050. value={accessStore.googleApiVersion}
  1051. placeholder="2023-08-01-preview"
  1052. onChange={(e) =>
  1053. accessStore.update(
  1054. (access) =>
  1055. (access.googleApiVersion =
  1056. e.currentTarget.value),
  1057. )
  1058. }
  1059. ></input>
  1060. </ListItem>
  1061. </>
  1062. )}
  1063. {accessStore.provider === ServiceProvider.Anthropic && (
  1064. <>
  1065. <ListItem
  1066. title={Locale.Settings.Access.Anthropic.Endpoint.Title}
  1067. subTitle={
  1068. Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
  1069. Anthropic.ExampleEndpoint
  1070. }
  1071. >
  1072. <input
  1073. type="text"
  1074. value={accessStore.anthropicUrl}
  1075. placeholder={Anthropic.ExampleEndpoint}
  1076. onChange={(e) =>
  1077. accessStore.update(
  1078. (access) =>
  1079. (access.anthropicUrl = e.currentTarget.value),
  1080. )
  1081. }
  1082. ></input>
  1083. </ListItem>
  1084. <ListItem
  1085. title={Locale.Settings.Access.Anthropic.ApiKey.Title}
  1086. subTitle={
  1087. Locale.Settings.Access.Anthropic.ApiKey.SubTitle
  1088. }
  1089. >
  1090. <PasswordInput
  1091. value={accessStore.anthropicApiKey}
  1092. type="text"
  1093. placeholder={
  1094. Locale.Settings.Access.Anthropic.ApiKey.Placeholder
  1095. }
  1096. onChange={(e) => {
  1097. accessStore.update(
  1098. (access) =>
  1099. (access.anthropicApiKey =
  1100. e.currentTarget.value),
  1101. );
  1102. }}
  1103. />
  1104. </ListItem>
  1105. <ListItem
  1106. title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
  1107. subTitle={
  1108. Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
  1109. }
  1110. >
  1111. <input
  1112. type="text"
  1113. value={accessStore.anthropicApiVersion}
  1114. placeholder={Anthropic.Vision}
  1115. onChange={(e) =>
  1116. accessStore.update(
  1117. (access) =>
  1118. (access.anthropicApiVersion =
  1119. e.currentTarget.value),
  1120. )
  1121. }
  1122. ></input>
  1123. </ListItem>
  1124. </>
  1125. )}
  1126. {accessStore.provider === ServiceProvider.Baidu && (
  1127. <>
  1128. <ListItem
  1129. title={Locale.Settings.Access.Baidu.Endpoint.Title}
  1130. subTitle={
  1131. Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
  1132. Baidu.ExampleEndpoint
  1133. }
  1134. >
  1135. <input
  1136. type="text"
  1137. value={accessStore.baiduUrl}
  1138. placeholder={Baidu.ExampleEndpoint}
  1139. onChange={(e) =>
  1140. accessStore.update(
  1141. (access) =>
  1142. (access.baiduUrl = e.currentTarget.value),
  1143. )
  1144. }
  1145. ></input>
  1146. </ListItem>
  1147. <ListItem
  1148. title={Locale.Settings.Access.Baidu.ApiKey.Title}
  1149. subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle}
  1150. >
  1151. <PasswordInput
  1152. value={accessStore.baiduApiKey}
  1153. type="text"
  1154. placeholder={
  1155. Locale.Settings.Access.Baidu.ApiKey.Placeholder
  1156. }
  1157. onChange={(e) => {
  1158. accessStore.update(
  1159. (access) =>
  1160. (access.baiduApiKey = e.currentTarget.value),
  1161. );
  1162. }}
  1163. />
  1164. </ListItem>
  1165. <ListItem
  1166. title={Locale.Settings.Access.Baidu.SecretKey.Title}
  1167. subTitle={
  1168. Locale.Settings.Access.Baidu.SecretKey.SubTitle
  1169. }
  1170. >
  1171. <PasswordInput
  1172. value={accessStore.baiduSecretKey}
  1173. type="text"
  1174. placeholder={
  1175. Locale.Settings.Access.Baidu.SecretKey.Placeholder
  1176. }
  1177. onChange={(e) => {
  1178. accessStore.update(
  1179. (access) =>
  1180. (access.baiduSecretKey = e.currentTarget.value),
  1181. );
  1182. }}
  1183. />
  1184. </ListItem>
  1185. </>
  1186. )}
  1187. {accessStore.provider === ServiceProvider.ByteDance && (
  1188. <>
  1189. <ListItem
  1190. title={Locale.Settings.Access.ByteDance.Endpoint.Title}
  1191. subTitle={
  1192. Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
  1193. ByteDance.ExampleEndpoint
  1194. }
  1195. >
  1196. <input
  1197. type="text"
  1198. value={accessStore.bytedanceUrl}
  1199. placeholder={ByteDance.ExampleEndpoint}
  1200. onChange={(e) =>
  1201. accessStore.update(
  1202. (access) =>
  1203. (access.bytedanceUrl = e.currentTarget.value),
  1204. )
  1205. }
  1206. ></input>
  1207. </ListItem>
  1208. <ListItem
  1209. title={Locale.Settings.Access.ByteDance.ApiKey.Title}
  1210. subTitle={
  1211. Locale.Settings.Access.ByteDance.ApiKey.SubTitle
  1212. }
  1213. >
  1214. <PasswordInput
  1215. value={accessStore.bytedanceApiKey}
  1216. type="text"
  1217. placeholder={
  1218. Locale.Settings.Access.ByteDance.ApiKey.Placeholder
  1219. }
  1220. onChange={(e) => {
  1221. accessStore.update(
  1222. (access) =>
  1223. (access.bytedanceApiKey =
  1224. e.currentTarget.value),
  1225. );
  1226. }}
  1227. />
  1228. </ListItem>
  1229. </>
  1230. )}
  1231. </>
  1232. )}
  1233. </>
  1234. )}
  1235. {!shouldHideBalanceQuery && !clientConfig?.isApp ? (
  1236. <ListItem
  1237. title={Locale.Settings.Usage.Title}
  1238. subTitle={
  1239. showUsage
  1240. ? loadingUsage
  1241. ? Locale.Settings.Usage.IsChecking
  1242. : Locale.Settings.Usage.SubTitle(
  1243. usage?.used ?? "[?]",
  1244. usage?.subscription ?? "[?]",
  1245. )
  1246. : Locale.Settings.Usage.NoAccess
  1247. }
  1248. >
  1249. {!showUsage || loadingUsage ? (
  1250. <div />
  1251. ) : (
  1252. <IconButton
  1253. icon={<ResetIcon></ResetIcon>}
  1254. text={Locale.Settings.Usage.Check}
  1255. onClick={() => checkUsage(true)}
  1256. />
  1257. )}
  1258. </ListItem>
  1259. ) : null}
  1260. <ListItem
  1261. title={Locale.Settings.Access.CustomModel.Title}
  1262. subTitle={Locale.Settings.Access.CustomModel.SubTitle}
  1263. >
  1264. <input
  1265. type="text"
  1266. value={config.customModels}
  1267. placeholder="model1,model2,model3"
  1268. onChange={(e) =>
  1269. config.update(
  1270. (config) => (config.customModels = e.currentTarget.value),
  1271. )
  1272. }
  1273. ></input>
  1274. </ListItem>
  1275. </List>
  1276. <List>
  1277. <ModelConfigList
  1278. modelConfig={config.modelConfig}
  1279. updateConfig={(updater) => {
  1280. const modelConfig = { ...config.modelConfig };
  1281. updater(modelConfig);
  1282. config.update((config) => (config.modelConfig = modelConfig));
  1283. }}
  1284. />
  1285. </List>
  1286. {shouldShowPromptModal && (
  1287. <UserPromptModal onClose={() => setShowPromptModal(false)} />
  1288. )}
  1289. <DangerItems />
  1290. </div>
  1291. </ErrorBoundary>
  1292. );
  1293. }