mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			206 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { useEffect, useRef } from "react";
 | 
						|
 | 
						|
import styles from "./home.module.scss";
 | 
						|
 | 
						|
import { IconButton } from "./button";
 | 
						|
import SettingsIcon from "../icons/settings.svg";
 | 
						|
import GithubIcon from "../icons/github.svg";
 | 
						|
import ChatGptIcon from "../icons/chatgpt.svg";
 | 
						|
import AddIcon from "../icons/add.svg";
 | 
						|
import CloseIcon from "../icons/close.svg";
 | 
						|
import MaskIcon from "../icons/mask.svg";
 | 
						|
import PluginIcon from "../icons/plugin.svg";
 | 
						|
 | 
						|
import Locale from "../locales";
 | 
						|
 | 
						|
import { useAppConfig, useChatStore } from "../store";
 | 
						|
 | 
						|
import {
 | 
						|
  MAX_SIDEBAR_WIDTH,
 | 
						|
  MIN_SIDEBAR_WIDTH,
 | 
						|
  NARROW_SIDEBAR_WIDTH,
 | 
						|
  Path,
 | 
						|
  REPO_URL,
 | 
						|
} from "../constant";
 | 
						|
 | 
						|
import { Link, useNavigate } from "react-router-dom";
 | 
						|
import { useMobileScreen } from "../utils";
 | 
						|
import dynamic from "next/dynamic";
 | 
						|
import { showToast } from "./ui-lib";
 | 
						|
 | 
						|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
 | 
						|
  loading: () => null,
 | 
						|
});
 | 
						|
 | 
						|
