feat: improve file upload message handling and UI

Backend:
- Handle both string and list format for message content in uploads middleware
- Extract text content from structured message blocks
- Add logging for debugging file upload flow

Frontend:
- Separate file display from message bubble for human messages
- Show uploaded files outside the message bubble for cleaner layout
- Improve file card border styling with subtle border color
- Add debug logging for message submission with files

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ruitanglin
2026-01-29 12:51:21 +08:00
parent 2ec506d590
commit 341397562a
3 changed files with 102 additions and 3 deletions

View File

@@ -189,7 +189,20 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
# Create files message and prepend to the last human message content
files_message = self._create_files_message(files)
original_content = last_message.content if isinstance(last_message.content, str) else ""
# Extract original content - handle both string and list formats
original_content = ""
if isinstance(last_message.content, str):
original_content = last_message.content
elif isinstance(last_message.content, list):
# Content is a list of content blocks (e.g., [{"type": "text", "text": "..."}])
text_parts = []
for block in last_message.content:
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
original_content = "\n".join(text_parts)
logger.info(f"Original message content: {original_content[:100] if original_content else '(empty)'}")
# Create new message with combined content
updated_message = HumanMessage(

View File

@@ -115,6 +115,86 @@ function MessageContent_({
const { thread_id } = useParams<{ thread_id: string }>();
// For human messages with uploaded files, render files outside the bubble
if (isHuman && uploadedFiles.length > 0) {
return (
<div className={cn("ml-auto flex flex-col gap-2", className)}>
{/* Uploaded files outside the message bubble */}
<UploadedFilesList files={uploadedFiles} threadId={thread_id} />
{/* Message content inside the bubble (only if there's text) */}
{cleanContent && (
<AIElementMessageContent className="w-fit">
<AIElementMessageResponse
remarkPlugins={[[remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
components={{
a: ({
href,
children,
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (!href) {
return <span>{children}</span>;
}
// Check if this link matches a citation
const citation = citationMap.get(href);
if (citation) {
return (
<CitationLink citation={citation} href={href}>
{children}
</CitationLink>
);
}
// Regular external link
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline underline-offset-2 hover:no-underline"
>
{children}
</a>
);
},
img: ({ src, alt }: React.ImgHTMLAttributes<HTMLImageElement>) => {
if (!src) return null;
if (typeof src !== "string") {
return (
<img
className="max-w-full overflow-hidden rounded-lg"
src={src}
alt={alt}
/>
);
}
let url = src;
if (src.startsWith("/mnt/")) {
url = resolveArtifactURL(src, thread_id);
}
return (
<a href={url} target="_blank" rel="noopener noreferrer">
<img
className="max-w-full overflow-hidden rounded-lg"
src={url}
alt={alt}
/>
</a>
);
},
}}
>
{cleanContent}
</AIElementMessageResponse>
</AIElementMessageContent>
)}
</div>
);
}
// Default rendering for non-human messages or human messages without files
return (
<AIElementMessageContent className={className}>
{/* Uploaded files for human messages - show first */}
@@ -287,7 +367,7 @@ function UploadedFileCard({
href={imageUrl}
target="_blank"
rel="noopener noreferrer"
className="group relative block overflow-hidden rounded-lg border"
className="group relative block overflow-hidden rounded-lg border border-border/40"
>
<img
src={imageUrl}
@@ -300,7 +380,7 @@ function UploadedFileCard({
// For non-image files, show file card
return (
<div className="bg-background flex min-w-[120px] max-w-[200px] flex-col gap-1 rounded-lg border p-3 shadow-sm">
<div className="bg-background flex min-w-[120px] max-w-[200px] flex-col gap-1 rounded-lg border border-border/40 p-3 shadow-sm">
<div className="flex items-start gap-2">
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
<span

View File

@@ -73,6 +73,12 @@ export function useSubmitThread({
const callback = useCallback(
async (message: PromptInputMessage) => {
const text = message.text.trim();
console.log('[useSubmitThread] Submitting message:', {
text,
hasFiles: !!message.files?.length,
filesCount: message.files?.length || 0
});
// Upload files first if any
if (message.files && message.files.length > 0) {