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,8 @@
.loading-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}

View File

@@ -0,0 +1,13 @@
import BotIcon from "@/app/icons/bot.svg";
import LoadingIcon from "@/app/icons/three-dots.svg";
import styles from "./index.module.scss";
export default function Loading(props: { noLogo?: boolean }) {
return (
<div className={styles["loading-content"] + " no-dark"}>
{!props.noLogo && <BotIcon />}
<LoadingIcon />
</div>
);
}

View File

@@ -0,0 +1,119 @@
import { useState } from "react";
export default function Popover(props: {
content?: JSX.Element | string;
children?: JSX.Element;
show?: boolean;
onShow?: (v: boolean) => void;
className?: string;
popoverClassName?: string;
trigger?: "hover" | "click";
placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b";
noArrow?: boolean;
}) {
const {
content,
children,
show,
onShow,
className,
popoverClassName,
trigger = "hover",
placement = "t",
noArrow = false,
} = props;
const [internalShow, setShow] = useState(false);
const mergedShow = show ?? internalShow;
let placementClassName;
let arrowClassName =
"rotate-45 w-[8.5px] h-[8.5px] left-[50%] translate-x-[calc(-50%)] bg-black rounded-[1px] ";
switch (placement) {
case "b":
placementClassName =
"bottom-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
arrowClassName += "bottom-[-5px] ";
break;
// case 'l':
// placementClassName = '';
// break;
// case 'r':
// placementClassName = '';
// break;
case "rb":
placementClassName = "bottom-[calc(-100%-0.5rem)]";
arrowClassName += "bottom-[-5px] ";
break;
case "lt":
placementClassName =
"top-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
arrowClassName += "top-[-5px] ";
break;
case "lb":
placementClassName =
"bottom-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
arrowClassName += "bottom-[-5px] ";
break;
case "rt":
placementClassName = "top-[calc(-100%-0.5rem)]";
arrowClassName += "top-[-5px] ";
break;
case "t":
default:
placementClassName =
"top-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
arrowClassName += "top-[-5px] ";
}
const popoverCommonClass = "absolute p-2 box-border";
if (noArrow) {
arrowClassName = "hidden";
}
if (trigger === "click") {
return (
<div
className={`relative ${className}`}
onClick={(e) => {
e.preventDefault();
onShow?.(!mergedShow);
setShow(!mergedShow);
}}
>
{children}
{mergedShow && (
<>
{!noArrow && (
<div className={`absolute ${arrowClassName}`}>&nbsp;</div>
)}
<div
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
>
{content}
</div>
</>
)}
</div>
);
}
return (
<div className={`group relative ${className}`}>
{children}
{!noArrow && (
<div className={`hidden group-hover:block absolute ${arrowClassName}`}>
&nbsp;
</div>
)}
<div
className={`hidden group-hover:block ${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
>
{content}
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
import { useLocation } from "react-router-dom";
import { useMemo, ReactNode, useLayoutEffect } from "react";
import { DEFAULT_SIDEBAR_WIDTH, Path, SlotID } from "@/app/constant";
import { getLang } from "@/app/locales";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { isIOS } from "@/app/utils";
import backgroundUrl from "!url-loader!@/app/icons/background.svg";
import useListenWinResize from "@/app/hooks/useListenWinResize";
interface ScreenProps {
children: ReactNode;
noAuth: ReactNode;
sidebar: ReactNode;
}
export default function Screen(props: ScreenProps) {
const location = useLocation();
const isAuth = location.pathname === Path.Auth;
const isHome = location.pathname === Path.Home;
const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo(
() => isIOS() && isMobileScreen,
[isMobileScreen],
);
useListenWinResize();
let containerClassName = "flex h-[100%] w-[100%]";
let pageClassName = "flex-1 h-[100%]";
let sidebarClassName = "basis-sidebar h-[100%]";
if (isMobileScreen) {
containerClassName = "h-[100%] w-[100%] relative bg-center";
pageClassName = `absolute top-0 h-[100%] w-[100%] ${
!isHome ? "left-0" : "left-[101%]"
} z-10`;
sidebarClassName = `h-[100%] w-[100%]`;
}
return (
<div
className={containerClassName}
style={{
backgroundImage: `url(${backgroundUrl})`,
direction: getLang() === "ar" ? "rtl" : "ltr",
}}
>
{isAuth ? (
props.noAuth
) : (
<>
<div className={sidebarClassName}>{props.sidebar}</div>
<div
className={pageClassName}
id={SlotID.AppBody}
style={{
// #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
}}
>
{props.children}
</div>
</>
)}
</div>
);
}

View File

@@ -1,70 +0,0 @@
import { isValidElement } from "react";
type IconMap = {
active: JSX.Element;
inactive: JSX.Element;
};
interface Action {
id: string;
icons: JSX.Element | IconMap;
className?: string;
}
export interface TabActionsProps {
actionsShema: Action[];
onSelect: (id: string) => void;
selected: string;
groups: string[][];
className?: string;
}
export default function TabActions(props: TabActionsProps) {
const { actionsShema, onSelect, selected, groups, className } = props;
const content = groups.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;
if (isValidElement(icons)) {
activeIcon = icons;
inactiveIcon = icons;
} else {
activeIcon = (icons as IconMap).active;
inactiveIcon = (icons as IconMap).inactive;
}
return (
<div
key={action.id}
className={` ${
selected === action.id ? "bg-blue-900" : "bg-transparent"
} p-3 rounded-md items-center ${action.className}`}
onClick={(e) => {
e.preventDefault();
if (selected !== action.id) {
onSelect?.(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 flex-col items-center ${className}`}>{content}</div>
);
}

View File

@@ -30,7 +30,6 @@ import { getClientConfig } from "../config/client";
import { ClientApi } from "../client/api";
import { useAccessStore } from "../store";
import { identifyDefaultClaudeModel } from "../utils/checkers";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import backgroundUrl from "!url-loader!@/app/icons/background.svg";
export function Loading(props: { noLogo?: boolean }) {
@@ -126,11 +125,9 @@ const loadAsyncGoogleFont = () => {
};
function Screen() {
const config = useAppConfig();
const location = useLocation();
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
useEffect(() => {
loadAsyncGoogleFont();
@@ -155,7 +152,7 @@ function Screen() {
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<div
className={`flex flex-col h-[100%] w-[--window-content-width`}
className={`flex flex-col h-[100%] w-[--window-content-width]`}
id={SlotID.AppBody}
>
<ErrorBoundary>

View File

@@ -177,13 +177,14 @@ export function Markdown(
fontSize?: number;
parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean;
className?: string;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
return (
<div
className="markdown-body"
className={`markdown-body ${props.className}`}
style={{
fontSize: `${props.fontSize ?? 14}px`,
}}