refactor(chat): update user message layout and styling
This commit is contained in:
@@ -706,39 +706,33 @@ 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'
|
<MessageBody
|
||||||
)}
|
messageId={message.info.id}
|
||||||
style={{
|
parts={visibleParts}
|
||||||
borderColor: 'color-mix(in srgb, var(--primary-muted, var(--primary)) 40%, var(--interactive-border, transparent))'
|
isUser={isUser}
|
||||||
}}
|
isMessageCompleted={isMessageCompleted}
|
||||||
>
|
syntaxTheme={syntaxTheme}
|
||||||
<MessageBody
|
isMobile={isMobile}
|
||||||
messageId={message.info.id}
|
hasTouchInput={hasTouchInput}
|
||||||
parts={visibleParts}
|
copiedCode={copiedCode}
|
||||||
isUser={isUser}
|
onCopyCode={handleCopyCode}
|
||||||
isMessageCompleted={isMessageCompleted}
|
expandedTools={expandedTools}
|
||||||
syntaxTheme={syntaxTheme}
|
onToggleTool={handleToggleTool}
|
||||||
isMobile={isMobile}
|
onShowPopup={handleShowPopup}
|
||||||
hasTouchInput={hasTouchInput}
|
streamPhase={streamPhase}
|
||||||
copiedCode={copiedCode}
|
allowAnimation={allowAnimation}
|
||||||
onCopyCode={handleCopyCode}
|
onContentChange={onContentChange}
|
||||||
expandedTools={expandedTools}
|
shouldShowHeader={false}
|
||||||
onToggleTool={handleToggleTool}
|
hasTextContent={hasTextContent}
|
||||||
onShowPopup={handleShowPopup}
|
onCopyMessage={handleCopyMessage}
|
||||||
streamPhase={streamPhase}
|
copiedMessage={copiedMessage}
|
||||||
allowAnimation={allowAnimation}
|
showReasoningTraces={showReasoningTraces}
|
||||||
onContentChange={onContentChange}
|
onAuxiliaryContentComplete={handleAuxiliaryContentComplete}
|
||||||
shouldShowHeader={false}
|
agentMention={agentMention}
|
||||||
hasTextContent={hasTextContent}
|
/>
|
||||||
onCopyMessage={handleCopyMessage}
|
</div>
|
||||||
copiedMessage={copiedMessage}
|
|
||||||
showReasoningTraces={showReasoningTraces}
|
|
||||||
onAuxiliaryContentComplete={handleAuxiliaryContentComplete}
|
|
||||||
agentMention={agentMention}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</FadeInOnReveal>
|
</FadeInOnReveal>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -208,68 +208,60 @@ 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
|
{textParts.map((part, index) => {
|
||||||
type="button"
|
let mentionForPart: AgentMentionInfo | undefined;
|
||||||
variant="ghost"
|
if (agentMention && mentionToken && !mentionInjected) {
|
||||||
size="icon"
|
const candidateText = extractTextContent(part);
|
||||||
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
|
if (candidateText.includes(mentionToken)) {
|
||||||
className={cn(
|
mentionForPart = agentMention;
|
||||||
'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',
|
mentionInjected = true;
|
||||||
'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={() => {
|
return (
|
||||||
if (!isMessageCopied) {
|
<FadeInOnReveal key={`user-text-${index}`}>
|
||||||
setCopyHintVisible(false);
|
<UserTextPart
|
||||||
}
|
part={part}
|
||||||
}}
|
messageId={messageId}
|
||||||
>
|
isMobile={isMobile}
|
||||||
{isMessageCopied ? (
|
agentMention={mentionForPart}
|
||||||
<RiCheckLine className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
|
/>
|
||||||
) : (
|
</FadeInOnReveal>
|
||||||
<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) => {
|
|
||||||
let mentionForPart: AgentMentionInfo | undefined;
|
|
||||||
if (agentMention && mentionToken && !mentionInjected) {
|
|
||||||
const candidateText = extractTextContent(part);
|
|
||||||
if (candidateText.includes(mentionToken)) {
|
|
||||||
mentionForPart = agentMention;
|
|
||||||
mentionInjected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FadeInOnReveal key={`user-text-${index}`}>
|
|
||||||
<UserTextPart
|
|
||||||
part={part}
|
|
||||||
messageId={messageId}
|
|
||||||
isMobile={isMobile}
|
|
||||||
agentMention={mentionForPart}
|
|
||||||
/>
|
|
||||||
</FadeInOnReveal>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user