refactor: init switching to nextjs router

This commit is contained in:
Fred
2024-05-10 14:57:55 +08:00
parent 00b1a9781d
commit 0c53579996
25 changed files with 473 additions and 123 deletions

102
app/(app)/chat/layout.tsx Normal file
View File

@@ -0,0 +1,102 @@
"use client";
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
Path,
} from "@/app/constant";
import useDrag from "@/app/hooks/useDrag";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { updateGlobalCSSVars } from "@/app/utils/client";
import { useRef, useState } from "react";
import { useAppConfig } from "@/app/store/config";
import React from "react";
import { AuthPage } from "@/app/components/auth";
import { SideBar } from "@/app/containers/Sidebar";
import Screen from "@/app/components/Screen";
import { useSwitchTheme } from "@/app/hooks/useSwitchTheme";
import Chat from "@/app/containers/Chat/ChatPanel";
export default function Layout({ children }: { children: React.ReactNode }) {
const [showPanel, setShowPanel] = useState(false);
const [externalProps, setExternalProps] = useState({});
const config = useAppConfig();
useSwitchTheme();
const isMobileScreen = useMobileScreen();
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
// drag side bar
const { onDragStart } = useDrag({
customToggle: () => {
config.update((config) => {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
});
},
customDragMove: (nextWidth: number) => {
const { menuWidth } = updateGlobalCSSVars(nextWidth);
document.documentElement.style.setProperty(
"--menu-width",
`${menuWidth}px`,
);
config.update((config) => {
config.sidebarWidth = nextWidth;
});
},
customLimit: (x: number) =>
Math.max(
MIN_SIDEBAR_WIDTH,
Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
),
});
return (
<div
className={`
w-[100%] relative bg-center
max-md:h-[100%]
md:flex md:my-2.5
`}
>
<div
className={`
flex flex-col px-6
h-[100%]
max-md:w-[100%] max-md:px-4 max-md:pb-4 max-md:flex-1
md:relative md:basis-sidebar md:pb-6 md:rounded-md md:bg-menu
`}
>
{children}
</div>
{!isMobileScreen && (
<div
className={`group/menu-dragger cursor-col-resize w-[0.25rem] flex items-center justify-center`}
onPointerDown={(e) => {
startDragWidth.current = config.sidebarWidth;
onDragStart(e as any);
}}
>
<div className="w-[2px] opacity-0 group-hover/menu-dragger:opacity-100 bg-menu-dragger h-[100%] rounded-[2px]">
&nbsp;
</div>
</div>
)}
<div
className={`
md:flex-1 md:h-[100%] md:w-page
max-md:transition-all max-md:duration-300 max-md:absolute max-md:top-0 max-md:max-h-[100vh] max-md:w-[100%] ${
showPanel ? "max-md:left-0" : "max-md:left-[101%]"
} max-md:z-10
`}
>
{/* <PanelComponent
{...props}
{...externalProps}
setShowPanel={setShowPanel}
setExternalProps={setExternalProps}
showPanel={showPanel}
/> */}
{/* {children} */}
<Chat></Chat>
</div>
</div>
);
}

137
app/(app)/chat/page.tsx Normal file
View File

