fix(web): align uploaded attachments with user messages (#185)

Co-authored-by: Yaowei Zheng <hiyouga@buaa.edu.cn>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Laodouuu
2026-08-04 23:31:34 +08:00
committed by GitHub
parent 8362eca6e5
commit f8a5a1c12e
2 changed files with 30 additions and 9 deletions
@@ -3,8 +3,8 @@
* server appends aren't shown verbatim, they collapse into a single line reading
* "Attached files: a.pdf, b.csv" (paperclip icon + static text, no navigation — the files live
* in the session scratchpad and the model opens them by path); the body text around them is
* rendered as usual by the caller. Same shape as SkillsBanner, so the two notices a message
* can carry read as one family.
* rendered as usual by the caller. It keeps the same visual language as message-level notices;
* the caller owns its user-side alignment and timestamp footer.
*/
import { S } from "../../lib/strings";
import { attachmentFileName } from "../../lib/attachments";
@@ -22,7 +22,7 @@ export function AttachedFilesBanner({ files }: { files: string[] }) {
// into a paragraph-tall block above the message. The full list stays reachable as a title.
<p
title={label}
className="anim-msg my-2 flex w-fit max-w-full items-center gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400"
className="flex w-fit max-w-full items-center gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400"
>
<GlyphIcon d={PAPERCLIP_ICON} className="shrink-0 text-gray-400 dark:text-gray-500" />
<span className="min-w-0 truncate">{label}</span>
@@ -217,10 +217,6 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
<>
{scheduled && <ScheduledBanner origin={scheduled.origin} />}
{skills && <SkillsBanner names={skills.skills} />}
{/* Files uploaded with this message: named above the bubble, like the other
message-level notices — the bytes live in the session scratchpad, the model
opens them by path (goal mode never gets here: it takes text and images only). */}
{files.length > 0 && <AttachedFilesBanner files={files} />}
{text && (
<div className="anim-msg group my-4 flex flex-col items-end">
<div className="max-w-[88%] rounded-lg bg-gray-100 px-4 py-2.5 md:max-w-[75%] dark:bg-gray-800">
@@ -236,6 +232,19 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
/>
</div>
)}
{/* Files uploaded with this message: shown below the text in the same user-side
container and with the same timestamp footer as uploaded images. The bytes live in
the session scratchpad, where the model opens them by path (goal mode never gets
here: it takes text and images only). */}
{files.length > 0 && (
<div className="anim-msg group my-4 flex flex-col items-end">
<AttachedFilesBanner files={files} />
<MessageMeta
{...(item.atMs !== undefined ? { atMs: item.atMs } : {})}
align="right"
/>
</div>
)}
{images.map((src, i) => (
<div key={i} className="anim-msg group my-4 flex flex-col items-end">
<div className="max-w-[88%] rounded-lg bg-gray-100 p-1.5 md:max-w-[75%] dark:bg-gray-800">
@@ -262,8 +271,15 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
// Images sent with the message live inside the same chip: `item.images` on a vision
// model (delivered as image messages right behind the text), or restored from the
// [attached image: …] path lines without one — the same two shapes a user_text bubble
// handles, so both render identically here.
const { text: steerText, images: steerImages } = splitAttachments(item.text);
// handles, so both render identically here. Files ride steering only as
// [attached file: …] rows (there is no item.files channel) and collapse into the same
// banner a full prompt uses, kept inside the chip — a files-only steering would
// otherwise render as an empty chip with the filenames lost.
const {
text: steerText,
images: steerImages,
files: steerFiles,
} = splitAttachments(item.text);
const shown = [...steerImages, ...(item.images ?? [])];
return (
<div className="anim-msg group my-2 flex flex-col items-end">
@@ -292,6 +308,11 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo
))}
</div>
)}
{steerFiles.length > 0 && (
<div className="flex justify-end">
<AttachedFilesBanner files={steerFiles} />
</div>
)}
</div>
<MessageMeta
{...(item.atMs !== undefined ? { atMs: item.atMs } : {})}