|
@@ -1,4 +1,4 @@
|
|
|
-import { useEffect, useRef, useMemo } from "react";
|
|
|
|
|
|
|
+import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
|
|
|
|
|
|
|
|
import styles from "./home.module.scss";
|
|
import styles from "./home.module.scss";
|
|
|
|
|
|
|
@@ -10,8 +10,8 @@ import AddIcon from "../icons/add.svg";
|
|
|
import CloseIcon from "../icons/close.svg";
|
|
import CloseIcon from "../icons/close.svg";
|
|
|
import DeleteIcon from "../icons/delete.svg";
|
|
import DeleteIcon from "../icons/delete.svg";
|
|
|
import MaskIcon from "../icons/mask.svg";
|
|
import MaskIcon from "../icons/mask.svg";
|
|
|
-import PluginIcon from "../icons/plugin.svg";
|
|
|
|
|
import DragIcon from "../icons/drag.svg";
|
|
import DragIcon from "../icons/drag.svg";
|
|
|
|
|
+import DiscoveryIcon from "../icons/discovery.svg";
|
|
|
|
|
|
|
|
import Locale from "../locales";
|
|
import Locale from "../locales";
|
|
|
|
|
|
|
@@ -23,19 +23,20 @@ import {
|
|
|
MIN_SIDEBAR_WIDTH,
|
|
MIN_SIDEBAR_WIDTH,
|
|
|
NARROW_SIDEBAR_WIDTH,
|
|
NARROW_SIDEBAR_WIDTH,
|
|
|
Path,
|
|
Path,
|
|
|
|
|
+ PLUGINS,
|
|
|
REPO_URL,
|
|
REPO_URL,
|
|
|
} from "../constant";
|
|
} from "../constant";
|
|
|
|
|
|
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
|
import { isIOS, useMobileScreen } from "../utils";
|
|
import { isIOS, useMobileScreen } from "../utils";
|
|
|
import dynamic from "next/dynamic";
|
|
import dynamic from "next/dynamic";
|
|
|
-import { showConfirm, showToast } from "./ui-lib";
|
|
|
|
|
|
|
+import { showConfirm, Selector } from "./ui-lib";
|
|
|
|
|
|
|
|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
|
|
loading: () => null,
|
|
loading: () => null,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-function useHotKey() {
|
|
|
|
|
|
|
+export function useHotKey() {
|
|
|
const chatStore = useChatStore();
|
|
const chatStore = useChatStore();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -54,7 +55,7 @@ function useHotKey() {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function useDragSideBar() {
|
|
|
|
|
|
|
+export function useDragSideBar() {
|
|
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
|
|
|
|
|
|
|
const config = useAppConfig();
|
|
const config = useAppConfig();
|
|
@@ -127,25 +128,21 @@ function useDragSideBar() {
|
|
|
shouldNarrow,
|
|
shouldNarrow,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-export function SideBar(props: { className?: string }) {
|
|
|
|
|
- const chatStore = useChatStore();
|
|
|
|
|
-
|
|
|
|
|
- // drag side bar
|
|
|
|
|
- const { onDragStart, shouldNarrow } = useDragSideBar();
|
|
|
|
|
- const navigate = useNavigate();
|
|
|
|
|
- const config = useAppConfig();
|
|
|
|
|
|
|
+export function SideBarContainer(props: {
|
|
|
|
|
+ children: React.ReactNode;
|
|
|
|
|
+ onDragStart: (e: MouseEvent) => void;
|
|
|
|
|
+ shouldNarrow: boolean;
|
|
|
|
|
+ className?: string;
|
|
|
|
|
+}) {
|
|
|
const isMobileScreen = useMobileScreen();
|
|
const isMobileScreen = useMobileScreen();
|
|
|
const isIOSMobile = useMemo(
|
|
const isIOSMobile = useMemo(
|
|
|
() => isIOS() && isMobileScreen,
|
|
() => isIOS() && isMobileScreen,
|
|
|
[isMobileScreen],
|
|
[isMobileScreen],
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
- useHotKey();
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const { children, className, onDragStart, shouldNarrow } = props;
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
- className={`${styles.sidebar} ${props.className} ${
|
|
|
|
|
|
|
+ className={`${styles.sidebar} ${className} ${
|
|
|
shouldNarrow && styles["narrow-sidebar"]
|
|
shouldNarrow && styles["narrow-sidebar"]
|
|
|
}`}
|
|
}`}
|
|
|
style={{
|
|
style={{
|
|
@@ -153,43 +150,128 @@ export function SideBar(props: { className?: string }) {
|
|
|
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
|
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
|
|
+ {children}
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={styles["sidebar-drag"]}
|
|
|
|
|
+ onPointerDown={(e) => onDragStart(e as any)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <DragIcon />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function SideBarHeader(props: {
|
|
|
|
|
+ title?: string | React.ReactNode;
|
|
|
|
|
+ subTitle?: string | React.ReactNode;
|
|
|
|
|
+ logo?: React.ReactNode;
|
|
|
|
|
+ children?: React.ReactNode;
|
|
|
|
|
+}) {
|
|
|
|
|
+ const { title, subTitle, logo, children } = props;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Fragment>
|
|
|
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
|
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
|
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
|
|
- NextChat
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className={styles["sidebar-sub-title"]}>
|
|
|
|
|
- Build your own AI assistant.
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className={styles["sidebar-logo"] + " no-dark"}>
|
|
|
|
|
- <ChatGptIcon />
|
|
|
|
|
|
|
+ {title}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
|
|
|
|
+ <div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </Fragment>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- <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={<PluginIcon />}
|
|
|
|
|
- text={shouldNarrow ? undefined : Locale.Plugin.Name}
|
|
|
|
|
- className={styles["sidebar-bar-button"]}
|
|
|
|
|
- onClick={() => showToast(Locale.WIP)}
|
|
|
|
|
- shadow
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+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>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- <div
|
|
|
|
|
- className={styles["sidebar-body"]}
|
|
|
|
|
|
|
+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="NextChat"
|
|
|
|
|
+ subTitle="Build your own AI assistant."
|
|
|
|
|
+ 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) => {
|
|
onClick={(e) => {
|
|
|
if (e.target === e.currentTarget) {
|
|
if (e.target === e.currentTarget) {
|
|
|
navigate(Path.Home);
|
|
navigate(Path.Home);
|
|
@@ -197,32 +279,33 @@ export function SideBar(props: { className?: string }) {
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
<ChatList narrow={shouldNarrow} />
|
|
<ChatList narrow={shouldNarrow} />
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className={styles["sidebar-tail"]}>
|
|
|
|
|
- <div className={styles["sidebar-actions"]}>
|
|
|
|
|
- <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>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
|
|
+ </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
|
|
<IconButton
|
|
|
icon={<AddIcon />}
|
|
icon={<AddIcon />}
|
|
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
|
@@ -236,15 +319,8 @@ export function SideBar(props: { className?: string }) {
|
|
|
}}
|
|
}}
|
|
|
shadow
|
|
shadow
|
|
|
/>
|
|
/>
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div
|
|
|
|
|
- className={styles["sidebar-drag"]}
|
|
|
|
|
- onPointerDown={(e) => onDragStart(e as any)}
|
|
|
|
|
- >
|
|
|
|
|
- <DragIcon />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ </SideBarContainer>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|