Feat: An image carousel is displayed at the bottom of the agent's chat messages. #12076 (#12215)

### What problem does this PR solve?

Feat: An image carousel is displayed at the bottom of the agent's chat
messages. #12076

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-25 19:02:49 +08:00
committed by GitHub
parent cfd1250615
commit c7b5bfb809
7 changed files with 1665 additions and 440 deletions

View File

@ -36,6 +36,7 @@ import { Button } from '../ui/button';
import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less';
import { ReferenceDocumentList } from './reference-document-list';
import { ReferenceImageList } from './reference-image-list';
import { UploadedMessageFiles } from './uploaded-message-files';
interface IProps
@ -295,6 +296,13 @@ function MessageItem({
{renderContent()}
{isAssistant && (
<ReferenceImageList
referenceChunks={reference?.chunks}
messageContent={messageContent}
></ReferenceImageList>
)}
{isAssistant && referenceDocuments.length > 0 && (
<ReferenceDocumentList
list={referenceDocuments}

View File

@ -7,22 +7,34 @@ import {
CarouselPrevious,
} from '@/components/ui/carousel';
import { IReferenceChunk } from '@/interfaces/database/chat';
import { useResponsive } from 'ahooks';
import { isPlainObject } from 'lodash';
import { useMemo } from 'react';
import { extractNumbersFromMessageContent } from './utils';
type IProps = {
referenceChunks: IReferenceChunk[];
referenceChunks?: IReferenceChunk[] | Record<string, IReferenceChunk>;
messageContent: string;
};
function ImageCarousel({
imageIds,
hideButtons,
}: {
hideButtons?: boolean;
imageIds: string[];
}) {
type ImageItem = {
id: string;
index: number;
};
const getButtonVisibilityClass = (imageCount: number) => {
const map: Record<number, string> = {
1: 'hidden',
2: '@sm:hidden',
3: '@md:hidden',
4: '@lg:hidden',
5: '@lg:hidden',
};
return map[imageCount] || (imageCount >= 6 ? '@2xl:hidden' : '');
};
function ImageCarousel({ images }: { images: ImageItem[] }) {
const buttonVisibilityClass = getButtonVisibilityClass(images.length);
return (
<Carousel
className="w-full"
@ -31,22 +43,27 @@ function ImageCarousel({
}}
>
<CarouselContent>
{imageIds.map((imageId, index) => (
<CarouselItem key={index} className="md:basis-1/2 2xl:basis-1/6">
{images.map(({ id, index }) => (
<CarouselItem
key={index}
className="
basis-full
@sm:basis-1/2
@md:basis-1/3
@lg:basis-1/4
@2xl:basis-1/6
"
>
<Image
id={imageId}
id={id}
className="h-40 w-full"
label={`Fig. ${(index + 1).toString()}`}
/>
</CarouselItem>
))}
</CarouselContent>
{!hideButtons && (
<>
<CarouselPrevious />
<CarouselNext />
</>
)}
<CarouselPrevious className={buttonVisibilityClass} />
<CarouselNext className={buttonVisibilityClass} />
</Carousel>
);
}
@ -55,30 +72,38 @@ export function ReferenceImageList({
referenceChunks,
messageContent,
}: IProps) {
const imageIds = useMemo(() => {
return referenceChunks
.filter((_, idx) =>
extractNumbersFromMessageContent(messageContent).includes(idx),
)
.map((chunk) => chunk.image_id);
}, [messageContent, referenceChunks]);
const imageCount = imageIds.length;
const allChunkIndexes = extractNumbersFromMessageContent(messageContent);
const images = useMemo(() => {
if (Array.isArray(referenceChunks)) {
return referenceChunks
.map((chunk, idx) => ({ id: chunk.image_id, index: idx }))
.filter((item, idx) => allChunkIndexes.includes(idx) && item.id);
}
const responsive = useResponsive();
if (isPlainObject(referenceChunks)) {
return Object.entries(referenceChunks || {}).reduce<ImageItem[]>(
(pre, [idx, chunk]) => {
if (allChunkIndexes.includes(Number(idx)) && chunk.image_id) {
return pre.concat({ id: chunk.image_id, index: Number(idx) });
}
return pre;
},
[],
);
}
const { isMd, is2xl } = useMemo(() => {
return {
isMd: responsive.md,
is2xl: responsive['2xl'],
};
}, [responsive]);
return [];
}, [allChunkIndexes, referenceChunks]);
// If there are few images, hide the previous/next buttons.
const hideButtons = is2xl ? imageCount <= 6 : isMd ? imageCount <= 2 : false;
const imageCount = images?.length || 0;
if (imageCount === 0) {
return <></>;
}
return <ImageCarousel imageIds={imageIds} hideButtons={hideButtons} />;
return (
<section className="@container w-full">
<ImageCarousel images={images} />
</section>
);
}