ChatGPT-Next-Web/app/components/home.tsx

265 lines
7.1 KiB
TypeScript
Raw Normal View History

2023-03-09 17:01:40 +00:00
"use client";
require("../polyfill");
2023-04-10 18:54:31 +00:00
import { useState, useEffect, useRef } from "react";
2023-03-11 17:14:07 +00:00
2023-03-09 17:01:40 +00:00
import { IconButton } from "./button";
2023-03-12 19:06:21 +00:00
import styles from "./home.module.scss";
2023-03-09 17:01:40 +00:00
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
2023-04-02 15:05:54 +00:00
2023-03-09 17:01:40 +00:00
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
2023-03-10 18:25:33 +00:00
import LoadingIcon from "../icons/three-dots.svg";
2023-03-14 17:44:42 +00:00
import CloseIcon from "../icons/close.svg";
2023-03-10 18:25:33 +00:00
import { useChatStore } from "../store";
import { isMobileScreen } from "../utils";
2023-03-21 14:56:27 +00:00
import Locale from "../locales";
2023-04-02 15:05:54 +00:00
import { Chat } from "./chat";
2023-03-13 16:25:07 +00:00
import dynamic from "next/dynamic";
2023-03-23 16:01:00 +00:00
import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error";
2023-03-21 14:56:27 +00:00
export function Loading(props: { noLogo?: boolean }) {
return (
<div className={styles["loading-content"]}>
{!props.noLogo && <BotIcon />}
<LoadingIcon />
</div>
);
}
2023-03-21 14:56:27 +00:00
const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />,
});
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => <Loading noLogo />,
2023-03-21 14:56:27 +00:00
});
2023-03-11 12:54:24 +00:00
2023-03-12 19:06:21 +00:00
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
2023-03-12 19:06:21 +00:00
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
}
const metaDescriptionDark = document.querySelector(
'meta[name="theme-color"][media]',
);
const metaDescriptionLight = document.querySelector(
'meta[name="theme-color"]:not([media])',
);
2023-03-15 17:24:03 +00:00
if (config.theme === "auto") {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
} else {
const themeColor = getComputedStyle(document.body)
.getPropertyValue("--theme-color")
.trim();
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
}
}, [config.theme]);
2023-03-19 15:13:10 +00:00
}
2023-04-09 16:54:17 +00:00
function useDragSideBar() {
const limit = (x: number) => Math.min(500, Math.max(220, x));
const chatStore = useChatStore();
const startX = useRef(0);
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 100) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = chatStore.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);
};
useEffect(() => {
if (isMobileScreen()) {
return;
}
2023-04-09 16:54:17 +00:00
document.documentElement.style.setProperty(
"--sidebar-width",
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
);
}, [chatStore.config.sidebarWidth]);
return {
onDragMouseDown,
};
}
2023-03-27 08:58:53 +00:00
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
useEffect(() => {
setHasHydrated(true);
}, []);
return hasHydrated;
};
function _Home() {
2023-03-21 14:56:27 +00:00
const [createNewSession, currentIndex, removeSession] = useChatStore(
(state) => [
state.newSession,
state.currentSessionIndex,
2023-04-01 18:56:45 +00:00
state.removeSession,
],
2023-03-21 14:56:27 +00:00
);
2023-04-06 16:14:27 +00:00
const chatStore = useChatStore();
2023-03-27 08:58:53 +00:00
const loading = !useHasHydrated();
2023-03-14 17:44:42 +00:00
const [showSideBar, setShowSideBar] = useState(true);
2023-03-10 18:25:33 +00:00
2023-03-16 16:08:16 +00:00
// setting
2023-03-11 17:14:07 +00:00
const [openSettings, setOpenSettings] = useState(false);
2023-03-12 19:21:48 +00:00
const config = useChatStore((state) => state.config);
2023-03-11 17:14:07 +00:00
2023-04-09 16:54:17 +00:00
// drag side bar
const { onDragMouseDown } = useDragSideBar();
2023-03-12 19:06:21 +00:00
useSwitchTheme();
2023-03-13 16:25:07 +00:00
if (loading) {
return <Loading />;
2023-03-13 16:25:07 +00:00
}
2023-03-09 17:01:40 +00:00
return (
2023-04-10 18:54:31 +00:00
<>
2023-03-14 17:44:42 +00:00
<div
2023-04-10 18:54:31 +00:00
className={`${
config.tightBorder && !isMobileScreen()
? styles["tight-container"]
: styles.container
}`}
2023-03-14 17:44:42 +00:00
>
2023-03-12 19:06:21 +00:00
<div
2023-04-10 18:54:31 +00:00
className={
styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`
}
2023-03-12 19:06:21 +00:00
>
2023-04-10 18:54:31 +00:00
<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"]}>
<ChatGptIcon />
</div>
</div>
2023-03-09 17:01:40 +00:00
2023-04-10 18:54:31 +00:00
<div
className={styles["sidebar-body"]}
onClick={() => {
setOpenSettings(false);
setShowSideBar(false);
}}
>
<ChatList />
</div>
<div className={styles["sidebar-tail"]}>
<div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
onClick={chatStore.deleteSession}
/>
</div>
<div className={styles["sidebar-action"]}>
<IconButton
icon={<SettingsIcon />}
onClick={() => {
setOpenSettings(true);
setShowSideBar(false);
}}
shadow
/>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank">
<IconButton icon={<GithubIcon />} shadow />
</a>
</div>
2023-03-14 17:44:42 +00:00
</div>
2023-04-10 18:54:31 +00:00
<div>
2023-03-11 17:14:07 +00:00
<IconButton
2023-04-10 18:54:31 +00:00
icon={<AddIcon />}
text={Locale.Home.NewChat}
onClick={() => {
2023-04-10 18:54:31 +00:00
createNewSession();
2023-03-21 14:56:27 +00:00
setShowSideBar(false);
}}
shadow
2023-03-11 17:14:07 +00:00
/>
2023-03-09 17:01:40 +00:00
</div>
</div>
2023-04-10 18:54:31 +00:00
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
</div>
<div className={styles["window-content"]}>
{openSettings ? (
<Settings
closeSettings={() => {
setOpenSettings(false);
setShowSideBar(true);
}}
2023-03-10 18:25:33 +00:00
/>
2023-04-10 18:54:31 +00:00
) : (
<Chat
key="chat"
showSideBar={() => setShowSideBar(true)}
sideBarShowing={showSideBar}
/>
)}
2023-03-09 17:01:40 +00:00
</div>
2023-03-11 17:14:07 +00:00
</div>
2023-04-10 18:54:31 +00:00
</>
2023-03-09 17:01:40 +00:00
);
}
export function Home() {
return (
<ErrorBoundary>
<_Home></_Home>
</ErrorBoundary>
);
}