sidebar.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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 = useCallback((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) => (config.sidebarWidth = nextWidth));
  58. });
  59. const handleMouseUp = useRef(() => {
  60. // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
  61. // startDragWidth.current = config.sidebarWidth ?? 300;
  62. window.removeEventListener("mousemove", handleMouseMove.current);
  63. window.removeEventListener("mouseup", handleMouseUp.current);
  64. });
  65. const onDragMouseDown = (e: MouseEvent) => {
  66. startX.current = e.clientX;
  67. // Remembers the initial width each time the mouse is pressed
  68. startDragWidth.current = config.sidebarWidth
  69. window.addEventListener("mousemove", handleMouseMove.current);
  70. window.addEventListener("mouseup", handleMouseUp.current);
  71. };
  72. const isMobileScreen = useMobileScreen();
  73. const shouldNarrow =
  74. !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
  75. useEffect(() => {
  76. const barWidth = shouldNarrow
  77. ? NARROW_SIDEBAR_WIDTH
  78. : limit(config.sidebarWidth ?? 300);
  79. const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
  80. document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
  81. }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
  82. return {
  83. onDragMouseDown,
  84. shouldNarrow,
  85. };
  86. }
  87. export function SideBar(props: { className?: string }) {
  88. const chatStore = useChatStore();
  89. // drag side bar
  90. const { onDragMouseDown, shouldNarrow } = useDragSideBar();
  91. const navigate = useNavigate();
  92. const config = useAppConfig();
  93. useHotKey();
  94. return (
  95. <div
  96. className={`${styles.sidebar} ${props.className} ${
  97. shouldNarrow && styles["narrow-sidebar"]
  98. }`}
  99. >
  100. <div className={styles["sidebar-header"]} data-tauri-drag-region>
  101. <div className={styles["sidebar-title"]} data-tauri-drag-region>
  102. ChatGPT Next
  103. </div>
  104. <div className={styles["sidebar-sub-title"]}>
  105. Build your own AI assistant.
  106. </div>
  107. <div className={styles["sidebar-logo"] + " no-dark"}>
  108. <ChatGptIcon />
  109. </div>
  110. </div>
  111. <div className={styles["sidebar-header-bar"]}>
  112. <IconButton
  113. icon={<MaskIcon />}
  114. text={shouldNarrow ? undefined : Locale.Mask.Name}
  115. className={styles["sidebar-bar-button"]}
  116. onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
  117. shadow
  118. />
  119. <IconButton
  120. icon={<PluginIcon />}
  121. text={shouldNarrow ? undefined : Locale.Plugin.Name}
  122. className={styles["sidebar-bar-button"]}
  123. onClick={() => showToast(Locale.WIP)}
  124. shadow
  125. />
  126. </div>
  127. <div
  128. className={styles["sidebar-body"]}
  129. onClick={(e) => {
  130. if (e.target === e.currentTarget) {
  131. navigate(Path.Home);
  132. }
  133. }}
  134. >
  135. <ChatList narrow={shouldNarrow} />
  136. </div>
  137. <div className={styles["sidebar-tail"]}>
  138. <div className={styles["sidebar-actions"]}>
  139. <div className={styles["sidebar-action"] + " " + styles.mobile}>
  140. <IconButton
  141. icon={<CloseIcon />}
  142. onClick={async () => {
  143. if (await showConfirm(Locale.Home.DeleteChat)) {
  144. chatStore.deleteSession(chatStore.currentSessionIndex);
  145. }
  146. }}
  147. />
  148. </div>
  149. <div className={styles["sidebar-action"]}>
  150. <Link to={Path.Settings}>
  151. <IconButton icon={<SettingsIcon />} shadow />
  152. </Link>
  153. </div>
  154. <div className={styles["sidebar-action"]}>
  155. <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
  156. <IconButton icon={<GithubIcon />} shadow />
  157. </a>
  158. </div>
  159. </div>
  160. <div>
  161. <IconButton
  162. icon={<AddIcon />}
  163. text={shouldNarrow ? undefined : Locale.Home.NewChat}
  164. onClick={() => {
  165. if (config.dontShowMaskSplashScreen) {
  166. chatStore.newSession();
  167. navigate(Path.Chat);
  168. } else {
  169. navigate(Path.NewChat);
  170. }
  171. }}
  172. shadow
  173. />
  174. </div>
  175. </div>
  176. <div
  177. className={styles["sidebar-drag"]}
  178. onMouseDown={(e) => onDragMouseDown(e as any)}
  179. >
  180. <DragIcon />
  181. </div>
  182. </div>
  183. );
  184. }