mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-08 19:16:37 +08:00
238 lines
6.8 KiB
TypeScript
238 lines
6.8 KiB
TypeScript
import React, { useEffect, useRef, useMemo, useState, Fragment } 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 DeleteIcon from "../icons/delete.svg";
|
|
import MaskIcon from "../icons/mask.svg";
|
|
import DragIcon from "../icons/drag.svg";
|
|
import DiscoveryIcon from "../icons/discovery.svg";
|
|
|
|
import Locale from "../locales";
|
|
|
|
import { useAppConfig, useChatStore } from "../store";
|
|
|
|
|
|
|
|
import {
|
|
DEFAULT_SIDEBAR_WIDTH,
|
|
MAX_SIDEBAR_WIDTH,
|
|
MIN_SIDEBAR_WIDTH,
|
|
NARROW_SIDEBAR_WIDTH,
|
|
Path,
|
|
PLUGINS,
|
|
REPO_URL,
|
|
} from "../constant";
|
|
|
|
import { signOut } from "next-auth/react";
|
|
import dynamic from "next/dynamic";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { isIOS, useMobileScreen } from "../utils";
|
|
import dynamic from "next/dynamic";
|
|
import { showConfirm, Selector } from "./ui-lib";
|
|
|
|
|
|
|
|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
|
loading: () => null,
|
|
});
|
|
|
|
export function useHotKey() {
|
|
const chatStore = useChatStore();
|
|
|
|
useEffect(() => {
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (e.altKey || e.ctrlKey) {
|
|
if (e.key === "ArrowUp") {
|
|
chatStore.nextSession(-1);
|
|
} else if (e.key === "ArrowDown") {
|
|
chatStore.nextSession(1);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
});
|
|
}
|
|
|
|
export function useDragSideBar() {
|
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
|
|
|
const config = useAppConfig();
|
|
const startX = useRef(0);
|
|
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
|
const lastUpdateTime = useRef(Date.now());
|
|
|
|
const toggleSideBar = () => {
|
|
config.update((config) => {
|
|
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
|
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
|
} else {
|
|
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Fragment>
|
|
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
|
{title}
|
|
</div>
|
|
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
|
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
|
</div>
|
|
{children}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
export function SideBarBody(props: {
|
|
children: React.ReactNode;
|
|
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
|
}) {
|
|
const { onClick, children } = props;
|
|
return (
|
|
<div className={styles["sidebar-body"]} onClick={onClick}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SideBarTail(props: {
|
|
primaryAction?: React.ReactNode;
|
|
secondaryAction?: React.ReactNode;
|
|
}) {
|
|
const { primaryAction, secondaryAction } = props;
|
|
|
|
return (
|
|
<div className={styles["sidebar-tail"]}>
|
|
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
|
|
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SideBar(props: { className?: string }) {
|
|
useHotKey();
|
|
const { onDragStart, shouldNarrow } = useDragSideBar();
|
|
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
|
const navigate = useNavigate();
|
|
const config = useAppConfig();
|
|
const chatStore = useChatStore();
|
|
|
|
return (
|
|
<SideBarContainer
|
|
onDragStart={onDragStart}
|
|
shouldNarrow={shouldNarrow}
|
|
{...props}
|
|
>
|
|
<SideBarHeader
|
|
title="AdExGPT - via API"
|
|
subTitle="secure local UI for OpenAI API."
|
|
logo={<ChatGptIcon />}
|
|
>
|
|
<div className={styles["sidebar-header-bar"]}>
|
|
<IconButton
|
|
icon={<MaskIcon />}
|
|
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
|
className={styles["sidebar-bar-button"]}
|
|
onClick={() => {
|
|
if (config.dontShowMaskSplashScreen !== true) {
|
|
navigate(Path.NewChat, { state: { fromHome: true } });
|
|
} else {
|
|
navigate(Path.Masks, { state: { fromHome: true } });
|
|
}
|
|
}}
|
|
shadow
|
|
/>
|
|
<IconButton
|
|
icon={<DiscoveryIcon />}
|
|
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
|
className={styles["sidebar-bar-button"]}
|
|
onClick={() => setShowPluginSelector(true)}
|
|
shadow
|
|
/>
|
|
</div>
|
|
{showPluginSelector && (
|
|
<Selector
|
|
items={[
|
|
{
|
|
title: "👇 Please select the plugin you need to use",
|
|
value: "-",
|
|
disable: true,
|
|
},
|
|
...PLUGINS.map((item) => {
|
|
return {
|
|
title: item.name,
|
|
value: item.path,
|
|
};
|
|
}),
|
|
]}
|
|
onClose={() => setShowPluginSelector(false)}
|
|
onSelection={(s) => {
|
|
navigate(s[0], { state: { fromHome: true } });
|
|
}}
|
|
/>
|
|
)}
|
|
</SideBarHeader>
|
|
<SideBarBody
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
navigate(Path.Home);
|
|
}
|
|
}}
|
|
>
|
|
<ChatList narrow={shouldNarrow} />
|
|
</SideBarBody>
|
|
<SideBarTail
|
|
primaryAction={
|
|
<>
|
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
|
<IconButton
|
|
icon={<DeleteIcon />}
|
|
onClick={async () => {
|
|
if (await showConfirm(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" rel="noopener noreferrer">
|
|
<IconButton icon={<GithubIcon />} shadow />
|
|
</a>
|
|
</div>
|
|
</>
|
|
}
|
|
secondaryAction={
|
|
<IconButton
|
|
icon={<AddIcon />}
|
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
|
onClick={() => {
|
|
if (config.dontShowMaskSplashScreen) {
|
|
chatStore.newSession();
|
|
navigate(Path.Chat);
|
|
} else {
|
|
navigate(Path.NewChat);
|
|
}
|
|
}}
|
|
shadow
|
|
/>
|
|
}
|
|
/>
|
|
</SideBarContainer>
|
|
);
|
|
}
|