settings.tsx 15 KB

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