LibreChat/client/src/components/Chat/Messages/Content/ContentParts.tsx
Danny Avila de5b14fe9d
fix: Allow dynamic content creation in useChatFunctions
- Updated the initial response handling to avoid pre-initializing content types, enabling dynamic creation of content parts based on incoming delta events. This change supports various content types such as think and text.
2025-12-13 09:11:24 -05:00

152 lines
4.6 KiB
TypeScript

import { memo, useMemo } from 'react';
import { ContentTypes } from 'librechat-data-provider';
import type {
TMessageContentParts,
SearchResultData,
TAttachment,
Agents,
} from 'librechat-data-provider';
import { MessageContext, SearchContext } from '~/Providers';
import { EditTextPart, EmptyText } from './Parts';
import MemoryArtifacts from './MemoryArtifacts';
import Sources from '~/components/Web/Sources';
import { mapAttachments } from '~/utils/map';
import Container from './Container';
import Part from './Part';
type ContentPartsProps = {
content: Array<TMessageContentParts | undefined> | undefined;
messageId: string;
conversationId?: string | null;
attachments?: TAttachment[];
searchResults?: { [key: string]: SearchResultData };
isCreatedByUser: boolean;
isLast: boolean;
isSubmitting: boolean;
isLatestMessage?: boolean;
edit?: boolean;
enterEdit?: (cancel?: boolean) => void | null | undefined;
siblingIdx?: number;
setSiblingIdx?:
| ((value: number) => void | React.Dispatch<React.SetStateAction<number>>)
| null
| undefined;
};
const ContentParts = memo(
({
content,
messageId,
conversationId,
attachments,
searchResults,
isCreatedByUser,
isLast,
isSubmitting,
isLatestMessage,
edit,
enterEdit,
siblingIdx,
setSiblingIdx,
}: ContentPartsProps) => {
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
if (!content) {
return null;
}
if (edit === true && enterEdit && setSiblingIdx) {
return (
<>
{content.map((part, idx) => {
if (!part) {
return null;
}
const isTextPart =
part?.type === ContentTypes.TEXT ||
typeof (part as unknown as Agents.MessageContentText)?.text !== 'string';
const isThinkPart =
part?.type === ContentTypes.THINK ||
typeof (part as unknown as Agents.ReasoningDeltaUpdate)?.think !== 'string';
if (!isTextPart && !isThinkPart) {
return null;
}
const isToolCall =
part.type === ContentTypes.TOOL_CALL || part['tool_call_ids'] != null;
if (isToolCall) {
return null;
}
return (
<EditTextPart
index={idx}
part={part as Agents.MessageContentText | Agents.ReasoningDeltaUpdate}
messageId={messageId}
isSubmitting={isSubmitting}
enterEdit={enterEdit}
siblingIdx={siblingIdx ?? null}
setSiblingIdx={setSiblingIdx}
key={`edit-${messageId}-${idx}`}
/>
);
})}
</>
);
}
/** Show cursor placeholder when content is empty but actively submitting */
const showEmptyCursor = content.length === 0 && effectiveIsSubmitting;
return (
<>
<SearchContext.Provider value={{ searchResults }}>
<MemoryArtifacts attachments={attachments} />
<Sources messageId={messageId} conversationId={conversationId || undefined} />
{showEmptyCursor && (
<Container>
<EmptyText />
</Container>
)}
{content.map((part, idx) => {
if (!part) {
return null;
}
const toolCallId =
(part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
const partAttachments = attachmentMap[toolCallId];
return (
<MessageContext.Provider
key={`provider-${messageId}-${idx}`}
value={{
messageId,
isExpanded: true,
conversationId,
partIndex: idx,
nextType: content[idx + 1]?.type,
isSubmitting: effectiveIsSubmitting,
isLatestMessage,
}}
>
<Part
part={part}
attachments={partAttachments}
isSubmitting={effectiveIsSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
isLast={idx === content.length - 1}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>
);
})}
</SearchContext.Provider>
</>
);
},
);
export default ContentParts;