sidebar.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 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) => (config.sidebarWidth = nextWidth));
  58. });
  59. const handleMouseUp = useRef(() => {
  60. startDragWidth.current = config.sidebarWidth ?? 300;
  61. window.removeEventListener("mousemove", handleMouseMove.current);
  62. window.removeEventListener("mouseup", handleMouseUp.current);
  63. });
  64. const onDragMouseDown = (e: MouseEvent) => {
  65. startX.current = e.clientX;
  66. window.addEventListener("mousemove", handleMouseMove.current);
  67. window.addEventListener("mouseup", handleMouseUp.current);
  68. };
  69. const isMobileScreen = useMobileScreen();
  70. const shouldNarrow =
  71. !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
  72. useEffect(() => {
  73. const barWidth = shouldNarrow
  74. ? NARROW_SIDEBAR_WIDTH
  75. : limit(config.sidebarWidth ?? 300);
  76. const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
  77. document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
  78. }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
  79. return {
  80. onDragMouseDown,
  81. shouldNarrow,
  82. };
  83. }
  84. export function SideBar(props: { className?: string }) {
  85. const chatStore = useChatStore();
  86. // drag side bar
  87. const { onDragMouseDown, shouldNarrow } = useDragSideBar();
  88. const navigate = useNavigate();
  89. const config = useAppConfig();
  90. useHotKey();
  91. return (
  92. <div
  93. className={`${styles.sidebar} ${props.className} ${
  94. shouldNarrow && styles["narrow-sidebar"]
  95. }`}
  96. >
  97. <div className={styles["sidebar-header"]} data-tauri-drag-region>
  98. <div className={styles["sidebar-title"]} data-tauri-drag-region>
  99. ChatGPT Next
  100. </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={async () => {
  140. if (await showConfirm(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. >
  177. <DragIcon />
  178. </div>
  179. </div>
  180. );
  181. }