sidebar.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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. return (
  73. <div
  74. className={`${styles.sidebar} ${props.className} ${
  75. shouldNarrow && styles["narrow-sidebar"]
  76. }`}
  77. >
  78. <div className={styles["sidebar-header"]}>
  79. <div className={styles["sidebar-title"]}>ChatGPT Next</div>
  80. <div className={styles["sidebar-sub-title"]}>
  81. Build your own AI assistant.
  82. </div>
  83. <div className={styles["sidebar-logo"] + " no-dark"}>
  84. <ChatGptIcon />
  85. </div>
  86. </div>
  87. <div className={styles["sidebar-header-bar"]}>
  88. <IconButton
  89. icon={<MaskIcon />}
  90. text="Mask"
  91. className={styles["sidebar-bar-button"]}
  92. onClick={() => navigate(Path.Masks)}
  93. shadow
  94. />
  95. <IconButton
  96. icon={<PluginIcon />}
  97. text="Plugins"
  98. className={styles["sidebar-bar-button"]}
  99. onClick={() => showToast(Locale.WIP)}
  100. shadow
  101. />
  102. </div>
  103. <div
  104. className={styles["sidebar-body"]}
  105. onClick={(e) => {
  106. if (e.target === e.currentTarget) {
  107. navigate(Path.Home);
  108. }
  109. }}
  110. >
  111. <ChatList narrow={shouldNarrow} />
  112. </div>
  113. <div className={styles["sidebar-tail"]}>
  114. <div className={styles["sidebar-actions"]}>
  115. <div className={styles["sidebar-action"] + " " + styles.mobile}>
  116. <IconButton
  117. icon={<CloseIcon />}
  118. onClick={chatStore.deleteSession}
  119. />
  120. </div>
  121. <div className={styles["sidebar-action"]}>
  122. <Link to={Path.Settings}>
  123. <IconButton icon={<SettingsIcon />} shadow />
  124. </Link>
  125. </div>
  126. <div className={styles["sidebar-action"]}>
  127. <a href={REPO_URL} target="_blank">
  128. <IconButton icon={<GithubIcon />} shadow />
  129. </a>
  130. </div>
  131. </div>
  132. <div>
  133. <IconButton
  134. icon={<AddIcon />}
  135. text={shouldNarrow ? undefined : Locale.Home.NewChat}
  136. onClick={() => {
  137. navigate(Path.NewChat);
  138. }}
  139. shadow
  140. />
  141. </div>
  142. </div>
  143. <div
  144. className={styles["sidebar-drag"]}
  145. onMouseDown={(e) => onDragMouseDown(e as any)}
  146. ></div>
  147. </div>
  148. );
  149. }