settings.tsx 15 KB

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