diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index e7619e92b..bd2ae7d4b 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -231,10 +231,12 @@ animation: slide-in ease 0.3s; - $linear: linear-gradient(to right, - rgba(0, 0, 0, 0), - rgba(0, 0, 0, 1), - rgba(0, 0, 0, 0)); + $linear: linear-gradient( + to right, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 1), + rgba(0, 0, 0, 0) + ); mask-image: $linear; @mixin show { @@ -340,10 +342,14 @@ margin: 0 10px; opacity: 0; pointer-events: none; + z-index: 1; .chat-input-actions { display: flex; flex-wrap: nowrap; + transition: all ease 0.3s; + background-color: #fff; + border-radius: 12px; } } } @@ -367,7 +373,7 @@ } } -.chat-message-user>.chat-message-container { +.chat-message-user > .chat-message-container { align-items: flex-end; } @@ -450,23 +456,27 @@ border: rgba($color: #888, $alpha: 0.2) 1px solid; } - @media only screen and (max-width: 600px) { - $calc-image-width: calc(100vw/3*2/var(--image-count)); + $calc-image-width: calc(100vw / 3 * 2 / var(--image-count)); .chat-message-item-image-multi { width: $calc-image-width; height: $calc-image-width; } - + .chat-message-item-image { - max-width: calc(100vw/3*2); + max-width: calc(100vw / 3 * 2); } } @media screen and (min-width: 600px) { - $max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count)); - $image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count)); + $max-image-width: calc( + calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count) + ); + $image-width: calc( + calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 / + var(--image-count) + ); .chat-message-item-image-multi { width: $image-width; @@ -476,7 +486,7 @@ } .chat-message-item-image { - max-width: calc(calc(1200px - var(--sidebar-width))/3*2); + max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2); } } @@ -494,7 +504,7 @@ z-index: 1; } -.chat-message-user>.chat-message-container>.chat-message-item { +.chat-message-user > .chat-message-container > .chat-message-item { background-color: var(--second); &:hover { @@ -605,7 +615,8 @@ min-height: 68px; } -.chat-input:focus {} +.chat-input:focus { +} .chat-input-send { background-color: var(--primary); @@ -624,4 +635,4 @@ .chat-input-send { bottom: 30px; } -} \ No newline at end of file +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index b18c86708..ac257c277 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -55,7 +55,7 @@ import { import { copyToClipboard, - selectOrCopy, + isNotSelectRange, autoGrowTextArea, useMobileScreen, getMessageTextContent, @@ -250,6 +250,108 @@ function useSubmitHandler() { }; } +function ChatMessageActions(props: { + index: number; + message: ChatMessage; + position: { x: number; y: number }; + isHover: boolean; + onUserStop: (messageId: string) => void; + onResend: (message: ChatMessage) => void; + onDelete: (messageId: string) => void; + onPinMessage: (message: ChatMessage) => void; +}) { + const { + index, + message, + position, + isHover, + onUserStop, + onResend, + onDelete, + onPinMessage, + } = props; + + const actionsRef = useRef(null); + + const [rect, setRect] = useState({ + x: 0, + y: 0, + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + toJSON: () => ({}), + }); + + const isUserMessage = message.role === "user"; + + const [translate, setTranslate] = useState({ x: 0, y: 0 }); + + useEffect(() => { + if (position.x && position.y && isHover) { + const x = position.x - rect.x - (isUserMessage ? rect.width : 0); + const y = position.y - rect.y - (isUserMessage ? rect.height : 0); + setTranslate({ x, y }); + } else { + setTranslate({ x: 0, y: 0 }); + } + }, [position, isHover]); + + useEffect(() => { + const div = actionsRef.current; + if (div) { + const rect = div.getBoundingClientRect(); + setRect(rect); + } + }, []); + + return ( +
+
+ {message.streaming ? ( + } + onClick={() => onUserStop(message.id ?? index)} + /> + ) : ( + <> + } + onClick={() => onResend(message)} + /> + + } + onClick={() => onDelete(message.id ?? index)} + /> + + } + onClick={() => onPinMessage(message)} + /> + } + onClick={() => copyToClipboard(getMessageTextContent(message))} + /> + + )} +
+
+ ); +} + export type RenderPrompt = Pick; export function PromptHints(props: { @@ -792,6 +894,13 @@ function _Chat() { const [attachImages, setAttachImages] = useState([]); const [uploading, setUploading] = useState(false); + const [isMessageContainerHover, setIsMessageContainerHover] = + useState(false); + const [actionsPosition, setActionsPosition] = useState<{ + x: number; + y: number; + }>({ x: 0, y: 0 }); + // prompt hints const promptStore = usePromptStore(); const [promptHints, setPromptHints] = useState([]); @@ -947,16 +1056,24 @@ function _Chat() { e.preventDefault(); } }; - const onRightClick = (e: any, message: ChatMessage) => { - // copy to clipboard - if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) { - if (userInput.length === 0) { - setUserInput(getMessageTextContent(message)); - } + const onRightClick = (e: any) => { + // move actions to the right click position + if (isNotSelectRange()) { e.preventDefault(); + + setActionsPosition({ + x: e.clientX, + y: e.clientY, + }); } }; + // handle mouse hover to reset actions position + useEffect(() => { + if (!isMessageContainerHover) { + setActionsPosition({ x: 0, y: 0 }); + } + }, [isMessageContainerHover]); const deleteMessage = (msgId?: string) => { chatStore.updateCurrentSession( @@ -1405,7 +1522,14 @@ function _Chat() { isUser ? styles["chat-message-user"] : styles["chat-message"] } > -
+
{ + setActionsPosition({ x: 0, y: 0 }); + }} + onMouseEnter={() => setIsMessageContainerHover(true)} + onMouseLeave={() => setIsMessageContainerHover(false)} + >
@@ -1462,46 +1586,16 @@ function _Chat() {
{showActions && ( -
-
- {message.streaming ? ( - } - onClick={() => onUserStop(message.id ?? i)} - /> - ) : ( - <> - } - onClick={() => onResend(message)} - /> - - } - onClick={() => onDelete(message.id ?? i)} - /> - - } - onClick={() => onPinMessage(message)} - /> - } - onClick={() => - copyToClipboard( - getMessageTextContent(message), - ) - } - /> - - )} -
-
+ )}
{showTyping && ( @@ -1518,7 +1612,7 @@ function _Chat() { message.content.length === 0 && !isUser } - onContextMenu={(e) => onRightClick(e, message)} + onContextMenu={(e) => onRightClick(e)} onDoubleClickCapture={() => { if (!isMobileScreen) return; setUserInput(getMessageTextContent(message)); diff --git a/app/utils.ts b/app/utils.ts index 2a2922907..668483df5 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -145,15 +145,13 @@ export function isFirefox() { ); } -export function selectOrCopy(el: HTMLElement, content: string) { +export function isNotSelectRange() { const currentSelection = window.getSelection(); if (currentSelection?.type === "Range") { return false; } - copyToClipboard(content); - return true; }