settings.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
  2. import styles from "./settings.module.scss";
  3. import ResetIcon from "../icons/reload.svg";
  4. import CloseIcon from "../icons/close.svg";
  5. import CopyIcon from "../icons/copy.svg";
  6. import ClearIcon from "../icons/clear.svg";
  7. import EditIcon from "../icons/edit.svg";
  8. import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
  9. import { ModelConfigList } from "./model-config";
  10. import { IconButton } from "./button";
  11. import {
  12. SubmitKey,
  13. useChatStore,
  14. Theme,
  15. useUpdateStore,
  16. useAccessStore,
  17. useAppConfig,
  18. } from "../store";
  19. import Locale, { AllLangs, changeLang, getLang } from "../locales";
  20. import { copyToClipboard } from "../utils";
  21. import Link from "next/link";
  22. import { Path, UPDATE_URL } from "../constant";
  23. import { Prompt, SearchService, usePromptStore } from "../store/prompt";
  24. import { ErrorBoundary } from "./error";
  25. import { InputRange } from "./input-range";
  26. import { useNavigate } from "react-router-dom";
  27. import { Avatar, AvatarPicker } from "./emoji";
  28. function UserPromptModal(props: { onClose?: () => void }) {
  29. const promptStore = usePromptStore();
  30. const userPrompts = promptStore.getUserPrompts();
  31. const builtinPrompts = SearchService.builtinPrompts;
  32. const allPrompts = userPrompts.concat(builtinPrompts);
  33. const [searchInput, setSearchInput] = useState("");
  34. const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
  35. const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
  36. useEffect(() => {
  37. if (searchInput.length > 0) {
  38. const searchResult = SearchService.search(searchInput);
  39. setSearchPrompts(searchResult);
  40. } else {
  41. setSearchPrompts([]);
  42. }
  43. }, [searchInput]);
  44. return (
  45. <div className="modal-mask">
  46. <Modal
  47. title={Locale.Settings.Prompt.Modal.Title}
  48. onClose={() => props.onClose?.()}
  49. actions={[
  50. <IconButton
  51. key="add"
  52. onClick={() => promptStore.add({ title: "", content: "" })}
  53. icon={<ClearIcon />}
  54. bordered
  55. text={Locale.Settings.Prompt.Modal.Add}
  56. />,
  57. ]}
  58. >
  59. <div className={styles["user-prompt-modal"]}>
  60. <input
  61. type="text"
  62. className={styles["user-prompt-search"]}
  63. placeholder={Locale.Settings.Prompt.Modal.Search}
  64. value={searchInput}
  65. onInput={(e) => setSearchInput(e.currentTarget.value)}
  66. ></input>
  67. <div className={styles["user-prompt-list"]}>
  68. {prompts.map((v, _) => (
  69. <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
  70. <div className={styles["user-prompt-header"]}>
  71. <input
  72. type="text"
  73. className={styles["user-prompt-title"]}
  74. value={v.title}
  75. readOnly={!v.isUser}
  76. onChange={(e) => {
  77. if (v.isUser) {
  78. promptStore.updateUserPrompts(
  79. v.id!,
  80. (prompt) => (prompt.title = e.currentTarget.value),
  81. );
  82. }
  83. }}
  84. ></input>
  85. <div className={styles["user-prompt-buttons"]}>
  86. {v.isUser && (
  87. <IconButton
  88. icon={<ClearIcon />}
  89. bordered
  90. className={styles["user-prompt-button"]}
  91. onClick={() => promptStore.remove(v.id!)}
  92. />
  93. )}
  94. <IconButton
  95. icon={<CopyIcon />}
  96. bordered
  97. className={styles["user-prompt-button"]}
  98. onClick={() => copyToClipboard(v.content)}
  99. />
  100. </div>
  101. </div>
  102. <Input
  103. rows={2}
  104. value={v.content}
  105. className={styles["user-prompt-content"]}
  106. readOnly={!v.isUser}
  107. onChange={(e) => {
  108. if (v.isUser) {
  109. promptStore.updateUserPrompts(
  110. v.id!,
  111. (prompt) => (prompt.content = e.currentTarget.value),
  112. );
  113. }
  114. }}
  115. />
  116. </div>
  117. ))}
  118. </div>
  119. </div>
  120. </Modal>
  121. </div>
  122. );
  123. }
  124. export function Settings() {
  125. const navigate = useNavigate();
  126. const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  127. const config = useAppConfig();
  128. const updateConfig = config.update;
  129. const resetConfig = config.reset;
  130. const chatStore = useChatStore();
  131. const updateStore = useUpdateStore();
  132. const [checkingUpdate, setCheckingUpdate] = useState(false);
  133. const currentVersion = updateStore.version;
  134. const remoteId = updateStore.remoteVersion;
  135. const hasNewVersion = currentVersion !== remoteId;
  136. function checkUpdate(force = false) {
  137. setCheckingUpdate(true);
  138. updateStore.getLatestVersion(force).then(() => {
  139. setCheckingUpdate(false);
  140. });
  141. }
  142. const usage = {
  143. used: updateStore.used,
  144. subscription: updateStore.subscription,
  145. };
  146. const [loadingUsage, setLoadingUsage] = useState(false);
  147. function checkUsage(force = false) {
  148. setLoadingUsage(true);
  149. updateStore.updateUsage(force).finally(() => {
  150. setLoadingUsage(false);
  151. });
  152. }
  153. const accessStore = useAccessStore();
  154. const enabledAccessControl = useMemo(
  155. () => accessStore.enabledAccessControl(),
  156. // eslint-disable-next-line react-hooks/exhaustive-deps
  157. [],
  158. );
  159. const promptStore = usePromptStore();
  160. const builtinCount = SearchService.count.builtin;
  161. const customCount = promptStore.getUserPrompts().length ?? 0;
  162. const [shouldShowPromptModal, setShowPromptModal] = useState(false);
  163. const showUsage = accessStore.isAuthorized();
  164. useEffect(() => {
  165. // checks per minutes
  166. checkUpdate();
  167. showUsage && checkUsage();
  168. // eslint-disable-next-line react-hooks/exhaustive-deps
  169. }, []);
  170. useEffect(() => {
  171. const keydownEvent = (e: KeyboardEvent) => {
  172. if (e.key === "Escape") {
  173. navigate(Path.Home);
  174. }
  175. };
  176. document.addEventListener("keydown", keydownEvent);
  177. return () => {
  178. document.removeEventListener("keydown", keydownEvent);
  179. };
  180. // eslint-disable-next-line react-hooks/exhaustive-deps
  181. }, []);
  182. return (
  183. <ErrorBoundary>
  184. <div className="window-header">
  185. <div className="window-header-title">
  186. <div className="window-header-main-title">
  187. {Locale.Settings.Title}
  188. </div>
  189. <div className="window-header-sub-title">
  190. {Locale.Settings.SubTitle}
  191. </div>
  192. </div>
  193. <div className="window-actions">
  194. <div className="window-action-button">
  195. <IconButton
  196. icon={<ClearIcon />}
  197. onClick={() => {
  198. if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
  199. chatStore.clearAllData();
  200. }
  201. }}
  202. bordered
  203. title={Locale.Settings.Actions.ClearAll}
  204. />
  205. </div>
  206. <div className="window-action-button">
  207. <IconButton
  208. icon={<ResetIcon />}
  209. onClick={() => {
  210. if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
  211. resetConfig();
  212. }
  213. }}
  214. bordered
  215. title={Locale.Settings.Actions.ResetAll}
  216. />
  217. </div>
  218. <div className="window-action-button">
  219. <IconButton
  220. icon={<CloseIcon />}
  221. onClick={() => navigate(Path.Home)}
  222. bordered
  223. title={Locale.Settings.Actions.Close}
  224. />
  225. </div>
  226. </div>
  227. </div>
  228. <div className={styles["settings"]}>
  229. <List>
  230. <ListItem title={Locale.Settings.Avatar}>
  231. <Popover
  232. onClose={() => setShowEmojiPicker(false)}
  233. content={
  234. <AvatarPicker
  235. onEmojiClick={(avatar: string) => {
  236. updateConfig((config) => (config.avatar = avatar));
  237. setShowEmojiPicker(false);
  238. }}
  239. />
  240. }
  241. open={showEmojiPicker}
  242. >
  243. <div
  244. className={styles.avatar}
  245. onClick={() => setShowEmojiPicker(true)}
  246. >
  247. <Avatar avatar={config.avatar} />
  248. </div>
  249. </Popover>
  250. </ListItem>
  251. <ListItem
  252. title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
  253. subTitle={
  254. checkingUpdate
  255. ? Locale.Settings.Update.IsChecking
  256. : hasNewVersion
  257. ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
  258. : Locale.Settings.Update.IsLatest
  259. }
  260. >
  261. {checkingUpdate ? (
  262. <div />
  263. ) : hasNewVersion ? (
  264. <Link href={UPDATE_URL} target="_blank" className="link">
  265. {Locale.Settings.Update.GoToUpdate}
  266. </Link>
  267. ) : (
  268. <IconButton
  269. icon={<ResetIcon></ResetIcon>}
  270. text={Locale.Settings.Update.CheckUpdate}
  271. onClick={() => checkUpdate(true)}
  272. />
  273. )}
  274. </ListItem>
  275. <ListItem title={Locale.Settings.SendKey}>
  276. <select
  277. value={config.submitKey}
  278. onChange={(e) => {
  279. updateConfig(
  280. (config) =>
  281. (config.submitKey = e.target.value as any as SubmitKey),
  282. );
  283. }}
  284. >
  285. {Object.values(SubmitKey).map((v) => (
  286. <option value={v} key={v}>
  287. {v}
  288. </option>
  289. ))}
  290. </select>
  291. </ListItem>
  292. <ListItem title={Locale.Settings.Theme}>
  293. <select
  294. value={config.theme}
  295. onChange={(e) => {
  296. updateConfig(
  297. (config) => (config.theme = e.target.value as any as Theme),
  298. );
  299. }}
  300. >
  301. {Object.values(Theme).map((v) => (
  302. <option value={v} key={v}>
  303. {v}
  304. </option>
  305. ))}
  306. </select>
  307. </ListItem>
  308. <ListItem title={Locale.Settings.Lang.Name}>
  309. <select
  310. value={getLang()}
  311. onChange={(e) => {
  312. changeLang(e.target.value as any);
  313. }}
  314. >
  315. {AllLangs.map((lang) => (
  316. <option value={lang} key={lang}>
  317. {Locale.Settings.Lang.Options[lang]}
  318. </option>
  319. ))}
  320. </select>
  321. </ListItem>
  322. <ListItem
  323. title={Locale.Settings.FontSize.Title}
  324. subTitle={Locale.Settings.FontSize.SubTitle}
  325. >
  326. <InputRange
  327. title={`${config.fontSize ?? 14}px`}
  328. value={config.fontSize}
  329. min="12"
  330. max="18"
  331. step="1"
  332. onChange={(e) =>
  333. updateConfig(
  334. (config) =>
  335. (config.fontSize = Number.parseInt(e.currentTarget.value)),
  336. )
  337. }
  338. ></InputRange>
  339. </ListItem>
  340. <ListItem
  341. title={Locale.Settings.SendPreviewBubble.Title}
  342. subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
  343. >
  344. <input
  345. type="checkbox"
  346. checked={config.sendPreviewBubble}
  347. onChange={(e) =>
  348. updateConfig(
  349. (config) =>
  350. (config.sendPreviewBubble = e.currentTarget.checked),
  351. )
  352. }
  353. ></input>
  354. </ListItem>
  355. <ListItem
  356. title={Locale.Settings.Mask.Title}
  357. subTitle={Locale.Settings.Mask.SubTitle}
  358. >
  359. <input
  360. type="checkbox"
  361. checked={!config.dontShowMaskSplashScreen}
  362. onChange={(e) =>
  363. updateConfig(
  364. (config) =>
  365. (config.dontShowMaskSplashScreen =
  366. !e.currentTarget.checked),
  367. )
  368. }
  369. ></input>
  370. </ListItem>
  371. </List>
  372. <List>
  373. {enabledAccessControl ? (
  374. <ListItem
  375. title={Locale.Settings.AccessCode.Title}
  376. subTitle={Locale.Settings.AccessCode.SubTitle}
  377. >
  378. <PasswordInput
  379. value={accessStore.accessCode}
  380. type="text"
  381. placeholder={Locale.Settings.AccessCode.Placeholder}
  382. onChange={(e) => {
  383. accessStore.updateCode(e.currentTarget.value);
  384. }}
  385. />
  386. </ListItem>
  387. ) : (
  388. <></>
  389. )}
  390. <ListItem
  391. title={Locale.Settings.Token.Title}
  392. subTitle={Locale.Settings.Token.SubTitle}
  393. >
  394. <PasswordInput
  395. value={accessStore.token}
  396. type="text"
  397. placeholder={Locale.Settings.Token.Placeholder}
  398. onChange={(e) => {
  399. accessStore.updateToken(e.currentTarget.value);
  400. }}
  401. />
  402. </ListItem>
  403. <ListItem
  404. title={Locale.Settings.Usage.Title}
  405. subTitle={
  406. showUsage
  407. ? loadingUsage
  408. ? Locale.Settings.Usage.IsChecking
  409. : Locale.Settings.Usage.SubTitle(
  410. usage?.used ?? "[?]",
  411. usage?.subscription ?? "[?]",
  412. )
  413. : Locale.Settings.Usage.NoAccess
  414. }
  415. >
  416. {!showUsage || loadingUsage ? (
  417. <div />
  418. ) : (
  419. <IconButton
  420. icon={<ResetIcon></ResetIcon>}
  421. text={Locale.Settings.Usage.Check}
  422. onClick={() => checkUsage(true)}
  423. />
  424. )}
  425. </ListItem>
  426. </List>
  427. <List>
  428. <ListItem
  429. title={Locale.Settings.Prompt.Disable.Title}
  430. subTitle={Locale.Settings.Prompt.Disable.SubTitle}
  431. >
  432. <input
  433. type="checkbox"
  434. checked={config.disablePromptHint}
  435. onChange={(e) =>
  436. updateConfig(
  437. (config) =>
  438. (config.disablePromptHint = e.currentTarget.checked),
  439. )
  440. }
  441. ></input>
  442. </ListItem>
  443. <ListItem
  444. title={Locale.Settings.Prompt.List}
  445. subTitle={Locale.Settings.Prompt.ListCount(
  446. builtinCount,
  447. customCount,
  448. )}
  449. >
  450. <IconButton
  451. icon={<EditIcon />}
  452. text={Locale.Settings.Prompt.Edit}
  453. onClick={() => setShowPromptModal(true)}
  454. />
  455. </ListItem>
  456. </List>
  457. <List>
  458. <ModelConfigList
  459. modelConfig={config.modelConfig}
  460. updateConfig={(upater) => {
  461. const modelConfig = { ...config.modelConfig };
  462. upater(modelConfig);
  463. config.update((config) => (config.modelConfig = modelConfig));
  464. }}
  465. />
  466. </List>
  467. {shouldShowPromptModal && (
  468. <UserPromptModal onClose={() => setShowPromptModal(false)} />
  469. )}
  470. </div>
  471. </ErrorBoundary>
  472. );
  473. }