mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(chat): render non-image attachments on content-part messages
Container is only reached by messages that carry no content, so an assistant turn with both reasoning and a generated file rendered the reasoning and dropped the file: images have an image_file part, everything else lives only on message.files. ContentParts now renders the non-image half of that list.
This commit is contained in:
parent
a4bd2ed2ae
commit
f025c19085
5 changed files with 35 additions and 9 deletions
|
|
@ -9,10 +9,10 @@ const Container = ({ children, message }: { children: React.ReactNode; message?:
|
|||
dir="auto"
|
||||
>
|
||||
{message?.isCreatedByUser === true && <MessageQuotes quotes={message.quotes} />}
|
||||
{/* Not user-only: this is the sole consumer of `message.files`, and an
|
||||
imported assistant message can carry attachments — voice-mode audio —
|
||||
that no content part renders. */}
|
||||
{message?.files != null && message.files.length > 0 && <Files message={message} />}
|
||||
{/* Not user-only: an assistant message can carry attachments — voice-mode
|
||||
audio — that no content part renders. `ContentParts` renders the same
|
||||
files for messages that never reach this container. */}
|
||||
{message?.files != null && message.files.length > 0 && <Files files={message.files} />}
|
||||
{message?.isCreatedByUser === true && (
|
||||
<>
|
||||
<SkillPills skills={message.alwaysAppliedSkills} source="always-apply" />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
TMessageContentParts,
|
||||
SearchResultData,
|
||||
TAttachment,
|
||||
TMessage,
|
||||
Agents,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ToolCallGroupExpansionState } from './ToolCallGroup';
|
||||
|
|
@ -16,6 +17,7 @@ import ApprovalProvider from './ApprovalContext';
|
|||
import MemoryArtifacts from './MemoryArtifacts';
|
||||
import ToolCallGroup from './ToolCallGroup';
|
||||
import Container from './Container';
|
||||
import Files from './Files';
|
||||
import Part from './Part';
|
||||
|
||||
const getToolCallId = (part: TMessageContentParts): string =>
|
||||
|
|
@ -119,6 +121,15 @@ type ContentPartsProps = {
|
|||
isLast: boolean;
|
||||
isSubmitting: boolean;
|
||||
isLatestMessage?: boolean;
|
||||
/**
|
||||
* The message's own attachments. Images among them already render as
|
||||
* `image_file` parts, so only the rest are shown here — but the rest have
|
||||
* no content part at all, and `Container` (the only other consumer of this
|
||||
* field) is never reached by a message that has content. Without this, an
|
||||
* assistant turn that carries both reasoning and a generated file renders
|
||||
* the reasoning and silently drops the file.
|
||||
*/
|
||||
files?: TMessage['files'];
|
||||
edit?: boolean;
|
||||
enterEdit?: (cancel?: boolean) => void | null | undefined;
|
||||
siblingIdx?: number;
|
||||
|
|
@ -136,6 +147,7 @@ type ContentPartsProps = {
|
|||
*/
|
||||
const ContentParts = memo(function ContentParts({
|
||||
edit,
|
||||
files,
|
||||
isLast,
|
||||
content,
|
||||
manualSkills,
|
||||
|
|
@ -152,6 +164,10 @@ const ContentParts = memo(function ContentParts({
|
|||
createdAt,
|
||||
}: ContentPartsProps) {
|
||||
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
||||
const nonImageFiles = useMemo(
|
||||
() => files?.filter((file) => file.type?.startsWith('image/') !== true),
|
||||
[files],
|
||||
);
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
const toolGroupExpansionRef = useRef(new Map<string, ToolCallGroupExpansionState>());
|
||||
const fallbackScopeRef = useRef({ messageId, scope: 0 });
|
||||
|
|
@ -407,6 +423,11 @@ const ContentParts = memo(function ContentParts({
|
|||
<ApprovalProvider>
|
||||
<SearchContext.Provider value={{ searchResults }}>
|
||||
<MemoryArtifacts attachments={attachments} />
|
||||
{nonImageFiles != null && nonImageFiles.length > 0 && (
|
||||
<Container>
|
||||
<Files files={nonImageFiles} />
|
||||
</Container>
|
||||
)}
|
||||
{renderPendingSkills()}
|
||||
{showEmptyCursor && (
|
||||
<Container>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
|||
import FilePreviewDialog from './FilePreviewDialog';
|
||||
import Image from './Image';
|
||||
|
||||
const Files = ({ message }: { message?: TMessage }) => {
|
||||
/** Takes the file list rather than the message: the content-part renderer has
|
||||
* to show the non-image half of it without handing the whole message object to
|
||||
* a memoized subtree. */
|
||||
const Files = ({ files }: { files?: TMessage['files'] }) => {
|
||||
const imageFiles = useMemo(() => {
|
||||
return message?.files?.filter((file) => file.type?.startsWith('image/')) || [];
|
||||
}, [message?.files]);
|
||||
return files?.filter((file) => file.type?.startsWith('image/')) || [];
|
||||
}, [files]);
|
||||
|
||||
const otherFiles = useMemo(() => {
|
||||
return message?.files?.filter((file) => !file.type?.startsWith('image/')) || [];
|
||||
}, [message?.files]);
|
||||
return files?.filter((file) => !file.type?.startsWith('image/')) || [];
|
||||
}, [files]);
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<Partial<TFile> | null>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ function MessageParts(props: TMessageProps) {
|
|||
<ContentParts
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
files={message.files}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
attachments={attachments}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ const ContentRender = memo(function ContentRender({
|
|||
<ContentParts
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
files={msg.files}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
messageId={msg.messageId}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue