feat(ui): highlight delete targets on hover
This commit is contained in:
@@ -198,6 +198,7 @@ interface MessageContentItemProps {
|
||||
onFork?: (messageId?: string) => void
|
||||
onContentRendered?: () => void
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
function isSupportedPartType(part: unknown): boolean {
|
||||
@@ -285,6 +286,7 @@ function MessageContentItem(props: MessageContentItemProps) {
|
||||
isQueued={isQueued()}
|
||||
showAgentMeta={showAgentMeta()}
|
||||
showDeleteMessage={props.showDeleteMessage}
|
||||
onDeleteMessageHoverChange={props.onDeleteMessageHoverChange}
|
||||
onRevert={props.onRevert}
|
||||
onFork={props.onFork}
|
||||
onContentRendered={props.onContentRendered}
|
||||
@@ -302,12 +304,14 @@ interface ToolCallItemProps {
|
||||
partId: string
|
||||
onContentRendered?: () => void
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
function ToolCallItem(props: ToolCallItemProps) {
|
||||
const { t } = useI18n()
|
||||
const [deleting, setDeleting] = createSignal(false)
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||
|
||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||
@@ -399,7 +403,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
return (
|
||||
<Show when={toolPart()}>
|
||||
{(resolvedToolPart) => (
|
||||
<>
|
||||
<div class="delete-hover-scope" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
|
||||
<div class="tool-call-header-label">
|
||||
<div class="tool-call-header-meta">
|
||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||
@@ -426,6 +430,8 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
type="button"
|
||||
disabled={deleteDisabled()}
|
||||
onClick={handleDeleteToolPart}
|
||||
onMouseEnter={() => setHoverDeletePart(true)}
|
||||
onMouseLeave={() => setHoverDeletePart(false)}
|
||||
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||
>
|
||||
@@ -438,6 +444,8 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
type="button"
|
||||
disabled={deletingMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
@@ -457,7 +465,7 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
sessionId={props.sessionId}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
@@ -519,6 +527,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
||||
const [deleteMessageHovered, setDeleteMessageHovered] = createSignal(false)
|
||||
|
||||
const handleDeleteMessageHoverChange = (hovered: boolean) => {
|
||||
setDeleteMessageHovered(hovered)
|
||||
}
|
||||
|
||||
const block = createMemo<MessageDisplayBlock | null>(() => {
|
||||
const current = record()
|
||||
@@ -707,7 +720,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
return (
|
||||
<Show when={block()}>
|
||||
{(resolvedBlock) => (
|
||||
<div class="message-stream-block" data-message-id={resolvedBlock().record.id}>
|
||||
<div
|
||||
class="message-stream-block"
|
||||
data-message-id={resolvedBlock().record.id}
|
||||
data-delete-message-hover={deleteMessageHovered() ? "true" : undefined}
|
||||
>
|
||||
<For each={resolvedBlock().items}>
|
||||
{(item, index) => (
|
||||
<Switch>
|
||||
@@ -721,6 +738,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
messageIndex={props.messageIndex}
|
||||
lastAssistantIndex={props.lastAssistantIndex}
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
onRevert={props.onRevert}
|
||||
onFork={props.onFork}
|
||||
onContentRendered={props.onContentRendered}
|
||||
@@ -738,6 +756,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
messageId={toolItem.messageId}
|
||||
partId={toolItem.partId}
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</div>
|
||||
@@ -754,6 +773,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
messageId={props.messageId}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "step-finish"}>
|
||||
@@ -767,6 +787,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
messageId={props.messageId}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "compaction"}>
|
||||
@@ -779,6 +800,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
messageId={(item as CompactionDisplayItem).messageId}
|
||||
partId={(item as CompactionDisplayItem).partId}
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "reasoning"}>
|
||||
@@ -792,6 +814,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
||||
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -814,6 +837,7 @@ interface StepCardProps {
|
||||
instanceId?: string
|
||||
sessionId?: string
|
||||
messageId?: string
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
interface CompactionCardProps {
|
||||
@@ -825,12 +849,14 @@ interface CompactionCardProps {
|
||||
messageId: string
|
||||
partId: string
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
function CompactionCard(props: CompactionCardProps) {
|
||||
const { t } = useI18n()
|
||||
const [deleting, setDeleting] = createSignal(false)
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||
const isAuto = () => Boolean((props.part as any)?.auto)
|
||||
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
|
||||
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
|
||||
@@ -881,7 +907,8 @@ function CompactionCard(props: CompactionCardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`${containerClass()} relative`}
|
||||
class={`delete-hover-scope ${containerClass()} relative`}
|
||||
data-delete-part-hover={hoverDeletePart() ? "true" : undefined}
|
||||
style={{ "border-left": `4px solid ${borderColor()}` }}
|
||||
role="status"
|
||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
||||
@@ -893,6 +920,8 @@ function CompactionCard(props: CompactionCardProps) {
|
||||
class="tool-call-header-button"
|
||||
disabled={!canDeleteMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
@@ -905,6 +934,8 @@ function CompactionCard(props: CompactionCardProps) {
|
||||
class="tool-call-header-button"
|
||||
disabled={!canDelete()}
|
||||
onClick={handleDelete}
|
||||
onMouseEnter={() => setHoverDeletePart(true)}
|
||||
onMouseLeave={() => setHoverDeletePart(false)}
|
||||
title={t("messagePart.actions.deleteTitle")}
|
||||
aria-label={t("messagePart.actions.deleteTitle")}
|
||||
>
|
||||
@@ -1025,6 +1056,8 @@ function StepCard(props: StepCardProps) {
|
||||
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
|
||||
disabled={!canDeleteMessage()}
|
||||
onClick={handleDeleteMessage}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
@@ -1072,6 +1105,7 @@ interface ReasoningCardProps {
|
||||
showAgentMeta?: boolean
|
||||
defaultExpanded?: boolean
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
function ReasoningCard(props: ReasoningCardProps) {
|
||||
@@ -1079,6 +1113,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||
const [deleting, setDeleting] = createSignal(false)
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
setExpanded(Boolean(props.defaultExpanded))
|
||||
@@ -1188,7 +1223,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="message-reasoning-card">
|
||||
<div class="delete-hover-scope message-reasoning-card" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
|
||||
<div class="message-reasoning-header">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1239,6 +1274,8 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
class="message-action-button"
|
||||
onClick={handleDelete}
|
||||
disabled={!canDelete()}
|
||||
onMouseEnter={() => setHoverDeletePart(true)}
|
||||
onMouseLeave={() => setHoverDeletePart(false)}
|
||||
aria-label={t("messagePart.actions.deleteTitle")}
|
||||
title={t("messagePart.actions.deleteTitle")}
|
||||
>
|
||||
@@ -1252,6 +1289,8 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={!canDeleteMessage()}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,7 @@ interface MessageItemProps {
|
||||
showAgentMeta?: boolean
|
||||
onContentRendered?: () => void
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
||||
}
|
||||
|
||||
export default function MessageItem(props: MessageItemProps) {
|
||||
@@ -29,6 +30,7 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [hoveredDeletePartId, setHoveredDeletePartId] = createSignal<string | null>(null)
|
||||
|
||||
const isUser = () => props.record.role === "user"
|
||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||
@@ -352,6 +354,8 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={deletingMessage()}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
@@ -377,6 +381,8 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
class="message-action-button"
|
||||
onClick={() => void handleDeletePart(partId())}
|
||||
disabled={isDeletingPart(partId())}
|
||||
onMouseEnter={() => setHoveredDeletePartId(partId())}
|
||||
onMouseLeave={() => setHoveredDeletePartId(null)}
|
||||
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||
>
|
||||
@@ -390,6 +396,8 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
class="message-action-button"
|
||||
onClick={handleDeleteMessage}
|
||||
disabled={deletingMessage()}
|
||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||
>
|
||||
@@ -430,16 +438,27 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
</Show>
|
||||
|
||||
<For each={messageParts()}>
|
||||
{(part) => (
|
||||
<MessagePart
|
||||
part={part}
|
||||
messageType={props.record.role}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
primaryUserTextPartId={primaryUserTextPartId()}
|
||||
onRendered={props.onContentRendered}
|
||||
/>
|
||||
)}
|
||||
{(part) => {
|
||||
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
||||
const isHoveredDeleteTarget = () => Boolean(partId) && hoveredDeletePartId() === partId
|
||||
|
||||
return (
|
||||
<div
|
||||
class="delete-hover-scope message-part-shell"
|
||||
data-part-id={partId}
|
||||
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
||||
>
|
||||
<MessagePart
|
||||
part={part}
|
||||
messageType={props.record.role}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
primaryUserTextPartId={primaryUserTextPartId()}
|
||||
onRendered={props.onContentRendered}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show when={fileAttachments().length > 0}>
|
||||
@@ -448,8 +467,13 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
{(attachment) => {
|
||||
const name = getAttachmentName(attachment)
|
||||
const isImage = isImageAttachment(attachment)
|
||||
const isHoveredDeleteTarget = () => hoveredDeletePartId() === attachment.id
|
||||
return (
|
||||
<div class={`attachment-chip ${isImage ? "attachment-chip-image" : ""}`} title={name}>
|
||||
<div
|
||||
class={`delete-hover-scope attachment-chip ${isImage ? "attachment-chip-image" : ""}`}
|
||||
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
||||
title={name}
|
||||
>
|
||||
<Show when={isImage} fallback={
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
@@ -483,6 +507,8 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
onClick={() => void handleDeletePart(attachment.id)}
|
||||
class="attachment-remove"
|
||||
disabled={isDeletingPart(attachment.id)}
|
||||
onMouseEnter={() => (attachment.id ? setHoveredDeletePartId(attachment.id) : undefined)}
|
||||
onMouseLeave={() => setHoveredDeletePartId(null)}
|
||||
aria-label={t("messagePart.actions.deleteTitle")}
|
||||
title={t("messagePart.actions.deleteTitle")}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "./messaging/prompt-input.css";
|
||||
@import "./messaging/message-section.css";
|
||||
@import "./messaging/message-block-list.css";
|
||||
@import "./messaging/delete-overlays.css";
|
||||
@import "./messaging/message-timeline.css";
|
||||
@import "./messaging/tool-call.css";
|
||||
@import "./messaging/log-view.css";
|
||||
@@ -110,4 +111,3 @@
|
||||
.reasoning-label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
|
||||
37
packages/ui/src/styles/messaging/delete-overlays.css
Normal file
37
packages/ui/src/styles/messaging/delete-overlays.css
Normal file
@@ -0,0 +1,37 @@
|
||||
/* Hover overlays for destructive actions (delete part / delete message). */
|
||||
|
||||
.message-stream-block[data-delete-message-hover="true"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-stream-block[data-delete-message-hover="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: var(--status-error-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
/* Overlay must sit above the message cards (they have opaque backgrounds). */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.message-part-shell {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.delete-hover-scope[data-delete-part-hover="true"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.delete-hover-scope[data-delete-part-hover="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: var(--status-error-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
/* Overlay must sit above the part card background. */
|
||||
z-index: 10;
|
||||
}
|
||||
Reference in New Issue
Block a user