浏览代码

Feat: Render the uploaded agent message file #3221 (#9081)

### What problem does this PR solve?

Feat: Render the uploaded agent message file #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.0
balibabu 3 个月前
父节点
当前提交
7e7619bdc0
没有帐户链接到提交者的电子邮件

+ 10
- 78
web/src/components/next-message-item/index.tsx 查看文件

useState, useState,
} from 'react'; } from 'react';


import {
useFetchDocumentInfosByIds,
useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { INodeEvent } from '@/hooks/use-send-message'; import { INodeEvent } from '@/hooks/use-send-message';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline'; import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline';
import { IMessage } from '@/pages/chat/interface'; import { IMessage } from '@/pages/chat/interface';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal'; import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link';
import MarkdownContent from '../next-markdown-content'; import MarkdownContent from '../next-markdown-content';
import { RAGFlowAvatar } from '../ragflow-avatar';
import { useTheme } from '../theme-provider'; import { useTheme } from '../theme-provider';
import { AssistantGroupButton, UserGroupButton } from './group-button'; import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less'; import styles from './index.less';
import { ReferenceDocumentList } from './reference-document-list'; import { ReferenceDocumentList } from './reference-document-list';

const { Text } = Typography;
import { UploadedMessageFiles } from './uploaded-message-files';


interface IProps interface IProps
extends Partial<IRemoveMessageById>, extends Partial<IRemoveMessageById>,
const { theme } = useTheme(); const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant; const isAssistant = item.role === MessageType.Assistant;
const isUser = item.role === MessageType.User; const isUser = item.role === MessageType.User;
const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
const { data: documentThumbnails, setDocumentIds: setIds } =
useFetchDocumentThumbnailsByIds();
const { visible, hideModal, showModal } = useSetModalState(); const { visible, hideModal, showModal } = useSetModalState();
const [clickedDocumentId, setClickedDocumentId] = useState(''); const [clickedDocumentId, setClickedDocumentId] = useState('');


return Object.values(docs); return Object.values(docs);
}, [reference?.doc_aggs]); }, [reference?.doc_aggs]);


const handleUserDocumentClick = useCallback(
(id: string) => () => {
setClickedDocumentId(id);
showModal();
},
[showModal],
);

const handleRegenerateMessage = useCallback(() => { const handleRegenerateMessage = useCallback(() => {
regenerateMessage?.(item); regenerateMessage?.(item);
}, [regenerateMessage, item]); }, [regenerateMessage, item]);


useEffect(() => {
const ids = item?.doc_ids ?? [];
if (ids.length) {
setDocumentIds(ids);
const documentIds = ids.filter((x) => !(x in documentThumbnails));
if (documentIds.length) {
setIds(documentIds);
}
}
}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);

