mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
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:
@@ -189,7 +189,20 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
|
|||||||
|
|
||||||
# Create files message and prepend to the last human message content
|
# Create files message and prepend to the last human message content
|
||||||
files_message = self._create_files_message(files)
|
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
|
# Create new message with combined content
|
||||||
updated_message = HumanMessage(
|
updated_message = HumanMessage(
|
||||||
|
|||||||
@@ -115,6 +115,86 @@ function MessageContent_({
|
|||||||
|
|
||||||
const { thread_id } = useParams<{ thread_id: string }>();
|
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 (
|
return (
|
||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
{/* Uploaded files for human messages - show first */}
|
{/* Uploaded files for human messages - show first */}
|
||||||
@@ -287,7 +367,7 @@ function UploadedFileCard({
|
|||||||
href={imageUrl}
|
href={imageUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
@@ -300,7 +380,7 @@ function UploadedFileCard({
|
|||||||
|
|
||||||
// For non-image files, show file card
|
// For non-image files, show file card
|
||||||
return (
|
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">
|
<div className="flex items-start gap-2">
|
||||||
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export function useSubmitThread({
|
|||||||
const callback = useCallback(
|
const callback = useCallback(
|
||||||
async (message: PromptInputMessage) => {
|
async (message: PromptInputMessage) => {
|
||||||
const text = message.text.trim();
|
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
|
// Upload files first if any
|
||||||
if (message.files && message.files.length > 0) {
|
if (message.files && message.files.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user