mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-07 16:47:03 +08:00
feat: chat panel redesigned ui
This commit is contained in:
35
app/containers/Sidebar/MenuWrapper.tsx
Normal file
35
app/containers/Sidebar/MenuWrapper.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Path } from "@/app/constant";
|
||||
import { ComponentType } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export interface MenuWrapperProps {
|
||||
show: boolean;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
export default function MenuWrapper<ComponentProps>(
|
||||
Component: ComponentType<ComponentProps>,
|
||||
) {
|
||||
return function MenuHood(props: MenuWrapperProps & ComponentProps) {
|
||||
const { show, wrapperClassName } = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col px-6 pb-6 ${wrapperClassName}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
navigate(Path.Home);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Component {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
@@ -134,11 +134,17 @@ export default function SessionList(props: ListHoodProps) {
|
||||
moveSession(source.index, destination.index);
|
||||
};
|
||||
|
||||
let layoutClassName = "py-7 px-0";
|
||||
|
||||
if (isMobileScreen) {
|
||||
layoutClassName = "h-menu-title-mobile py-6";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-tauri-drag-region>
|
||||
<div
|
||||
className="flex items-center justify-between py-7 px-0"
|
||||
className={`flex items-center justify-between ${layoutClassName}`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="">
|
||||
|
121
app/containers/Sidebar/TabActions.tsx
Normal file
121
app/containers/Sidebar/TabActions.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { isValidElement } from "react";
|
||||
|
||||
type IconMap = {
|
||||
active?: JSX.Element;
|
||||
inactive?: JSX.Element;
|
||||
mobileActive?: JSX.Element;
|
||||
mobileInactive?: JSX.Element;
|
||||
};
|
||||
interface Action {
|
||||
id: string;
|
||||
title?: string;
|
||||
icons: JSX.Element | IconMap;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type Groups = {
|
||||
normal: string[][];
|
||||
mobile: string[][];
|
||||
};
|
||||
|
||||
export interface TabActionsProps {
|
||||
actionsShema: Action[];
|
||||
onSelect: (id: string) => void;
|
||||
selected: string;
|
||||
groups: string[][] | Groups;
|
||||
className?: string;
|
||||
inMobile: boolean;
|
||||
}
|
||||
|
||||
export default function TabActions(props: TabActionsProps) {
|
||||
const { actionsShema, onSelect, selected, groups, className, inMobile } =
|
||||
props;
|
||||
|
||||
const handlerClick = (id: string) => (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
if (selected !== id) {
|
||||
onSelect?.(id);
|
||||
}
|
||||
};
|
||||
|
||||
const internalGroup = Array.isArray(groups)
|
||||
? groups
|
||||
: inMobile
|
||||
? groups.mobile
|
||||
: groups.normal;
|
||||
|
||||
const content = internalGroup.reduce((res, group, ind, arr) => {
|
||||
res.push(
|
||||
...group.map((i) => {
|
||||
const action = actionsShema.find((a) => a.id === i);
|
||||
if (!action) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const { icons } = action;
|
||||
let activeIcon, inactiveIcon, mobileActiveIcon, mobileInactiveIcon;
|
||||
|
||||
if (isValidElement(icons)) {
|
||||
activeIcon = icons;
|
||||
inactiveIcon = icons;
|
||||
mobileActiveIcon = icons;
|
||||
mobileInactiveIcon = icons;
|
||||
} else {
|
||||
activeIcon = (icons as IconMap).active;
|
||||
inactiveIcon = (icons as IconMap).inactive;
|
||||
mobileActiveIcon = (icons as IconMap).mobileActive;
|
||||
mobileInactiveIcon = (icons as IconMap).mobileInactive;
|
||||
}
|
||||
|
||||
if (inMobile) {
|
||||
return (
|
||||
<div
|
||||
key={action.id}
|
||||
className={` shrink-1 grow-0 basis-[${
|
||||
(100 - 1) / arr.length
|
||||
}%] flex flex-col items-center justify-center gap-0.5
|
||||
${
|
||||
selected === action.id
|
||||
? "text-blue-700"
|
||||
: "text-gray-400"
|
||||
}
|
||||
`}
|
||||
onClick={handlerClick(action.id)}
|
||||
>
|
||||
{selected === action.id ? mobileActiveIcon : mobileInactiveIcon}
|
||||
<div className=" leading-3 text-sm-mobile-tab h-3 font-common w-[100%]">
|
||||
{action.title || " "}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={action.id}
|
||||
className={` ${
|
||||
selected === action.id ? "bg-blue-900" : "bg-transparent"
|
||||
} p-3 rounded-md items-center ${action.className}`}
|
||||
onClick={handlerClick(action.id)}
|
||||
>
|
||||
{selected === action.id ? activeIcon : inactiveIcon}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (ind < arr.length - 1) {
|
||||
res.push(<div className=" flex-1"></div>);
|
||||
}
|
||||
return res;
|
||||
}, [] as JSX.Element[]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${
|
||||
inMobile ? "justify-around" : "flex-col"
|
||||
} items-center ${className}`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from "react";
|
||||
import DragIcon from "@/app/icons/drag.svg";
|
||||
import DiscoverIcon from "@/app/icons/discoverActive.svg";
|
||||
import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
|
||||
@@ -7,30 +6,34 @@ import SettingIcon from "@/app/icons/settingActive.svg";
|
||||
import DiscoverInactiveIcon from "@/app/icons/discoverInactive.svg";
|
||||
import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
|
||||
import SettingInactiveIcon from "@/app/icons/settingInactive.svg";
|
||||
import SettingMobileActive from "@/app/icons/settingMobileActive.svg";
|
||||
import DiscoverMobileActive from "@/app/icons/discoverMobileActive.svg";
|
||||
import AssistantMobileActive from "@/app/icons/assistantMobileActive.svg";
|
||||
import AssistantMobileInactive from "@/app/icons/assistantMobileInactive.svg";
|
||||
|
||||
import { useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import { useAppConfig } from "@/app/store";
|
||||
import { Path, REPO_URL } from "@/app/constant";
|
||||
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { isIOS } from "@/app/utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import useHotKey from "@/app/hooks/useHotKey";
|
||||
import useDragSideBar from "@/app/hooks/useDragSideBar";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import TabActions from "@/app/components/TabActions";
|
||||
import TabActions from "./TabActions";
|
||||
import MenuWrapper from "./MenuWrapper";
|
||||
|
||||
const SessionList = dynamic(async () => await import("./SessionList"), {
|
||||
loading: () => null,
|
||||
});
|
||||
const SessionList = MenuWrapper(
|
||||
dynamic(async () => await import("./SessionList"), {
|
||||
loading: () => null,
|
||||
}),
|
||||
);
|
||||
|
||||
const SettingList = dynamic(async () => await import("./SettingList"), {
|
||||
loading: () => null,
|
||||
});
|
||||
const SettingList = MenuWrapper(
|
||||
dynamic(async () => await import("./SettingList"), {
|
||||
loading: () => null,
|
||||
}),
|
||||
);
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragStart } = useDragSideBar();
|
||||
|
||||
@@ -39,10 +42,6 @@ export function SideBar(props: { className?: string }) {
|
||||
|
||||
const config = useAppConfig();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const isIOSMobile = useMemo(
|
||||
() => isIOS() && isMobileScreen,
|
||||
[isMobileScreen],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
@@ -60,29 +59,41 @@ export function SideBar(props: { className?: string }) {
|
||||
selectedTab = Path.Chat;
|
||||
}
|
||||
|
||||
let containerClassName = "relative flex h-[100%] w-[100%]";
|
||||
let tabActionsClassName = "2xl:px-5 xl:px-4 px-2 py-6";
|
||||
let menuClassName =
|
||||
"max-md:px-4 max-md:pb-4 rounded-md my-2.5 bg-gray-50 flex-1";
|
||||
|
||||
if (isMobileScreen) {
|
||||
containerClassName = "flex flex-col-reverse w-[100%] h-[100%]";
|
||||
tabActionsClassName = "bg-gray-100 rounded-tl-md rounded-tr-md h-mobile";
|
||||
menuClassName = `flex-1 px-4`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={` inline-flex h-[100%] ${props.className} relative`}
|
||||
style={{
|
||||
// #3016 disable transition on ios mobile screen
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={`${containerClassName}`}>
|
||||
<TabActions
|
||||
inMobile={isMobileScreen}
|
||||
actionsShema={[
|
||||
{
|
||||
id: Path.Masks,
|
||||
icons: {
|
||||
active: <DiscoverIcon />,
|
||||
inactive: <DiscoverInactiveIcon />,
|
||||
mobileActive: <DiscoverMobileActive />,
|
||||
mobileInactive: <DiscoverInactiveIcon />,
|
||||
},
|
||||
title: "Discover",
|
||||
},
|
||||
{
|
||||
id: Path.Chat,
|
||||
icons: {
|
||||
active: <AssistantActiveIcon />,
|
||||
inactive: <AssistantInactiveIcon />,
|
||||
mobileActive: <AssistantMobileActive />,
|
||||
mobileInactive: <AssistantMobileInactive />,
|
||||
},
|
||||
title: "Assistant",
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
@@ -94,8 +105,11 @@ export function SideBar(props: { className?: string }) {
|
||||
icons: {
|
||||
active: <SettingIcon />,
|
||||
inactive: <SettingInactiveIcon />,
|
||||
mobileActive: <SettingMobileActive />,
|
||||
mobileInactive: <SettingInactiveIcon />,
|
||||
},
|
||||
className: "p-2",
|
||||
title: "Settrings",
|
||||
},
|
||||
]}
|
||||
onSelect={(id) => {
|
||||
@@ -111,27 +125,25 @@ export function SideBar(props: { className?: string }) {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
groups={[
|
||||
[Path.Chat, Path.Masks],
|
||||
["github", Path.Settings],
|
||||
]}
|
||||
groups={{
|
||||
normal: [
|
||||
[Path.Chat, Path.Masks],
|
||||
["github", Path.Settings],
|
||||
],
|
||||
mobile: [[Path.Chat, Path.Masks, Path.Settings]],
|
||||
}}
|
||||
selected={selectedTab}
|
||||
className="px-5 py-6"
|
||||
className={tabActionsClassName}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`flex flex-col w-md lg:w-lg 2xl:w-2xl px-6 pb-6 max-md:px-4 max-md:pb-4 bg-gray-50 rounded-md my-2.5 ${
|
||||
isMobileScreen && `bg-gray-300`
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
navigate(Path.Home);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedTab === Path.Chat && <SessionList />}
|
||||
{loc.pathname === Path.Settings && <SettingList />}
|
||||
</div>
|
||||
<SessionList
|
||||
show={selectedTab === Path.Chat}
|
||||
wrapperClassName={menuClassName}
|
||||
/>
|
||||
<SettingList
|
||||
show={selectedTab === Path.Settings}
|
||||
wrapperClassName={menuClassName}
|
||||
/>
|
||||
|
||||
{!isMobileScreen && (
|
||||
<div
|
||||
|
Reference in New Issue
Block a user