settings.tsx 14 KB

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