feat: chat panel UE done
|
@ -11,6 +11,7 @@ interface Action {
|
|||
title?: string;
|
||||
icons: JSX.Element | IconMap;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
type Groups = {
|
||||
|
@ -18,25 +19,29 @@ type Groups = {
|
|||
mobile: string[][];
|
||||
};
|
||||
|
||||
export interface TabActionsProps {
|
||||
export interface ActionsBarProps {
|
||||
actionsShema: Action[];
|
||||
onSelect: (id: string) => void;
|
||||
selected: string;
|
||||
onSelect?: (id: string) => void;
|
||||
selected?: string;
|
||||
groups: string[][] | Groups;
|
||||
className?: string;
|
||||
inMobile: boolean;
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function TabActions(props: TabActionsProps) {
|
||||
export default function ActionsBar(props: ActionsBarProps) {
|
||||
const { actionsShema, onSelect, selected, groups, className, inMobile } =
|
||||
props;
|
||||
|
||||
const handlerClick = (id: string) => (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
if (selected !== id) {
|
||||
onSelect?.(id);
|
||||
}
|
||||
};
|
||||
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
|
||||
|
@ -80,7 +85,7 @@ export default function TabActions(props: TabActionsProps) {
|
|||
: "text-gray-400"
|
||||
}
|
||||
`}
|
||||
onClick={handlerClick(action.id)}
|
||||
onClick={handlerClick(action)}
|
||||
>
|
||||
{selected === action.id ? mobileActiveIcon : mobileInactiveIcon}
|
||||
<div className=" leading-3 text-sm-mobile-tab h-3 font-common w-[100%]">
|
||||
|
@ -93,10 +98,10 @@ export default function TabActions(props: TabActionsProps) {
|
|||
return (
|
||||
<div
|
||||
key={action.id}
|
||||
className={` ${
|
||||
className={`p-3 ${
|
||||
selected === action.id ? "bg-blue-900" : "bg-transparent"
|
||||
} p-3 rounded-md items-center ${action.className}`}
|
||||
onClick={handlerClick(action.id)}
|
||||
} rounded-md items-center ${action.className}`}
|
||||
onClick={handlerClick(action)}
|
||||
>
|
||||
{selected === action.id ? activeIcon : inactiveIcon}
|
||||
</div>
|
||||
|
@ -104,18 +109,10 @@ export default function TabActions(props: TabActionsProps) {
|
|||
}),
|
||||
);
|
||||
if (ind < arr.length - 1) {
|
||||
res.push(<div className=" flex-1"></div>);
|
||||
res.push(<div key={String(ind)} className=" flex-1"></div>);
|
||||
}
|
||||
return res;
|
||||
}, [] as JSX.Element[]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${
|
||||
inMobile ? "justify-around" : "flex-col"
|
||||
} items-center ${className}`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
return <div className={`flex items-center ${className} `}>{content}</div>;
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 />} />
|
||||
|
|
|
@ -24,16 +24,13 @@ import SettingsIcon from "@/app/icons/chat-settings.svg";
|
|||
import ImageIcon from "@/app/icons/image.svg";
|
||||
import AddCircleIcon from "@/app/icons/addCircle.svg";
|
||||
|
||||
import ChatAction from "./ChatAction";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
import Popover from "@/app/components/Popover";
|
||||
|
||||
export function ChatActions(props: {
|
||||
uploadImage: () => void;
|
||||
setAttachImages: (images: string[]) => void;
|
||||
setUploading: (uploading: boolean) => void;
|
||||
showPromptModal: () => void;
|
||||
showChatSetting: () => void;
|
||||
scrollToBottom: () => void;
|
||||
showPromptHints: () => void;
|
||||
showModelSelector: (show: boolean) => void;
|
||||
|
@ -105,9 +102,9 @@ export function ChatActions(props: {
|
|||
placement: "left",
|
||||
},
|
||||
{
|
||||
onClick: props.showPromptModal,
|
||||
onClick: props.showChatSetting,
|
||||
text: Locale.Chat.InputActions.Settings,
|
||||
isShow: props.hitBottom,
|
||||
isShow: true,
|
||||
icon: <SettingsIcon />,
|
||||
placement: "right",
|
||||
},
|
||||
|
@ -178,28 +175,36 @@ export function ChatActions(props: {
|
|||
|
||||
if (props.isMobileScreen) {
|
||||
const content = (
|
||||
<div>
|
||||
<div className="w-[100%]">
|
||||
{actions.map((act) => {
|
||||
return (
|
||||
<div
|
||||
key={act.text}
|
||||
className={`flex p-3 bg-white hover:bg-select-btn rounded-action-btn`}
|
||||
className={`flex items-center gap-3 p-3 bg-white hover:bg-select-btn rounded-action-btn leading-6`}
|
||||
>
|
||||
{act.icon}
|
||||
{act.text}
|
||||
<div className="flex-1 text-common text-actions-popover-menu-item">
|
||||
{act.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Popover content={content}>
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="lt"
|
||||
noArrow
|
||||
popoverClassName="border-actions-popover border-gray-200 rounded-md shadow-actions-popover w-actions-popover bg-white "
|
||||
>
|
||||
<AddCircleIcon />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const popoverClassName = `bg-gray-800 whitespace-nowrap px-3 py-2.5 text-white text-sm-title rounded-md`;
|
||||
const popoverClassName = `bg-chat-actions-popover-color whitespace-nowrap px-3 py-2.5 text-white text-sm-title rounded-md`;
|
||||
|
||||
return (
|
||||
<div className={`flex gap-2 item-center ${props.className}`}>
|
||||
|
@ -214,7 +219,7 @@ export function ChatActions(props: {
|
|||
placement={ind ? "t" : "rt"}
|
||||
>
|
||||
<div
|
||||
className="h-[32px] w-[32px] flex items-center justify-center"
|
||||
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn"
|
||||
onClick={act.onClick}
|
||||
>
|
||||
{act.icon}
|
||||
|
@ -234,7 +239,7 @@ export function ChatActions(props: {
|
|||
placement={ind === arr.length - 1 ? "lt" : "t"}
|
||||
>
|
||||
<div
|
||||
className="h-[32px] w-[32px] flex items-center justify-center"
|
||||
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn"
|
||||
onClick={act.onClick}
|
||||
>
|
||||
{act.icon}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import useUploadImage from "@/app/hooks/useUploadImage";
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import Locale from "@/app/locales";
|
||||
|
||||
import useSubmitHandler from "@/app/hooks/useSubmitHandler";
|
||||
|
@ -17,13 +16,14 @@ import usePaste from "@/app/hooks/usePaste";
|
|||
import { ChatActions } from "./ChatActions";
|
||||
import PromptHints, { RenderPompt } from "./PromptHint";
|
||||
|
||||
import SendWhiteIcon from "@/app/icons/send-white.svg";
|
||||
import DeleteIcon from "@/app/icons/clear.svg";
|
||||
// import CEIcon from "@/app/icons/command&enterIcon.svg";
|
||||
// import EnterIcon from "@/app/icons/enterIcon.svg";
|
||||
import SendIcon from "@/app/icons/sendIcon.svg";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
import Btn from "@/app/components/Btn";
|
||||
import Thumbnail from "@/app/components/ThumbnailImg";
|
||||
|
||||
export interface ChatInputPanelProps {
|
||||
scrollRef: React.RefObject<HTMLDivElement>;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
isMobileScreen: boolean;
|
||||
renderMessages: any[];
|
||||
|
@ -34,26 +34,19 @@ export interface ChatInputPanelProps {
|
|||
setAttachImages: (imgs: string[]) => void;
|
||||
setUserInput: (v: string) => void;
|
||||
setIsLoading: (value: boolean) => void;
|
||||
setShowPromptModal: (value: boolean) => void;
|
||||
showChatSetting: (value: boolean) => void;
|
||||
_setMsgRenderIndex: (value: number) => void;
|
||||
showModelSelector: (value: boolean) => void;
|
||||
setAutoScroll: (value: boolean) => void;
|
||||
scrollDomToBottom: () => void;
|
||||
}
|
||||
|
||||
export interface ChatInputPanelInstance {
|
||||
setUploading: (v: boolean) => void;
|
||||
doSubmit: (userInput: string) => void;
|
||||
setAutoScroll: (v: boolean) => void;
|
||||
setMsgRenderIndex: (v: number) => void;
|
||||
}
|
||||
|
||||
export function DeleteImageButton(props: { deleteImage: () => void }) {
|
||||
return (
|
||||
<div className={styles["delete-image"]} onClick={props.deleteImage}>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// only search prompts when user input is short
|
||||
const SEARCH_TEXT_LIMIT = 30;
|
||||
|
||||
|
@ -67,13 +60,14 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
isMobileScreen,
|
||||
setUserInput,
|
||||
setIsLoading,
|
||||
setShowPromptModal,
|
||||
showChatSetting,
|
||||
renderMessages,
|
||||
scrollRef,
|
||||
_setMsgRenderIndex,
|
||||
hitBottom,
|
||||
inputRows,
|
||||
showModelSelector,
|
||||
setAutoScroll,
|
||||
scrollDomToBottom,
|
||||
} = props;
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
@ -91,18 +85,6 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
|
||||
const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
|
||||
|
||||
const isScrolledToBottom = scrollRef?.current
|
||||
? Math.abs(
|
||||
scrollRef.current.scrollHeight -
|
||||
(scrollRef.current.scrollTop + scrollRef.current.clientHeight),
|
||||
) <= 1
|
||||
: false;
|
||||
|
||||
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
|
||||
scrollRef,
|
||||
isScrolledToBottom,
|
||||
);
|
||||
|
||||
// chat commands shortcuts
|
||||
const chatCommands = useChatCommand({
|
||||
new: () => chatStore.newSession(),
|
||||
|
@ -226,12 +208,14 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
|
||||
let inputClassName = " flex flex-col px-5 pb-5";
|
||||
let actionsClassName = "py-2.5";
|
||||
let inputTextAreaClassName = "";
|
||||
let labelClassName = "rounded-md p-4 gap-4";
|
||||
let textarea = "min-h-chat-input";
|
||||
|
||||
if (isMobileScreen) {
|
||||
inputClassName = "flex flex-row-reverse items-center gap-2 p-3";
|
||||
actionsClassName = "";
|
||||
inputTextAreaClassName = "";
|
||||
labelClassName = " rounded-chat-input p-3 gap-3 flex-1";
|
||||
textarea = "h-chat-input-mobile";
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -241,7 +225,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
<PromptHints
|
||||
prompts={promptHints}
|
||||
onPromptSelect={onPromptSelect}
|
||||
className=""
|
||||
className=" border-gray-200"
|
||||
/>
|
||||
|
||||
<div className={`${inputClassName}`}>
|
||||
|
@ -250,7 +234,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
uploadImage={uploadImage}
|
||||
setAttachImages={setAttachImages}
|
||||
setUploading={setUploading}
|
||||
showPromptModal={() => setShowPromptModal(true)}
|
||||
showChatSetting={() => showChatSetting(true)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
uploading={uploading}
|
||||
|
@ -269,18 +253,35 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
isMobileScreen={isMobileScreen}
|
||||
/>
|
||||
<label
|
||||
className={`${styles["chat-input-panel-inner"]} ${
|
||||
attachImages.length != 0
|
||||
? styles["chat-input-panel-inner-attach"]
|
||||
: ""
|
||||
} ${inputTextAreaClassName}`}
|
||||
className={`cursor-text flex flex-col bg-white border-[1px] border-white focus-within:border-blue-300 focus-within:shadow-chat-input ${labelClassName}`}
|
||||
htmlFor="chat-input"
|
||||
>
|
||||
{attachImages.length != 0 && (
|
||||
<div className={`flex gap-2`}>
|
||||
{attachImages.map((image, index) => {
|
||||
return (
|
||||
<Thumbnail
|
||||
key={index}
|
||||
deleteImage={() => {
|
||||
setAttachImages(
|
||||
attachImages.filter((_, i) => i !== index),
|
||||
);
|
||||
}}
|
||||
image={image}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
id="chat-input"
|
||||
ref={inputRef}
|
||||
className={styles["chat-input"]}
|
||||
placeholder={Locale.Chat.Input(submitKey)}
|
||||
className={`leading-[19px] flex-1 focus:outline-none focus:shadow-none focus:border-none ${textarea} resize-none`}
|
||||
placeholder={
|
||||
isMobileScreen
|
||||
? Locale.Chat.Input(submitKey, isMobileScreen)
|
||||
: undefined
|
||||
}
|
||||
onInput={(e) => onInput(e.currentTarget.value)}
|
||||
value={userInput}
|
||||
onKeyDown={onInputKeyDown}
|
||||
|
@ -293,36 +294,22 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
|||
fontSize: config.fontSize,
|
||||
}}
|
||||
/>
|
||||
{attachImages.length != 0 && (
|
||||
<div className={styles["attach-images"]}>
|
||||
{attachImages.map((image, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={styles["attach-image"]}
|
||||
style={{ backgroundImage: `url("${image}")` }}
|
||||
>
|
||||
<div className={styles["attach-image-mask"]}>
|
||||
<DeleteImageButton
|
||||
deleteImage={() => {
|
||||
setAttachImages(
|
||||
attachImages.filter((_, i) => i !== index),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!isMobileScreen && (
|
||||
<div className="flex items-center justify-center text-sm gap-3">
|
||||
<div className="flex-1"> </div>
|
||||
<div className="text-gray-500 text-time line-clamp-1">
|
||||
{Locale.Chat.Input(submitKey)}
|
||||
</div>
|
||||
<Btn
|
||||
className="min-w-[77px]"
|
||||
icon={<SendIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
// className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,35 +1,27 @@
|
|||
import { Fragment, useMemo } from "react";
|
||||
import { Fragment, useEffect, useMemo } from "react";
|
||||
import { ChatMessage, useChatStore } from "@/app/store/chat";
|
||||
import { CHAT_PAGE_SIZE } from "@/app/constant";
|
||||
import Locale from "@/app/locales";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
import {
|
||||
copyToClipboard,
|
||||
getMessageImages,
|
||||
getMessageTextContent,
|
||||
selectOrCopy,
|
||||
} from "@/app/utils";
|
||||
import { showPrompt, showToast } from "@/app/components/ui-lib";
|
||||
|
||||
import CopyIcon from "@/app/icons/copy.svg";
|
||||
import ResetIcon from "@/app/icons/reload.svg";
|
||||
import DeleteIcon from "@/app/icons/clear.svg";
|
||||
import PinIcon from "@/app/icons/pin.svg";
|
||||
import EditIcon from "@/app/icons/rename.svg";
|
||||
import StopIcon from "@/app/icons/pause.svg";
|
||||
import LoadingIcon from "@/app/icons/three-dots.svg";
|
||||
|
||||
import { MultimodalContent } from "@/app/client/api";
|
||||
import { Avatar } from "@/app/components/emoji";
|
||||
import { MaskAvatar } from "@/app/components/mask";
|
||||
import { useAppConfig } from "@/app/store/config";
|
||||
import ChatAction from "./ChatAction";
|
||||
import { ChatControllerPool } from "@/app/client/controller";
|
||||
import ClearContextDivider from "./ClearContextDivider";
|
||||
import dynamic from "next/dynamic";
|
||||
import useRelativePosition, {
|
||||
Orientation,
|
||||
} from "@/app/hooks/useRelativePosition";
|
||||
import MessageActions, { RenderMessage } from "./MessageActions";
|
||||
|
||||
export type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
export type { RenderMessage };
|
||||
|
||||
export interface ChatMessagePanelProps {
|
||||
scrollRef: React.RefObject<HTMLDivElement>;
|
||||
|
@ -39,6 +31,7 @@ export interface ChatMessagePanelProps {
|
|||
userInput: string;
|
||||
context: any[];
|
||||
renderMessages: RenderMessage[];
|
||||
scrollDomToBottom: () => void;
|
||||
setAutoScroll?: (value: boolean) => void;
|
||||
setMsgRenderIndex?: (newIndex: number) => void;
|
||||
setHitBottom?: (value: boolean) => void;
|
||||
|
@ -47,8 +40,17 @@ export interface ChatMessagePanelProps {
|
|||
setShowPromptModal?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
let MarkdownLoadedCallback: () => void;
|
||||
|
||||
const Markdown = dynamic(
|
||||
async () => (await import("@/app/components/markdown")).Markdown,
|
||||
async () => {
|
||||
const bundle = await import("@/app/components/markdown");
|
||||
|
||||
if (MarkdownLoadedCallback) {
|
||||
MarkdownLoadedCallback();
|
||||
}
|
||||
return bundle.Markdown;
|
||||
},
|
||||
{
|
||||
loading: () => <LoadingIcon />,
|
||||
},
|
||||
|
@ -69,6 +71,7 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
renderMessages,
|
||||
setIsLoading,
|
||||
setShowPromptModal,
|
||||
scrollDomToBottom,
|
||||
} = props;
|
||||
|
||||
const chatStore = useChatStore();
|
||||
|
@ -76,6 +79,32 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
const config = useAppConfig();
|
||||
const fontSize = config.fontSize;
|
||||
|
||||
const { position, getRelativePosition } = useRelativePosition({
|
||||
containerRef: scrollRef,
|
||||
delay: 0,
|
||||
offsetDistance: 20,
|
||||
});
|
||||
|
||||
// clear context index = context length + index in messages
|
||||
const clearContextIndex =
|
||||
(session.clearContextIndex ?? -1) >= 0
|
||||
? session.clearContextIndex! + context.length - msgRenderIndex
|
||||
: -1;
|
||||
|
||||
if (!MarkdownLoadedCallback) {
|
||||
MarkdownLoadedCallback = () => {
|
||||
window.setTimeout(scrollDomToBottom, 100);
|
||||
};
|
||||
}
|
||||
|
||||
const messages = useMemo(() => {
|
||||
const endRenderIndex = Math.min(
|
||||
msgRenderIndex + 3 * CHAT_PAGE_SIZE,
|
||||
renderMessages.length,
|
||||
);
|
||||
return renderMessages.slice(msgRenderIndex, endRenderIndex);
|
||||
}, [msgRenderIndex, renderMessages]);
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
const bottomHeight = e.scrollTop + e.clientHeight;
|
||||
const edgeThreshold = e.clientHeight;
|
||||
|
@ -109,110 +138,9 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const deleteMessage = (msgId?: string) => {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) =>
|
||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
||||
);
|
||||
};
|
||||
|
||||
const onDelete = (msgId: string) => {
|
||||
deleteMessage(msgId);
|
||||
};
|
||||
|
||||
const onResend = (message: ChatMessage) => {
|
||||
// when it is resending a message
|
||||
// 1. for a user's message, find the next bot response
|
||||
// 2. for a bot's message, find the last user's input
|
||||
// 3. delete original user input and bot's message
|
||||
// 4. resend the user's input
|
||||
|
||||
const resendingIndex = session.messages.findIndex(
|
||||
(m) => m.id === message.id,
|
||||
);
|
||||
|
||||
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
|
||||
console.error("[Chat] failed to find resending message", message);
|
||||
return;
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage | undefined;
|
||||
let botMessage: ChatMessage | undefined;
|
||||
|
||||
if (message.role === "assistant") {
|
||||
// if it is resending a bot's message, find the user input for it
|
||||
botMessage = message;
|
||||
for (let i = resendingIndex; i >= 0; i -= 1) {
|
||||
if (session.messages[i].role === "user") {
|
||||
userMessage = session.messages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (message.role === "user") {
|
||||
// if it is resending a user's input, find the bot's response
|
||||
userMessage = message;
|
||||
for (let i = resendingIndex; i < session.messages.length; i += 1) {
|
||||
if (session.messages[i].role === "assistant") {
|
||||
botMessage = session.messages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userMessage === undefined) {
|
||||
console.error("[Chat] failed to resend", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// delete the original messages
|
||||
deleteMessage(userMessage.id);
|
||||
deleteMessage(botMessage?.id);
|
||||
|
||||
// resend the message
|
||||
setIsLoading?.(true);
|
||||
const textContent = getMessageTextContent(userMessage);
|
||||
const images = getMessageImages(userMessage);
|
||||
chatStore
|
||||
.onUserInput(textContent, images)
|
||||
.then(() => setIsLoading?.(false));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const onPinMessage = (message: ChatMessage) => {
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.mask.context.push(message),
|
||||
);
|
||||
|
||||
showToast(Locale.Chat.Actions.PinToastContent, {
|
||||
text: Locale.Chat.Actions.PinToastAction,
|
||||
onClick: () => {
|
||||
setShowPromptModal?.(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// clear context index = context length + index in messages
|
||||
const clearContextIndex =
|
||||
(session.clearContextIndex ?? -1) >= 0
|
||||
? session.clearContextIndex! + context.length - msgRenderIndex
|
||||
: -1;
|
||||
|
||||
const messages = useMemo(() => {
|
||||
const endRenderIndex = Math.min(
|
||||
msgRenderIndex + 3 * CHAT_PAGE_SIZE,
|
||||
renderMessages.length,
|
||||
);
|
||||
return renderMessages.slice(msgRenderIndex, endRenderIndex);
|
||||
}, [msgRenderIndex, renderMessages]);
|
||||
|
||||
// stop response
|
||||
const onUserStop = (messageId: string) => {
|
||||
ChatControllerPool.stop(session.id, messageId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex-1 overscroll-y-none overflow-x-hidden px-3 pb-5`}
|
||||
className={`relative flex-1 overscroll-y-none overflow-y-auto overflow-x-hidden px-3 pb-6`}
|
||||
ref={scrollRef}
|
||||
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||
onMouseDown={() => inputRef.current?.blur()}
|
||||
|
@ -228,10 +156,15 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
i > 0 &&
|
||||
!(message.preview || message.content.length === 0) &&
|
||||
!isContext;
|
||||
// const showTyping = message.preview || message.streaming;
|
||||
|
||||
const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
||||
|
||||
const actionsBarPosition =
|
||||
position?.id === message.id &&
|
||||
position?.poi.overlapPositions[Orientation.bottom]
|
||||
? "bottom-[calc(100%-0.25rem)]"
|
||||
: "top-[calc(100%-0.25rem)]";
|
||||
|
||||
return (
|
||||
<Fragment key={message.id}>
|
||||
<div
|
||||
|
@ -253,11 +186,6 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* {showTyping && (
|
||||
<div className={styles["chat-message-status"]}>
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)} */}
|
||||
<div className={`group relative max-w-message-width`}>
|
||||
<div
|
||||
className={` pointer-events-none text-gray-500 text-right text-time whitespace-nowrap transition-all duration-500 text-sm absolute z-1 ${
|
||||
|
@ -269,9 +197,14 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
: message.date.toLocaleString()}
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 select-text break-words font-common text-sm-title rounded-message box-border peer py-2 px-3 ${
|
||||
className={`transition-all duration-300 select-text break-words font-common text-sm-title ${
|
||||
isUser ? "rounded-user-message" : "rounded-bot-message"
|
||||
} box-border peer py-2 px-3 ${
|
||||
isUser ? "text-right bg-message-bg" : " bg-white"
|
||||
}`}
|
||||
onPointerMoveCapture={(e) =>
|
||||
getRelativePosition(e.currentTarget, message.id)
|
||||
}
|
||||
>
|
||||
<Markdown
|
||||
content={getMessageTextContent(message)}
|
||||
|
@ -290,26 +223,12 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
defaultShow={i >= messages.length - 6}
|
||||
className={isUser ? " text-white" : "text-black"}
|
||||
/>
|
||||
{getMessageImages(message).length == 1 && (
|
||||
<img
|
||||
className={` w-[100%] mt-2.5`}
|
||||
src={getMessageImages(message)[0]}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
{getMessageImages(message).length > 1 && (
|
||||
<div
|
||||
className={`styles["chat-message-item-images"] w-[100%]`}
|
||||
style={
|
||||
{
|
||||
"--image-count": getMessageImages(message).length,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{getMessageImages(message).length > 0 && (
|
||||
<div className={`w-[100%]`}>
|
||||
{getMessageImages(message).map((image, index) => {
|
||||
return (
|
||||
<img
|
||||
className={styles["chat-message-item-image-multi"]}
|
||||
className={`w-[100%] mt-2.5 rounded-chat-img`}
|
||||
key={index}
|
||||
src={image}
|
||||
alt=""
|
||||
|
@ -319,86 +238,15 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showActions && (
|
||||
<div
|
||||
className={` absolute ${
|
||||
isUser ? "right-0" : "left-0"
|
||||
} top-[100%] hidden group-hover:block`}
|
||||
>
|
||||
<div className={styles["chat-input-actions"]}>
|
||||
{message.streaming ? (
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Stop}
|
||||
icon={<StopIcon />}
|
||||
onClick={() => onUserStop(message.id ?? i)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Retry}
|
||||
icon={<ResetIcon />}
|
||||
onClick={() => onResend(message)}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Delete}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Pin}
|
||||
icon={<PinIcon />}
|
||||
onClick={() => onPinMessage(message)}
|
||||
/>
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
icon={<CopyIcon />}
|
||||
onClick={() =>
|
||||
copyToClipboard(getMessageTextContent(message))
|
||||
}
|
||||
/>
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
icon={<EditIcon />}
|
||||
onClick={async () => {
|
||||
const newMessage = await showPrompt(
|
||||
Locale.Chat.Actions.Edit,
|
||||
getMessageTextContent(message),
|
||||
10,
|
||||
);
|
||||
let newContent: string | MultimodalContent[] =
|
||||
newMessage;
|
||||
const images = getMessageImages(message);
|
||||
if (images.length > 0) {
|
||||
newContent = [
|
||||
{ type: "text", text: newMessage },
|
||||
];
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
newContent.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: images[i],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
const m = session.mask.context
|
||||
.concat(session.messages)
|
||||
.find((m) => m.id === message.id);
|
||||
if (m) {
|
||||
m.content = newContent;
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<MessageActions
|
||||
className={actionsBarPosition}
|
||||
message={message}
|
||||
inputRef={inputRef}
|
||||
isUser={isUser}
|
||||
showActions={showActions}
|
||||
setIsLoading={setIsLoading}
|
||||
setShowPromptModal={setShowPromptModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowClearContextDivider && <ClearContextDivider />}
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
import Locale from "@/app/locales";
|
||||
|
||||
import StopIcon from "@/app/icons/pause.svg";
|
||||
import DeleteRequestIcon from "@/app/icons/deleteRequestIcon.svg";
|
||||
import RetryRequestIcon from "@/app/icons/retryRequestIcon.svg";
|
||||
import CopyRequestIcon from "@/app/icons/copyRequestIcon.svg";
|
||||
import EditRequestIcon from "@/app/icons/editRequestIcon.svg";
|
||||
import PinRequestIcon from "@/app/icons/pinRequestIcon.svg";
|
||||
import { showPrompt, showToast } from "@/app/components/ui-lib";
|
||||
import {
|
||||
copyToClipboard,
|
||||
getMessageImages,
|
||||
getMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { MultimodalContent } from "@/app/client/api";
|
||||
import { ChatMessage, useChatStore } from "@/app/store/chat";
|
||||
import ActionsBar from "@/app/components/ActionsBar";
|
||||
import { ChatControllerPool } from "@/app/client/controller";
|
||||
import { RefObject } from "react";
|
||||
|
||||
export type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
export interface MessageActionsProps {
|
||||
message: RenderMessage;
|
||||
isUser: boolean;
|
||||
showActions: boolean;
|
||||
inputRef: RefObject<HTMLTextAreaElement>;
|
||||
className?: string;
|
||||
setIsLoading?: (value: boolean) => void;
|
||||
setShowPromptModal?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const genActionsShema = (
|
||||
message: RenderMessage,
|
||||
{
|
||||
onEdit,
|
||||
onCopy,
|
||||
onPinMessage,
|
||||
onDelete,
|
||||
onResend,
|
||||
onUserStop,
|
||||
}: Record<
|
||||
| "onEdit"
|
||||
| "onCopy"
|
||||
| "onPinMessage"
|
||||
| "onDelete"
|
||||
| "onResend"
|
||||
| "onUserStop",
|
||||
(message: RenderMessage) => void
|
||||
>,
|
||||
) => {
|
||||
const className = "!p-1 hover:bg-gray-100 !rounded-actions-bar-btn";
|
||||
return [
|
||||
{
|
||||
id: "Edit",
|
||||
icons: <EditRequestIcon />,
|
||||
title: "Edit",
|
||||
className,
|
||||
onClick: () => onEdit(message),
|
||||
},
|
||||
{
|
||||
id: Locale.Chat.Actions.Copy,
|
||||
icons: <CopyRequestIcon />,
|
||||
title: Locale.Chat.Actions.Copy,
|
||||
className,
|
||||
onClick: () => onCopy(message),
|
||||
},
|
||||
{
|
||||
id: Locale.Chat.Actions.Pin,
|
||||
icons: <PinRequestIcon />,
|
||||
title: Locale.Chat.Actions.Pin,
|
||||
className,
|
||||
onClick: () => onPinMessage(message),
|
||||
},
|
||||
{
|
||||
id: Locale.Chat.Actions.Delete,
|
||||
icons: <DeleteRequestIcon />,
|
||||
title: Locale.Chat.Actions.Delete,
|
||||
className,
|
||||
onClick: () => onDelete(message),
|
||||
},
|
||||
{
|
||||
id: Locale.Chat.Actions.Retry,
|
||||
icons: <RetryRequestIcon />,
|
||||
title: Locale.Chat.Actions.Retry,
|
||||
className,
|
||||
onClick: () => onResend(message),
|
||||
},
|
||||
{
|
||||
id: Locale.Chat.Actions.Stop,
|
||||
icons: <StopIcon />,
|
||||
title: Locale.Chat.Actions.Stop,
|
||||
className,
|
||||
onClick: () => onUserStop(message),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function MessageActions(props: MessageActionsProps) {
|
||||
const {
|
||||
className,
|
||||
message,
|
||||
isUser,
|
||||
showActions,
|
||||
setIsLoading,
|
||||
inputRef,
|
||||
setShowPromptModal,
|
||||
} = props;
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
|
||||
const deleteMessage = (msgId?: string) => {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) =>
|
||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
||||
);
|
||||
};
|
||||
|
||||
const onDelete = (message: ChatMessage) => {
|
||||
deleteMessage(message.id);
|
||||
};
|
||||
|
||||
const onResend = (message: ChatMessage) => {
|
||||
// when it is resending a message
|
||||
// 1. for a user's message, find the next bot response
|
||||
// 2. for a bot's message, find the last user's input
|
||||
// 3. delete original user input and bot's message
|
||||
// 4. resend the user's input
|
||||
|
||||
const resendingIndex = session.messages.findIndex(
|
||||
(m) => m.id === message.id,
|
||||
);
|
||||
|
||||
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
|
||||
console.error("[Chat] failed to find resending message", message);
|
||||
return;
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage | undefined;
|
||||
let botMessage: ChatMessage | undefined;
|
||||
|
||||
if (message.role === "assistant") {
|
||||
// if it is resending a bot's message, find the user input for it
|
||||
botMessage = message;
|
||||
for (let i = resendingIndex; i >= 0; i -= 1) {
|
||||
if (session.messages[i].role === "user") {
|
||||
userMessage = session.messages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (message.role === "user") {
|
||||
// if it is resending a user's input, find the bot's response
|
||||
userMessage = message;
|
||||
for (let i = resendingIndex; i < session.messages.length; i += 1) {
|
||||
if (session.messages[i].role === "assistant") {
|
||||
botMessage = session.messages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userMessage === undefined) {
|
||||
console.error("[Chat] failed to resend", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// delete the original messages
|
||||
deleteMessage(userMessage.id);
|
||||
deleteMessage(botMessage?.id);
|
||||
|
||||
// resend the message
|
||||
setIsLoading?.(true);
|
||||
const textContent = getMessageTextContent(userMessage);
|
||||
const images = getMessageImages(userMessage);
|
||||
chatStore
|
||||
.onUserInput(textContent, images)
|
||||
.then(() => setIsLoading?.(false));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const onPinMessage = (message: ChatMessage) => {
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.mask.context.push(message),
|
||||
);
|
||||
|
||||
showToast(Locale.Chat.Actions.PinToastContent, {
|
||||
text: Locale.Chat.Actions.PinToastAction,
|
||||
onClick: () => {
|
||||
setShowPromptModal?.(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// stop response
|
||||
const onUserStop = (message: ChatMessage) => {
|
||||
ChatControllerPool.stop(session.id, message.id);
|
||||
};
|
||||
|
||||
const onEdit = async () => {
|
||||
const newMessage = await showPrompt(
|
||||
Locale.Chat.Actions.Edit,
|
||||
getMessageTextContent(message),
|
||||
10,
|
||||
);
|
||||
let newContent: string | MultimodalContent[] = newMessage;
|
||||
const images = getMessageImages(message);
|
||||
if (images.length > 0) {
|
||||
newContent = [{ type: "text", text: newMessage }];
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
newContent.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: images[i],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
const m = session.mask.context
|
||||
.concat(session.messages)
|
||||
.find((m) => m.id === message.id);
|
||||
if (m) {
|
||||
m.content = newContent;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCopy = () => copyToClipboard(getMessageTextContent(message));
|
||||
|
||||
return (
|
||||
showActions && (
|
||||
<div
|
||||
className={`transition-all duration-500 absolute z-10 ${
|
||||
isUser ? "right-0" : "left-0"
|
||||
} opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-all bg-white rounded-md shadow-actions-bar ${className}`}
|
||||
>
|
||||
<ActionsBar
|
||||
actionsShema={genActionsShema(message, {
|
||||
onCopy,
|
||||
onDelete,
|
||||
onPinMessage,
|
||||
onEdit,
|
||||
onResend,
|
||||
onUserStop,
|
||||
})}
|
||||
groups={
|
||||
message.streaming
|
||||
? [[Locale.Chat.Actions.Stop]]
|
||||
: [
|
||||
[
|
||||
Locale.Chat.Actions.Retry,
|
||||
"Edit",
|
||||
Locale.Chat.Actions.Copy,
|
||||
Locale.Chat.Actions.Pin,
|
||||
Locale.Chat.Actions.Delete,
|
||||
],
|
||||
]
|
||||
}
|
||||
className="flex flex-row gap-1 p-1"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { Prompt } from "@/app/store/prompt";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
import useShowPromptHint from "@/app/hooks/useShowPromptHint";
|
||||
|
||||
export type RenderPompt = Pick<Prompt, "title" | "content">;
|
||||
|
||||
|
@ -11,9 +12,13 @@ export default function PromptHints(props: {
|
|||
className?: string;
|
||||
}) {
|
||||
const noPrompts = props.prompts.length === 0;
|
||||
|
||||
const [selectIndex, setSelectIndex] = useState(0);
|
||||
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { internalPrompts, notShowPrompt } = useShowPromptHint({ ...props });
|
||||
|
||||
useEffect(() => {
|
||||
setSelectIndex(0);
|
||||
}, [props.prompts.length]);
|
||||
|
@ -55,10 +60,24 @@ export default function PromptHints(props: {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.prompts.length, selectIndex]);
|
||||
|
||||
if (noPrompts) return null;
|
||||
if (!internalPrompts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles["prompt-hints"]} ${props.className}`}>
|
||||
{props.prompts.map((prompt, i) => (
|
||||
<div
|
||||
className={`
|
||||
${styles["prompt-hints"]}
|
||||
transition-all duration-300 shadow-inner rounded-none w-[100%] flex flex-col-reverse overflow-auto
|
||||
${
|
||||
notShowPrompt
|
||||
? "max-h-[0vh] border-none"
|
||||
: "border-b-[1px] pt-2.5 max-h-[50vh]"
|
||||
}
|
||||
${props.className}
|
||||
`}
|
||||
>
|
||||
{internalPrompts.map((prompt, i) => (
|
||||
<div
|
||||
ref={i === selectIndex ? selectedRef : null}
|
||||
className={
|
||||
|
|
|
@ -3,8 +3,6 @@ import Locale from "@/app/locales";
|
|||
|
||||
import BrainIcon from "@/app/icons/brain.svg";
|
||||
|
||||
import SessionConfigModel from "./SessionConfigModal";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
export default function PromptToast(props: {
|
||||
|
@ -30,9 +28,6 @@ export default function PromptToast(props: {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{props.showModal && (
|
||||
<SessionConfigModel onClose={() => props.setShowModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -526,25 +526,12 @@
|
|||
}
|
||||
|
||||
.prompt-hints {
|
||||
min-height: 20px;
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
background-color: var(--white);
|
||||
border: var(--border-in-light);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.prompt-hint {
|
||||
color: var(--black);
|
||||
padding: 6px 10px;
|
||||
animation: slide-in ease 0.3s;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.3s;
|
||||
// animation: slide-in ease 0.3s;
|
||||
// cursor: pointer;
|
||||
// transition: all ease 0.3s;
|
||||
border: transparent 1px solid;
|
||||
margin: 4px;
|
||||
border-radius: 8px;
|
||||
|
@ -573,13 +560,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.chat-input-panel-inner {
|
||||
cursor: text;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
}
|
||||
// .chat-input-panel-inner {
|
||||
// cursor: text;
|
||||
// display: flex;
|
||||
// flex: 1;
|
||||
// border-radius: 10px;
|
||||
// border: var(--border-in-light);
|
||||
// }
|
||||
|
||||
.chat-input-panel-inner-attach {
|
||||
padding-bottom: 80px;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useDebouncedCallback } from "use-debounce";
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import {
|
||||
useChatStore,
|
||||
|
@ -8,7 +7,6 @@ import {
|
|||
useAppConfig,
|
||||
ModelType,
|
||||
} from "@/app/store";
|
||||
import { autoGrowTextArea, useMobileScreen } from "@/app/utils";
|
||||
import Locale from "@/app/locales";
|
||||
import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
|
||||
import {
|
||||
|
@ -26,6 +24,10 @@ import ChatHeader from "./ChatHeader";
|
|||
import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
|
||||
import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
|
||||
import { useAllModels } from "@/app/utils/hooks";
|
||||
import useRows from "@/app/hooks/useRows";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import SessionConfigModel from "./SessionConfigModal";
|
||||
import useScrollToBottom from "@/app/hooks/useScrollToBottom";
|
||||
|
||||
function _Chat() {
|
||||
const chatStore = useChatStore();
|
||||
|
@ -47,22 +49,11 @@ function _Chat() {
|
|||
const [attachImages, setAttachImages] = useState<string[]>([]);
|
||||
|
||||
// auto grow input
|
||||
const [inputRows, setInputRows] = useState(2);
|
||||
const measure = useDebouncedCallback(
|
||||
() => {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
20,
|
||||
Math.max(2 + Number(!isMobileScreen), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
},
|
||||
100,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
const { measure, inputRows } = useRows({
|
||||
inputRef,
|
||||
});
|
||||
|
||||
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(scrollRef);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(measure, [userInput]);
|
||||
|
@ -224,7 +215,6 @@ function _Chat() {
|
|||
}, []);
|
||||
|
||||
const chatinputPanelProps = {
|
||||
scrollRef,
|
||||
inputRef,
|
||||
isMobileScreen,
|
||||
renderMessages,
|
||||
|
@ -235,9 +225,11 @@ function _Chat() {
|
|||
setAttachImages,
|
||||
setUserInput,
|
||||
setIsLoading,
|
||||
setShowPromptModal,
|
||||
showChatSetting: setShowPromptModal,
|
||||
_setMsgRenderIndex,
|
||||
showModelSelector: setShowModelSelector,
|
||||
scrollDomToBottom,
|
||||
setAutoScroll,
|
||||
};
|
||||
|
||||
const chatMessagePanelProps = {
|
||||
|
@ -248,12 +240,13 @@ function _Chat() {
|
|||
userInput,
|
||||
context,
|
||||
renderMessages,
|
||||
setAutoScroll: chatInputPanelRef.current?.setAutoScroll,
|
||||
setAutoScroll,
|
||||
setMsgRenderIndex: chatInputPanelRef.current?.setMsgRenderIndex,
|
||||
setHitBottom,
|
||||
setUserInput,
|
||||
setIsLoading,
|
||||
setShowPromptModal,
|
||||
scrollDomToBottom,
|
||||
};
|
||||
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
|
@ -265,9 +258,11 @@ function _Chat() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col h-[100%] overflow-hidden ${
|
||||
className={`flex flex-col ${
|
||||
isMobileScreen ? "h-[100%]" : "h-[calc(100%-1.25rem)]"
|
||||
} overflow-hidden ${
|
||||
isMobileScreen ? "" : `my-2.5 ml-1 mr-2.5 rounded-md`
|
||||
} bg-gray-50`}
|
||||
} bg-chat-panel`}
|
||||
key={session.id}
|
||||
>
|
||||
<ChatHeader
|
||||
|
@ -299,6 +294,10 @@ function _Chat() {
|
|||
setShowModal={setShowPromptModal}
|
||||
/>
|
||||
|
||||
{showPromptModal && (
|
||||
<SessionConfigModel onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
|
||||
{showModelSelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={currentModel}
|
||||
|
|
|
@ -18,8 +18,8 @@ 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 "./TabActions";
|
||||
import MenuWrapper from "./MenuWrapper";
|
||||
import ActionsBar from "@/app/components/ActionsBar";
|
||||
|
||||
const SessionList = MenuWrapper(
|
||||
dynamic(async () => await import("./SessionList"), {
|
||||
|
@ -72,7 +72,7 @@ export function SideBar(props: { className?: string }) {
|
|||
|
||||
return (
|
||||
<div className={`${containerClassName}`}>
|
||||
<TabActions
|
||||
<ActionsBar
|
||||
inMobile={isMobileScreen}
|
||||
actionsShema={[
|
||||
{
|
||||
|
@ -133,7 +133,9 @@ export function SideBar(props: { className?: string }) {
|
|||
mobile: [[Path.Chat, Path.Masks, Path.Settings]],
|
||||
}}
|
||||
selected={selectedTab}
|
||||
className={tabActionsClassName}
|
||||
className={`${
|
||||
isMobileScreen ? "justify-around" : "flex-col"
|
||||
} ${tabActionsClassName}`}
|
||||
/>
|
||||
|
||||
<SessionList
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import { RefObject, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export interface Options {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
delay?: number;
|
||||
offsetDistance?: number;
|
||||
}
|
||||
|
||||
export enum Orientation {
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
top,
|
||||
}
|
||||
|
||||
export type X = Orientation.left | Orientation.right;
|
||||
export type Y = Orientation.top | Orientation.bottom;
|
||||
|
||||
interface Position {
|
||||
id: string;
|
||||
poi: {
|
||||
targetH: number;
|
||||
targetW: number;
|
||||
distanceToRightBoundary: number;
|
||||
distanceToLeftBoundary: number;
|
||||
distanceToTopBoundary: number;
|
||||
distanceToBottomBoundary: number;
|
||||
overlapPositions: Record<Orientation, boolean>;
|
||||
relativePosition: [X, Y];
|
||||
};
|
||||
}
|
||||
|
||||
export default function useRelativePosition({
|
||||
containerRef,
|
||||
delay = 100,
|
||||
offsetDistance = 0,
|
||||
}: Options) {
|
||||
const [position, setPosition] = useState<Position | undefined>();
|
||||
|
||||
const getRelativePosition = useDebouncedCallback(
|
||||
(target: HTMLDivElement, id: string) => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
width: targetW,
|
||||
height: targetH,
|
||||
} = target.getBoundingClientRect();
|
||||
const {
|
||||
x: containerX,
|
||||
y: containerY,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
} = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const distanceToRightBoundary =
|
||||
containerX + containerWidth - (targetX + targetW) - offsetDistance;
|
||||
const distanceToLeftBoundary = targetX - containerX - offsetDistance;
|
||||
const distanceToTopBoundary = targetY - containerY - offsetDistance;
|
||||
const distanceToBottomBoundary =
|
||||
containerY + containerHeight - (targetY + targetH) - offsetDistance;
|
||||
|
||||
setPosition({
|
||||
id,
|
||||
poi: {
|
||||
targetW: targetW + 2 * offsetDistance,
|
||||
targetH: targetH + 2 * offsetDistance,
|
||||
distanceToRightBoundary,
|
||||
distanceToLeftBoundary,
|
||||
distanceToTopBoundary,
|
||||
distanceToBottomBoundary,
|
||||
overlapPositions: {
|
||||
[Orientation.left]: distanceToLeftBoundary <= 0,
|
||||
[Orientation.top]: distanceToTopBoundary <= 0,
|
||||
[Orientation.right]: distanceToRightBoundary <= 0,
|
||||
[Orientation.bottom]: distanceToBottomBoundary <= 0,
|
||||
},
|
||||
relativePosition: [
|
||||
distanceToLeftBoundary <= distanceToRightBoundary
|
||||
? Orientation.left
|
||||
: Orientation.right,
|
||||
distanceToTopBoundary <= distanceToBottomBoundary
|
||||
? Orientation.top
|
||||
: Orientation.bottom,
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
delay,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
getRelativePosition,
|
||||
position,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { autoGrowTextArea } from "../utils";
|
||||
import useMobileScreen from "./useMobileScreen";
|
||||
|
||||
export default function useRows({
|
||||
inputRef,
|
||||
}: {
|
||||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}) {
|
||||
const [inputRows, setInputRows] = useState(2);
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const measure = useDebouncedCallback(
|
||||
() => {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
20,
|
||||
Math.max(2 + (isMobileScreen ? -1 : 1), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
},
|
||||
100,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
inputRows,
|
||||
measure,
|
||||
};
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
import { RefObject, useEffect, useState } from "react";
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function useScrollToBottom(
|
||||
scrollRef: RefObject<HTMLDivElement>,
|
||||
detach: boolean = false,
|
||||
) {
|
||||
// for auto-scroll
|
||||
const detach = scrollRef?.current
|
||||
? Math.abs(
|
||||
scrollRef.current.scrollHeight -
|
||||
(scrollRef.current.scrollTop + scrollRef.current.clientHeight),
|
||||
) <= 1
|
||||
: false;
|
||||
|
||||
const initScrolled = useRef(false);
|
||||
// for auto-scroll
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
function scrollDomToBottom() {
|
||||
const dom = scrollRef.current;
|
||||
|
@ -19,10 +25,11 @@ export default function useScrollToBottom(
|
|||
|
||||
// auto scroll
|
||||
useEffect(() => {
|
||||
if (autoScroll && !detach) {
|
||||
if (autoScroll && !detach && !initScrolled.current) {
|
||||
scrollDomToBottom();
|
||||
initScrolled.current = true;
|
||||
}
|
||||
});
|
||||
}, [autoScroll, detach]);
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useShowPromptHint<RenderPompt>(props: {
|
||||
prompts: RenderPompt[];
|
||||
}) {
|
||||
const [internalPrompts, setInternalPrompts] = useState<RenderPompt[]>([]);
|
||||
const [notShowPrompt, setNotShowPrompt] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.prompts.length !== 0) {
|
||||
setInternalPrompts(props.prompts);
|
||||
|
||||
window.setTimeout(() => {
|
||||
setNotShowPrompt(false);
|
||||
}, 50);
|
||||
|
||||
return;
|
||||
}
|
||||
setNotShowPrompt(true);
|
||||
window.setTimeout(() => {
|
||||
setInternalPrompts(props.prompts);
|
||||
}, 300);
|
||||
}, [props.prompts]);
|
||||
|
||||
return {
|
||||
notShowPrompt,
|
||||
internalPrompts,
|
||||
};
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
|
||||
type Size = {
|
||||
width: number;
|
||||
|
@ -7,15 +7,6 @@ type Size = {
|
|||
|
||||
export function useWindowSize(callback: (size: Size) => void) {
|
||||
const callbackRef = useRef<typeof callback>();
|
||||
const hascalled = useRef(false);
|
||||
|
||||
if (typeof window !== "undefined" && !hascalled.current) {
|
||||
callback({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
hascalled.current = true;
|
||||
}
|
||||
|
||||
callbackRef.current = callback;
|
||||
|
||||
|
@ -29,6 +20,11 @@ export function useWindowSize(callback: (size: Size) => void) {
|
|||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
callback({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="36" height="20" viewBox="0 0 36 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="20" rx="4" fill="#F0F0F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 7.04688C5.5 6.19256 6.19256 5.5 7.04688 5.5C7.90119 5.5 8.59375 6.19256 8.59375 7.04688V7.65625H11.4062V7.04688C11.4062 6.19255 12.0988 5.5 12.9531 5.5C13.8074 5.5 14.5 6.19256 14.5 7.04688C14.5 7.90118 13.8074 8.59375 12.9531 8.59375H12.3438V11.3594H12.9531C13.8074 11.3594 14.5 12.0519 14.5 12.9062C14.5 13.7606 13.8074 14.4531 12.9531 14.4531C12.0988 14.4531 11.4062 13.7606 11.4062 12.9062V12.2969H8.59375V12.9062C8.59375 13.7606 7.90119 14.4531 7.04688 14.4531C6.19255 14.4531 5.5 13.7606 5.5 12.9062C5.5 12.0519 6.19256 11.3594 7.04688 11.3594H7.65625V8.59375H7.04688C6.19256 8.59375 5.5 7.90119 5.5 7.04688ZM7.04688 6.4375C6.71033 6.4375 6.4375 6.71033 6.4375 7.04688C6.4375 7.38342 6.71033 7.65625 7.04688 7.65625H7.65625V7.04688C7.65625 6.71033 7.38342 6.4375 7.04688 6.4375ZM7.04688 12.2969C6.71032 12.2969 6.4375 12.5697 6.4375 12.9062C6.4375 13.2428 6.71033 13.5156 7.04688 13.5156C7.38342 13.5156 7.65625 13.2428 7.65625 12.9062V12.2969H7.04688ZM8.59375 11.3594V8.59375H11.4062V11.3594H8.59375ZM12.9531 6.4375C12.6166 6.4375 12.3438 6.71033 12.3438 7.04688V7.65625H12.9531C13.2897 7.65625 13.5625 7.38342 13.5625 7.04688C13.5625 6.71032 13.2897 6.4375 12.9531 6.4375ZM12.3438 12.9062V12.2969H12.9531C13.2897 12.2969 13.5625 12.5697 13.5625 12.9062C13.5625 13.2428 13.2897 13.5156 12.9531 13.5156C12.6166 13.5156 12.3438 13.2428 12.3438 12.9062Z" fill="#88889A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.0156 12.8125C22.0156 13.0714 22.2255 13.2812 22.4844 13.2812H27.5938C29.4577 13.2812 30.9688 11.7702 30.9688 9.90625C30.9688 8.04229 29.4577 6.53125 27.5938 6.53125H22.4844C22.2255 6.53125 22.0156 6.74112 22.0156 7C22.0156 7.25888 22.2255 7.46875 22.4844 7.46875H27.5938C28.9399 7.46875 30.0313 8.56006 30.0313 9.90625C30.0313 11.2524 28.9399 12.3438 27.5938 12.3438H22.4844C22.2255 12.3438 22.0156 12.5536 22.0156 12.8125Z" fill="#88889A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.6215 14.8224C23.7996 14.6344 23.7916 14.3378 23.6036 14.1597L22.1816 12.8125L21.5368 13.4931L22.9589 14.8403C23.1468 15.0183 23.4435 15.0103 23.6215 14.8224ZM21.5368 13.4931C21.1465 13.1233 21.1465 12.5017 21.5368 12.1319L22.9589 10.7847C23.1468 10.6067 23.4435 10.6147 23.6215 10.8026C23.7996 10.9906 23.7916 11.2872 23.6036 11.4653L22.1816 12.8125L21.5368 13.4931Z" fill="#88889A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2.625C11.7248 2.625 12.2055 2.62633 12.5626 2.67435C12.9018 2.71995 13.0345 2.79709 13.1187 2.88128C13.2029 2.96547 13.2801 3.09821 13.3257 3.43737C13.3737 3.79452 13.375 4.27523 13.375 5V8.94186C13.375 9.44328 13.3743 9.7756 13.3503 10.0289C13.3272 10.273 13.2868 10.3787 13.2469 10.4442C13.1744 10.563 13.0746 10.6628 12.9558 10.7353C12.8904 10.7752 12.7846 10.8156 12.5406 10.8387C12.4195 10.8502 12.2804 10.8563 12.1134 10.8596L12.1134 7.46904C12.1134 6.7983 12.1134 6.23273 12.0529 5.78244C11.9886 5.30467 11.8461 4.86418 11.491 4.50903C11.1358 4.15388 10.6953 4.01136 10.2176 3.94712C9.76726 3.88658 9.20172 3.8866 8.53096 3.88663L5.1387 3.88663C5.14095 3.71923 5.1456 3.58282 5.15527 3.46541C5.17281 3.25236 5.20362 3.15794 5.23345 3.09992C5.31695 2.93752 5.44915 2.80532 5.61154 2.72183C5.66957 2.69199 5.76399 2.66118 5.97704 2.64364C6.19747 2.6255 6.48488 2.625 6.91861 2.625H11ZM3.88843 3.89878C3.89083 3.69998 3.89643 3.52141 3.90949 3.36285C3.93344 3.07185 3.98563 2.79318 4.12178 2.52836C4.32454 2.13398 4.64561 1.81292 5.03999 1.61015C5.30481 1.474 5.58348 1.42181 5.87447 1.39786C6.15232 1.37498 6.49155 1.37499 6.89348 1.375L11.0426 1.375C11.7133 1.37497 12.2789 1.37495 12.7292 1.43549C13.207 1.49973 13.6475 1.64225 14.0026 1.9974C14.3578 2.35255 14.5003 2.79304 14.5645 3.27081C14.625 3.7211 14.625 4.28663 14.625 4.95738L14.625 8.97099C14.625 9.43552 14.625 9.82753 14.5948 10.1469C14.563 10.4821 14.4935 10.801 14.3139 11.0954C14.1378 11.3839 13.8955 11.6262 13.607 11.8023C13.3126 11.9819 12.9937 12.0514 12.6585 12.0831C12.4924 12.0989 12.3067 12.1064 12.1013 12.11C12.0927 12.3358 12.078 12.5424 12.0529 12.7292C11.9886 13.207 11.8461 13.6475 11.491 14.0026C11.1358 14.3578 10.6953 14.5003 10.2176 14.5645C9.76728 14.625 9.20176 14.625 8.53103 14.625H4.95735C4.28662 14.625 3.7211 14.625 3.27081 14.5645C2.79304 14.5003 2.35255 14.3578 1.9974 14.0026C1.64225 13.6475 1.49973 13.207 1.43549 12.7292C1.37495 12.2789 1.37497 11.7133 1.375 11.0426V7.46905C1.37497 6.79828 1.37495 6.23274 1.43549 5.78244C1.49973 5.30467 1.64225 4.86418 1.9974 4.50903C2.35255 4.15388 2.79304 4.01136 3.27081 3.94712C3.45718 3.92206 3.66329 3.90738 3.88843 3.89878ZM2.88128 5.39291C2.96547 5.30872 3.09821 5.23157 3.43737 5.18597C3.79452 5.13796 4.27523 5.13663 5 5.13663H8.48837C9.21315 5.13663 9.69386 5.13796 10.051 5.18597C10.3902 5.23157 10.5229 5.30872 10.6071 5.39291C10.6913 5.4771 10.7684 5.60984 10.814 5.949C10.862 6.30615 10.8634 6.78685 10.8634 7.51163V11C10.8634 11.7248 10.862 12.2055 10.814 12.5626C10.7684 12.9018 10.6913 13.0345 10.6071 13.1187C10.5229 13.2029 10.3902 13.2801 10.051 13.3257C9.69386 13.3737 9.21315 13.375 8.48837 13.375H5C4.27523 13.375 3.79452 13.3737 3.43737 13.3257C3.09821 13.2801 2.96547 13.2029 2.88128 13.1187C2.79709 13.0345 2.71995 12.9018 2.67435 12.5626C2.62633 12.2055 2.625 11.7248 2.625 11V7.51163C2.625 6.78685 2.62633 6.30615 2.67435 5.949C2.71995 5.60984 2.79709 5.4771 2.88128 5.39291Z" fill="#717187"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.74419 1.375C6.39901 1.375 6.11919 1.65482 6.11919 2C6.11919 2.34518 6.39901 2.625 6.74419 2.625H9.25581C9.60099 2.625 9.88081 2.34518 9.88081 2C9.88081 1.65482 9.60099 1.375 9.25581 1.375H6.74419ZM2 3.39825C1.65482 3.39825 1.375 3.67808 1.375 4.02325C1.375 4.36843 1.65482 4.64825 2 4.64825H3.25873V12.5C3.25873 13.6736 4.21012 14.625 5.38373 14.625H10.6163C11.7899 14.625 12.7413 13.6736 12.7413 12.5V4.64825H14C14.3452 4.64825 14.625 4.36843 14.625 4.02325C14.625 3.67808 14.3452 3.39825 14 3.39825H12.1163H3.88373H2ZM4.50873 12.5V4.64825H11.4913V12.5C11.4913 12.9832 11.0995 13.375 10.6163 13.375H5.38373C4.90048 13.375 4.50873 12.9832 4.50873 12.5ZM8.625 6.88372C8.625 6.53854 8.34518 6.25872 8 6.25872C7.65482 6.25872 7.375 6.53854 7.375 6.88372V10.7907C7.375 11.1359 7.65482 11.4157 8 11.4157C8.34518 11.4157 8.625 11.1359 8.625 10.7907V6.88372Z" fill="#717187"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.27273 1.875H7.7094H7.70939C6.45174 1.87499 5.44964 1.87498 4.65834 1.97278C3.8431 2.07354 3.16658 2.28637 2.60987 2.77458C2.49091 2.87891 2.37891 2.99091 2.27458 3.10987C1.7864 3.66653 1.57355 4.34287 1.47278 5.15785C1.37498 5.94886 1.37499 6.95053 1.375 8.20755V8.20757V8.29243V8.29245C1.37499 9.54947 1.37498 10.5511 1.47278 11.3421C1.57355 12.1571 1.7864 12.8335 2.27458 13.3901C2.37891 13.5091 2.49091 13.6211 2.60987 13.7254C3.16653 14.2136 3.84287 14.4264 4.65785 14.5272C5.44887 14.625 6.45053 14.625 7.70756 14.625H7.79244C9.04947 14.625 10.0511 14.625 10.8421 14.5272C11.6571 14.4264 12.3335 14.2136 12.8901 13.7254C13.0091 13.6211 13.1211 13.5091 13.2254 13.3901C13.7136 12.8334 13.9265 12.1569 14.0272 11.3417C14.125 10.5504 14.125 9.54825 14.125 8.2906V7.72727C14.125 7.3821 13.8452 7.10227 13.5 7.10227C13.1548 7.10227 12.875 7.3821 12.875 7.72727V8.24818C12.875 9.55791 12.8739 10.4826 12.7867 11.1883C12.7013 11.879 12.5419 12.2737 12.2856 12.5659C12.2173 12.6439 12.1439 12.7173 12.0659 12.7856C11.7737 13.0419 11.379 13.2013 10.6888 13.2867C9.98339 13.3739 9.05913 13.375 7.75 13.375C6.44087 13.375 5.51661 13.3739 4.81124 13.2867C4.12096 13.2013 3.72631 13.0419 3.43406 12.7856C3.35611 12.7173 3.28273 12.6439 3.21438 12.5659C2.95808 12.2737 2.79869 11.879 2.71334 11.1888C2.62612 10.4834 2.625 9.55913 2.625 8.25C2.625 6.94087 2.62612 6.01661 2.71334 5.31124C2.79869 4.62096 2.95808 4.22631 3.21438 3.93406C3.28273 3.85611 3.35611 3.78273 3.43406 3.71438C3.72627 3.45812 4.12102 3.2987 4.81167 3.21334C5.51736 3.12612 6.44209 3.125 7.75182 3.125H8.27273C8.61791 3.125 8.89773 2.84518 8.89773 2.5C8.89773 2.15482 8.61791 1.875 8.27273 1.875ZM12.2881 3.26785C12.0658 3.10115 11.7602 3.10115 11.538 3.26785C11.5114 3.28778 11.475 3.322 11.3115 3.48547L11.2318 3.56523L12.4348 4.76831L12.5146 4.68855C12.6781 4.52507 12.7123 4.48861 12.7322 4.46206C12.8989 4.23981 12.8989 3.93421 12.7322 3.71197C12.7123 3.68541 12.6781 3.64895 12.5146 3.48548C12.3511 3.322 12.3146 3.28778 12.2881 3.26785ZM5.94247 7.08675L9.8995 3.12972L9.90588 3.12324L9.91235 3.11686L10.4276 2.60159L10.4544 2.57481L10.4544 2.57478C10.5758 2.45322 10.6803 2.34867 10.7879 2.26792C11.4546 1.76779 12.3715 1.76779 13.0382 2.26792C13.1458 2.34867 13.2503 2.45324 13.3717 2.57481L13.3985 2.60159L13.4253 2.62836C13.5468 2.74979 13.6514 2.85423 13.7321 2.96188C14.2323 3.6286 14.2323 4.54542 13.7321 5.21214C13.6514 5.31979 13.5468 5.42424 13.4252 5.54567L13.3985 5.57243L8.91331 10.0576L8.85201 10.119C8.55248 10.4195 8.28665 10.6861 7.9389 10.8302C7.59114 10.9742 7.21462 10.9736 6.79036 10.973L6.7036 10.9729H5.65218C5.307 10.9729 5.02718 10.6931 5.02718 10.3479V9.29646L5.02709 9.2097C5.02644 8.78544 5.02586 8.40892 5.1699 8.06116C5.31395 7.71341 5.5806 7.44757 5.88105 7.14804L5.94247 7.08675ZM11.5509 5.65219L10.3479 4.44912L6.82635 7.97063C6.429 8.36798 6.36126 8.45137 6.32475 8.53952C6.28824 8.62766 6.27718 8.73452 6.27718 9.29646V9.72288H6.7036C7.26553 9.72288 7.37239 9.71182 7.46054 9.67531C7.54869 9.63879 7.63208 9.57105 8.02942 9.17371L11.5509 5.65219Z" fill="#717187"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" rx="4" fill="#F0F0F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.01563 12.8125C6.01563 13.0714 6.2255 13.2812 6.48438 13.2812H11.5938C13.4577 13.2812 14.9688 11.7702 14.9688 9.90625C14.9688 8.04229 13.4577 6.53125 11.5938 6.53125H6.48438C6.2255 6.53125 6.01563 6.74112 6.01563 7C6.01563 7.25888 6.2255 7.46875 6.48438 7.46875H11.5938C12.9399 7.46875 14.0313 8.56006 14.0313 9.90625C14.0313 11.2524 12.9399 12.3438 11.5938 12.3438H6.48438C6.2255 12.3438 6.01563 12.5536 6.01563 12.8125Z" fill="#88889A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.62154 14.8224C7.79959 14.6344 7.79157 14.3378 7.60363 14.1597L6.18158 12.8125L5.53682 13.4931L6.95887 14.8403C7.14681 15.0183 7.4435 15.0103 7.62154 14.8224ZM5.53682 13.4931C5.1465 13.1233 5.1465 12.5017 5.53682 12.1319L6.95887 10.7847C7.14681 10.6067 7.4435 10.6147 7.62154 10.8026C7.79959 10.9906 7.79157 11.2872 7.60363 11.4653L6.18158 12.8125L5.53682 13.4931Z" fill="#88889A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 1.66675C5.40841 1.66675 1.66675 5.40841 1.66675 10.0001C1.66675 14.5917 5.40841 18.3334 10.0001 18.3334C14.5917 18.3334 18.3334 14.5917 18.3334 10.0001C18.3334 5.40841 14.5917 1.66675 10.0001 1.66675ZM12.8001 11.9167C13.0417 12.1584 13.0417 12.5584 12.8001 12.8001C12.6751 12.9251 12.5167 12.9834 12.3584 12.9834C12.2001 12.9834 12.0417 12.9251 11.9167 12.8001L10.0001 10.8834L8.08342 12.8001C7.95841 12.9251 7.80008 12.9834 7.64175 12.9834C7.48342 12.9834 7.32508 12.9251 7.20008 12.8001C6.95842 12.5584 6.95842 12.1584 7.20008 11.9167L9.11675 10.0001L7.20008 8.08342C6.95842 7.84175 6.95842 7.44175 7.20008 7.20008C7.44175 6.95842 7.84175 6.95842 8.08342 7.20008L10.0001 9.11675L11.9167 7.20008C12.1584 6.95842 12.5584 6.95842 12.8001 7.20008C13.0417 7.44175 13.0417 7.84175 12.8001 8.08342L10.8834 10.0001L12.8001 11.9167Z" fill="white" fill-opacity="0.7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 982 B |
|
@ -0,0 +1,6 @@
|
|||
<svg width="80" height="40" viewBox="0 0 80 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.0249 11.1C32.0249 6.69412 35.594 3.125 39.9999 3.125C40.9652 3.125 41.8377 3.28005 42.623 3.51474C43.0199 3.63334 43.2454 4.05122 43.1268 4.44809C43.0082 4.84496 42.5904 5.07053 42.1935 4.95193C41.5288 4.75328 40.8013 4.625 39.9999 4.625C36.4224 4.625 33.5249 7.52255 33.5249 11.1C33.5249 14.6863 36.423 17.5833 39.9999 17.5833C43.5774 17.5833 46.4749 14.6858 46.4749 11.1083C46.4749 9.77862 46.0719 8.5392 45.3842 7.50769C45.1544 7.16304 45.2476 6.69739 45.5922 6.46763C45.9369 6.23786 46.4025 6.33099 46.6323 6.67564C47.4779 7.94414 47.9749 9.47137 47.9749 11.1083C47.9749 15.5142 44.4058 19.0833 39.9999 19.0833C35.5935 19.0833 32.0249 15.5136 32.0249 11.1Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.6229 1.19534C40.8832 0.968709 41.278 0.996045 41.5046 1.2564L43.913 4.02307C44.1396 4.28342 44.1123 4.6782 43.8519 4.90484C43.5915 5.13147 43.1968 5.10414 42.9701 4.84378L40.5618 2.07711C40.3352 1.81676 40.3625 1.42198 40.6229 1.19534Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.9465 4.0649C44.15 4.34369 44.089 4.73469 43.8102 4.9382L41.0018 6.9882C40.723 7.19172 40.332 7.13069 40.1285 6.85189C39.925 6.57309 39.986 6.1821 40.2648 5.97858L43.0732 3.92858C43.352 3.72507 43.743 3.7861 43.9465 4.0649Z" fill="white"/>
|
||||
<path d="M28.2487 27.944V30.896H29.3647C29.8767 30.896 30.2647 30.764 30.5287 30.5C30.7927 30.228 30.9247 29.868 30.9247 29.42C30.9247 28.436 30.3847 27.944 29.3047 27.944H28.2487ZM31.2127 35L29.3167 31.928H28.2487V35H27.0127V26.912H29.2687C30.1967 26.912 30.9127 27.14 31.4167 27.596C31.9207 28.044 32.1727 28.656 32.1727 29.432C32.1647 30 32.0207 30.484 31.7407 30.884C31.4607 31.276 31.0727 31.56 30.5767 31.736L32.6887 35H31.2127ZM37.565 31.52V31.304C37.549 30.848 37.425 30.488 37.193 30.224C36.961 29.952 36.629 29.816 36.197 29.816C35.797 29.816 35.453 29.964 35.165 30.26C34.877 30.548 34.697 30.968 34.625 31.52H37.565ZM38.321 33.62V34.676C37.873 34.932 37.281 35.06 36.545 35.06C35.601 35.06 34.849 34.776 34.289 34.208C33.729 33.64 33.449 32.896 33.449 31.976C33.449 31 33.705 30.236 34.217 29.684C34.729 29.124 35.381 28.844 36.173 28.844C36.949 28.844 37.565 29.088 38.021 29.576C38.477 30.056 38.705 30.752 38.705 31.664C38.705 31.912 38.681 32.172 38.633 32.444H34.637C34.717 32.972 34.941 33.376 35.309 33.656C35.677 33.936 36.141 34.076 36.701 34.076C37.349 34.076 37.889 33.924 38.321 33.62ZM41.6137 29.828V33.176C41.6137 33.76 41.8937 34.052 42.4537 34.052C42.7817 34.052 43.0577 33.964 43.2817 33.788V34.844C43.0177 34.988 42.6937 35.06 42.3097 35.06C41.0537 35.06 40.4257 34.424 40.4257 33.152V29.828H39.4777V28.916H40.4257V27.428H41.6137V28.916H43.2097V29.828H41.6137ZM45.754 28.916V29.984C46.05 29.232 46.582 28.856 47.35 28.856C47.462 28.856 47.586 28.868 47.722 28.892V30.044C47.554 29.972 47.366 29.936 47.158 29.936C46.75 29.936 46.414 30.116 46.15 30.476C45.886 30.836 45.754 31.264 45.754 31.76V35H44.578V28.916H45.754ZM50.3244 34.868L47.9004 28.916H49.2084L49.9644 30.896L50.9124 33.464C50.9844 33.224 51.2724 32.368 51.7764 30.896L52.4604 28.916H53.7084L51.5124 34.832C50.9284 36.392 50.0364 37.172 48.8364 37.172C48.6204 37.172 48.4364 37.152 48.2844 37.112V36.104C48.4124 36.136 48.5484 36.152 48.6924 36.152C49.4684 36.152 50.0124 35.724 50.3244 34.868Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="80" height="40" viewBox="0 0 80 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.9167 10.0001C30.9167 5.00253 35.0025 0.916748 40.0001 0.916748C44.9976 0.916748 49.0834 5.00253 49.0834 10.0001C49.0834 14.9976 44.9976 19.0834 40.0001 19.0834C35.0025 19.0834 30.9167 14.9976 30.9167 10.0001ZM40.0001 2.41675C35.831 2.41675 32.4167 5.83096 32.4167 10.0001C32.4167 14.1692 35.831 17.5834 40.0001 17.5834C44.1692 17.5834 47.5834 14.1692 47.5834 10.0001C47.5834 5.83096 44.1692 2.41675 40.0001 2.41675Z" fill="#FF5454"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 5.91675C40.4142 5.91675 40.75 6.25253 40.75 6.66675V10.8334C40.75 11.2476 40.4142 11.5834 40 11.5834C39.5858 11.5834 39.25 11.2476 39.25 10.8334V6.66675C39.25 6.25253 39.5858 5.91675 40 5.91675Z" fill="#FF5454"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.2456 13.3335C39.2456 12.9193 39.5814 12.5835 39.9956 12.5835H40.0031C40.4173 12.5835 40.7531 12.9193 40.7531 13.3335C40.7531 13.7477 40.4173 14.0835 40.0031 14.0835H39.9956C39.5814 14.0835 39.2456 13.7477 39.2456 13.3335Z" fill="#FF5454"/>
|
||||
<path d="M29.9268 27.944H26.5788V30.596H29.5188V31.628H26.5788V35H25.3428V26.912H29.9268V27.944ZM34.4285 32.288H33.5165C33.0685 32.288 32.7285 32.376 32.4965 32.552C32.2725 32.728 32.1605 32.952 32.1605 33.224C32.1605 33.504 32.2485 33.724 32.4245 33.884C32.6005 34.044 32.8565 34.124 33.1925 34.124C33.5765 34.124 33.8765 34.02 34.0925 33.812C34.3165 33.596 34.4285 33.316 34.4285 32.972V32.288ZM31.3685 30.368V29.228C31.8405 28.956 32.4645 28.82 33.2405 28.82C34.8325 28.82 35.6285 29.592 35.6285 31.136V35H34.4645V34.28C34.1525 34.792 33.6045 35.048 32.8205 35.048C32.2445 35.048 31.7805 34.884 31.4285 34.556C31.0845 34.22 30.9125 33.796 30.9125 33.284C30.9125 32.7 31.1325 32.248 31.5725 31.928C32.0125 31.6 32.6285 31.436 33.4205 31.436H34.4285V31.088C34.4285 30.672 34.3285 30.36 34.1285 30.152C33.9285 29.936 33.5965 29.828 33.1325 29.828C32.4845 29.828 31.8965 30.008 31.3685 30.368ZM37.2608 35V28.916H38.4488V35H37.2608ZM37.3208 27.584C37.1848 27.44 37.1168 27.264 37.1168 27.056C37.1168 26.848 37.1848 26.672 37.3208 26.528C37.4648 26.384 37.6408 26.312 37.8488 26.312C38.0568 26.312 38.2328 26.384 38.3768 26.528C38.5208 26.672 38.5928 26.848 38.5928 27.056C38.5928 27.256 38.5208 27.428 38.3768 27.572C38.2328 27.716 38.0568 27.788 37.8488 27.788C37.6408 27.788 37.4648 27.72 37.3208 27.584ZM40.2022 35V26.516H41.3902V35H40.2022ZM46.8756 31.52V31.304C46.8596 30.848 46.7356 30.488 46.5036 30.224C46.2716 29.952 45.9396 29.816 45.5076 29.816C45.1076 29.816 44.7636 29.964 44.4756 30.26C44.1876 30.548 44.0076 30.968 43.9356 31.52H46.8756ZM47.6316 33.62V34.676C47.1836 34.932 46.5916 35.06 45.8556 35.06C44.9116 35.06 44.1596 34.776 43.5996 34.208C43.0396 33.64 42.7596 32.896 42.7596 31.976C42.7596 31 43.0156 30.236 43.5276 29.684C44.0396 29.124 44.6916 28.844 45.4836 28.844C46.2596 28.844 46.8756 29.088 47.3316 29.576C47.7876 30.056 48.0156 30.752 48.0156 31.664C48.0156 31.912 47.9916 32.172 47.9436 32.444H43.9476C44.0276 32.972 44.2516 33.376 44.6196 33.656C44.9876 33.936 45.4516 34.076 46.0116 34.076C46.6596 34.076 47.1996 33.924 47.6316 33.62ZM51.8603 34.052C52.3243 34.052 52.7163 33.876 53.0363 33.524C53.3563 33.172 53.5163 32.636 53.5163 31.916C53.5163 31.22 53.3483 30.704 53.0123 30.368C52.6843 30.032 52.3123 29.864 51.8963 29.864C51.4243 29.864 51.0363 30.052 50.7323 30.428C50.4283 30.796 50.2763 31.324 50.2763 32.012C50.2763 32.62 50.4243 33.112 50.7203 33.488C51.0243 33.864 51.4043 34.052 51.8603 34.052ZM53.5163 26.516H54.7043V35H53.5163V34.076C53.0923 34.748 52.4723 35.084 51.6563 35.084C50.8563 35.084 50.2243 34.784 49.7603 34.184C49.2963 33.576 49.0643 32.844 49.0643 31.988C49.0643 31.036 49.3083 30.272 49.7963 29.696C50.2843 29.12 50.9123 28.832 51.6803 28.832C52.4723 28.832 53.0843 29.168 53.5163 29.84V26.516Z" fill="#FF5454"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9.09091" stroke="white" stroke-opacity="0.4" stroke-width="1.81818"/>
|
||||
<path d="M1 10C1 5.02944 5.02944 1 10 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 322 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.09828 1.375C4.89492 1.375 4.70427 1.47394 4.58721 1.64024C4.47015 1.80654 4.44132 2.01939 4.50992 2.21084L5.04632 3.70775C5.05117 3.72127 5.05365 3.73554 5.05365 3.74991V6.01435C5.05365 6.04395 5.04314 6.0726 5.024 6.09518L3.76817 7.57663C2.73545 8.79491 3.60134 10.6641 5.19843 10.6641H7.37514V14.25C7.37514 14.5952 7.65496 14.875 8.00014 14.875C8.34532 14.875 8.62514 14.5952 8.62514 14.25V10.6641H10.8018C12.3989 10.6641 13.2648 8.79491 12.2321 7.57663L10.9762 6.09518C10.9571 6.0726 10.9466 6.04395 10.9466 6.01435V3.74991C10.9466 3.73554 10.9491 3.72128 10.9539 3.70775L11.4903 2.21084C11.5589 2.01939 11.5301 1.80654 11.413 1.64024C11.296 1.47394 11.1053 1.375 10.902 1.375H5.09828ZM6.22305 3.28607L5.98616 2.625H10.0141L9.77718 3.28607C9.72385 3.4349 9.69659 3.59182 9.69659 3.74991V6.01435C9.69659 6.33999 9.81216 6.65506 10.0227 6.90346L11.2786 8.38492C11.6228 8.79101 11.3342 9.41406 10.8018 9.41406H5.19843C4.66607 9.41406 4.37744 8.79101 4.72168 8.38492L5.97751 6.90346C6.18808 6.65506 6.30365 6.33999 6.30365 6.01435V3.74991C6.30365 3.59182 6.27639 3.4349 6.22305 3.28607Z" fill="#717187"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
<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="#434360"/>
|
||||
</svg>
|
After Width: | Height: | Size: 279 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0625 3.72652C11.8478 2.28905 10.0309 1.375 8 1.375C4.34111 1.375 1.375 4.34111 1.375 8C1.375 11.6589 4.34111 14.625 8 14.625C11.3073 14.625 14.0476 12.2021 14.5446 9.03438C14.5981 8.69338 14.3651 8.37356 14.0241 8.32005C13.6831 8.26655 13.3632 8.49961 13.3097 8.84062C12.9066 11.4097 10.6822 13.375 8 13.375C5.03147 13.375 2.625 10.9685 2.625 8C2.625 5.03147 5.03147 2.625 8 2.625C9.60139 2.625 11.0397 3.3251 12.025 4.4375H11.5625C11.2173 4.4375 10.9375 4.71732 10.9375 5.0625C10.9375 5.40768 11.2173 5.6875 11.5625 5.6875H13.6875C14.0327 5.6875 14.3125 5.40768 14.3125 5.0625V2.9375C14.3125 2.59232 14.0327 2.3125 13.6875 2.3125C13.3423 2.3125 13.0625 2.59232 13.0625 2.9375V3.72652Z" fill="#717187"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5447 3.84421C15.0636 2.36142 13.6383 0.936098 12.1555 1.45507L2.58182 4.80587C0.978998 5.36685 0.887921 7.59931 2.43972 8.289L5.86922 9.81322C6.01061 9.87606 6.12367 9.98912 6.18651 10.1305L7.71073 13.56C8.40042 15.1118 10.6329 15.0207 11.1939 13.4179L14.5447 3.84421ZM12.4338 2.68201L2.99476 5.98569C2.46049 6.17268 2.43013 6.91684 2.94739 7.14673L6.37689 8.67095C6.39252 8.6779 6.40804 8.68505 6.42344 8.69241L12.4338 2.68201ZM7.30732 9.57629C7.31468 9.59169 7.32183 9.60721 7.32878 9.62284L8.853 13.0523C9.08289 13.5696 9.82705 13.5392 10.014 13.005L13.3177 3.5659L7.30732 9.57629Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 757 B |
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @next/next/no-page-custom-font */
|
||||
// import "./styles/globals.scss";
|
||||
import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/highlight.scss";
|
||||
import "./styles/globals.css";
|
||||
|
|
|
@ -67,9 +67,9 @@ const cn = {
|
|||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
Input: (submitKey: string) => {
|
||||
Input: (submitKey: string, isMobileScreen?: boolean) => {
|
||||
var inputHints = `${submitKey} 发送`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
if (submitKey === String(SubmitKey.Enter) && !isMobileScreen) {
|
||||
inputHints += ",Shift + Enter 换行";
|
||||
}
|
||||
return inputHints + ",/ 触发补全,: 触发命令";
|
||||
|
|
|
@ -23,3 +23,8 @@ body {
|
|||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
--tip-popover-color: #434360;
|
||||
--chat-panel-bg: rgb(249, 250, 251, 1);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,11 @@ module.exports = {
|
|||
'sm-title': '0.875rem',
|
||||
'sm-mobile-tab': '0.625rem',
|
||||
'chat-header-title': '1rem',
|
||||
'actions-popover-menu-item': '15px',
|
||||
},
|
||||
fontFamily: {
|
||||
'common': ['Satoshi Variable', 'Variable'],
|
||||
'time': ['Hind', 'Variable']
|
||||
},
|
||||
screens: {
|
||||
sm: '480px',
|
||||
|
@ -23,34 +28,53 @@ module.exports = {
|
|||
// return map;
|
||||
// }, {}),
|
||||
extend: {
|
||||
minHeight: {
|
||||
'chat-input-mobile': '19px',
|
||||
'chat-input': '60px',
|
||||
},
|
||||
width: {
|
||||
'md': '15rem',
|
||||
'lg': '21.25rem',
|
||||
'2xl': '27.5rem',
|
||||
'page': 'calc(100% - var(--sidebar-width))',
|
||||
'thumbnail': '5rem',
|
||||
'actions-popover': '203px',
|
||||
},
|
||||
height: {
|
||||
mobile: '3.125rem',
|
||||
'menu-title-mobile': '3rem',
|
||||
'thumbnail': '5rem',
|
||||
'chat-input-mobile': '19px',
|
||||
'chat-input': '60px',
|
||||
},
|
||||
flexBasis: {
|
||||
'sidebar': 'var(--sidebar-width)',
|
||||
'page': 'calc(100%-var(--sidebar-width))',
|
||||
'page': 'calc(100% - var(--sidebar-width))',
|
||||
},
|
||||
spacing: {
|
||||
'chat-header-gap': '0.625rem',
|
||||
},
|
||||
backgroundImage: {
|
||||
'message-bg': 'linear-gradient(259deg, #9786FF 8.42%, #4A5CFF 90.13%)',
|
||||
'thumbnail-mask': 'linear-gradient(0deg, rgba(0, 0, 0, 0.50) 0%, rgba(0, 0, 0, 0.50) 100%)',
|
||||
},
|
||||
transitionProperty: {
|
||||
'time': 'all ease 0.6s',
|
||||
'message': 'all ease 0.3s',
|
||||
},
|
||||
maxWidth: {
|
||||
'message-width': 'var(--max-message-width, 70%)'
|
||||
'message-width': 'var(--max-message-width, 80%)'
|
||||
},
|
||||
backgroundColor: {
|
||||
'select-btn': 'rgba(0, 0, 0, 0.05)',
|
||||
'chat-actions-popover-color': 'var(--tip-popover-color)',
|
||||
'chat-panel': 'var(--chat-panel-bg)',
|
||||
},
|
||||
boxShadow: {
|
||||
'btn': '0px 4px 10px 0px rgba(60, 68, 255, 0.14)',
|
||||
'chat-input': '0px 4px 20px 0px rgba(60, 68, 255, 0.13)',
|
||||
'actions-popover': '0px 14px 40px 0px rgba(0, 0, 0, 0.12)',
|
||||
'actions-bar': '0px 4px 30px 0px rgba(0, 0, 0, 0.10)',
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
|
@ -59,13 +83,23 @@ module.exports = {
|
|||
DEFAULT: '0.25rem',
|
||||
'md': '0.75rem',
|
||||
'lg': '1rem',
|
||||
'message': '16px 4px 16px 16px',
|
||||
'user-message': '16px 4px 16px 16px',
|
||||
'bot-message': '4px 16px 16px 16px',
|
||||
'action-btn': '0.5rem',
|
||||
'actions-bar-btn': '0.375rem',
|
||||
'chat-input': '0.5rem',
|
||||
'chat-img': '0.5rem',
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: '1px',
|
||||
'0': '0',
|
||||
'2': '2px',
|
||||
'3': '3px',
|
||||
'4': '4px',
|
||||
'6': '6px',
|
||||
'8': '8px',
|
||||
'actions-popover': '1px',
|
||||
},
|
||||
fontFamily: {
|
||||
'common': ['Satoshi Variable', 'Variable'],
|
||||
'time': ['Hind', 'Variable']
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|