sidebar.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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"]}>ChatGPT Next</div>
  101. <div className={styles["sidebar-sub-title"]}>
  102. Build your own AI assistant.
  103. </div>
  104. <div className={styles["sidebar-logo"] + " no-dark"}>
  105. <ChatGptIcon />
  106. </div>
  107. </div>
  108. <div className={styles["sidebar-header-bar"]}>
  109. <IconButton
  110. icon={<MaskIcon />}
  111. text={shouldNarrow ? undefined : Locale.Mask.Name}
  112. className={styles["sidebar-bar-button"]}
  113. onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
  114. shadow
  115. />
  116. <IconButton
  117. icon={<PluginIcon />}
  118. text={shouldNarrow ? undefined : Locale.Plugin.Name}
  119. className={styles["sidebar-bar-button"]}
  120. onClick={() => showToast(Locale.WIP)}
  121. shadow
  122. />
  123. </div>
  124. <div
  125. className={styles["sidebar-body"]}
  126. onClick={(e) => {
  127. if (e.target === e.currentTarget) {
  128. navigate(Path.Home);
  129. }
  130. }}
  131. >
  132. <ChatList narrow={shouldNarrow} />
  133. </div>
  134. <div className={styles["sidebar-tail"]}>
  135. <div className={styles["sidebar-actions"]}>
  136. <div className={styles["sidebar-action"] + " " + styles.mobile}>
  137. <IconButton
  138. icon={<CloseIcon />}
  139. onClick={() => {
  140. if (confirm(Locale.Home.DeleteChat)) {
  141. chatStore.deleteSession(chatStore.currentSessionIndex);
  142. }
  143. }}
  144. />
  145. </div>
  146. <div className={styles["sidebar-action"]}>
  147. <Link to={Path.Settings}>
  148. <IconButton icon={<SettingsIcon />} shadow />
  149. </Link>
  150. </div>
  151. <div className={styles["sidebar-action"]}>
  152. <a href={REPO_URL} target="_blank">
  153. <IconButton icon={<GithubIcon />} shadow />
  154. </a>
  155. </div>
  156. </div>
  157. <div>
  158. <IconButton
  159. icon={<AddIcon />}
  160. text={shouldNarrow ? undefined : Locale.Home.NewChat}
  161. onClick={() => {
  162. if (config.dontShowMaskSplashScreen) {
  163. chatStore.newSession();
  164. navigate(Path.Chat);
  165. } else {
  166. navigate(Path.NewChat);
  167. }
  168. }}
  169. shadow
  170. />
  171. </div>
  172. </div>
  173. <div
  174. className={styles["sidebar-drag"]}
  175. onMouseDown={(e) => onDragMouseDown(e as any)}
  176. ></div>
  177. </div>
  178. );
  179. }