settings.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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, 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. 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={clearSessions}
  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. </List>
  261. <List>
  262. <SettingItem
  263. title={Locale.Settings.Prompt.Disable.Title}
  264. subTitle={Locale.Settings.Prompt.Disable.SubTitle}
  265. >
  266. <input
  267. type="checkbox"
  268. checked={config.disablePromptHint}
  269. onChange={(e) =>
  270. updateConfig(
  271. (config) =>
  272. (config.disablePromptHint = e.currentTarget.checked),
  273. )
  274. }
  275. ></input>
  276. </SettingItem>
  277. <SettingItem
  278. title={Locale.Settings.Prompt.List}
  279. subTitle={Locale.Settings.Prompt.ListCount(
  280. builtinCount,
  281. customCount,
  282. )}
  283. >
  284. <IconButton
  285. icon={<EditIcon />}
  286. text={Locale.Settings.Prompt.Edit}
  287. onClick={() => showToast(Locale.WIP)}
  288. />
  289. </SettingItem>
  290. </List>
  291. <List>
  292. {enabledAccessControl ? (
  293. <SettingItem
  294. title={Locale.Settings.AccessCode.Title}
  295. subTitle={Locale.Settings.AccessCode.SubTitle}
  296. >
  297. <input
  298. value={accessStore.accessCode}
  299. type="text"
  300. placeholder={Locale.Settings.AccessCode.Placeholder}
  301. onChange={(e) => {
  302. accessStore.updateCode(e.currentTarget.value);
  303. }}
  304. ></input>
  305. </SettingItem>
  306. ) : (
  307. <></>
  308. )}
  309. <SettingItem
  310. title={Locale.Settings.Token.Title}
  311. subTitle={Locale.Settings.Token.SubTitle}
  312. >
  313. <input
  314. value={accessStore.token}
  315. type="text"
  316. placeholder={Locale.Settings.Token.Placeholder}
  317. onChange={(e) => {
  318. accessStore.updateToken(e.currentTarget.value);
  319. }}
  320. ></input>
  321. </SettingItem>
  322. <SettingItem
  323. title={Locale.Settings.Usage.Title}
  324. subTitle={
  325. loadingUsage
  326. ? Locale.Settings.Usage.IsChecking
  327. : Locale.Settings.Usage.SubTitle(
  328. usage?.granted ?? "[?]",
  329. usage?.used ?? "[?]",
  330. )
  331. }
  332. >
  333. {loadingUsage ? (
  334. <div />
  335. ) : (
  336. <IconButton
  337. icon={<ResetIcon></ResetIcon>}
  338. text={Locale.Settings.Usage.Check}
  339. onClick={checkUsage}
  340. />
  341. )}
  342. </SettingItem>
  343. <SettingItem
  344. title={Locale.Settings.HistoryCount.Title}
  345. subTitle={Locale.Settings.HistoryCount.SubTitle}
  346. >
  347. <input
  348. type="range"
  349. title={config.historyMessageCount.toString()}
  350. value={config.historyMessageCount}
  351. min="0"
  352. max="25"
  353. step="2"
  354. onChange={(e) =>
  355. updateConfig(
  356. (config) =>
  357. (config.historyMessageCount = e.target.valueAsNumber),
  358. )
  359. }
  360. ></input>
  361. </SettingItem>
  362. <SettingItem
  363. title={Locale.Settings.CompressThreshold.Title}
  364. subTitle={Locale.Settings.CompressThreshold.SubTitle}
  365. >
  366. <input
  367. type="number"
  368. min={500}
  369. max={4000}
  370. value={config.compressMessageLengthThreshold}
  371. onChange={(e) =>
  372. updateConfig(
  373. (config) =>
  374. (config.compressMessageLengthThreshold =
  375. e.currentTarget.valueAsNumber),
  376. )
  377. }
  378. ></input>
  379. </SettingItem>
  380. </List>
  381. <List>
  382. <SettingItem title={Locale.Settings.Model}>
  383. <select
  384. value={config.modelConfig.model}
  385. onChange={(e) => {
  386. updateConfig(
  387. (config) =>
  388. (config.modelConfig.model = e.currentTarget.value),
  389. );
  390. }}
  391. >
  392. {ALL_MODELS.map((v) => (
  393. <option value={v.name} key={v.name} disabled={!v.available}>
  394. {v.name}
  395. </option>
  396. ))}
  397. </select>
  398. </SettingItem>
  399. <SettingItem
  400. title={Locale.Settings.Temperature.Title}
  401. subTitle={Locale.Settings.Temperature.SubTitle}
  402. >
  403. <input
  404. type="range"
  405. value={config.modelConfig.temperature.toFixed(1)}
  406. min="0"
  407. max="2"
  408. step="0.1"
  409. onChange={(e) => {
  410. updateConfig(
  411. (config) =>
  412. (config.modelConfig.temperature =
  413. e.currentTarget.valueAsNumber),
  414. );
  415. }}
  416. ></input>
  417. </SettingItem>
  418. <SettingItem
  419. title={Locale.Settings.MaxTokens.Title}
  420. subTitle={Locale.Settings.MaxTokens.SubTitle}
  421. >
  422. <input
  423. type="number"
  424. min={100}
  425. max={4096}
  426. value={config.modelConfig.max_tokens}
  427. onChange={(e) =>
  428. updateConfig(
  429. (config) =>
  430. (config.modelConfig.max_tokens =
  431. e.currentTarget.valueAsNumber),
  432. )
  433. }
  434. ></input>
  435. </SettingItem>
  436. <SettingItem
  437. title={Locale.Settings.PresencePenlty.Title}
  438. subTitle={Locale.Settings.PresencePenlty.SubTitle}
  439. >
  440. <input
  441. type="range"
  442. value={config.modelConfig.presence_penalty.toFixed(1)}
  443. min="-2"
  444. max="2"
  445. step="0.5"
  446. onChange={(e) => {
  447. updateConfig(
  448. (config) =>
  449. (config.modelConfig.presence_penalty =
  450. e.currentTarget.valueAsNumber),
  451. );
  452. }}
  453. ></input>
  454. </SettingItem>
  455. </List>
  456. </div>
  457. </>
  458. );
  459. }