refactor(chat): update user message layout and styling

This commit is contained in:
Bohdan Triapitsyn
2025-12-20 23:44:53 +02:00
parent 64a0b80710
commit e349f885b6
3 changed files with 106 additions and 112 deletions
@@ -706,14 +706,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
<div className="chat-column"> <div className="chat-column">
{isUser ? ( {isUser ? (
<FadeInOnReveal> <FadeInOnReveal>
<div <div className="flex justify-end">
className={cn( <div className="max-w-[85%] rounded-xl rounded-br-xs bg-primary/10 dark:bg-primary/8 px-3.5 pt-2.5 pb-1.5">
'rounded-xl border bg-input/10 dark:bg-input/30 pt-2 pb-2.5 relative'
)}
style={{
borderColor: 'color-mix(in srgb, var(--primary-muted, var(--primary)) 40%, var(--interactive-border, transparent))'
}}
>
<MessageBody <MessageBody
messageId={message.info.id} messageId={message.info.id}
parts={visibleParts} parts={visibleParts}
@@ -738,7 +732,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
onAuxiliaryContentComplete={handleAuxiliaryContentComplete} onAuxiliaryContentComplete={handleAuxiliaryContentComplete}
agentMention={agentMention} agentMention={agentMention}
/> />
</div>
</div> </div>
</FadeInOnReveal> </FadeInOnReveal>
) : ( ) : (
@@ -208,45 +208,7 @@ const UserMessageBody: React.FC<{
style={{ contain: 'layout', transform: 'translateZ(0)' }} style={{ contain: 'layout', transform: 'translateZ(0)' }}
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined} onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
> >
{canCopyMessage && ( <div className="leading-normal overflow-hidden text-foreground/85">
<Button
type="button"
variant="ghost"
size="icon"
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
className={cn(
'absolute z-10 flex h-7 w-7 items-center justify-center rounded-full border border-border/40 shadow-none bg-background/95 supports-[backdrop-filter]:bg-background/80 hover:bg-accent duration-150',
'opacity-0 pointer-events-none disabled:opacity-30 disabled:text-muted-foreground/40',
hasCopyableText &&
'group-hover/message:opacity-60 group-hover/message:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto',
(copyHintVisible || isMessageCopied) && 'opacity-100 pointer-events-auto'
)}
style={{ insetInlineEnd: '0.32rem', insetBlockStart: '-0.28rem' }}
disabled={!hasCopyableText}
aria-label="Copy message text"
aria-hidden={!hasCopyableText}
onPointerDown={(event) => event.stopPropagation()}
onClick={handleCopyButtonClick}
onFocus={() => {
if (hasCopyableText) {
setCopyHintVisible(true);
}
}}
onBlur={() => {
if (!isMessageCopied) {
setCopyHintVisible(false);
}
}}
>
{isMessageCopied ? (
<RiCheckLine className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
) : (
<RiFileCopyLine className="h-3.5 w-3.5 text-foreground hover:text-primary focus-visible:text-primary" />
)}
</Button>
)}
<div className="px-3">
<div className="leading-normal overflow-hidden text-foreground/90">
{textParts.map((part, index) => { {textParts.map((part, index) => {
let mentionForPart: AgentMentionInfo | undefined; let mentionForPart: AgentMentionInfo | undefined;
if (agentMention && mentionToken && !mentionInjected) { if (agentMention && mentionToken && !mentionInjected) {
@@ -269,7 +231,37 @@ const UserMessageBody: React.FC<{
})} })}
</div> </div>
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} /> <MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
{canCopyMessage && hasCopyableText && (
<div className="mt-1 flex items-center justify-end gap-2 opacity-0 pointer-events-none transition-opacity duration-150 group-hover/message:opacity-100 group-hover/message:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto">
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
className="h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Copy message text"
onPointerDown={(event) => event.stopPropagation()}
onClick={handleCopyButtonClick}
onFocus={() => setCopyHintVisible(true)}
onBlur={() => {
if (!isMessageCopied) {
setCopyHintVisible(false);
}
}}
>
{isMessageCopied ? (
<RiCheckLine className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
) : (
<RiFileCopyLine className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Copy message</TooltipContent>
</Tooltip>
</div> </div>
)}
</div> </div>
); );
}; };
@@ -1075,6 +1067,21 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
const footerButtons = ( const footerButtons = (
<> <>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
onPointerDown={(event) => event.stopPropagation()}
onClick={handleForkClick}
>
<RiChatNewLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Start new session from this answer</TooltipContent>
</Tooltip>
{onCopyMessage && ( {onCopyMessage && (
<Tooltip delayDuration={1000}> <Tooltip delayDuration={1000}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -1085,8 +1092,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined} data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
className={cn( className={cn(
'h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50', 'h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50',
!hasCopyableText && 'opacity-50', !hasCopyableText && 'opacity-50'
(copyHintVisible || isMessageCopied) && 'text-primary'
)} )}
disabled={!hasCopyableText} disabled={!hasCopyableText}
aria-label="Copy message text" aria-label="Copy message text"
@@ -1114,21 +1120,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
<TooltipContent sideOffset={6}>Copy answer</TooltipContent> <TooltipContent sideOffset={6}>Copy answer</TooltipContent>
</Tooltip> </Tooltip>
)} )}
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
onPointerDown={(event) => event.stopPropagation()}
onClick={handleForkClick}
>
<RiChatNewLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>Start new session from this answer</TooltipContent>
</Tooltip>
</> </>
); );
@@ -29,9 +29,18 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
React.useEffect(() => { React.useEffect(() => {
const el = textRef.current; const el = textRef.current;
if (el && !isExpanded) { if (!el || isExpanded) return;
const checkTruncation = () => {
setIsTruncated(el.scrollHeight > el.clientHeight); setIsTruncated(el.scrollHeight > el.clientHeight);
} };
checkTruncation();
const resizeObserver = new ResizeObserver(checkTruncation);
resizeObserver.observe(el);
return () => resizeObserver.disconnect();
}, [textContent, isExpanded]); }, [textContent, isExpanded]);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {