Commit c3f99f53 authored by edy's avatar edy

feat(ui): add chat message delete action

parent 286f1972
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
RedBookIcon, RedBookIcon,
RefreshIcon, RefreshIcon,
ThumbIcon, ThumbIcon,
TrashIcon,
getIntentSuggestionIcon, getIntentSuggestionIcon,
renderExpertIcon renderExpertIcon
} from "./components/icons/AppIcons"; } from "./components/icons/AppIcons";
...@@ -404,7 +405,8 @@ export default function App() { ...@@ -404,7 +405,8 @@ export default function App() {
renameMessageTrace, renameMessageTrace,
initializeMessageTrace, initializeMessageTrace,
setMessageTraceExpanded, setMessageTraceExpanded,
collapseMessageTrace collapseMessageTrace,
removeMessageTrace
} = useMessageTraces(); } = useMessageTraces();
const { const {
saveConfig, saveConfig,
...@@ -1024,6 +1026,28 @@ export default function App() { ...@@ -1024,6 +1026,28 @@ export default function App() {
} }
} }
function deleteMessage(messageId: string) {
if (!visibleSessionId) {
return;
}
updateSessionMessages(visibleSessionId, (current) => {
const nextMessages = current.filter((message) => message.id !== messageId);
return nextMessages.length === current.length ? current : nextMessages;
});
removeMessageTrace(messageId);
setMessageReactions((current) => {
if (!(messageId in current)) {
return current;
}
const { [messageId]: _removed, ...rest } = current;
return rest;
});
if (copiedToken === `message:${messageId}`) {
setCopiedToken("");
}
}
async function regenerateAssistantMessage(messageId: string) { async function regenerateAssistantMessage(messageId: string) {
if (sending || !visibleSessionId || !sessionScopeProjectId) { if (sending || !visibleSessionId || !sessionScopeProjectId) {
return; return;
...@@ -1219,6 +1243,7 @@ export default function App() { ...@@ -1219,6 +1243,7 @@ export default function App() {
}, },
copyIcon: <CopyIcon />, copyIcon: <CopyIcon />,
copiedIcon: <CheckIcon />, copiedIcon: <CheckIcon />,
deleteIcon: <TrashIcon />,
regenerateIcon: <RefreshIcon />, regenerateIcon: <RefreshIcon />,
renderThumbIcon: (direction) => <ThumbIcon direction={direction} />, renderThumbIcon: (direction) => <ThumbIcon direction={direction} />,
renderMarkdownContent, renderMarkdownContent,
...@@ -1226,6 +1251,7 @@ export default function App() { ...@@ -1226,6 +1251,7 @@ export default function App() {
formatMessageTimestamp, formatMessageTimestamp,
onMessageListScroll: handleMessageListScroll, onMessageListScroll: handleMessageListScroll,
onCopyText: handleCopyText, onCopyText: handleCopyText,
onDeleteMessage: deleteMessage,
onTraceExpandedChange: setMessageTraceExpanded, onTraceExpandedChange: setMessageTraceExpanded,
onRegenerateAssistantMessage: regenerateAssistantMessage, onRegenerateAssistantMessage: regenerateAssistantMessage,
onToggleMessageReaction: toggleMessageReaction, onToggleMessageReaction: toggleMessageReaction,
......
...@@ -309,6 +309,17 @@ export function CheckIcon() { ...@@ -309,6 +309,17 @@ export function CheckIcon() {
); );
} }
export function TrashIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
<path d="M5 7h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M10 11v6M14 11v6" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
<path d="M8 7l.6 12.2A2 2 0 0 0 10.6 21h2.8a2 2 0 0 0 2-1.8L16 7" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9 7V5.6A1.6 1.6 0 0 1 10.6 4h2.8A1.6 1.6 0 0 1 15 5.6V7" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function ArrowUpIcon() { export function ArrowUpIcon() {
return ( return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
......
...@@ -87,6 +87,7 @@ interface ConversationWorkspaceViewProps { ...@@ -87,6 +87,7 @@ interface ConversationWorkspaceViewProps {
} }
copyIcon: ReactNode copyIcon: ReactNode
copiedIcon: ReactNode copiedIcon: ReactNode
deleteIcon: ReactNode
regenerateIcon: ReactNode regenerateIcon: ReactNode
renderThumbIcon: (direction: MessageReaction) => ReactNode renderThumbIcon: (direction: MessageReaction) => ReactNode
renderMarkdownContent: ( renderMarkdownContent: (
...@@ -105,6 +106,7 @@ interface ConversationWorkspaceViewProps { ...@@ -105,6 +106,7 @@ interface ConversationWorkspaceViewProps {
formatMessageTimestamp: (value: string) => string formatMessageTimestamp: (value: string) => string
onMessageListScroll: (event: ReactUIEvent<HTMLDivElement>) => void onMessageListScroll: (event: ReactUIEvent<HTMLDivElement>) => void
onCopyText: (token: string, text: string) => void | Promise<void> onCopyText: (token: string, text: string) => void | Promise<void>
onDeleteMessage: (messageId: string) => void
onTraceExpandedChange: (messageId: string, expanded: boolean) => void onTraceExpandedChange: (messageId: string, expanded: boolean) => void
onRegenerateAssistantMessage: (messageId: string) => void | Promise<void> onRegenerateAssistantMessage: (messageId: string) => void | Promise<void>
onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void
...@@ -187,6 +189,7 @@ export function ConversationWorkspaceView({ ...@@ -187,6 +189,7 @@ export function ConversationWorkspaceView({
messageLabels, messageLabels,
copyIcon, copyIcon,
copiedIcon, copiedIcon,
deleteIcon,
regenerateIcon, regenerateIcon,
renderThumbIcon, renderThumbIcon,
renderMarkdownContent, renderMarkdownContent,
...@@ -194,6 +197,7 @@ export function ConversationWorkspaceView({ ...@@ -194,6 +197,7 @@ export function ConversationWorkspaceView({
formatMessageTimestamp, formatMessageTimestamp,
onMessageListScroll, onMessageListScroll,
onCopyText, onCopyText,
onDeleteMessage,
onTraceExpandedChange, onTraceExpandedChange,
onRegenerateAssistantMessage, onRegenerateAssistantMessage,
onToggleMessageReaction, onToggleMessageReaction,
...@@ -342,6 +346,7 @@ export function ConversationWorkspaceView({ ...@@ -342,6 +346,7 @@ export function ConversationWorkspaceView({
labels={messageLabels} labels={messageLabels}
copyIcon={copyIcon} copyIcon={copyIcon}
copiedIcon={copiedIcon} copiedIcon={copiedIcon}
deleteIcon={deleteIcon}
regenerateIcon={regenerateIcon} regenerateIcon={regenerateIcon}
renderThumbIcon={renderThumbIcon} renderThumbIcon={renderThumbIcon}
renderMarkdownContent={renderMarkdownContent} renderMarkdownContent={renderMarkdownContent}
...@@ -349,6 +354,7 @@ export function ConversationWorkspaceView({ ...@@ -349,6 +354,7 @@ export function ConversationWorkspaceView({
formatMessageTimestamp={formatMessageTimestamp} formatMessageTimestamp={formatMessageTimestamp}
onScroll={onMessageListScroll} onScroll={onMessageListScroll}
onCopyText={onCopyText} onCopyText={onCopyText}
onDeleteMessage={onDeleteMessage}
onTraceExpandedChange={onTraceExpandedChange} onTraceExpandedChange={onTraceExpandedChange}
onRegenerateAssistantMessage={onRegenerateAssistantMessage} onRegenerateAssistantMessage={onRegenerateAssistantMessage}
onToggleMessageReaction={onToggleMessageReaction} onToggleMessageReaction={onToggleMessageReaction}
......
...@@ -41,6 +41,7 @@ interface MessageListProps { ...@@ -41,6 +41,7 @@ interface MessageListProps {
labels: MessageListLabels labels: MessageListLabels
copyIcon: ReactNode copyIcon: ReactNode
copiedIcon: ReactNode copiedIcon: ReactNode
deleteIcon: ReactNode
regenerateIcon: ReactNode regenerateIcon: ReactNode
renderThumbIcon: (direction: MessageReaction) => ReactNode renderThumbIcon: (direction: MessageReaction) => ReactNode
renderMarkdownContent: ( renderMarkdownContent: (
...@@ -55,6 +56,7 @@ interface MessageListProps { ...@@ -55,6 +56,7 @@ interface MessageListProps {
formatMessageTimestamp: (value: string) => string formatMessageTimestamp: (value: string) => string
onScroll: (event: UIEvent<HTMLDivElement>) => void onScroll: (event: UIEvent<HTMLDivElement>) => void
onCopyText: (token: string, text: string) => void | Promise<void> onCopyText: (token: string, text: string) => void | Promise<void>
onDeleteMessage: (messageId: string) => void
onTraceExpandedChange: (messageId: string, expanded: boolean) => void onTraceExpandedChange: (messageId: string, expanded: boolean) => void
onRegenerateAssistantMessage: (messageId: string) => void | Promise<void> onRegenerateAssistantMessage: (messageId: string) => void | Promise<void>
onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void
...@@ -189,6 +191,7 @@ export function MessageList({ ...@@ -189,6 +191,7 @@ export function MessageList({
labels, labels,
copyIcon, copyIcon,
copiedIcon, copiedIcon,
deleteIcon,
regenerateIcon, regenerateIcon,
renderThumbIcon, renderThumbIcon,
renderMarkdownContent, renderMarkdownContent,
...@@ -196,6 +199,7 @@ export function MessageList({ ...@@ -196,6 +199,7 @@ export function MessageList({
formatMessageTimestamp, formatMessageTimestamp,
onScroll, onScroll,
onCopyText, onCopyText,
onDeleteMessage,
onTraceExpandedChange, onTraceExpandedChange,
onRegenerateAssistantMessage, onRegenerateAssistantMessage,
onToggleMessageReaction onToggleMessageReaction
...@@ -220,129 +224,155 @@ export function MessageList({ ...@@ -220,129 +224,155 @@ export function MessageList({
const traceDisplayLines = hasTrace ? getTraceDisplayLines(messageTrace?.items ?? []) : [] const traceDisplayLines = hasTrace ? getTraceDisplayLines(messageTrace?.items ?? []) : []
const traceTitle = getTraceStripTitle(messageTrace?.items ?? [], labels.thinking, message.statusLabel, message.statusDetail) const traceTitle = getTraceStripTitle(messageTrace?.items ?? [], labels.thinking, message.statusLabel, message.statusDetail)
const canCopyMessage = Boolean(message.content.trim()) const canCopyMessage = Boolean(message.content.trim())
const hasVisibleAttachments = message.role === "user" && Array.isArray(message.attachments) && message.attachments.some((attachment) => attachment.localPath && attachment.name)
const canDeleteMessage = message.streamState !== "streaming" && (canCopyMessage || hasVisibleAttachments)
const copyToken = `message:${message.id}` const copyToken = `message:${message.id}`
const reaction = messageReactions[message.id] const reaction = messageReactions[message.id]
void reaction void reaction
const messageActions = (canCopyMessage || canDeleteMessage) ? (
return ( <div className="message-card-actions">
<article key={message.id} className={"message-card group " + message.role + (message.streamState ? " " + message.streamState : "")}> {canCopyMessage ? (
<div className={"message-bubble" + (message.role === "assistant" ? " message-bubble-assistant" : " message-bubble-user")}> <button
{showReasoningStrip ? ( type="button"
<div className={"message-trace" + (message.streamState === "streaming" ? " streaming" : "")} aria-live={message.streamState === "streaming" ? "polite" : undefined}> className={"message-action-icon" + (copiedToken === copyToken ? " copied" : "")}
<button onClick={() => void onCopyText(copyToken, message.content)}
type="button" aria-label="复制消息"
className={"reasoning-strip" + (isTraceExpanded ? " expanded" : "") + (message.streamState === "error" ? " error" : "") + (message.streamState === "streaming" ? " streaming" : "") + (isWaitingForContent ? " waiting" : "")} title="复制消息"
onClick={() => onTraceExpandedChange(message.id, !isTraceExpanded)} >
aria-expanded={isTraceExpanded} {copiedToken === copyToken ? copiedIcon : copyIcon}
> </button>
<span className="reasoning-strip-leading" aria-hidden="true"> ) : null}
<span className="reasoning-strip-dots"> {canDeleteMessage ? (
<span className="reasoning-strip-dot" /> <button
<span className="reasoning-strip-dot" /> type="button"
<span className="reasoning-strip-dot" /> className="message-action-icon message-action-delete"
</span> onClick={() => onDeleteMessage(message.id)}
</span> aria-label="删除消息"
<span className="reasoning-strip-title">{traceTitle}</span> title="删除消息"
<span className="reasoning-strip-action">{isTraceExpanded ? labels.hideTrace : labels.traceCollapsed}</span> >
</button> {deleteIcon}
{isTraceExpanded ? ( </button>
<div className="message-trace-content" aria-label="公开推理摘要"> ) : null}
{traceDisplayLines.map((line) => { {message.role === "assistant" ? (
return ( <>
<p key={line.key} className={getTraceLineClassName(line.tone, line.kind)}>
<span className="message-trace-marker" aria-hidden="true" />
<span className="message-trace-main">
<span className="message-trace-row">
<span className="message-trace-text">{line.title}</span>
<span className="message-trace-time">{line.time}</span>
</span>
{line.detail ? <span className="message-trace-detail">{line.detail}</span> : null}
</span>
</p>
)
})}
</div>
) : null}
</div>
) : null}
{isWaitingForContent && videoStatusCard ? (
<div className="generation-status-card" aria-live="polite">
<div className="generation-status-leading">
<span className="thinking-spinner generation-status-spinner" aria-hidden="true" />
</div>
<div className="generation-status-body">
<span className="generation-status-kicker">抖音专家执行中</span>
<strong className="generation-status-title">{videoStatusCard.title}</strong>
{videoStatusCard.meta ? <span className="generation-status-meta">{videoStatusCard.meta}</span> : null}
{videoStatusCard.hint ? <span className="generation-status-hint">{videoStatusCard.hint}</span> : null}
<span className="generation-status-progress" aria-hidden="true" />
</div>
</div>
) : message.content ? (
message.role === "assistant" ? (
<div className="markdown-body">
{renderMarkdownContent(message.content, {
messageId: message.id,
copiedToken,
onCopy: onCopyText
})}
{message.streamState === "streaming" ? <span className="message-cursor" aria-hidden="true" /> : null}
</div>
) : (
<p className="message-plain-text">
{message.content}
{message.streamState === "streaming" ? <span className="message-cursor" aria-hidden="true" /> : null}
</p>
)
) : null}
{message.role === "user" ? <MessageAttachmentStrip attachments={message.attachments} /> : null}
</div>
<span className="message-timestamp" aria-hidden="true">{formatMessageTimestamp(message.createdAt)}</span>
{message.role === "assistant" && canCopyMessage ? (
<div className="message-card-actions">
<button <button
type="button" type="button"
className={"message-action-icon" + (copiedToken === copyToken ? " copied" : "")} className="hidden"
onClick={() => void onCopyText(copyToken, message.content)} onClick={() => void onRegenerateAssistantMessage(message.id)}
aria-label="复制消息" disabled={sending}
title="复制消息" aria-label="重新生成"
title="重新生成"
> >
{copiedToken === copyToken ? copiedIcon : copyIcon} {regenerateIcon}
</button> </button>
{message.role === "assistant" ? ( <button
<> type="button"
<button className="hidden"
type="button" onClick={() => onToggleMessageReaction(message.id, "up")}
className="hidden" aria-label="赞"
onClick={() => void onRegenerateAssistantMessage(message.id)} title="赞"
disabled={sending} >
aria-label="重新生成" {renderThumbIcon("up")}
title="重新生成" </button>
> <button
{regenerateIcon} type="button"
</button> className="hidden"
<button onClick={() => onToggleMessageReaction(message.id, "down")}
type="button" aria-label="踩"
className="hidden" title="踩"
onClick={() => onToggleMessageReaction(message.id, "up")} >
aria-label="赞" {renderThumbIcon("down")}
title="赞" </button>
> </>
{renderThumbIcon("up")} ) : null}
</button> </div>
) : null
return (
<article key={message.id} className={"message-card group " + message.role + (message.streamState ? " " + message.streamState : "")}>
<div className={"message-card-body message-card-body-" + message.role}>
<div className={"message-bubble" + (message.role === "assistant" ? " message-bubble-assistant" : " message-bubble-user")}>
{showReasoningStrip ? (
<div className={"message-trace" + (message.streamState === "streaming" ? " streaming" : "")} aria-live={message.streamState === "streaming" ? "polite" : undefined}>
<button <button
type="button" type="button"
className="hidden" className={"reasoning-strip" + (isTraceExpanded ? " expanded" : "") + (message.streamState === "error" ? " error" : "") + (message.streamState === "streaming" ? " streaming" : "") + (isWaitingForContent ? " waiting" : "")}
onClick={() => onToggleMessageReaction(message.id, "down")} onClick={() => onTraceExpandedChange(message.id, !isTraceExpanded)}
aria-label="踩" aria-expanded={isTraceExpanded}
title="踩"
> >
{renderThumbIcon("down")} <span className="reasoning-strip-leading" aria-hidden="true">
<span className="reasoning-strip-dots">
<span className="reasoning-strip-dot" />
<span className="reasoning-strip-dot" />
<span className="reasoning-strip-dot" />
</span>
</span>
<span className="reasoning-strip-title">{traceTitle}</span>
<span className="reasoning-strip-action">{isTraceExpanded ? labels.hideTrace : labels.traceCollapsed}</span>
</button> </button>
{isTraceExpanded ? (
<div className="message-trace-content" aria-label="公开推理摘要">
{traceDisplayLines.map((line) => {
return (
<p key={line.key} className={getTraceLineClassName(line.tone, line.kind)}>
<span className="message-trace-marker" aria-hidden="true" />
<span className="message-trace-main">
<span className="message-trace-row">
<span className="message-trace-text">{line.title}</span>
<span className="message-trace-time">{line.time}</span>
</span>
{line.detail ? <span className="message-trace-detail">{line.detail}</span> : null}
</span>
</p>
)
})}
</div>
) : null}
</div>
) : null}
{isWaitingForContent && videoStatusCard ? (
<div className="generation-status-card" aria-live="polite">
<div className="generation-status-leading">
<span className="thinking-spinner generation-status-spinner" aria-hidden="true" />
</div>
<div className="generation-status-body">
<span className="generation-status-kicker">抖音专家执行中</span>
<strong className="generation-status-title">{videoStatusCard.title}</strong>
{videoStatusCard.meta ? <span className="generation-status-meta">{videoStatusCard.meta}</span> : null}
{videoStatusCard.hint ? <span className="generation-status-hint">{videoStatusCard.hint}</span> : null}
<span className="generation-status-progress" aria-hidden="true" />
</div>
</div>
) : message.content ? (
message.role === "assistant" ? (
<div className="markdown-body">
{renderMarkdownContent(message.content, {
messageId: message.id,
copiedToken,
onCopy: onCopyText
})}
{message.streamState === "streaming" ? <span className="message-cursor" aria-hidden="true" /> : null}
</div>
) : (
<>
<p className="message-plain-text">
{message.content}
{message.streamState === "streaming" ? <span className="message-cursor" aria-hidden="true" /> : null}
</p>
{message.role === "user" ? <MessageAttachmentStrip attachments={message.attachments} /> : null}
</>
)
) : message.role === "user" ? (
<>
<MessageAttachmentStrip attachments={message.attachments} />
</> </>
) : null} ) : null}
</div> </div>
) : null} <div className="message-card-meta">
<span className="message-timestamp" aria-hidden="true">{formatMessageTimestamp(message.createdAt)}</span>
{messageActions}
</div>
</div>
</article> </article>
) )
})} })}
......
...@@ -96,12 +96,23 @@ export function useMessageTraces() { ...@@ -96,12 +96,23 @@ export function useMessageTraces() {
setMessageTraceExpanded(messageId, false) setMessageTraceExpanded(messageId, false)
}, [setMessageTraceExpanded]) }, [setMessageTraceExpanded])
const removeMessageTrace = useCallback((messageId: string) => {
setMessageTraces((current) => {
if (!(messageId in current)) {
return current
}
const { [messageId]: _removed, ...rest } = current
return rest
})
}, [])
return { return {
messageTraces, messageTraces,
renameMessageTrace, renameMessageTrace,
initializeMessageTrace, initializeMessageTrace,
appendTrace, appendTrace,
setMessageTraceExpanded, setMessageTraceExpanded,
collapseMessageTrace collapseMessageTrace,
removeMessageTrace
} }
} }
...@@ -747,10 +747,17 @@ ...@@ -747,10 +747,17 @@
} }
.conversation-shell .message-bubble, .conversation-shell .message-bubble,
.conversation-shell .message-card-body,
.conversation-shell .message-bubble-assistant, .conversation-shell .message-bubble-assistant,
.conversation-shell .message-card.assistant .message-bubble { .conversation-shell .message-card.assistant .message-bubble {
max-width: 100%; max-width: 100%;
} }
.conversation-shell .message-card-meta {
opacity: 0.76;
pointer-events: auto;
transform: translateY(0);
}
} }
@media (max-width: 720px) { @media (max-width: 720px) {
......
...@@ -270,6 +270,33 @@ ...@@ -270,6 +270,33 @@
margin-top: 6px; margin-top: 6px;
} }
.message-card-body {
display: grid;
gap: 4px;
max-width: 100%;
}
.message-card-body-user {
justify-self: end;
}
.message-card-body-assistant {
justify-self: start;
}
.message-card-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 28px;
padding: 0 2px;
opacity: 0;
pointer-events: none;
transform: translateY(-2px);
transition: opacity 150ms ease, transform 150ms ease;
}
.thinking-spinner { .thinking-spinner {
width: 16px; width: 16px;
height: 16px; height: 16px;
...@@ -819,30 +846,57 @@ ...@@ -819,30 +846,57 @@
.message-card-actions { .message-card-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 1px;
margin-top: 12px; width: max-content;
justify-content: flex-start; margin-top: 0;
opacity: 0; padding: 2px;
transition: opacity 150ms ease; border: 1px solid rgba(203, 213, 225, 0.68);
border-radius: 999px;
background: rgba(248, 250, 252, 0.9);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
backdrop-filter: blur(12px);
justify-content: flex-end;
pointer-events: auto;
opacity: 1;
} }
.message-card:hover .message-card-actions { .message-card:hover .message-card-meta,
.message-card:focus-within .message-card-meta {
pointer-events: auto;
opacity: 1; opacity: 1;
transform: translateY(0);
}
.message-action-delete:hover {
background: #fef2f2;
color: #dc2626;
} }
.message-action-icon { .message-action-icon {
width: 32px; width: 26px;
height: 32px; height: 26px;
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: #ffffff; background: transparent;
color: #5f7773; color: #738196;
box-shadow: 0 10px 24px rgba(17, 24, 39, 0.08); cursor: pointer;
box-shadow: none;
transition: background-color 150ms ease, color 150ms ease, transform 120ms ease;
} }
.message-action-icon:hover { .message-action-icon:hover {
background: #f4fbfa; background: #eef6ff;
color: #0f67de;
}
.message-action-icon:active {
transform: scale(0.94);
}
.message-action-icon:focus-visible {
outline: 2px solid rgba(15, 103, 222, 0.28);
outline-offset: 2px;
} }
.message-action-icon.copied { .message-action-icon.copied {
......
...@@ -1014,6 +1014,23 @@ ...@@ -1014,6 +1014,23 @@
justify-content: flex-start; justify-content: flex-start;
} }
.conversation-shell .message-card-body {
width: auto;
max-width: min(72%, 760px);
}
.conversation-shell .message-card-body-user {
margin-left: auto;
}
.conversation-shell .message-card-body-assistant {
max-width: min(100%, 880px);
}
.conversation-shell .message-card-body .message-bubble {
max-width: 100%;
}
.conversation-shell .message-bubble { .conversation-shell .message-bubble {
width: auto; width: auto;
display: grid; display: grid;
...@@ -1095,30 +1112,17 @@ ...@@ -1095,30 +1112,17 @@
} }
.conversation-shell .message-timestamp { .conversation-shell .message-timestamp {
position: absolute; position: static;
bottom: 0;
left: 4px;
z-index: 1;
color: #7c8da3; color: #7c8da3;
font-size: 11px; font-size: 11px;
line-height: 1; line-height: 1;
opacity: 0;
pointer-events: none;
transition: opacity 140ms ease;
}
.conversation-shell .message-card.user .message-timestamp {
left: auto;
right: 4px;
}
.conversation-shell .message-card:hover .message-timestamp,
.conversation-shell .message-card:focus-within .message-timestamp {
opacity: 1; opacity: 1;
pointer-events: none;
white-space: nowrap;
} }
.conversation-shell .message-card-actions { .conversation-shell .message-card-actions {
margin-top: 8px; margin-right: 0;
} }
.conversation-shell .composer-shell { .conversation-shell .composer-shell {
......
...@@ -73,6 +73,7 @@ ...@@ -73,6 +73,7 @@
.nav-item-icon svg, .nav-item-icon svg,
.expert-chip-icon svg, .expert-chip-icon svg,
.message-action-icon svg, .message-action-icon svg,
.message-action-delete svg,
.composer-submit svg, .composer-submit svg,
.attachment-trigger svg, .attachment-trigger svg,
.markdown-code-copy svg { .markdown-code-copy svg {
...@@ -104,11 +105,13 @@ ...@@ -104,11 +105,13 @@
@apply mb-0; @apply mb-0;
} }
.message-card.assistant .message-card-actions { .message-card .message-card-meta {
@apply pointer-events-none; @apply pointer-events-none;
} }
.message-card.assistant:hover .message-card-actions { .message-card:hover .message-card-meta,
.message-card:focus-within .message-card-meta,
.message-card .message-card-meta:focus-within {
@apply pointer-events-auto; @apply pointer-events-auto;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment