Merge pull request #959 from Yidadaa/0420-mask
refactor: close #643 use react router
This commit is contained in:
commit
a62bca442e
@ -10,6 +10,8 @@ import {
|
|||||||
import { useChatStore } from "../store";
|
import { useChatStore } from "../store";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
export function ChatItem(props: {
|
export function ChatItem(props: {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@ -20,6 +22,7 @@ export function ChatItem(props: {
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
index: number;
|
index: number;
|
||||||
|
narrow?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||||
@ -33,6 +36,10 @@ export function ChatItem(props: {
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
>
|
>
|
||||||
|
{props.narrow ? (
|
||||||
|
<div className={styles["chat-item-narrow"]}>{props.count}</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className={styles["chat-item-title"]}>{props.title}</div>
|
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||||
<div className={styles["chat-item-info"]}>
|
<div className={styles["chat-item-info"]}>
|
||||||
<div className={styles["chat-item-count"]}>
|
<div className={styles["chat-item-count"]}>
|
||||||
@ -40,6 +47,9 @@ export function ChatItem(props: {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +59,7 @@ export function ChatItem(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatList() {
|
export function ChatList(props: { narrow?: boolean }) {
|
||||||
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
|
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
|
||||||
useChatStore((state) => [
|
useChatStore((state) => [
|
||||||
state.sessions,
|
state.sessions,
|
||||||
@ -59,6 +69,7 @@ export function ChatList() {
|
|||||||
state.moveSession,
|
state.moveSession,
|
||||||
]);
|
]);
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = (result) => {
|
const onDragEnd: OnDragEndResponder = (result) => {
|
||||||
const { destination, source } = result;
|
const { destination, source } = result;
|
||||||
@ -94,8 +105,16 @@ export function ChatList() {
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
index={i}
|
index={i}
|
||||||
selected={i === selectedIndex}
|
selected={i === selectedIndex}
|
||||||
onClick={() => selectSession(i)}
|
onClick={() => {
|
||||||
onDelete={() => chatStore.deleteSession(i)}
|
navigate(Path.Chat);
|
||||||
|
selectSession(i);
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
|
||||||
|
chatStore.deleteSession(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
narrow={props.narrow}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
@ -54,6 +54,8 @@ import styles from "./home.module.scss";
|
|||||||
import chatStyle from "./chat.module.scss";
|
import chatStyle from "./chat.module.scss";
|
||||||
|
|
||||||
import { Input, Modal, showModal } from "./ui-lib";
|
import { Input, Modal, showModal } from "./ui-lib";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
const Markdown = dynamic(
|
const Markdown = dynamic(
|
||||||
async () => memo((await import("./markdown")).Markdown),
|
async () => memo((await import("./markdown")).Markdown),
|
||||||
@ -418,10 +420,7 @@ export function ChatActions(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat(props: {
|
export function Chat() {
|
||||||
showSideBar?: () => void;
|
|
||||||
sideBarShowing?: boolean;
|
|
||||||
}) {
|
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@ -439,6 +438,7 @@ export function Chat(props: {
|
|||||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||||
const [hitBottom, setHitBottom] = useState(false);
|
const [hitBottom, setHitBottom] = useState(false);
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onChatBodyScroll = (e: HTMLElement) => {
|
const onChatBodyScroll = (e: HTMLElement) => {
|
||||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||||
@ -641,7 +641,7 @@ export function Chat(props: {
|
|||||||
|
|
||||||
// Auto focus
|
// Auto focus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.sideBarShowing && isMobileScreen) return;
|
if (isMobileScreen) return;
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@ -666,7 +666,7 @@ export function Chat(props: {
|
|||||||
icon={<ReturnIcon />}
|
icon={<ReturnIcon />}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Chat.Actions.ChatList}
|
title={Locale.Chat.Actions.ChatList}
|
||||||
onClick={props?.showSideBar}
|
onClick={() => navigate(Path.Home)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -830,7 +830,7 @@ export function Chat(props: {
|
|||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
setTimeout(() => setPromptHints([]), 500);
|
setTimeout(() => setPromptHints([]), 500);
|
||||||
}}
|
}}
|
||||||
autoFocus={!props?.sideBarShowing}
|
autoFocus
|
||||||
rows={inputRows}
|
rows={inputRows}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: width ease 0.1s;
|
transition: width ease 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-drag {
|
.sidebar-drag {
|
||||||
@ -126,11 +126,13 @@
|
|||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sub-title {
|
.sidebar-sub-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400px;
|
font-weight: 400px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-body {
|
.sidebar-body {
|
||||||
@ -171,6 +173,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item-delete {
|
.chat-item-delete {
|
||||||
@ -197,6 +200,7 @@
|
|||||||
color: rgb(166, 166, 166);
|
color: rgb(166, 166, 166);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item-count,
|
.chat-item-count,
|
||||||
@ -206,6 +210,69 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.narrow-sidebar {
|
||||||
|
.sidebar-title,
|
||||||
|
.sidebar-sub-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.chat-item-narrow {
|
||||||
|
transform: scale(0.7) translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-narrow {
|
||||||
|
font-weight: bolder;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 0;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: var(--black);
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
opacity: 0.1;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-delete {
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item:hover > .chat-item-delete {
|
||||||
|
opacity: 0.5;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tail {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.sidebar-action {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-tail {
|
.sidebar-tail {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -2,32 +2,31 @@
|
|||||||
|
|
||||||
require("../polyfill");
|
require("../polyfill");
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
import SettingsIcon from "../icons/settings.svg";
|
|
||||||
import GithubIcon from "../icons/github.svg";
|
|
||||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
|
||||||
|
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
import AddIcon from "../icons/add.svg";
|
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
|
||||||
|
|
||||||
import { useChatStore } from "../store";
|
import { useChatStore } from "../store";
|
||||||
import { getCSSVar, useMobileScreen } from "../utils";
|
import { getCSSVar, useMobileScreen } from "../utils";
|
||||||
import Locale from "../locales";
|
|
||||||
import { Chat } from "./chat";
|
import { Chat } from "./chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { REPO_URL } from "../constant";
|
import { Path } from "../constant";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HashRouter as Router,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
useLocation,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles["loading-content"]}>
|
<div className={styles["loading-content"] + " no-dark"}>
|
||||||
{!props.noLogo && <BotIcon />}
|
{!props.noLogo && <BotIcon />}
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
</div>
|
</div>
|
||||||
@ -38,11 +37,11 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
|||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,50 +72,6 @@ function useSwitchTheme() {
|
|||||||
}, [config.theme]);
|
}, [config.theme]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
const isMobileScreen = useMobileScreen();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sideBarWidth = isMobileScreen
|
|
||||||
? "100vw"
|
|
||||||
: `${limit(chatStore.config.sidebarWidth ?? 300)}px`;
|
|
||||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
|
||||||
}, [chatStore.config.sidebarWidth, isMobileScreen]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
onDragMouseDown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const useHasHydrated = () => {
|
const useHasHydrated = () => {
|
||||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -127,130 +82,58 @@ const useHasHydrated = () => {
|
|||||||
return hasHydrated;
|
return hasHydrated;
|
||||||
};
|
};
|
||||||
|
|
||||||
function _Home() {
|
function WideScreen() {
|
||||||
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
|
||||||
(state) => [
|
|
||||||
state.newSession,
|
|
||||||
state.currentSessionIndex,
|
|
||||||
state.removeSession,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const chatStore = useChatStore();
|
|
||||||
const loading = !useHasHydrated();
|
|
||||||
const [showSideBar, setShowSideBar] = useState(true);
|
|
||||||
|
|
||||||
// setting
|
|
||||||
const [openSettings, setOpenSettings] = useState(false);
|
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
// drag side bar
|
|
||||||
const { onDragMouseDown } = useDragSideBar();
|
|
||||||
const isMobileScreen = useMobileScreen();
|
|
||||||
|
|
||||||
useSwitchTheme();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
config.tightBorder && !isMobileScreen
|
config.tightBorder ? styles["tight-container"] : styles.container
|
||||||
? styles["tight-container"]
|
|
||||||
: styles.container
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<SideBar />
|
||||||
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
icon={<AddIcon />}
|
|
||||||
text={Locale.Home.NewChat}
|
|
||||||
onClick={() => {
|
|
||||||
createNewSession();
|
|
||||||
setShowSideBar(false);
|
|
||||||
}}
|
|
||||||
shadow
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles["sidebar-drag"]}
|
|
||||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles["window-content"]}>
|
<div className={styles["window-content"]}>
|
||||||
{openSettings ? (
|
<Routes>
|
||||||
<Settings
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
closeSettings={() => {
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
setOpenSettings(false);
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
setShowSideBar(true);
|
</Routes>
|
||||||
}}
|
</div>
|
||||||
/>
|
</div>
|
||||||
) : (
|
);
|
||||||
<Chat
|
}
|
||||||
key="chat"
|
|
||||||
showSideBar={() => setShowSideBar(true)}
|
function MobileScreen() {
|
||||||
sideBarShowing={showSideBar}
|
const location = useLocation();
|
||||||
/>
|
const isHome = location.pathname === Path.Home;
|
||||||
)}
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||||
|
|
||||||
|
<div className={styles["window-content"]}>
|
||||||
|
<Routes>
|
||||||
|
<Route path={Path.Home} element={null} />
|
||||||
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
useSwitchTheme();
|
||||||
|
|
||||||
|
if (!useHasHydrated()) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<_Home></_Home>
|
<Router>{isMobileScreen ? <MobileScreen /> : <WideScreen />}</Router>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,11 @@ import { Avatar } from "./chat";
|
|||||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||||
import { copyToClipboard, getEmojiUrl } from "../utils";
|
import { copyToClipboard, getEmojiUrl } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UPDATE_URL } from "../constant";
|
import { Path, UPDATE_URL } from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function UserPromptModal(props: { onClose?: () => void }) {
|
function UserPromptModal(props: { onClose?: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
@ -176,7 +177,8 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Settings(props: { closeSettings: () => void }) {
|
export function Settings() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
||||||
useChatStore((state) => [
|
useChatStore((state) => [
|
||||||
@ -235,7 +237,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keydownEvent = (e: KeyboardEvent) => {
|
const keydownEvent = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
props.closeSettings();
|
navigate(Path.Home);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", keydownEvent);
|
document.addEventListener("keydown", keydownEvent);
|
||||||
@ -290,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon />}
|
icon={<CloseIcon />}
|
||||||
onClick={props.closeSettings}
|
onClick={() => navigate(Path.Home)}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Settings.Actions.Close}
|
title={Locale.Settings.Actions.Close}
|
||||||
/>
|
/>
|
||||||
|
146
app/components/sidebar.tsx
Normal file
146
app/components/sidebar.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { useState, 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 Locale from "../locales";
|
||||||
|
|
||||||
|
import { useChatStore } from "../store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MAX_SIDEBAR_WIDTH,
|
||||||
|
MIN_SIDEBAR_WIDTH,
|
||||||
|
NARROW_SIDEBAR_WIDTH,
|
||||||
|
Path,
|
||||||
|
REPO_URL,
|
||||||
|
} from "../constant";
|
||||||
|
|
||||||
|
import { HashRouter as Router, Link, useNavigate } from "react-router-dom";
|
||||||
|
import { useMobileScreen } from "../utils";
|
||||||
|
import { ChatList } from "./chat-list";
|
||||||
|
|
||||||
|
function useDragSideBar() {
|
||||||
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, 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 + 50) {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const shouldNarrow =
|
||||||
|
!isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const barWidth = shouldNarrow
|
||||||
|
? NARROW_SIDEBAR_WIDTH
|
||||||
|
: limit(chatStore.config.sidebarWidth ?? 300);
|
||||||
|
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||||
|
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||||
|
}, [chatStore.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();
|
||||||
|
|
||||||
|
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"]}>
|
||||||
|
<ChatGptIcon />
|
||||||
|
</div>
|
||||||
|
</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={chatStore.deleteSession}
|
||||||
|
/>
|
||||||
|
</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={() => {
|
||||||
|
chatStore.newSession();
|
||||||
|
}}
|
||||||
|
shadow
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["sidebar-drag"]}
|
||||||
|
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -6,3 +6,13 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`;
|
|||||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
|
export enum Path {
|
||||||
|
Home = "/",
|
||||||
|
Chat = "/chat",
|
||||||
|
Settings = "/settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_SIDEBAR_WIDTH = 500;
|
||||||
|
export const MIN_SIDEBAR_WIDTH = 230;
|
||||||
|
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||||
|
@ -49,7 +49,7 @@ export function isIOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useMobileScreen() {
|
export function useMobileScreen() {
|
||||||
const [isMobileScreen_, setIsMobileScreen] = useState(false);
|
const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
setIsMobileScreen(isMobileScreen());
|
setIsMobileScreen(isMobileScreen());
|
||||||
@ -66,6 +66,9 @@ export function useMobileScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isMobileScreen() {
|
export function isMobileScreen() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return window.innerWidth <= 600;
|
return window.innerWidth <= 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
|
"react-router-dom": "^6.10.0",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.2",
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -1189,6 +1189,11 @@
|
|||||||
tiny-glob "^0.2.9"
|
tiny-glob "^0.2.9"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@remix-run/router@1.5.0":
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc"
|
||||||
|
integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==
|
||||||
|
|
||||||
"@rushstack/eslint-patch@^1.1.3":
|
"@rushstack/eslint-patch@^1.1.3":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
|
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
|
||||||
@ -4296,6 +4301,21 @@ react-redux@^8.0.4:
|
|||||||
react-is "^18.0.0"
|
react-is "^18.0.0"
|
||||||
use-sync-external-store "^1.0.0"
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
|
react-router-dom@^6.10.0:
|
||||||
|
version "6.10.0"
|
||||||
|
resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f"
|
||||||
|
integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/router" "1.5.0"
|
||||||
|
react-router "6.10.0"
|
||||||
|
|
||||||
|
react-router@6.10.0:
|
||||||
|
version "6.10.0"
|
||||||
|
resolved "https://registry.npmmirror.com/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971"
|
||||||
|
integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/router" "1.5.0"
|
||||||
|
|
||||||
react@^18.2.0:
|
react@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user