settings.tsx 17 KB

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