@@ -0,0 +1,137 @@
"use client";
import {
DragDropContext,
Droppable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { useAppConfig, useChatStore } from "@/app/store";
import Locale from "@/app/locales";
// import { useLocation, useNavigate } from "react-router-dom";
import { useRouter, usePathname } from "next/navigation";
import AddIcon from "@/app/icons/addIcon.svg";
import NextChatTitle from "@/app/icons/nextchatTitle.svg";
import Modal from "@/app/components/Modal";
import SessionItem from "@/app/containers/Chat/components/SessionItem";
export default function Page() {
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
(state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.moveSession,
],
);
const config = useAppConfig();
const { isMobileScreen } = config;
const chatStore = useChatStore();
const pathname = usePathname();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
moveSession(source.index, destination.index);
};
return (
<div
className={`
h-[100%] flex flex-col
md:px-0
`}
>
<div data-tauri-drag-region>
<div
className={`
flex items-center justify-between
py-6 max-md:box-content max-md:h-0
md:py-7
`}
data-tauri-drag-region
>
<div className="">
<NextChatTitle />
</div>
<div
className="cursor-pointer "
onClick={() => {
// if (config.dontShowMaskSplashScreen) {
// chatStore.newSession();
// navigate(Path.Chat);
// } else {
// navigate(Path.NewChat);
// }
}}
>
<AddIcon />
</div>
</div>
<div
className={`pb-3 text-sm sm:text-sm-mobile text-text-chat-header-subtitle`}
>
Build your own AI assistant.
</div>
</div>
<div className={`flex-1 overflow-y-auto max-md:pb-chat-panel-mobile `}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chat-list">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={`w-[100%]`}
>
{sessions.map((item, i) => (
<SessionItem
title={item.topic}
time={new Date(item.lastUpdate).toLocaleString()}
count={item.messages.length}
key={item.id}
id={item.id}
index={i}
selected={i === selectedIndex}
onClick={() => {
// navigate(Path.Chat);
// selectSession(i);
}}
onDelete={async () => {
if (
await Modal.warn({
okText: Locale.ChatItem.DeleteOkBtn,
cancelText: Locale.ChatItem.DeleteCancelBtn,
title: Locale.ChatItem.DeleteTitle,
content: Locale.ChatItem.DeleteContent,
})
) {
chatStore.deleteSession(i);
}
}}
mask={item.mask}
isMobileScreen={isMobileScreen}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
}

21
app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,21 @@
"use client";
import React from "react";
import { AuthPage } from "@/app/components/auth";
import { SideBar } from "@/app/containers/Sidebar";
import Screen from "@/app/components/Screen";
export interface MenuWrapperInspectProps {
setExternalProps?: (v: Record<string, any>) => void;
setShowPanel?: (v: boolean) => void;
showPanel?: boolean;
[k: string]: any;
}
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<Screen noAuth={<AuthPage />} sidebar={<SideBar />}>
{children}
</Screen>
);
}

View File

@@ -0,0 +1,4 @@
import React from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useSearchParams } from "react-router-dom"; // import { useSearchParams } from "react-router-dom";
import { useSearchParams } from "next/navigation";
import Locale from "./locales"; import Locale from "./locales";
type Command = (param: string) => void; type Command = (param: string) => void;
@@ -14,22 +15,23 @@ interface Commands {
export function useCommand(commands: Commands = {}) { export function useCommand(commands: Commands = {}) {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => { // fixme: update commands
let shouldUpdate = false; // useEffect(() => {
searchParams.forEach((param, name) => { // let shouldUpdate = false;
const commandName = name as keyof Commands; // searchParams.forEach((param, name) => {
if (typeof commands[commandName] === "function") { // const commandName = name as keyof Commands;
commands[commandName]!(param); // if (typeof commands[commandName] === "function") {
searchParams.delete(name); // commands[commandName]!(param);
shouldUpdate = true; // searchParams.delete(name);
} // shouldUpdate = true;
}); // }
// });
if (shouldUpdate) { // if (shouldUpdate) {
setSearchParams(searchParams); // setSearchParams(searchParams);
} // }
// eslint-disable-next-line react-hooks/exhaustive-deps // // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, commands]); // }, [searchParams, commands]);
} }
interface ChatCommands { interface ChatCommands {

View File

@@ -53,6 +53,8 @@ export interface TriggerProps
const baseZIndex = 150; const baseZIndex = 150;
let div: HTMLDivElement | null = null;
const Modal = (props: ModalProps) => { const Modal = (props: ModalProps) => {
const { const {
onOk, onOk,
@@ -78,6 +80,16 @@ const Modal = (props: ModalProps) => {
const mergeOpen = visible ?? open; const mergeOpen = visible ?? open;
useLayoutEffect(() => {
const div: HTMLDivElement = document.createElement("div");
div.id = "confirm-root";
div.style.height = "0px";
document.body.appendChild(div);
}, []);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
};
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
onCancel?.(); onCancel?.();
@@ -121,7 +133,7 @@ const Modal = (props: ModalProps) => {
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}> <AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
<AlertDialog.Portal> <AlertDialog.Portal>
<AlertDialog.Overlay <AlertDialog.Overlay
className="bg-modal-mask fixed inset-0 animate-mask " className="fixed inset-0 bg-modal-mask animate-mask "
style={{ zIndex: baseZIndex - 1 }} style={{ zIndex: baseZIndex - 1 }}
onClick={() => { onClick={() => {
if (maskCloseble) { if (maskCloseble) {
@@ -165,7 +177,7 @@ const Modal = (props: ModalProps) => {
${titleClassName} ${titleClassName}
`} `}
> >
<div className="flex gap-3 justify-start flex-1 items-center text-text-modal-title text-chat-header-title"> <div className="flex items-center justify-start flex-1 gap-3 text-text-modal-title text-chat-header-title">
{title} {title}
</div> </div>
{closeble && ( {closeble && (
@@ -283,11 +295,6 @@ export const Warn = ({
); );
}; };
const div = document.createElement("div");
div.id = "confirm-root";
div.style.height = "0px";
document.body.appendChild(div);
Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => { Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
const root = createRoot(div); const root = createRoot(div);
const closeModal = () => { const closeModal = () => {

View File

@@ -1,3 +1,4 @@
"use client";
import useRelativePosition from "@/app/hooks/useRelativePosition"; import useRelativePosition from "@/app/hooks/useRelativePosition";
import { import {
RefObject, RefObject,
@@ -36,19 +37,6 @@ const ArrowIcon = ({ sibling }: { sibling: RefObject<HTMLDivElement> }) => {
const baseZIndex = 100; const baseZIndex = 100;
const popoverRootName = "popoverRoot"; const popoverRootName = "popoverRoot";
let popoverRoot = document.querySelector(
`#${popoverRootName}`,
) as HTMLDivElement;
if (!popoverRoot) {
popoverRoot = document.createElement("div");
document.body.appendChild(popoverRoot);
popoverRoot.style.height = "0px";
popoverRoot.style.width = "100%";
popoverRoot.style.position = "fixed";
popoverRoot.style.bottom = "0";
popoverRoot.style.zIndex = "10000";
popoverRoot.id = "popover-root";
}
export interface PopoverProps { export interface PopoverProps {
content?: JSX.Element | string; content?: JSX.Element | string;
@@ -65,6 +53,8 @@ export interface PopoverProps {
getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void; getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void;
} }
let popoverRoot: HTMLDivElement;
export default function Popover(props: PopoverProps) { export default function Popover(props: PopoverProps) {
const { const {
content, content,
@@ -184,6 +174,26 @@ export default function Popover(props: PopoverProps) {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
const closeTimer = useRef<number>(0); const closeTimer = useRef<number>(0);
useLayoutEffect(() => {
if (popoverRoot) {
return;
}
popoverRoot = document.querySelector(
`#${popoverRootName}`,
) as HTMLDivElement;
if (!popoverRoot) {
popoverRoot = document.createElement("div");
document.body.appendChild(popoverRoot);
popoverRoot.style.height = "0px";
popoverRoot.style.width = "100%";
popoverRoot.style.position = "fixed";
popoverRoot.style.bottom = "0";
popoverRoot.style.zIndex = "10000";
popoverRoot.id = "popover-root";
}
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
getPopoverPanelRef?.(popoverRef); getPopoverPanelRef?.(popoverRef);
onShow?.(internalShow); onShow?.(internalShow);
@@ -207,6 +217,10 @@ export default function Popover(props: PopoverProps) {
window.document.documentElement.style.overflow = "auto"; window.document.documentElement.style.overflow = "auto";
}; };
if (mergedShow) {
return null;
}
return ( return (
<div <div
className={`relative ${className}`} className={`relative ${className}`}

View File

@@ -1,11 +1,12 @@
import { useLocation } from "react-router-dom";
import { useMemo, ReactNode } from "react"; import { useMemo, ReactNode } from "react";
import { Path, SIDEBAR_ID, SlotID } from "@/app/constant"; import { Path, SIDEBAR_ID, SlotID } from "@/app/constant";
import { getLang } from "@/app/locales"; import { getLang } from "@/app/locales";
import useMobileScreen from "@/app/hooks/useMobileScreen"; import useMobileScreen from "@/app/hooks/useMobileScreen";
import { isIOS } from "@/app/utils";
import useListenWinResize from "@/app/hooks/useListenWinResize"; import useListenWinResize from "@/app/hooks/useListenWinResize";
import { usePathname } from "next/navigation";
import { useDeviceInfo } from "@/app/hooks/useDeviceInfo";
interface ScreenProps { interface ScreenProps {
children: ReactNode; children: ReactNode;
@@ -14,15 +15,11 @@ interface ScreenProps {
} }
export default function Screen(props: ScreenProps) { export default function Screen(props: ScreenProps) {
const location = useLocation(); const pathname = usePathname();
const isAuth = location.pathname === Path.Auth; const isAuth = pathname === Path.Auth;
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo( const { deviceType, systemInfo } = useDeviceInfo();
() => isIOS() && isMobileScreen,
[isMobileScreen],
);
useListenWinResize(); useListenWinResize();
return ( return (
@@ -59,7 +56,10 @@ export default function Screen(props: ScreenProps) {
id={SlotID.AppBody} id={SlotID.AppBody}
style={{ style={{
// #3016 disable transition on ios mobile screen // #3016 disable transition on ios mobile screen
transition: isIOSMobile ? "none" : undefined, transition:
systemInfo === "iOS" && deviceType === "mobile"
? "none"
: undefined,
}} }}
> >
{props.children} {props.children}

View File

@@ -1,7 +1,8 @@
"use client";
import styles from "./auth.module.scss"; import styles from "./auth.module.scss";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { useNavigate } from "react-router-dom"; import { useRouter } from "next/navigation";
import { Path } from "../constant"; import { Path } from "../constant";
import { useAccessStore } from "../store"; import { useAccessStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
@@ -11,11 +12,11 @@ import { useEffect } from "react";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
export function AuthPage() { export function AuthPage() {
const navigate = useNavigate(); const router = useRouter();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home); const goHome = () => router.push(Path.Home);
const goChat = () => navigate(Path.Chat); const goChat = () => router.push(Path.Chat);
const resetAccessCode = () => { const resetAccessCode = () => {
accessStore.update((access) => { accessStore.update((access) => {
access.openaiApiKey = ""; access.openaiApiKey = "";

View File

@@ -12,13 +12,14 @@ import {
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import { Link, useLocation, useNavigate } from "react-router-dom"; // import { Link, useLocation, useNavigate } from "react-router-dom";
import { Path } from "../constant"; import { Path } from "../constant";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask"; import { Mask } from "../store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils"; import { useMobileScreen } from "../utils";
import { usePathname, useRouter } from "next/navigation";
export function ChatItem(props: { export function ChatItem(props: {
onClick?: () => void; onClick?: () => void;
@@ -41,14 +42,14 @@ export function ChatItem(props: {
} }
}, [props.selected]); }, [props.selected]);
const { pathname: currentPath } = useLocation(); const pathname = usePathname();
return ( return (
<Draggable draggableId={`${props.id}`} index={props.index}> <Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => ( {(provided) => (
<div <div
className={`${styles["chat-item"]} ${ className={`${styles["chat-item"]} ${
props.selected && props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home) && (pathname === Path.Chat || pathname === Path.Home) &&
styles["chat-item-selected"] styles["chat-item-selected"]
}`} }`}
onClick={props.onClick} onClick={props.onClick}
@@ -112,8 +113,8 @@ export function ChatList(props: { narrow?: boolean }) {
], ],
); );
const chatStore = useChatStore(); const chatStore = useChatStore();
const navigate = useNavigate();
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const router = useRouter();
const onDragEnd: OnDragEndResponder = (result) => { const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result; const { destination, source } = result;
@@ -150,7 +151,8 @@ export function ChatList(props: { narrow?: boolean }) {
index={i} index={i}
selected={i === selectedIndex} selected={i === selectedIndex}
onClick={() => { onClick={() => {
navigate(Path.Chat); // navigate(Path.Chat);
router.push(Path.Chat);
selectSession(i); selectSession(i);
}} }}
onDelete={async () => { onDelete={async () => {

View File

@@ -97,6 +97,7 @@ import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api"; import { MultimodalContent } from "../client/api";
import { useRouter } from "next/navigation";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
@@ -428,7 +429,7 @@ export function ChatActions(props: {
uploading: boolean; uploading: boolean;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const router = useRouter();
const chatStore = useChatStore(); const chatStore = useChatStore();
// switch themes // switch themes
@@ -543,7 +544,8 @@ export function ChatActions(props: {
<ChatAction <ChatAction
onClick={() => { onClick={() => {
navigate(Path.Masks); // navigate(Path.Masks);
router.push(Path.Masks);
}} }}
text={Locale.Chat.InputActions.Masks} text={Locale.Chat.InputActions.Masks}
icon={<MaskIcon />} icon={<MaskIcon />}

View File

@@ -27,9 +27,9 @@ import {
} from "../constant"; } from "../constant";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { showConfirm, showToast } from "./ui-lib"; import { showConfirm, showToast } from "./ui-lib";
import { useDeviceInfo } from "../hooks/useDeviceInfo";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null, loading: () => null,
@@ -130,16 +130,11 @@ function useDragSideBar() {
export function SideBar(props: { className?: string }) { export function SideBar(props: { className?: string }) {
const chatStore = useChatStore(); const chatStore = useChatStore();
const { deviceType, systemInfo } = useDeviceInfo();
// drag side bar // drag side bar
const { onDragStart, shouldNarrow } = useDragSideBar(); const { onDragStart, shouldNarrow } = useDragSideBar();
const navigate = useNavigate(); const navigate = useNavigate();
const config = useAppConfig(); const config = useAppConfig();
const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo(
() => isIOS() && isMobileScreen,
[isMobileScreen],
);
useHotKey(); useHotKey();
@@ -150,7 +145,8 @@ export function SideBar(props: { className?: string }) {
}`} }`}
style={{ style={{
// #3016 disable transition on ios mobile screen // #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined, transition:
deviceType === "mobile" && systemInfo === "iOS" ? "none" : undefined,
}} }}
> >
<div className={styles["sidebar-header"]} data-tauri-drag-region> <div className={styles["sidebar-header"]} data-tauri-drag-region>

View File

@@ -3,7 +3,12 @@ import { BuildConfig, getBuildConfig } from "./build";
export function getClientConfig() { export function getClientConfig() {
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
// client side // client side
return JSON.parse(queryMeta("config")) as BuildConfig; try {
const config = JSON.parse(queryMeta("config")) as BuildConfig;
return config;
} catch (e) {
return null;
}
} }
if (typeof process !== "undefined") { if (typeof process !== "undefined") {

View File

@@ -1,5 +1,3 @@
import { useNavigate } from "react-router-dom";
import { ModelType, Theme, useAppConfig } from "@/app/store/config"; import { ModelType, Theme, useAppConfig } from "@/app/store/config";
import { useChatStore } from "@/app/store/chat"; import { useChatStore } from "@/app/store/chat";
import { ChatControllerPool } from "@/app/client/controller"; import { ChatControllerPool } from "@/app/client/controller";
@@ -22,6 +20,7 @@ import AddCircleIcon from "@/app/icons/addCircle.svg";
import Popover from "@/app/components/Popover"; import Popover from "@/app/components/Popover";
import ModelSelect from "./ModelSelect"; import ModelSelect from "./ModelSelect";
import { useRouter } from "next/navigation";
export interface Action { export interface Action {
onClick?: () => void; onClick?: () => void;
@@ -46,7 +45,7 @@ export function ChatActions(props: {
className?: string; className?: string;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const router = useRouter();
const chatStore = useChatStore(); const chatStore = useChatStore();
// switch themes // switch themes
@@ -146,7 +145,7 @@ export function ChatActions(props: {
}, },
{ {
onClick: () => { onClick: () => {
navigate(Path.Masks); router.push(Path.Masks);
}, },
text: Locale.Chat.InputActions.Masks, text: Locale.Chat.InputActions.Masks,
isShow: true, isShow: true,
@@ -206,7 +205,7 @@ export function ChatActions(props: {
placement="rt" placement="rt"
noArrow noArrow
popoverClassName="border border-chat-actions-popover-mobile rounded-md shadow-chat-actions-popover-mobile w-actions-popover bg-chat-actions-popover-panel-mobile " popoverClassName="border border-chat-actions-popover-mobile rounded-md shadow-chat-actions-popover-mobile w-actions-popover bg-chat-actions-popover-panel-mobile "
className=" cursor-pointer follow-parent-svg default-icon-color" className="cursor-pointer follow-parent-svg default-icon-color"
> >
<AddCircleIcon /> <AddCircleIcon />
</Popover> </Popover>

View File

@@ -1,4 +1,4 @@
import { useNavigate } from "react-router-dom"; import { useRouter } from "next/navigation";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat"; import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat";
@@ -17,8 +17,8 @@ export interface ChatHeaderProps {
export default function ChatHeader(props: ChatHeaderProps) { export default function ChatHeader(props: ChatHeaderProps) {
const { isMobileScreen, setIsEditingMessage, setShowExport } = props; const { isMobileScreen, setIsEditingMessage, setShowExport } = props;
const navigate = useNavigate(); // const navigate = useNavigate();
const router = useRouter();
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
@@ -39,8 +39,8 @@ export default function ChatHeader(props: ChatHeaderProps) {
{isMobileScreen ? ( {isMobileScreen ? (
<div <div
className=" cursor-pointer follow-parent-svg default-icon-color" className="cursor-pointer follow-parent-svg default-icon-color"
onClick={() => navigate(Path.Home)} onClick={() => router.push(Path.Home)}
> >
<GobackIcon /> <GobackIcon />
</div> </div>

View File

@@ -10,6 +10,7 @@ import { ChatCommandPrefix, useChatCommand } from "@/app/command";
import { useChatStore } from "@/app/store/chat"; import { useChatStore } from "@/app/store/chat";
import { usePromptStore } from "@/app/store/prompt"; import { usePromptStore } from "@/app/store/prompt";
import { useAppConfig } from "@/app/store/config"; import { useAppConfig } from "@/app/store/config";
import { useRouter } from "next/navigation";
import usePaste from "@/app/hooks/usePaste"; import usePaste from "@/app/hooks/usePaste";
import { ChatActions } from "./ChatActions"; import { ChatActions } from "./ChatActions";
@@ -71,7 +72,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]); const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
const chatStore = useChatStore(); const chatStore = useChatStore();
const navigate = useNavigate(); const router = useRouter();
const config = useAppConfig(); const config = useAppConfig();
const { uploadImage } = useUploadImage(attachImages, { const { uploadImage } = useUploadImage(attachImages, {
@@ -85,7 +86,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
// chat commands shortcuts // chat commands shortcuts
const chatCommands = useChatCommand({ const chatCommands = useChatCommand({
new: () => chatStore.newSession(), new: () => chatStore.newSession(),
newm: () => navigate(Path.NewChat), newm: () => router.push(Path.NewChat),
prev: () => chatStore.nextSession(-1), prev: () => chatStore.nextSession(-1),
next: () => chatStore.nextSession(1), next: () => chatStore.nextSession(1),
clear: () => clear: () =>
@@ -299,7 +300,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
}} }}
/> />
{!isMobileScreen && ( {!isMobileScreen && (
<div className="flex items-center justify-center text-sm gap-3"> <div className="flex items-center justify-center gap-3 text-sm">
<div className="flex-1">&nbsp;</div> <div className="flex-1">&nbsp;</div>
<div className="text-text-chat-input-placeholder font-common line-clamp-1"> <div className="text-text-chat-input-placeholder font-common line-clamp-1">
{Locale.Chat.Input(submitKey)} {Locale.Chat.Input(submitKey)}

View File

@@ -1,4 +1,4 @@
import { Fragment, useMemo } from "react"; import { Fragment, useEffect, useMemo } from "react";
import { ChatMessage, useChatStore } from "@/app/store/chat"; import { ChatMessage, useChatStore } from "@/app/store/chat";
import { CHAT_PAGE_SIZE } from "@/app/constant"; import { CHAT_PAGE_SIZE } from "@/app/constant";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
@@ -88,11 +88,13 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
? session.clearContextIndex! + context.length - msgRenderIndex ? session.clearContextIndex! + context.length - msgRenderIndex
: -1; : -1;
if (!MarkdownLoadedCallback) { useEffect(() => {
MarkdownLoadedCallback = () => { if (!MarkdownLoadedCallback) {
window.setTimeout(scrollDomToBottom, 100); MarkdownLoadedCallback = () => {
}; window.setTimeout(scrollDomToBottom, 100);
} };
}
}, [scrollDomToBottom]);
const messages = useMemo(() => { const messages = useMemo(() => {
const endRenderIndex = Math.min( const endRenderIndex = Math.min(

View File

@@ -1,11 +1,10 @@
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { useLocation } from "react-router-dom";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
import { Mask } from "@/app/store/mask"; import { Mask } from "@/app/store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { usePathname } from "next/navigation";
import DeleteChatIcon from "@/app/icons/deleteChatIcon.svg"; import DeleteChatIcon from "@/app/icons/deleteChatIcon.svg";
import { getTime } from "@/app/utils"; import { getTime } from "@/app/utils";
@@ -36,8 +35,7 @@ export default function SessionItem(props: {
}); });
} }
}, [props.selected]); }, [props.selected]);
const pathname = usePathname();
const { pathname: currentPath } = useLocation();
return ( return (
<Draggable draggableId={`${props.id}`} index={props.index}> <Draggable draggableId={`${props.id}`} index={props.index}>
@@ -51,7 +49,7 @@ export default function SessionItem(props: {
md:bg-chat-menu-session-unselected md:border-chat-menu-session-unselected md:bg-chat-menu-session-unselected md:border-chat-menu-session-unselected
${ ${
props.selected && props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home) (pathname === Path.Chat || pathname === Path.Home)
? ` ? `
md:!bg-chat-menu-session-selected md:!border-chat-menu-session-selected md:!bg-chat-menu-session-selected md:!border-chat-menu-session-selected
!bg-chat-menu-session-selected-mobile !border-chat-menu-session-selected-mobile !bg-chat-menu-session-selected-mobile !border-chat-menu-session-selected-mobile
@@ -70,7 +68,7 @@ export default function SessionItem(props: {
props.count, props.count,
)}`} )}`}
> >
<div className=" flex-shrink-0"> <div className="flex-shrink-0 ">
<LogIcon /> <LogIcon />
</div> </div>
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">

View File

@@ -7,7 +7,6 @@ import {
import { useAppConfig, useChatStore } from "@/app/store"; import { useAppConfig, useChatStore } from "@/app/store";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { useLocation, useNavigate } from "react-router-dom";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -18,6 +17,7 @@ import MenuLayout from "@/app/components/MenuLayout";
import Panel from "./ChatPanel"; import Panel from "./ChatPanel";
import Modal from "@/app/components/Modal"; import Modal from "@/app/components/Modal";
import SessionItem from "./components/SessionItem"; import SessionItem from "./components/SessionItem";
import { usePathname, useRouter } from "next/navigation";
export default MenuLayout(function SessionList(props) { export default MenuLayout(function SessionList(props) {
const { setShowPanel } = props; const { setShowPanel } = props;
@@ -30,17 +30,16 @@ export default MenuLayout(function SessionList(props) {
state.moveSession, state.moveSession,
], ],
); );
const navigate = useNavigate();
const config = useAppConfig(); const config = useAppConfig();
const { isMobileScreen } = config; const { isMobileScreen } = config;
const chatStore = useChatStore(); const chatStore = useChatStore();
const { pathname: currentPath } = useLocation(); const router = useRouter();
const pathname = usePathname();
useEffect(() => { useEffect(() => {
setShowPanel?.(currentPath === Path.Chat); setShowPanel?.(pathname === Path.Chat);
}, [currentPath]); }, [pathname]);
const onDragEnd: OnDragEndResponder = (result) => { const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result; const { destination, source } = result;
@@ -77,13 +76,15 @@ export default MenuLayout(function SessionList(props) {
<NextChatTitle /> <NextChatTitle />
</div> </div>
<div <div
className=" cursor-pointer" className="cursor-pointer "
onClick={() => { onClick={() => {
if (config.dontShowMaskSplashScreen) { if (config.dontShowMaskSplashScreen) {
chatStore.newSession(); chatStore.newSession();
navigate(Path.Chat); // navigate(Path.Chat);
router.push(Path.Chat);
} else { } else {
navigate(Path.NewChat); // navigate(Path.NewChat);
router.push(Path.NewChat);
} }
}} }}
> >
@@ -116,8 +117,9 @@ export default MenuLayout(function SessionList(props) {
index={i} index={i}
selected={i === selectedIndex} selected={i === selectedIndex}
onClick={() => { onClick={() => {
navigate(Path.Chat); // navigate(Path.Chat);
selectSession(i); selectSession(i);
router.push(Path.Chat);
}} }}
onDelete={async () => { onDelete={async () => {
if ( if (

View File

@@ -14,13 +14,14 @@ import AssistantMobileInactive from "@/app/icons/assistantMobileInactive.svg";
import { useAppConfig } from "@/app/store"; import { useAppConfig } from "@/app/store";
import { Path, REPO_URL } from "@/app/constant"; import { Path, REPO_URL } from "@/app/constant";
import { useNavigate, useLocation } from "react-router-dom";
import useHotKey from "@/app/hooks/useHotKey"; import useHotKey from "@/app/hooks/useHotKey";
import ActionsBar from "@/app/components/ActionsBar"; import ActionsBar from "@/app/components/ActionsBar";
import { usePathname, useRouter } from "next/navigation";
export function SideBar(props: { className?: string }) { export function SideBar(props: { className?: string }) {
const navigate = useNavigate(); // const navigate = useNavigate();
const loc = useLocation(); const pathname = usePathname();
const router = useRouter();
const config = useAppConfig(); const config = useAppConfig();
const { isMobileScreen } = config; const { isMobileScreen } = config;
@@ -28,8 +29,7 @@ export function SideBar(props: { className?: string }) {
useHotKey(); useHotKey();
let selectedTab: string; let selectedTab: string;
switch (pathname) {
switch (loc.pathname) {
case Path.Masks: case Path.Masks:
case Path.NewChat: case Path.NewChat:
selectedTab = Path.Masks; selectedTab = Path.Masks;
@@ -40,6 +40,7 @@ export function SideBar(props: { className?: string }) {
default: default:
selectedTab = Path.Home; selectedTab = Path.Home;
} }
console.log("======", selectedTab);
return ( return (
<div <div
@@ -98,12 +99,17 @@ export function SideBar(props: { className?: string }) {
return window.open(REPO_URL, "noopener noreferrer"); return window.open(REPO_URL, "noopener noreferrer");
} }
if (id !== Path.Masks) { if (id !== Path.Masks) {
return navigate(id); router.push(id);
return;
} }
if (config.dontShowMaskSplashScreen !== true) { if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } }); // navigate(Path.NewChat, { state: { fromHome: true } });
router.push(Path.NewChat);
return;
} else { } else {
navigate(Path.Masks, { state: { fromHome: true } }); // navigate(Path.Masks, { state: { fromHome: true } });
router.push(Path.Masks);
return;
} }
}} }}
groups={{ groups={{

View File

@@ -0,0 +1,44 @@
// retur user device info
import { useEffect, useState } from "react";
export function useDeviceInfo() {
const [deviceInfo, setDeviceInfo] = useState({});
const [systemInfo, setSystemInfo] = useState<string | null>(null);
const [deviceType, setDeviceType] = useState<string | null>(null);
useEffect(() => {
const userAgent = navigator.userAgent.toLowerCase();
if (/iphone|ipad|ipod/.test(userAgent)) {
setSystemInfo("iOS");
}
}, []);
useEffect(() => {
const onResize = () => {
setDeviceInfo({
width: window.innerWidth,
height: window.innerHeight,
});
};
if (window.innerWidth < 600) {
setDeviceType("mobile");
} else {
setDeviceType("desktop");
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
return {
windowSize: deviceInfo,
systemInfo,
deviceType,
};
}

View File

@@ -32,7 +32,7 @@ interface Position {
} }
export default function useRelativePosition({ export default function useRelativePosition({
containerRef = { current: window.document.body }, containerRef = { current: null },
delay = 100, delay = 100,
offsetDistance = 0, offsetDistance = 0,
}: Options) { }: Options) {

View File

@@ -1,4 +1,4 @@
import { useLayoutEffect, useRef, useState } from "react"; import { useEffect, useLayoutEffect, useRef, useState } from "react";
type Size = { type Size = {
width: number; width: number;
@@ -10,10 +10,14 @@ export function useWindowSize(callback?: (size: Size) => void) {
callbackRef.current = callback; callbackRef.current = callback;
const [size, setSize] = useState({ const [size, setSize] = useState({});
width: window.innerWidth,
height: window.innerHeight, useEffect(() => {
}); setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
const onResize = () => { const onResize = () => {

View File

@@ -291,18 +291,16 @@ export function getMessageImages(message: RequestMessage): string[] {
} }
export function isVisionModel(model: string) { export function isVisionModel(model: string) {
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using) // Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)
const visionKeywords = [ const visionKeywords = ["vision", "claude-3", "gemini-1.5-pro"];
"vision",
"claude-3",
"gemini-1.5-pro",
];
const isGpt4Turbo = model.includes("gpt-4-turbo") && !model.includes("preview"); const isGpt4Turbo =
model.includes("gpt-4-turbo") && !model.includes("preview");
return visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo; return (
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
);
} }
export function getTime(dateTime: string) { export function getTime(dateTime: string) {