sidebar.tsx 5.4 KB

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