feat: chat panel redesigned ui

This commit is contained in:
butterfly
2024-04-16 14:07:51 +08:00
parent 3fc9b91bf1
commit 51a1d9f92a
41 changed files with 1350 additions and 526 deletions

View 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>
);
};
}

View File

@@ -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="">

View 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>
);
}

View File

@@ -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