settings.tsx 47 KB

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