useEffect(() => { useEffect(() => {
if (typeof setCurrentMessageId === 'function') { if (typeof setCurrentMessageId === 'function') {
setCurrentMessageId(item.id); setCurrentMessageId(item.id);
> >
{visibleAvatar && {visibleAvatar &&
(item.role === MessageType.User ? ( (item.role === MessageType.User ? (
<Avatar size={40} src={avatar ?? '/logo.svg'} />
<RAGFlowAvatar avatar={avatar ?? '/logo.svg'} />
) : avatarDialog ? ( ) : avatarDialog ? (
<Avatar size={40} src={avatarDialog} />
<RAGFlowAvatar avatar={avatarDialog} />
) : ( ) : (
<AssistantIcon /> <AssistantIcon />
))} ))}


<Flex vertical gap={8} flex={1}>
<Space>
<section className="flex-col gap-2 flex-1">
<div className="space-x-1">
{isAssistant ? ( {isAssistant ? (
<AssistantGroupButton <AssistantGroupButton
messageId={item.id} messageId={item.id}
)} )}


{/* <b>{isAssistant ? '' : nickname}</b> */} {/* <b>{isAssistant ? '' : nickname}</b> */}
</Space>
</div>
<div <div
className={cn({ className={cn({
[theme === 'dark' [theme === 'dark'
canvasId={conversationId} canvasId={conversationId}
/> />
)} )}
{isUser && documentList.length > 0 && (
<List
bordered
dataSource={documentList}
renderItem={(item) => {
// TODO:
// const fileThumbnail =
// documentThumbnails[item.id] || documentThumbnails[item.id];
const fileExtension = getExtension(item.name);
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon id={item.id} name={item.name}></FileIcon>

{isImage(fileExtension) ? (
<NewDocumentLink
documentId={item.id}
documentName={item.name}
prefix="document"
>
{item.name}
</NewDocumentLink>
) : (
<Button
type={'text'}
onClick={handleUserDocumentClick(item.id)}
>
<Text
style={{ maxWidth: '40vw' }}
ellipsis={{ tooltip: item.name }}
>
{item.name}
</Text>
</Button>
)}
</Flex>
</List.Item>
);
}}
/>
{isUser && (
<UploadedMessageFiles files={item.files}></UploadedMessageFiles>
)} )}
</Flex>
</section>
</div> </div>
</section> </section>
{visible && ( {visible && (

+ 36
- 0
web/src/components/next-message-item/uploaded-message-files.tsx 查看文件

import { getExtension } from '@/utils/document-util';
import { formatBytes } from '@/utils/file-util';
import { memo } from 'react';
import SvgIcon from '../svg-icon';

interface IProps {
files?: File[];
}
export function InnerUploadedMessageFiles({ files = [] }: IProps) {
return (
<section className="flex gap-2 pt-2">
{files?.map((file, idx) => (
<div key={idx} className="flex gap-1 border rounded-md p-1.5">
{file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
alt={file.name}
className="size-10 object-cover"
/>
) : (
<SvgIcon
name={`file-icon/${getExtension(file.name)}`}
width={24}
></SvgIcon>
)}
<div className="text-xs max-w-20">
<div className="truncate">{file.name}</div>
<p className="text-text-sub-title pt-1">{formatBytes(file.size)}</p>
</div>
</div>
))}
</section>
);
}

export const UploadedMessageFiles = memo(InnerUploadedMessageFiles);

+ 1
- 0
web/src/interfaces/database/chat.ts 查看文件

id?: string; id?: string;
audio_binary?: string; audio_binary?: string;
data?: any; data?: any;
files?: File[];
} }


export interface IReferenceChunk { export interface IReferenceChunk {

+ 1
- 1
web/src/pages/agent/chat/box.tsx 查看文件

useCallback( useCallback(
async (files, options) => { async (files, options) => {
const ret = await uploadCanvasFile({ files, options }); const ret = await uploadCanvasFile({ files, options });
appendUploadResponseList(ret.data);
appendUploadResponseList(ret.data, files);
}, },
[appendUploadResponseList, uploadCanvasFile], [appendUploadResponseList, uploadCanvasFile],
); );

+ 14
- 8
web/src/pages/agent/chat/use-send-agent-message.ts 查看文件

const [uploadResponseList, setUploadResponseList] = useState< const [uploadResponseList, setUploadResponseList] = useState<
UploadResponseDataType[] UploadResponseDataType[]
>([]); >([]);
const [fileList, setFileList] = useState<File[]>([]);


const append = useCallback((data: UploadResponseDataType) => {
const append = useCallback((data: UploadResponseDataType, files: File[]) => {
setUploadResponseList((prev) => [...prev, data]); setUploadResponseList((prev) => [...prev, data]);
setFileList((pre) => [...pre, ...files]);
}, []); }, []);


const clear = useCallback(() => { const clear = useCallback(() => {
setUploadResponseList([]); setUploadResponseList([]);
setFileList([]);
}, []); }, []);


return { return {
uploadResponseList, uploadResponseList,
fileList,
setUploadResponseList, setUploadResponseList,
appendUploadResponseList: append, appendUploadResponseList: append,
clearUploadResponseList: clear, clearUploadResponseList: clear,
appendUploadResponseList, appendUploadResponseList,
clearUploadResponseList, clearUploadResponseList,
uploadResponseList, uploadResponseList,
fileList,
} = useSetUploadResponseData(); } = useSetUploadResponseData();


const sendMessage = useCallback( const sendMessage = useCallback(
const handlePressEnter = useCallback(() => { const handlePressEnter = useCallback(() => {
if (trim(value) === '') return; if (trim(value) === '') return;
const id = uuid(); const id = uuid();
const msgBody = {
id,
content: value.trim(),
role: MessageType.User,
};
if (done) { if (done) {
setValue(''); setValue('');
sendMessage({ sendMessage({
message: { id, content: value.trim(), role: MessageType.User },
message: msgBody,
}); });
} }
addNewestOneQuestion({
content: value,
id,
role: MessageType.User,
});
}, [value, done, addNewestOneQuestion, setValue, sendMessage]);
addNewestOneQuestion({ ...msgBody, files: fileList });
}, [value, done, addNewestOneQuestion, fileList, setValue, sendMessage]);


useEffect(() => { useEffect(() => {
const { content, id } = findMessageFromList(answerList); const { content, id } = findMessageFromList(answerList);

+ 1
- 3
web/src/pages/next-chats/share/index.tsx 查看文件

addEventList, addEventList,
setCurrentMessageId, setCurrentMessageId,
currentEventListWithoutMessageById, currentEventListWithoutMessageById,
clearEventList,
currentMessageId,
} = useCacheChatLog(); } = useCacheChatLog();
const { const {
handlePressEnter, handlePressEnter,
useCallback( useCallback(
async (files, options) => { async (files, options) => {
const ret = await uploadCanvasFile({ files, options }); const ret = await uploadCanvasFile({ files, options });
appendUploadResponseList(ret.data);
appendUploadResponseList(ret.data, files);
}, },
[appendUploadResponseList, uploadCanvasFile], [appendUploadResponseList, uploadCanvasFile],
); );

正在加载...
取消
保存