sidebar.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { useEffect, useRef, useCallback } 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 DragIcon from "../icons/drag.svg";
  12. import Locale from "../locales";
  13. import { useAppConfig, useChatStore } from "../store";
  14. import {
  15. MAX_SIDEBAR_WIDTH,
  16. MIN_SIDEBAR_WIDTH,
  17. NARROW_SIDEBAR_WIDTH,
  18. Path,
  19. REPO_URL,
  20. } from "../constant";
  21. import { Link, useNavigate } from "react-router-dom";
  22. import { useMobileScreen } from "../utils";
  23. import dynamic from "next/dynamic";
  24. import { showConfirm, showToast } from "./ui-lib";
  25. const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
  26. loading: () => null,
  27. });
  28. function useHotKey() {
  29. const chatStore = useChatStore();
  30. useEffect(() => {
  31. const onKeyDown = (e: KeyboardEvent) => {
  32. if (e.altKey || e.ctrlKey) {
  33. if (e.key === "ArrowUp") {
  34. chatStore.nextSession(-1);
  35. } else if (e.key === "ArrowDown") {
  36. chatStore.nextSession(1);
  37. }
  38. }
  39. };
  40. window.addEventListener("keydown", onKeyDown);
  41. return () => window.removeEventListener("keydown", onKeyDown);
  42. });
  43. }
  44. function useDragSideBar() {
  45. const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
  46. const config = useAppConfig();
  47. const startX = useRef(0);
  48. const startDragWidth = useRef(config.sidebarWidth ?? 300);
  49. const lastUpdateTime = useRef(Date.now());
  50. const handleMouseMove = useRef((e: MouseEvent) => {
  51. if (Date.now() < lastUpdateTime.current + 50) {
  52. return;
  53. }
  54. lastUpdateTime.current = Date.now();
  55. const d = e.clientX - startX.current;
  56. const nextWidth = limit(startDragWidth.current + d);
  57. config.update((config) => {
  58. if (nextWidth < MIN_SIDEBAR_WIDTH) {
  59. config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
  60. } else {
  61. config.sidebarWidth = nextWidth;
  62. }
  63. });
  64. });
  65. const handleMouseUp = useRef(() => {
  66. // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
  67. // startDragWidth.current = config.sidebarWidth ?? 300;
  68. window.removeEventListener("mousemove", handleMouseMove.current);
  69. window.removeEventListener("mouseup", handleMouseUp.current);
  70. });
  71. const onDragMouseDown = (e: MouseEvent) => {
  72. startX.current = e.clientX;
  73. // Remembers the initial width each time the mouse is pressed
  74. startDragWidth.current = config.sidebarWidth;
  75. window.addEventListener("mousemove", handleMouseMove.current);
  76. window.addEventListener("mouseup", handleMouseUp.current);
  77. };
  78. const isMobileScreen = useMobileScreen();
  79. const shouldNarrow =
  80. !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
  81. useEffect(() => {
  82. const barWidth = shouldNarrow
  83. ? NARROW_SIDEBAR_WIDTH
  84. : limit(config.sidebarWidth ?? 300);
  85. const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
  86. document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
  87. }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
  88. return {
  89. onDragMouseDown,
  90. shouldNarrow,
  91. };
  92. }
  93. export function SideBar(props: { className?: string }) {
  94. const chatStore = useChatStore();
  95. // drag side bar
  96. const { onDragMouseDown, shouldNarrow } = useDragSideBar();
  97. const navigate = useNavigate();
  98. const config = useAppConfig();
  99. useHotKey();
  100. return (
  101. <div
  102. className={`${styles.sidebar} ${props.className} ${
  103. shouldNarrow && styles["narrow-sidebar"]
  104. }`}
  105. >
  106. <div className={styles["sidebar-header"]} data-tauri-drag-region>
  107. <div className={styles["sidebar-title"]} data-tauri-drag-region>
  108. ChatGPT Next
  109. </div>
  110. <div className={styles["sidebar-sub-title"]}>
  111. Build your own AI assistant.
  112. </div>
  113. <div className={styles["sidebar-logo"] + " no-dark"}>
  114. <ChatGptIcon />
  115. </div>
  116. </div>
  117. <div className={styles["sidebar-header-bar"]}>
  118. <IconButton
  119. icon={<MaskIcon />}
  120. text={shouldNarrow ? undefined : Locale.Mask.Name}
  121. className={styles["sidebar-bar-button"]}
  122. onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
  123. shadow
  124. />
  125. <IconButton
  126. icon={<PluginIcon />}
  127. text={shouldNarrow ? undefined : Locale.Plugin.Name}
  128. className={styles["sidebar-bar-button"]}
  129. onClick={() => showToast(Locale.WIP)}
  130. shadow
  131. />
  132. </div>
  133. <div
  134. className={styles["sidebar-body"]}
  135. onClick={(e) => {
  136. if (e.target === e.currentTarget) {
  137. navigate(Path.Home);
  138. }
  139. }}
  140. >
  141. <ChatList narrow={shouldNarrow} />
  142. </div>
  143. <div className={styles["sidebar-tail"]}>
  144. <div className={styles["sidebar-actions"]}>
  145. <div className={styles["sidebar-action"] + " " + styles.mobile}>
  146. <IconButton
  147. icon={<CloseIcon />}
  148. onClick={async () => {
  149. if (await showConfirm(Locale.Home.DeleteChat)) {
  150. chatStore.deleteSession(chatStore.currentSessionIndex);
  151. }
  152. }}
  153. />
  154. </div>
  155. <div className={styles["sidebar-action"]}>
  156. <Link to={Path.Settings}>
  157. <IconButton icon={<SettingsIcon />} shadow />
  158. </Link>
  159. </div>
  160. <div className={styles["sidebar-action"]}>
  161. <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
  162. <IconButton icon={<GithubIcon />} shadow />
  163. </a>
  164. </div>
  165. </div>
  166. <div>
  167. <IconButton
  168. icon={<AddIcon />}
  169. text={shouldNarrow ? undefined : Locale.Home.NewChat}
  170. onClick={() => {
  171. if (config.dontShowMaskSplashScreen) {
  172. chatStore.newSession();
  173. navigate(Path.Chat);
  174. } else {
  175. navigate(Path.NewChat);
  176. }
  177. }}
  178. shadow
  179. />
  180. </div>
  181. </div>
  182. <div
  183. className={styles["sidebar-drag"]}
  184. onMouseDown={(e) => onDragMouseDown(e as any)}
  185. >
  186. <DragIcon />
  187. </div>
  188. </div>
  189. );
  190. }