mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-04 22:46:55 +08:00
feat: chat panel redesigned ui
This commit is contained in:
8
app/components/Loading/index.module.scss
Normal file
8
app/components/Loading/index.module.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
13
app/components/Loading/index.tsx
Normal file
13
app/components/Loading/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
119
app/components/Popover/index.tsx
Normal file
119
app/components/Popover/index.tsx
Normal 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}`}> </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}`}>
|
||||
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`hidden group-hover:block ${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
app/components/Screen/index.tsx
Normal file
71
app/components/Screen/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
|
@@ -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`,
|
||||
}}
|
||||
|
Reference in New Issue
Block a user