sidebar.tsx 5.9 KB

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