mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-04 22:46:55 +08:00
feat: chat panel UE done
This commit is contained in:
118
app/components/ActionsBar/index.tsx
Normal file
118
app/components/ActionsBar/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
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;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
type Groups = {
|
||||
normal: string[][];
|
||||
mobile: string[][];
|
||||
};
|
||||
|
||||
export interface ActionsBarProps {
|
||||
actionsShema: Action[];
|
||||
onSelect?: (id: string) => void;
|
||||
selected?: string;
|
||||
groups: string[][] | Groups;
|
||||
className?: string;
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function ActionsBar(props: ActionsBarProps) {
|
||||
const { actionsShema, onSelect, selected, groups, className, inMobile } =
|
||||
props;
|
||||
|
||||
const handlerClick =
|
||||
(action: Action) => (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
if (action.onClick) {
|
||||
action.onClick();
|
||||
}
|
||||
if (selected !== action.id) {
|
||||
onSelect?.(action.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)}
|
||||
>
|
||||
{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={`p-3 ${
|
||||
selected === action.id ? "bg-blue-900" : "bg-transparent"
|
||||
} rounded-md items-center ${action.className}`}
|
||||
onClick={handlerClick(action)}
|
||||
>
|
||||
{selected === action.id ? activeIcon : inactiveIcon}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (ind < arr.length - 1) {
|
||||
res.push(<div key={String(ind)} className=" flex-1"></div>);
|
||||
}
|
||||
return res;
|
||||
}, [] as JSX.Element[]);
|
||||
|
||||
return <div className={`flex items-center ${className} `}>{content}</div>;
|
||||
}
|
60
app/components/Btn/index.tsx
Normal file
60
app/components/Btn/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from "react";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
export default function IconButton(props: {
|
||||
onClick?: () => void;
|
||||
icon?: JSX.Element;
|
||||
type?: ButtonType;
|
||||
text?: string;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
const {
|
||||
onClick,
|
||||
icon,
|
||||
type,
|
||||
text,
|
||||
bordered,
|
||||
shadow,
|
||||
className,
|
||||
title,
|
||||
disabled,
|
||||
tabIndex,
|
||||
autoFocus,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${className ?? ""}
|
||||
py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn shadow-btn transition-all duration-300 select-none
|
||||
${
|
||||
type === "primary"
|
||||
? `${disabled ? "bg-blue-300" : "bg-blue-600"}`
|
||||
: `${disabled ? "bg-gray-100" : "bg-gray-300"}`
|
||||
}
|
||||
${disabled ? "cursor-not-allowed" : "cursor-pointer"}
|
||||
${type === "primary" ? `text-white` : `text-gray-500`}
|
||||
`}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
role="button"
|
||||
tabIndex={tabIndex}
|
||||
autoFocus={autoFocus}
|
||||
>
|
||||
{text && (
|
||||
<div className={`text-common text-sm-title leading-4 line-clamp-1`}>
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
{icon && <div className={`flex items-center justify-center`}>{icon}</div>}
|
||||
</button>
|
||||
);
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
@@ -1,12 +1,33 @@
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import BotIcon from "@/app/icons/bot.svg";
|
||||
import LoadingIcon from "@/app/icons/three-dots.svg";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
import { getCSSVar } from "@/app/utils";
|
||||
|
||||
export default function Loading({
|
||||
noLogo,
|
||||
useSkeleton = true,
|
||||
}: {
|
||||
noLogo?: boolean;
|
||||
useSkeleton?: boolean;
|
||||
}) {
|
||||
let theme;
|
||||
if (typeof window !== "undefined") {
|
||||
theme = getCSSVar("--chat-panel-bg");
|
||||
}
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
export default function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
<div className={styles["loading-content"] + " no-dark"}>
|
||||
{!props.noLogo && <BotIcon />}
|
||||
<div
|
||||
className={`flex flex-col justify-center items-center w-[100%] ${
|
||||
isMobileScreen
|
||||
? "h-[100%]"
|
||||
: `my-2.5 ml-1 mr-2.5 rounded-md h-[calc(100%-1.25rem)]`
|
||||
}`}
|
||||
style={{ background: useSkeleton ? theme : "" }}
|
||||
>
|
||||
{!noLogo && <BotIcon />}
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,4 +1,24 @@
|
||||
import { useState } from "react";
|
||||
import { getCSSVar } from "@/app/utils";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const ArrowIcon = ({ color }: { color: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="6"
|
||||
viewBox="0 0 16 6"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M16 0H0C1.28058 0 2.50871 0.508709 3.41421 1.41421L6.91 4.91C7.51199 5.51199 8.48801 5.51199 9.09 4.91L12.5858 1.41421C13.4913 0.508708 14.7194 0 16 0Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const baseZIndex = 100;
|
||||
|
||||
export default function Popover(props: {
|
||||
content?: JSX.Element | string;
|
||||
@@ -10,6 +30,7 @@ export default function Popover(props: {
|
||||
trigger?: "hover" | "click";
|
||||
placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b";
|
||||
noArrow?: boolean;
|
||||
bgcolor?: string;
|
||||
}) {
|
||||
const {
|
||||
content,
|
||||
@@ -21,6 +42,7 @@ export default function Popover(props: {
|
||||
trigger = "hover",
|
||||
placement = "t",
|
||||
noArrow = false,
|
||||
bgcolor,
|
||||
} = props;
|
||||
|
||||
const [internalShow, setShow] = useState(false);
|
||||
@@ -28,14 +50,15 @@ export default function Popover(props: {
|
||||
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] ";
|
||||
let arrowClassName = "absolute left-[50%] translate-x-[calc(-50%)]";
|
||||
// "absolute rotate-45 w-[8.5px] h-[8.5px] left-[50%] translate-x-[calc(-50%)] bg-black rounded-[1px] ";
|
||||
arrowClassName += " ";
|
||||
|
||||
switch (placement) {
|
||||
case "b":
|
||||
placementClassName =
|
||||
"bottom-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "bottom-[-5px] ";
|
||||
"top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
// case 'l':
|
||||
// placementClassName = '';
|
||||
@@ -44,28 +67,28 @@ export default function Popover(props: {
|
||||
// placementClassName = '';
|
||||
// break;
|
||||
case "rb":
|
||||
placementClassName = "bottom-[calc(-100%-0.5rem)]";
|
||||
arrowClassName += "bottom-[-5px] ";
|
||||
placementClassName = "top-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "lt":
|
||||
placementClassName =
|
||||
"top-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
|
||||
arrowClassName += "top-[-5px] ";
|
||||
"bottom-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lb":
|
||||
placementClassName =
|
||||
"bottom-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
|
||||
arrowClassName += "bottom-[-5px] ";
|
||||
"top-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "rt":
|
||||
placementClassName = "top-[calc(-100%-0.5rem)]";
|
||||
arrowClassName += "top-[-5px] ";
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "t":
|
||||
default:
|
||||
placementClassName =
|
||||
"top-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "top-[-5px] ";
|
||||
"bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
}
|
||||
|
||||
const popoverCommonClass = "absolute p-2 box-border";
|
||||
@@ -74,6 +97,10 @@ export default function Popover(props: {
|
||||
arrowClassName = "hidden";
|
||||
}
|
||||
|
||||
const internalBgColor = useMemo(() => {
|
||||
return bgcolor ?? getCSSVar("--tip-popover-color");
|
||||
}, [bgcolor]);
|
||||
|
||||
if (trigger === "click") {
|
||||
return (
|
||||
<div
|
||||
@@ -88,13 +115,27 @@ export default function Popover(props: {
|
||||
{mergedShow && (
|
||||
<>
|
||||
{!noArrow && (
|
||||
<div className={`absolute ${arrowClassName}`}> </div>
|
||||
<div className={`${arrowClassName}`}>
|
||||
<ArrowIcon color={internalBgColor} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||
style={{ zIndex: baseZIndex + 1 }}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<div
|
||||
className=" fixed w-[100%] h-[100%] top-0 left-0 right-0 bottom-0"
|
||||
style={{ zIndex: baseZIndex }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onShow?.(!mergedShow);
|
||||
setShow(!mergedShow);
|
||||
}}
|
||||
>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -105,8 +146,8 @@ export default function Popover(props: {
|
||||
<div className={`group relative ${className}`}>
|
||||
{children}
|
||||
{!noArrow && (
|
||||
<div className={`hidden group-hover:block absolute ${arrowClassName}`}>
|
||||
|
||||
<div className={`hidden group-hover:block ${arrowClassName}`}>
|
||||
<ArrowIcon color={internalBgColor} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useMemo, ReactNode, useLayoutEffect } from "react";
|
||||
import { DEFAULT_SIDEBAR_WIDTH, Path, SlotID } from "@/app/constant";
|
||||
import { useMemo, ReactNode } from "react";
|
||||
import { Path, SlotID } from "@/app/constant";
|
||||
import { getLang } from "@/app/locales";
|
||||
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
@@ -29,7 +29,7 @@ export default function Screen(props: ScreenProps) {
|
||||
useListenWinResize();
|
||||
|
||||
let containerClassName = "flex h-[100%] w-[100%]";
|
||||
let pageClassName = "flex-1 h-[100%]";
|
||||
let pageClassName = "flex-1 h-[100%] w-page";
|
||||
let sidebarClassName = "basis-sidebar h-[100%]";
|
||||
|
||||
if (isMobileScreen) {
|
||||
|
27
app/components/ThumbnailImg/index.tsx
Normal file
27
app/components/ThumbnailImg/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import ImgDeleteIcon from "@/app/icons/imgDeleteIcon.svg";
|
||||
|
||||
export interface ThumbnailProps {
|
||||
image: string;
|
||||
deleteImage: () => void;
|
||||
}
|
||||
|
||||
export default function Thumbnail(props: ThumbnailProps) {
|
||||
const { image, deleteImage } = props;
|
||||
return (
|
||||
<div
|
||||
className={` h-thumbnail w-thumbnail cursor-default border-1 border-black border-opacity-10 rounded-action-btn flex-0 bg-cover bg-center`}
|
||||
style={{ backgroundImage: `url("${image}")` }}
|
||||
>
|
||||
<div
|
||||
className={` w-[100%] h-[100%] opacity-0 transition-all duration-200 rounded-action-btn hover:opacity-100 hover:bg-thumbnail-mask`}
|
||||
>
|
||||
<div
|
||||
className={`cursor-pointer flex items-center justify-center float-right`}
|
||||
onClick={deleteImage}
|
||||
>
|
||||
<ImgDeleteIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -45,7 +45,7 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const Chat = dynamic(async () => await import("@/app/containers/Chat"), {
|
||||
const Chat = dynamic(async () => (await import("./chat")).Chat, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
@@ -151,10 +151,7 @@ function Screen() {
|
||||
<>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
|
||||
<div
|
||||
className={`flex flex-col h-[100%] w-[--window-content-width]`}
|
||||
id={SlotID.AppBody}
|
||||
>
|
||||
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={<Chat />} />
|
||||
|
Reference in New Issue
Block a user