feat(ui): add multi-select message deletion
This commit is contained in:
@@ -27,6 +27,8 @@ interface MessageBlockListProps {
|
||||
onContentRendered?: () => void
|
||||
deleteHover?: Accessor<DeleteHoverState>
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
selectedMessageIds?: Accessor<Set<string>>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
setBottomSentinel: (element: HTMLDivElement | null) => void
|
||||
suspendMeasurements?: () => boolean
|
||||
}
|
||||
@@ -57,6 +59,8 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
||||
showUsageMetrics={props.showUsageMetrics}
|
||||
deleteHover={props.deleteHover}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
onRevert={props.onRevert}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
onFork={props.onFork}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
||||
import MessageItem from "./message-item"
|
||||
import ToolCall from "./tool-call"
|
||||
@@ -208,6 +208,8 @@ interface MessageContentItemProps {
|
||||
onContentRendered?: () => void
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
function isSupportedPartType(part: unknown): boolean {
|
||||
@@ -296,6 +298,8 @@ function MessageContentItem(props: MessageContentItemProps) {
|
||||
showAgentMeta={showAgentMeta()}
|
||||
showDeleteMessage={props.showDeleteMessage}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
onRevert={props.onRevert}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
onFork={props.onFork}
|
||||
@@ -316,6 +320,8 @@ interface ToolCallItemProps {
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
function ToolCallItem(props: ToolCallItemProps) {
|
||||
@@ -323,6 +329,8 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||
|
||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||
const partEntry = createMemo(() => record()?.parts?.[props.partId])
|
||||
@@ -403,6 +411,24 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
<div class="delete-hover-scope">
|
||||
<div class="tool-call-header-label">
|
||||
<div class="tool-call-header-meta">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||
<span>{t("messageBlock.tool.header")}</span>
|
||||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||
@@ -516,6 +542,8 @@ interface MessageBlockProps {
|
||||
showUsageMetrics: () => boolean
|
||||
deleteHover?: () => DeleteHoverState
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
onRevert?: (messageId: string) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
onFork?: (messageId?: string) => void
|
||||
@@ -531,6 +559,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
const isDeleteMessageHovered = () => {
|
||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
||||
|
||||
const selected = props.selectedMessageIds?.() ?? new Set<string>()
|
||||
if (selected.has(props.messageId)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (hover.kind === "message") {
|
||||
return hover.messageId === props.messageId
|
||||
}
|
||||
@@ -755,6 +788,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onRevert={props.onRevert}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
onFork={props.onFork}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
@@ -773,6 +808,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</div>
|
||||
@@ -791,6 +828,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
messageId={props.messageId}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "step-finish"}>
|
||||
@@ -806,6 +845,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
messageId={props.messageId}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "compaction"}>
|
||||
@@ -819,6 +860,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={item.type === "reasoning"}>
|
||||
@@ -833,6 +876,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
showDeleteMessage={index() === 0}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -857,6 +902,8 @@ interface StepCardProps {
|
||||
messageId?: string
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
interface CompactionCardProps {
|
||||
@@ -869,12 +916,15 @@ interface CompactionCardProps {
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
function CompactionCard(props: CompactionCardProps) {
|
||||
const { t } = useI18n()
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||
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)
|
||||
@@ -956,6 +1006,24 @@ function CompactionCard(props: CompactionCardProps) {
|
||||
</div>
|
||||
|
||||
<div class="message-compaction-row">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
|
||||
<span class="message-compaction-label">{label()}</span>
|
||||
</div>
|
||||
@@ -967,6 +1035,7 @@ function StepCard(props: StepCardProps) {
|
||||
const { t } = useI18n()
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
const isSelectedForDeletion = () => Boolean(props.messageId && props.selectedMessageIds?.().has(props.messageId))
|
||||
const timestamp = () => {
|
||||
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
|
||||
const date = new Date(value)
|
||||
@@ -1078,6 +1147,24 @@ function StepCard(props: StepCardProps) {
|
||||
}
|
||||
return (
|
||||
<div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
|
||||
<Show when={props.showDeleteMessage && props.messageId}>
|
||||
<input
|
||||
class="message-select-checkbox absolute left-2 top-1/2 -translate-y-1/2"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
<button
|
||||
@@ -1114,10 +1201,28 @@ function StepCard(props: StepCardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={`message-step-card message-step-start`}>
|
||||
<div class={`message-step-card message-step-start relative`}>
|
||||
<div class="message-step-heading">
|
||||
<div class="message-step-title">
|
||||
<div class="message-step-title-left">
|
||||
<Show when={props.showDeleteMessage && props.messageId}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId!, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>{(value) => <span>{t("messageBlock.step.agentLabel", { agent: value() })}</span>}</Show>
|
||||
@@ -1149,6 +1254,8 @@ interface ReasoningCardProps {
|
||||
showDeleteMessage?: boolean
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
function ReasoningCard(props: ReasoningCardProps) {
|
||||
@@ -1156,6 +1263,13 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||
|
||||
let headerEl: HTMLDivElement | undefined
|
||||
let actionsEl: HTMLDivElement | undefined
|
||||
let primaryEl: HTMLSpanElement | undefined
|
||||
let metaMeasureEl: HTMLSpanElement | undefined
|
||||
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||
|
||||
createEffect(() => {
|
||||
setExpanded(Boolean(props.defaultExpanded))
|
||||
@@ -1182,6 +1296,35 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
return modelID
|
||||
}
|
||||
|
||||
const hasMeta = () => Boolean(props.showAgentMeta && (agentIdentifier() || modelIdentifier()))
|
||||
|
||||
const updateMetaLayout = () => {
|
||||
if (!hasMeta()) return
|
||||
if (!headerEl || !actionsEl || !primaryEl || !metaMeasureEl) return
|
||||
|
||||
const headerWidth = headerEl.getBoundingClientRect().width
|
||||
const actionsWidth = actionsEl.getBoundingClientRect().width
|
||||
const primaryWidth = primaryEl.getBoundingClientRect().width
|
||||
const metaWidth = metaMeasureEl.getBoundingClientRect().width
|
||||
|
||||
const availableLeft = Math.max(0, headerWidth - actionsWidth - 12)
|
||||
setShowMetaInline(primaryWidth + metaWidth + 8 <= availableLeft)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!hasMeta() || typeof ResizeObserver === "undefined") {
|
||||
setShowMetaInline(true)
|
||||
return
|
||||
}
|
||||
|
||||
updateMetaLayout()
|
||||
const observer = new ResizeObserver(() => updateMetaLayout())
|
||||
if (headerEl) observer.observe(headerEl)
|
||||
if (actionsEl) observer.observe(actionsEl)
|
||||
if (primaryEl) observer.observe(primaryEl)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
const reasoningText = () => {
|
||||
const part = props.part as any
|
||||
if (!part) return ""
|
||||
@@ -1260,7 +1403,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
|
||||
return (
|
||||
<div class="delete-hover-scope message-reasoning-card">
|
||||
<div class="message-reasoning-header">
|
||||
<div class="message-reasoning-header" ref={(el) => (headerEl = el)}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-reasoning-toggle"
|
||||
@@ -1268,9 +1411,30 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
aria-expanded={expanded()}
|
||||
aria-label={expanded() ? t("messageBlock.reasoning.collapseAriaLabel") : t("messageBlock.reasoning.expandAriaLabel")}
|
||||
>
|
||||
<span class="message-reasoning-label flex flex-wrap items-center gap-2">
|
||||
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||
<Show when={props.showAgentMeta && (agentIdentifier() || modelIdentifier())}>
|
||||
<span class="message-reasoning-label">
|
||||
<span class="message-reasoning-label-primary" ref={(el) => (primaryEl = el)}>
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.messageId, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||
</span>
|
||||
|
||||
<Show when={hasMeta() && showMetaInline()}>
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
@@ -1284,10 +1448,28 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
<Show when={hasMeta()}>
|
||||
<span
|
||||
ref={(el) => (metaMeasureEl = el)}
|
||||
class="message-step-meta-inline message-step-meta-inline--measure"
|
||||
>
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={modelIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="message-reasoning-actions">
|
||||
<div class="message-reasoning-actions" ref={(el) => (actionsEl = el)}>
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
@@ -1336,6 +1518,23 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={hasMeta() && !showMetaInline()}>
|
||||
<div class="message-reasoning-meta-row">
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={modelIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={expanded()}>
|
||||
<div class="message-reasoning-expanded">
|
||||
<div class="message-reasoning-body">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { Copy, ListStart, Split, Trash, Undo } from "lucide-solid"
|
||||
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
|
||||
import { partHasRenderableText } from "../types/message"
|
||||
@@ -27,6 +27,8 @@ interface MessageItemProps {
|
||||
isQueued?: boolean
|
||||
parts: ClientPart[]
|
||||
onRevert?: (messageId: string) => void
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
onFork?: (messageId?: string) => void
|
||||
showAgentMeta?: boolean
|
||||
@@ -41,6 +43,46 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.record.id))
|
||||
|
||||
let topRowEl: HTMLDivElement | undefined
|
||||
let actionsEl: HTMLDivElement | undefined
|
||||
let speakerPrimaryEl: HTMLDivElement | undefined
|
||||
let metaMeasureEl: HTMLSpanElement | undefined
|
||||
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||
|
||||
const metaText = () => agentMeta()
|
||||
|
||||
const updateMetaLayout = () => {
|
||||
const text = metaText()
|
||||
if (!text) return
|
||||
if (!topRowEl || !actionsEl || !speakerPrimaryEl || !metaMeasureEl) return
|
||||
|
||||
const rowWidth = topRowEl.getBoundingClientRect().width
|
||||
const actionsWidth = actionsEl.getBoundingClientRect().width
|
||||
const primaryWidth = speakerPrimaryEl.getBoundingClientRect().width
|
||||
const metaWidth = metaMeasureEl.getBoundingClientRect().width
|
||||
|
||||
// Allow for the flex gap between left and actions.
|
||||
const availableLeft = Math.max(0, rowWidth - actionsWidth - 12)
|
||||
setShowMetaInline(primaryWidth + metaWidth + 8 <= availableLeft)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const text = metaText()
|
||||
if (!text || typeof ResizeObserver === "undefined") {
|
||||
setShowMetaInline(true)
|
||||
return
|
||||
}
|
||||
|
||||
updateMetaLayout()
|
||||
const observer = new ResizeObserver(() => updateMetaLayout())
|
||||
if (topRowEl) observer.observe(topRowEl)
|
||||
if (actionsEl) observer.observe(actionsEl)
|
||||
if (speakerPrimaryEl) observer.observe(speakerPrimaryEl)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
const isUser = () => props.record.role === "user"
|
||||
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
|
||||
|
||||
@@ -284,14 +326,47 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
return (
|
||||
<div class={containerClass()}>
|
||||
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
|
||||
<div class="message-item-header-row message-item-header-row--top">
|
||||
<div class="message-speaker">
|
||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
||||
{speakerLabel()}
|
||||
</span>
|
||||
<div class="message-item-header-row message-item-header-row--top" ref={(el) => (topRowEl = el)}>
|
||||
<div class="message-header-left">
|
||||
<div class="message-speaker-primary" ref={(el) => (speakerPrimaryEl = el)}>
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedForDeletion()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
const next = Boolean((event.currentTarget as HTMLInputElement).checked)
|
||||
props.onToggleSelectedMessage?.(props.record.id, next)
|
||||
}}
|
||||
aria-label={t("messageItem.selection.checkboxAriaLabel")}
|
||||
title={t("messageItem.selection.checkboxAriaLabel")}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<span class="message-speaker-label" data-role={isUser() ? "user" : "assistant"}>
|
||||
{speakerLabel()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={metaText() && showMetaInline()}>
|
||||
<span class="message-agent-meta-inline">{metaText()}</span>
|
||||
</Show>
|
||||
|
||||
<Show when={metaText()}>
|
||||
<span
|
||||
ref={(el) => (metaMeasureEl = el)}
|
||||
class="message-agent-meta-inline message-agent-meta-inline--measure"
|
||||
>
|
||||
{metaText()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="message-item-actions">
|
||||
<div class="message-item-actions" ref={(el) => (actionsEl = el)}>
|
||||
<Show when={isUser()}>
|
||||
<div class="message-action-group">
|
||||
<button
|
||||
@@ -394,12 +469,10 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={agentMeta()}>
|
||||
{(meta) => (
|
||||
<div class="message-item-header-row message-item-header-row--bottom">
|
||||
<span class="message-agent-meta">{meta()}</span>
|
||||
</div>
|
||||
)}
|
||||
<Show when={metaText() && !showMetaInline()}>
|
||||
<div class="message-item-header-row message-item-header-row--meta">
|
||||
<span class="message-agent-meta-block">{metaText()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</header>
|
||||
|
||||
@@ -11,6 +11,8 @@ interface MessagePreviewProps {
|
||||
deleteHover?: () => DeleteHoverState
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
const MessagePreview: Component<MessagePreviewProps> = (props) => {
|
||||
@@ -31,6 +33,8 @@ const MessagePreview: Component<MessagePreviewProps> = (props) => {
|
||||
deleteHover={props.deleteHover}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Show, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||
import { CheckSquare, Trash, X } from "lucide-solid"
|
||||
import Kbd from "./kbd"
|
||||
import MessageBlockList, { getMessageAnchorId } from "./message-block-list"
|
||||
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
|
||||
@@ -9,6 +10,8 @@ import { useScrollCache } from "../lib/hooks/use-scroll-cache"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { copyToClipboard } from "../lib/clipboard"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { showAlertDialog } from "../stores/alerts"
|
||||
import { deleteMessage } from "../stores/session-actions"
|
||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||
import type { DeleteHoverState } from "../types/delete-hover"
|
||||
|
||||
@@ -149,6 +152,61 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
||||
|
||||
const [deleteHover, setDeleteHover] = createSignal<DeleteHoverState>({ kind: "none" })
|
||||
|
||||
const [selectedForDeletion, setSelectedForDeletion] = createSignal<Set<string>>(new Set<string>())
|
||||
const isDeleteMode = createMemo(() => selectedForDeletion().size > 0)
|
||||
const selectedDeleteCount = createMemo(() => selectedForDeletion().size)
|
||||
|
||||
const isMessageSelectedForDeletion = (messageId: string) => selectedForDeletion().has(messageId)
|
||||
|
||||
const setMessageSelectedForDeletion = (messageId: string, selected: boolean) => {
|
||||
if (!messageId) return
|
||||
setSelectedForDeletion((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (selected) {
|
||||
next.add(messageId)
|
||||
} else {
|
||||
next.delete(messageId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const clearDeleteMode = () => {
|
||||
setSelectedForDeletion(new Set<string>())
|
||||
setDeleteHover({ kind: "none" })
|
||||
}
|
||||
|
||||
const selectAllForDeletion = () => {
|
||||
setSelectedForDeletion(new Set<string>(messageIds()))
|
||||
}
|
||||
|
||||
const deleteSelectedMessages = async () => {
|
||||
const selected = selectedForDeletion()
|
||||
if (selected.size === 0) return
|
||||
|
||||
const idsInSessionOrder = messageIds()
|
||||
const toDelete: string[] = []
|
||||
for (let idx = idsInSessionOrder.length - 1; idx >= 0; idx -= 1) {
|
||||
const id = idsInSessionOrder[idx]
|
||||
if (selected.has(id)) {
|
||||
toDelete.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const messageId of toDelete) {
|
||||
await deleteMessage(props.instanceId, props.sessionId, messageId)
|
||||
}
|
||||
clearDeleteMode()
|
||||
} catch (error) {
|
||||
showAlertDialog(t("messageSection.bulkDelete.failedMessage"), {
|
||||
title: t("messageSection.bulkDelete.failedTitle"),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const changeToken = createMemo(() => String(sessionRevision()))
|
||||
const isActive = createMemo(() => props.isActive !== false)
|
||||
@@ -171,6 +229,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
const [autoScroll, setAutoScroll] = createSignal(true)
|
||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
||||
const [topSentinelVisible, setTopSentinelVisible] = createSignal(true)
|
||||
const [bottomSentinelVisible, setBottomSentinelVisible] = createSignal(true)
|
||||
const [quoteSelection, setQuoteSelection] = createSignal<{ text: string; top: number; left: number } | null>(null)
|
||||
@@ -855,7 +914,10 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
|
||||
return (
|
||||
<div class="message-stream-container">
|
||||
<div class={`message-layout${hasTimelineSegments() ? " message-layout--with-timeline" : ""}`}>
|
||||
<div
|
||||
class={`message-layout${hasTimelineSegments() ? " message-layout--with-timeline" : ""}`}
|
||||
data-scroll-buttons={scrollButtonsCount()}
|
||||
>
|
||||
<div class="message-stream-shell" ref={setShellElement}>
|
||||
<div class="message-stream" ref={setContainerRef} onScroll={handleScroll} onMouseUp={handleStreamMouseUp}>
|
||||
<div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
|
||||
@@ -906,6 +968,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
onContentRendered={handleContentRendered}
|
||||
deleteHover={deleteHover}
|
||||
onDeleteHoverChange={setDeleteHover}
|
||||
selectedMessageIds={selectedForDeletion}
|
||||
onToggleSelectedMessage={setMessageSelectedForDeletion}
|
||||
setBottomSentinel={setBottomSentinel}
|
||||
suspendMeasurements={() => !isActive()}
|
||||
/>
|
||||
@@ -967,9 +1031,53 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
deleteHover={deleteHover}
|
||||
onDeleteHoverChange={setDeleteHover}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={selectedForDeletion}
|
||||
onToggleSelectedMessage={setMessageSelectedForDeletion}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isDeleteMode()}>
|
||||
<div
|
||||
class="message-delete-mode-toolbar"
|
||||
role="toolbar"
|
||||
aria-label={t("messageSection.bulkDelete.toolbarAriaLabel", { count: selectedDeleteCount() })}
|
||||
>
|
||||
<span class="message-delete-mode-count" aria-hidden="true">
|
||||
{selectedDeleteCount()}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-delete-mode-button"
|
||||
onClick={() => void deleteSelectedMessages()}
|
||||
title={t("messageSection.bulkDelete.deleteSelectedTitle")}
|
||||
aria-label={t("messageSection.bulkDelete.deleteSelectedTitle")}
|
||||
>
|
||||
<Trash class="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-delete-mode-button"
|
||||
onClick={selectAllForDeletion}
|
||||
title={t("messageSection.bulkDelete.selectAllTitle")}
|
||||
aria-label={t("messageSection.bulkDelete.selectAllTitle")}
|
||||
>
|
||||
<CheckSquare class="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="message-delete-mode-button"
|
||||
onClick={clearDeleteMode}
|
||||
title={t("messageSection.bulkDelete.cancelTitle")}
|
||||
aria-label={t("messageSection.bulkDelete.cancelTitle")}
|
||||
>
|
||||
<X class="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,8 @@ interface MessageTimelineProps {
|
||||
deleteHover?: () => DeleteHoverState
|
||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
}
|
||||
|
||||
const MAX_TOOLTIP_LENGTH = 220
|
||||
@@ -417,6 +419,10 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
||||
|
||||
const isDeleteHovered = () => {
|
||||
const hover = deleteHover() as DeleteHoverState
|
||||
const selected = props.selectedMessageIds?.() ?? new Set<string>()
|
||||
if (selected.has(segment.messageId)) {
|
||||
return true
|
||||
}
|
||||
if (hover.kind === "message") {
|
||||
return hover.messageId === segment.messageId
|
||||
}
|
||||
@@ -502,6 +508,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
||||
deleteHover={props.deleteHover}
|
||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "Deleting...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "Delete failed",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "Failed to delete message",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Select message for deletion",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Selected messages ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Delete selected messages",
|
||||
"messageSection.bulkDelete.selectAllTitle": "Select all messages",
|
||||
"messageSection.bulkDelete.cancelTitle": "Cancel selection",
|
||||
"messageSection.bulkDelete.failedTitle": "Delete failed",
|
||||
"messageSection.bulkDelete.failedMessage": "Failed to delete selected messages",
|
||||
"messageItem.status.queued": "QUEUED",
|
||||
"messageItem.status.generating": "Generating...",
|
||||
"messageItem.status.sending": "Sending...",
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "Eliminando...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "Error al eliminar",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Seleccionar mensaje para eliminar",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Mensajes seleccionados ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Eliminar mensajes seleccionados",
|
||||
"messageSection.bulkDelete.selectAllTitle": "Seleccionar todos los mensajes",
|
||||
"messageSection.bulkDelete.cancelTitle": "Cancelar selección",
|
||||
"messageSection.bulkDelete.failedTitle": "Error al eliminar",
|
||||
"messageSection.bulkDelete.failedMessage": "No se pudieron eliminar los mensajes seleccionados",
|
||||
"messageItem.status.queued": "EN COLA",
|
||||
"messageItem.status.generating": "Generando...",
|
||||
"messageItem.status.sending": "Enviando...",
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "Suppression...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "Échec de suppression",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Sélectionner le message pour suppression",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Messages sélectionnés ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Supprimer les messages sélectionnés",
|
||||
"messageSection.bulkDelete.selectAllTitle": "Tout sélectionner",
|
||||
"messageSection.bulkDelete.cancelTitle": "Annuler la sélection",
|
||||
"messageSection.bulkDelete.failedTitle": "Échec de suppression",
|
||||
"messageSection.bulkDelete.failedMessage": "Impossible de supprimer les messages sélectionnés",
|
||||
"messageItem.status.queued": "EN FILE",
|
||||
"messageItem.status.generating": "Génération...",
|
||||
"messageItem.status.sending": "Envoi...",
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "削除中...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "削除するメッセージを選択",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "選択したメッセージ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "選択したメッセージを削除",
|
||||
"messageSection.bulkDelete.selectAllTitle": "すべて選択",
|
||||
"messageSection.bulkDelete.cancelTitle": "選択をキャンセル",
|
||||
"messageSection.bulkDelete.failedTitle": "削除に失敗しました",
|
||||
"messageSection.bulkDelete.failedMessage": "選択したメッセージの削除に失敗しました",
|
||||
"messageItem.status.queued": "待機中",
|
||||
"messageItem.status.generating": "生成中...",
|
||||
"messageItem.status.sending": "送信中...",
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "Удаление...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "Выбрать сообщение для удаления",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "Выбранные сообщения ({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "Удалить выбранные сообщения",
|
||||
"messageSection.bulkDelete.selectAllTitle": "Выбрать все сообщения",
|
||||
"messageSection.bulkDelete.cancelTitle": "Отменить выбор",
|
||||
"messageSection.bulkDelete.failedTitle": "Ошибка удаления",
|
||||
"messageSection.bulkDelete.failedMessage": "Не удалось удалить выбранные сообщения",
|
||||
"messageItem.status.queued": "В ОЧЕРЕДИ",
|
||||
"messageItem.status.generating": "Генерация…",
|
||||
"messageItem.status.sending": "Отправка…",
|
||||
|
||||
@@ -82,6 +82,15 @@ export const messagingMessages = {
|
||||
"messageItem.actions.deletingMessage": "正在删除...",
|
||||
"messageItem.actions.deleteMessageFailedTitle": "删除失败",
|
||||
"messageItem.actions.deleteMessageFailedMessage": "无法删除消息",
|
||||
|
||||
"messageItem.selection.checkboxAriaLabel": "选择要删除的消息",
|
||||
|
||||
"messageSection.bulkDelete.toolbarAriaLabel": "已选择的消息({count})",
|
||||
"messageSection.bulkDelete.deleteSelectedTitle": "删除已选择的消息",
|
||||
"messageSection.bulkDelete.selectAllTitle": "全选消息",
|
||||
"messageSection.bulkDelete.cancelTitle": "取消选择",
|
||||
"messageSection.bulkDelete.failedTitle": "删除失败",
|
||||
"messageSection.bulkDelete.failedMessage": "无法删除已选择的消息",
|
||||
"messageItem.status.queued": "排队中",
|
||||
"messageItem.status.generating": "正在生成...",
|
||||
"messageItem.status.sending": "正在发送...",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "./messaging/prompt-input.css";
|
||||
@import "./messaging/message-section.css";
|
||||
@import "./messaging/message-block-list.css";
|
||||
@import "./messaging/message-selection.css";
|
||||
@import "./messaging/delete-overlays.css";
|
||||
@import "./messaging/message-timeline.css";
|
||||
@import "./messaging/tool-call.css";
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
}
|
||||
|
||||
.message-item-header {
|
||||
@apply flex flex-col gap-0.5;
|
||||
@apply flex flex-col;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.message-item-header-row {
|
||||
@@ -19,12 +20,58 @@
|
||||
@apply flex justify-between items-start gap-2.5;
|
||||
}
|
||||
|
||||
.message-item-header-row--meta {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.message-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-item-header-row--bottom {
|
||||
@apply flex items-start;
|
||||
}
|
||||
|
||||
.message-speaker {
|
||||
@apply flex flex-col gap-0.5 text-xs;
|
||||
/* Allow agent meta to wrap to a second row with comfortable spacing. */
|
||||
@apply flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-speaker-primary {
|
||||
@apply inline-flex items-center;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.message-agent-meta-inline {
|
||||
@apply text-[11px] font-medium;
|
||||
color: var(--message-assistant-border);
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.message-agent-meta-inline--measure {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-agent-meta-block {
|
||||
@apply text-[11px] font-medium;
|
||||
color: var(--message-assistant-border);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.message-speaker-label {
|
||||
@@ -46,19 +93,19 @@
|
||||
|
||||
.message-item-actions {
|
||||
@apply flex items-center gap-2;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.message-action-group {
|
||||
@apply flex items-center gap-2;
|
||||
@apply flex items-center gap-0;
|
||||
}
|
||||
|
||||
.message-action-button {
|
||||
@apply bg-transparent border border-[var(--border-base)] text-[var(--text-muted)] cursor-pointer px-3 py-0.5 rounded text-xs font-semibold leading-none transition-all duration-200 flex items-center justify-center h-6;
|
||||
@apply bg-transparent border-0 text-[var(--text-muted)] cursor-pointer px-2 py-0.5 rounded text-xs font-semibold leading-none transition-all duration-200 flex items-center justify-center h-6;
|
||||
}
|
||||
|
||||
.message-action-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
@@ -296,6 +343,12 @@
|
||||
color: var(--message-assistant-border);
|
||||
}
|
||||
|
||||
/* Keep reasoning meta as a single unit so it drops to the next line when needed. */
|
||||
.message-reasoning-label .message-step-meta-inline {
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.message-step-reason {
|
||||
@apply text-[11px] font-medium;
|
||||
@@ -320,7 +373,7 @@
|
||||
|
||||
.message-reasoning-header {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
@@ -365,11 +418,36 @@
|
||||
}
|
||||
|
||||
.message-reasoning-label {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--message-assistant-border);
|
||||
}
|
||||
|
||||
.message-reasoning-label-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.message-step-meta-inline--measure {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-reasoning-meta-row {
|
||||
padding: 0 0.6rem 0.15rem 0.6rem;
|
||||
}
|
||||
|
||||
.message-reasoning-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
84
packages/ui/src/styles/messaging/message-selection.css
Normal file
84
packages/ui/src/styles/messaging/message-selection.css
Normal file
@@ -0,0 +1,84 @@
|
||||
/* Message multi-select delete mode UI. */
|
||||
|
||||
.message-select-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
accent-color: var(--status-error);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.message-delete-mode-toolbar {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
background: color-mix(in oklab, var(--surface-secondary) 92%, var(--status-error-bg));
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 12px;
|
||||
z-index: 50;
|
||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
/* Avoid covering the scroll-to-top/bottom floating buttons. */
|
||||
.message-layout[data-scroll-buttons="1"] .message-delete-mode-toolbar {
|
||||
bottom: 4.25rem;
|
||||
}
|
||||
|
||||
.message-layout[data-scroll-buttons="2"] .message-delete-mode-toolbar {
|
||||
bottom: 7.5rem;
|
||||
}
|
||||
|
||||
/* When timeline is visible, pin the toolbar to the stream edge. */
|
||||
.message-layout--with-timeline .message-delete-mode-toolbar {
|
||||
right: calc(64px + 12px);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.message-layout--with-timeline .message-delete-mode-toolbar {
|
||||
right: calc(40px + 12px);
|
||||
}
|
||||
}
|
||||
|
||||
.message-delete-mode-count {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-secondary);
|
||||
border: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.message-delete-mode-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 10px;
|
||||
color: var(--text-muted);
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.message-delete-mode-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-color: var(--status-error);
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
.message-delete-mode-button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent-primary) 45%, transparent);
|
||||
}
|
||||
Reference in New Issue
Block a user