diff --git a/app/containers/Sidebar/TabActions.tsx b/app/components/ActionsBar/index.tsx
similarity index 76%
rename from app/containers/Sidebar/TabActions.tsx
rename to app/components/ActionsBar/index.tsx
index 2594882dd..c121e1915 100644
--- a/app/containers/Sidebar/TabActions.tsx
+++ b/app/components/ActionsBar/index.tsx
@@ -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}
@@ -93,10 +98,10 @@ export default function TabActions(props: TabActionsProps) {
return (
{selected === action.id ? activeIcon : inactiveIcon}
@@ -104,18 +109,10 @@ export default function TabActions(props: TabActionsProps) {
}),
);
if (ind < arr.length - 1) {
- res.push(
);
+ res.push(
);
}
return res;
}, [] as JSX.Element[]);
- return (
-
- {content}
-
- );
+ return
{content}
;
}
diff --git a/app/components/Btn/index.tsx b/app/components/Btn/index.tsx
new file mode 100644
index 000000000..d05919d9c
--- /dev/null
+++ b/app/components/Btn/index.tsx
@@ -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 (
+
+ );
+}
diff --git a/app/components/Loading/index.module.scss b/app/components/Loading/index.module.scss
deleted file mode 100644
index 98f568e29..000000000
--- a/app/components/Loading/index.module.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.loading-content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 100%;
- width: 100%;
- }
\ No newline at end of file
diff --git a/app/components/Loading/index.tsx b/app/components/Loading/index.tsx
index 2ae0ef2a4..781d1dceb 100644
--- a/app/components/Loading/index.tsx
+++ b/app/components/Loading/index.tsx
@@ -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 (
-
- {!props.noLogo &&
}
+
+ {!noLogo && }
);
diff --git a/app/components/Popover/index.tsx b/app/components/Popover/index.tsx
index dcfb59c83..77c343614 100644
--- a/app/components/Popover/index.tsx
+++ b/app/components/Popover/index.tsx
@@ -1,4 +1,24 @@
-import { useState } from "react";
+import { getCSSVar } from "@/app/utils";
+import { useMemo, useState } from "react";
+
+const ArrowIcon = ({ color }: { color: string }) => {
+ return (
+
+ );
+};
+
+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 (
{!noArrow && (
-
+
)}
{content}
+
{
+ e.preventDefault();
+ onShow?.(!mergedShow);
+ setShow(!mergedShow);
+ }}
+ >
+
+
>
)}
@@ -105,8 +146,8 @@ export default function Popover(props: {
{children}
{!noArrow && (
-
-
+
)}
void;
+}
+
+export default function Thumbnail(props: ThumbnailProps) {
+ const { image, deleteImage } = props;
+ return (
+
+ );
+}
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 4749e25d9..767869f41 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -45,7 +45,7 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () =>
,
});
-const Chat = dynamic(async () => await import("@/app/containers/Chat"), {
+const Chat = dynamic(async () => (await import("./chat")).Chat, {
loading: () =>
,
});
@@ -151,10 +151,7 @@ function Screen() {
<>
-
+
} />
diff --git a/app/containers/Chat/ChatActions.tsx b/app/containers/Chat/ChatActions.tsx
index fd8a05088..262451884 100644
--- a/app/containers/Chat/ChatActions.tsx
+++ b/app/containers/Chat/ChatActions.tsx
@@ -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: ,
placement: "right",
},
@@ -178,28 +175,36 @@ export function ChatActions(props: {
if (props.isMobileScreen) {
const content = (
-
+
{actions.map((act) => {
return (
{act.icon}
- {act.text}
+
+ {act.text}
+
);
})}
);
return (
-
+
);
}
- 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 (
@@ -214,7 +219,7 @@ export function ChatActions(props: {
placement={ind ? "t" : "rt"}
>
{act.icon}
@@ -234,7 +239,7 @@ export function ChatActions(props: {
placement={ind === arr.length - 1 ? "lt" : "t"}
>
{act.icon}
diff --git a/app/containers/Chat/ChatInputPanel.tsx b/app/containers/Chat/ChatInputPanel.tsx
index 58638db6a..9d783165d 100644
--- a/app/containers/Chat/ChatInputPanel.tsx
+++ b/app/containers/Chat/ChatInputPanel.tsx
@@ -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
;
inputRef: React.RefObject;
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 (
-
-
-
- );
-}
-
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
@@ -67,13 +60,14 @@ export default forwardRef(
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(
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(
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(
@@ -250,7 +234,7 @@ export default forwardRef
(
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(
isMobileScreen={isMobileScreen}
/>
diff --git a/app/containers/Chat/ChatMessagePanel.tsx b/app/containers/Chat/ChatMessagePanel.tsx
index e2978f4d1..c81e13f74 100644
--- a/app/containers/Chat/ChatMessagePanel.tsx
+++ b/app/containers/Chat/ChatMessagePanel.tsx
@@ -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;
@@ -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: () => ,
},
@@ -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 (
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 (
)}
- {/* {showTyping && (
-
- {Locale.Chat.Typing}
-
- )} */}
+ getRelativePosition(e.currentTarget, message.id)
+ }
>
= messages.length - 6}
className={isUser ? " text-white" : "text-black"}
/>
- {getMessageImages(message).length == 1 && (
-
- )}
- {getMessageImages(message).length > 1 && (
-
+ {getMessageImages(message).length > 0 && (
+
{getMessageImages(message).map((image, index) => {
return (

)}
-
- {showActions && (
-
-
- {message.streaming ? (
- }
- onClick={() => onUserStop(message.id ?? i)}
- />
- ) : (
- <>
- }
- onClick={() => onResend(message)}
- />
-
- }
- onClick={() => onDelete(message.id ?? i)}
- />
-
- }
- onClick={() => onPinMessage(message)}
- />
- }
- onClick={() =>
- copyToClipboard(getMessageTextContent(message))
- }
- />
- }
- 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;
- }
- });
- }}
- />
- >
- )}
-
-
- )}
+
{shouldShowClearContextDivider &&
}
diff --git a/app/containers/Chat/MessageActions.tsx b/app/containers/Chat/MessageActions.tsx
new file mode 100644
index 000000000..300874c4c
--- /dev/null
+++ b/app/containers/Chat/MessageActions.tsx
@@ -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
;
+ 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: ,
+ title: "Edit",
+ className,
+ onClick: () => onEdit(message),
+ },
+ {
+ id: Locale.Chat.Actions.Copy,
+ icons: ,
+ title: Locale.Chat.Actions.Copy,
+ className,
+ onClick: () => onCopy(message),
+ },
+ {
+ id: Locale.Chat.Actions.Pin,
+ icons: ,
+ title: Locale.Chat.Actions.Pin,
+ className,
+ onClick: () => onPinMessage(message),
+ },
+ {
+ id: Locale.Chat.Actions.Delete,
+ icons: ,
+ title: Locale.Chat.Actions.Delete,
+ className,
+ onClick: () => onDelete(message),
+ },
+ {
+ id: Locale.Chat.Actions.Retry,
+ icons: ,
+ title: Locale.Chat.Actions.Retry,
+ className,
+ onClick: () => onResend(message),
+ },
+ {
+ id: Locale.Chat.Actions.Stop,
+ icons: ,
+ 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 && (
+
+ )
+ );
+}
diff --git a/app/containers/Chat/PromptHint.tsx b/app/containers/Chat/PromptHint.tsx
index 4fb1d816d..742b06e77 100644
--- a/app/containers/Chat/PromptHint.tsx
+++ b/app/containers/Chat/PromptHint.tsx
@@ -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;
@@ -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(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 (
-
- {props.prompts.map((prompt, i) => (
+
+ {internalPrompts.map((prompt, i) => (
)}
- {props.showModal && (
-
props.setShowModal(false)} />
- )}
);
}
diff --git a/app/containers/Chat/index.module.scss b/app/containers/Chat/index.module.scss
index b0a90346e..e44a13aaf 100644
--- a/app/containers/Chat/index.module.scss
+++ b/app/containers/Chat/index.module.scss
@@ -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;
diff --git a/app/containers/Chat/index.tsx b/app/containers/Chat/index.tsx
index 1811c2446..bee9a40f5 100644
--- a/app/containers/Chat/index.tsx
+++ b/app/containers/Chat/index.tsx
@@ -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
([]);
// 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 (
+ {showPromptModal && (
+
setShowPromptModal(false)} />
+ )}
+
{showModelSelector && (
await import("./SessionList"), {
@@ -72,7 +72,7 @@ export function SideBar(props: { className?: string }) {
return (
-
;
+ 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;
+ relativePosition: [X, Y];
+ };
+}
+
+export default function useRelativePosition({
+ containerRef,
+ delay = 100,
+ offsetDistance = 0,
+}: Options) {
+ const [position, setPosition] = useState();
+
+ 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,
+ };
+}
diff --git a/app/hooks/useRows.ts b/app/hooks/useRows.ts
new file mode 100644
index 000000000..30b4a4dfa
--- /dev/null
+++ b/app/hooks/useRows.ts
@@ -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;
+}) {
+ 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,
+ };
+}
diff --git a/app/hooks/useScrollToBottom.ts b/app/hooks/useScrollToBottom.ts
index e291ecf92..7fd604cda 100644
--- a/app/hooks/useScrollToBottom.ts
+++ b/app/hooks/useScrollToBottom.ts
@@ -1,11 +1,17 @@
-import { RefObject, useEffect, useState } from "react";
+import { RefObject, useEffect, useRef, useState } from "react";
export default function useScrollToBottom(
scrollRef: RefObject,
- 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,
diff --git a/app/hooks/useShowPromptHint.ts b/app/hooks/useShowPromptHint.ts
new file mode 100644
index 000000000..cb1b4b990
--- /dev/null
+++ b/app/hooks/useShowPromptHint.ts
@@ -0,0 +1,29 @@
+import { useEffect, useState } from "react";
+
+export default function useShowPromptHint(props: {
+ prompts: RenderPompt[];
+}) {
+ const [internalPrompts, setInternalPrompts] = useState([]);
+ 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,
+ };
+}
diff --git a/app/hooks/useWindowSize.ts b/app/hooks/useWindowSize.ts
index 21bed80de..20ad18a5a 100644
--- a/app/hooks/useWindowSize.ts
+++ b/app/hooks/useWindowSize.ts
@@ -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();
- 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);
};
diff --git a/app/icons/command&enterIcon.svg b/app/icons/command&enterIcon.svg
new file mode 100644
index 000000000..e021aa6d8
--- /dev/null
+++ b/app/icons/command&enterIcon.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/icons/copyRequestIcon.svg b/app/icons/copyRequestIcon.svg
new file mode 100644
index 000000000..a83c72e52
--- /dev/null
+++ b/app/icons/copyRequestIcon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/app/icons/deleteRequestIcon.svg b/app/icons/deleteRequestIcon.svg
new file mode 100644
index 000000000..c2b50dcc6
--- /dev/null
+++ b/app/icons/deleteRequestIcon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/app/icons/editRequestIcon.svg b/app/icons/editRequestIcon.svg
new file mode 100644
index 000000000..94d8cbfd3
--- /dev/null
+++ b/app/icons/editRequestIcon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/app/icons/enterIcon.svg b/app/icons/enterIcon.svg
new file mode 100644
index 000000000..d76d40f62
--- /dev/null
+++ b/app/icons/enterIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/icons/imgDeleteIcon.svg b/app/icons/imgDeleteIcon.svg
new file mode 100644
index 000000000..662d96f85
--- /dev/null
+++ b/app/icons/imgDeleteIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/icons/imgRetryUploadIcon.svg b/app/icons/imgRetryUploadIcon.svg
new file mode 100644
index 000000000..df86a37cf
--- /dev/null
+++ b/app/icons/imgRetryUploadIcon.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/icons/imgUploadFailedIcon.svg b/app/icons/imgUploadFailedIcon.svg
new file mode 100644
index 000000000..1310db4d8
--- /dev/null
+++ b/app/icons/imgUploadFailedIcon.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/icons/imgUploadIcon.svg b/app/icons/imgUploadIcon.svg
new file mode 100644
index 000000000..f08ed6752
--- /dev/null
+++ b/app/icons/imgUploadIcon.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/icons/pinRequestIcon.svg b/app/icons/pinRequestIcon.svg
new file mode 100644
index 000000000..2876b4c2b
--- /dev/null
+++ b/app/icons/pinRequestIcon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/app/icons/popoverArrowIcon.svg b/app/icons/popoverArrowIcon.svg
new file mode 100644
index 000000000..375bd89c9
--- /dev/null
+++ b/app/icons/popoverArrowIcon.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/app/icons/retryRequestIcon.svg b/app/icons/retryRequestIcon.svg
new file mode 100644
index 000000000..da583ea6a
--- /dev/null
+++ b/app/icons/retryRequestIcon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/app/icons/sendIcon.svg b/app/icons/sendIcon.svg
new file mode 100644
index 000000000..305e218ba
--- /dev/null
+++ b/app/icons/sendIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/layout.tsx b/app/layout.tsx
index 2d1aec2fb..203ea3823 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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";
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 941078b5f..3320bec71 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -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 + ",/ 触发补全,: 触发命令";
diff --git a/app/styles/globals.css b/app/styles/globals.css
index 9ba356416..3b9bb97d4 100644
--- a/app/styles/globals.css
+++ b/app/styles/globals.css
@@ -23,3 +23,8 @@ body {
width: 100%;
overflow: hidden;
}
+
+:root {
+ --tip-popover-color: #434360;
+ --chat-panel-bg: rgb(249, 250, 251, 1);
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 947477f38..a2c6db11b 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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: [],
}