mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix: match text and image attachment heights in conversation timeline (#23593)
## Problem Text attachments (`InlineTextAttachmentButton`) and image thumbnails (`ImageThumbnail`) rendered at different heights when displayed side by side in user messages. Text cards had no explicit height (content-driven), while images used `h-16` (64px). ## Changes **`ConversationTimeline.tsx`** - Added `h-16` to `InlineTextAttachmentButton` to match `ImageThumbnail` - Added `isPlaceholder` prop: when the content hasn't been fetched yet (file_id path), renders "Pasted text" in sans-serif `text-sm` with `items-center` alignment instead of monospace `text-xs` - Once real content loads, it still renders in `font-mono text-xs` with `formatTextAttachmentPreview()` **`ConversationTimeline.stories.tsx`** - Added `UserMessageWithMixedAttachments` story showing a text attachment and image side by side as a visual regression guard
This commit is contained in:
@@ -299,6 +299,42 @@ export const UserMessageWithTextAttachmentOnly: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Visual regression: text and image attachments render at the same height. */
|
||||
export const UserMessageWithMixedAttachments: Story = {
|
||||
args: {
|
||||
...defaultArgs,
|
||||
parsedMessages: parseMessagesWithMergedTools([
|
||||
{
|
||||
...baseMessage,
|
||||
id: 1,
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Here is a screenshot and some context" },
|
||||
{
|
||||
type: "file",
|
||||
media_type: "image/png",
|
||||
data: TEST_PNG_B64,
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
file_id: "storybook-test-text",
|
||||
media_type: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const images = canvas.getAllByRole("img", { name: "Attached image" });
|
||||
expect(images).toHaveLength(1);
|
||||
const textButtons = await canvas.findAllByRole("button", {
|
||||
name: "View text attachment",
|
||||
});
|
||||
expect(textButtons).toHaveLength(1);
|
||||
},
|
||||
};
|
||||
|
||||
/** Text-only messages must not produce spurious image thumbnails. */
|
||||
export const UserMessageTextOnly: Story = {
|
||||
args: {
|
||||
|
||||
@@ -123,20 +123,26 @@ const SmoothedResponse: FC<{
|
||||
const InlineTextAttachmentButton: FC<{
|
||||
content: string;
|
||||
onPreview?: (content: string) => void;
|
||||
}> = ({ content, onPreview }) => {
|
||||
isPlaceholder?: boolean;
|
||||
}> = ({ content, onPreview, isPlaceholder }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="View text attachment"
|
||||
className="inline-flex max-w-sm items-start gap-2 rounded-md border-0 bg-surface-tertiary px-3 py-2 text-left transition-colors hover:bg-surface-quaternary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
|
||||
className="inline-flex h-16 max-w-sm items-center gap-2 rounded-md border-0 bg-surface-tertiary px-3 py-2 text-left transition-colors hover:bg-surface-quaternary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPreview?.(content);
|
||||
}}
|
||||
>
|
||||
<FileTextIcon className="mt-0.5 size-icon-sm shrink-0 text-content-secondary" />
|
||||
<span className="line-clamp-2 min-w-0 font-mono text-xs text-content-secondary">
|
||||
{formatTextAttachmentPreview(content)}
|
||||
<FileTextIcon className="size-icon-sm shrink-0 text-content-secondary" />
|
||||
<span
|
||||
className={cn(
|
||||
"line-clamp-2 min-w-0 text-content-secondary",
|
||||
isPlaceholder ? "text-sm" : "font-mono text-xs",
|
||||
)}
|
||||
>
|
||||
{isPlaceholder ? content : formatTextAttachmentPreview(content)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@@ -156,6 +162,7 @@ const TextAttachmentButton: FC<{
|
||||
return (
|
||||
<InlineTextAttachmentButton
|
||||
content={content ?? "Pasted text"}
|
||||
isPlaceholder={content === null}
|
||||
onPreview={async () => {
|
||||
if (content !== null) {
|
||||
onPreview?.(content);
|
||||
|
||||
Reference in New Issue
Block a user