sidebar.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { useEffect, useRef } from "react";
  2. import styles from "./home.module.scss";
  3. import { IconButton } from "./button";
  4. import SettingsIcon from "../icons/settings.svg";
  5. import GithubIcon from "../icons/github.svg";
  6. import ChatGptIcon from "../icons/chatgpt.svg";
  7. import AddIcon from "../icons/add.svg";
  8. import CloseIcon from "../icons/close.svg";
  9. import MaskIcon from "../icons/mask.svg";
  10. import PluginIcon from "../icons/plugin.svg";
  11. import Locale from "../locales";
  12. import { useAppConfig, useChatStore } from "../store";
  13. import {
  14. MAX_SIDEBAR_WIDTH,
  15. MIN_SIDEBAR_WIDTH,
  16. NARROW_SIDEBAR_WIDTH,
  17. Path,
  18. REPO_URL,
  19. } from "../constant";
  20. import { Link, useNavigate } from "react-router-dom";
  21. import { useMobileScreen } from "../utils";
  22. import dynamic from "next/dynamic";
  23. import { showToast } from "./ui-lib";
  24. const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
  25. loading: () => null,
  26. });
  27. function useHotKey() {
  28. const chatStore = useChatStore();
  29. useEffect(() => {
  30. const onKeyDown = (e: KeyboardEvent) => {
  31. if (e.metaKey || e.altKey || e.ctrlKey) {
  32. const n = chatStore.sessions.length;
  33. const limit = (x: number) => (x + n) % n;
  34. const i = chatStore.currentSessionIndex;
  35. if (e.key === "ArrowUp") {
  36. chatStore.selectSession(limit(i - 1));
  37. } else if (e.key === "ArrowDown") {
  38. chatStore.selectSession(limit(i + 1));
  39. }
  40. }
  41. };
  42. window.addEventListener("keydown", onKeyDown);
  43. return () => window.removeEventListener("keydown", onKeyDown);
  44. });
  45. }
  46. function useDragSideBar() {
  47. const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
  48. const config = useAppConfig();
  49. const startX = useRef(0);
  50. const startDragWidth = useRef(config.sidebarWidth ?? 300);
  51. const lastUpdateTime = useRef(Date.now());
  52. const handleMouseMove = useRef((e: MouseEvent) => {
  53. if (Date.now() < lastUpdateTime.current + 50) {
  54. return;
  55. }
  56. lastUpdateTime.current = Date.now();
  57. const d = e.clientX - startX.current;
  58. const nextWidth = limit(startDragWidth.current + d);
  59. config.update((config) => (config.sidebarWidth = nextWidth));
  60. });
  61. const handleMouseUp = useRef(() => {
  62. startDragWidth.current = config.sidebarWidth ?? 300;
  63. window.removeEventListener("mousemove", handleMouseMove.current);
  64. window.removeEventListener("mouseup", handleMouseUp.current);
  65. });
  66. const onDragMouseDown = (e: MouseEvent) => {
  67. startX.current = e.clientX;
  68. window.addEventListener("mousemove", handleMouseMove.current);
  69. window.addEventListener("mouseup", handleMouseUp.current);
  70. };
  71. const isMobileScreen = useMobileScreen();
  72. const shouldNarrow =
  73. !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
  74. useEffect(() => {
  75. const barWidth = shouldNarrow
  76. ? NARROW_SIDEBAR_WIDTH
  77. : limit(config.sidebarWidth ?? 300);
  78. const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
  79. document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
  80. }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
  81. return {
  82. onDragMouseDown,
  83. shouldNarrow,
  84. };
  85. }
  86. export function SideBar(props: { className?: string }) {
  87. const chatStore = useChatStore();
  88. // drag side bar
  89. const { onDragMouseDown, shouldNarrow } = useDragSideBar();
  90. const navigate = useNavigate();
  91. const config = useAppConfig();
  92. useHotKey();
  93. return (
  94. <div
  95. className={`${styles.sidebar} ${props.className} ${
  96. shouldNarrow && styles["narrow-sidebar"]
  97. }`}
  98. >
  99. <div className={styles["sidebar-header"]}>
  100. <div className={styles["sidebar-title"]}>AI.DW</div>
  101. <div className={styles["sidebar-sub-title"]}></div>
  102. <div className={styles["sidebar-logo"] + " no-dark"}></div>
  103. </div>
  104. <div className={styles["sidebar-header-bar"]}>
  105. <IconButton
  106. icon={<MaskIcon />}
  107. text={shouldNarrow ? undefined : Locale.Mask.Name}
  108. className={styles["sidebar-bar-button"]}
  109. onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
  110. shadow
  111. />
  112. </div>
  113. <div
  114. className={styles["sidebar-body"]}
  115. onClick={(e) => {
  116. if (e.target === e.currentTarget) {
  117. navigate(Path.Home);
  118. }
  119. }}
  120. >
  121. <ChatList narrow={shouldNarrow} />
  122. </div>
  123. <div className={styles["sidebar-tail"]}>
  124. <div className={styles["sidebar-actions"]}>
  125. <div className={styles["sidebar-action"] + " " + styles.mobile}>
  126. <IconButton
  127. icon={<CloseIcon />}
  128. onClick={() => {
  129. if (confirm(Locale.Home.DeleteChat)) {
  130. chatStore.deleteSession(chatStore.currentSessionIndex);
  131. }
  132. }}
  133. />
  134. </div>
  135. <div className={styles["sidebar-action"]}>
  136. <Link to={Path.Settings}>
  137. <IconButton icon={<SettingsIcon />} shadow />
  138. </Link>
  139. </div>
  140. </div>
  141. <div>
  142. <IconButton
  143. icon={<AddIcon />}
  144. text={shouldNarrow ? undefined : Locale.Home.NewChat}
  145. onClick={() => {
  146. if (config.dontShowMaskSplashScreen) {
  147. chatStore.newSession();
  148. navigate(Path.Chat);
  149. } else {
  150. navigate(Path.NewChat);
  151. }
  152. }}
  153. shadow
  154. />
  155. </div>
  156. </div>
  157. <div
  158. className={styles["sidebar-drag"]}
  159. onMouseDown={(e) => onDragMouseDown(e as any)}
  160. ></div>
  161. </div>
  162. );
  163. }