function useHotKey() {
 | 
						|
  const chatStore = useChatStore();
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    const onKeyDown = (e: KeyboardEvent) => {
 | 
						|
      if (e.metaKey || e.altKey || e.ctrlKey) {
 | 
						|
        const n = chatStore.sessions.length;
 | 
						|
        const limit = (x: number) => (x + n) % n;
 | 
						|
        const i = chatStore.currentSessionIndex;
 | 
						|
        if (e.key === "ArrowUp") {
 | 
						|
          chatStore.selectSession(limit(i - 1));
 | 
						|
        } else if (e.key === "ArrowDown") {
 | 
						|
          chatStore.selectSession(limit(i + 1));
 | 
						|
        }
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    window.addEventListener("keydown", onKeyDown);
 | 
						|
    return () => window.removeEventListener("keydown", onKeyDown);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function useDragSideBar() {
 | 
						|
  const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 | 
						|
 | 
						|
  const config = useAppConfig();
 | 
						|
  const startX = useRef(0);
 | 
						|
  const startDragWidth = useRef(config.sidebarWidth ?? 300);
 | 
						|
  const lastUpdateTime = useRef(Date.now());
 | 
						|
 | 
						|
  const handleMouseMove = useRef((e: MouseEvent) => {
 | 
						|
    if (Date.now() < lastUpdateTime.current + 50) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    lastUpdateTime.current = Date.now();
 | 
						|
    const d = e.clientX - startX.current;
 | 
						|
    const nextWidth = limit(startDragWidth.current + d);
 | 
						|
    config.update((config) => (config.sidebarWidth = nextWidth));
 | 
						|
  });
 | 
						|
 | 
						|
  const handleMouseUp = useRef(() => {
 | 
						|
    startDragWidth.current = config.sidebarWidth ?? 300;
 | 
						|
    window.removeEventListener("mousemove", handleMouseMove.current);
 | 
						|
    window.removeEventListener("mouseup", handleMouseUp.current);
 | 
						|
  });
 | 
						|
 | 
						|
  const onDragMouseDown = (e: MouseEvent) => {
 | 
						|
    startX.current = e.clientX;
 | 
						|
 | 
						|
    window.addEventListener("mousemove", handleMouseMove.current);
 | 
						|
    window.addEventListener("mouseup", handleMouseUp.current);
 | 
						|
  };
 | 
						|
  const isMobileScreen = useMobileScreen();
 | 
						|
  const shouldNarrow =
 | 
						|
    !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    const barWidth = shouldNarrow
 | 
						|
      ? NARROW_SIDEBAR_WIDTH
 | 
						|
      : limit(config.sidebarWidth ?? 300);
 | 
						|
    const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
 | 
						|
    document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
 | 
						|
  }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
 | 
						|
 | 
						|
  return {
 | 
						|
    onDragMouseDown,
 | 
						|
    shouldNarrow,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export function SideBar(props: { className?: string }) {
 | 
						|
  const chatStore = useChatStore();
 | 
						|
 | 
						|
  // drag side bar
 | 
						|
  const { onDragMouseDown, shouldNarrow } = useDragSideBar();
 | 
						|
  const navigate = useNavigate();
 | 
						|
  const config = useAppConfig();
 | 
						|
 | 
						|
  useHotKey();
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className={`${styles.sidebar} ${props.className} ${
 | 
						|
        shouldNarrow && styles["narrow-sidebar"]
 | 
						|
      }`}
 | 
						|
    >
 | 
						|
      <div className={styles["sidebar-header"]}>
 | 
						|
        <div className={styles["sidebar-title"]}>ChatGPT Next</div>
 | 
						|
        <div className={styles["sidebar-sub-title"]}>
 | 
						|
          Build your own AI assistant.
 | 
						|
        </div>
 | 
						|
        <div className={styles["sidebar-logo"] + " no-dark"}>
 | 
						|
          <ChatGptIcon />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
 | 
						|
      <div className={styles["sidebar-header-bar"]}>
 | 
						|
        <IconButton
 | 
						|
          icon={<MaskIcon />}
 | 
						|
          text={shouldNarrow ? undefined : Locale.Mask.Name}
 | 
						|
          className={styles["sidebar-bar-button"]}
 | 
						|
          onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
 | 
						|
          shadow
 | 
						|
        />
 | 
						|
        <IconButton
 | 
						|
          icon={<PluginIcon />}
 | 
						|
          text={shouldNarrow ? undefined : Locale.Plugin.Name}
 | 
						|
          className={styles["sidebar-bar-button"]}
 | 
						|
          onClick={() => showToast(Locale.WIP)}
 | 
						|
          shadow
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
 | 
						|
      <div
 | 
						|
        className={styles["sidebar-body"]}
 | 
						|
        onClick={(e) => {
 | 
						|
          if (e.target === e.currentTarget) {
 | 
						|
            navigate(Path.Home);
 | 
						|
          }
 | 
						|
        }}
 | 
						|
      >
 | 
						|
        <ChatList narrow={shouldNarrow} />
 | 
						|
      </div>
 | 
						|
 | 
						|
      <div className={styles["sidebar-tail"]}>
 | 
						|
        <div className={styles["sidebar-actions"]}>
 | 
						|
          <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
						|
            <IconButton
 | 
						|
              icon={<CloseIcon />}
 | 
						|
              onClick={() => {
 | 
						|
                if (confirm(Locale.Home.DeleteChat)) {
 | 
						|
                  chatStore.deleteSession(chatStore.currentSessionIndex);
 | 
						|
                }
 | 
						|
              }}
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
          <div className={styles["sidebar-action"]}>
 | 
						|
            <Link to={Path.Settings}>
 | 
						|
              <IconButton icon={<SettingsIcon />} shadow />
 | 
						|
            </Link>
 | 
						|
          </div>
 | 
						|
          <div className={styles["sidebar-action"]}>
 | 
						|
            <a href={REPO_URL} target="_blank">
 | 
						|
              <IconButton icon={<GithubIcon />} shadow />
 | 
						|
            </a>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
        <div>
 | 
						|
          <IconButton
 | 
						|
            icon={<AddIcon />}
 | 
						|
            text={shouldNarrow ? undefined : Locale.Home.NewChat}
 | 
						|
            onClick={() => {
 | 
						|
              if (config.dontShowMaskSplashScreen) {
 | 
						|
                chatStore.newSession();
 | 
						|
                navigate(Path.Chat);
 | 
						|
              } else {
 | 
						|
                navigate(Path.NewChat);
 | 
						|
              }
 | 
						|
            }}
 | 
						|
            shadow
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
 | 
						|
      <div
 | 
						|
        className={styles["sidebar-drag"]}
 | 
						|
        onMouseDown={(e) => onDragMouseDown(e as any)}
 | 
						|
      ></div>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}
 |