mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Display file references for agent dialogues #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
71
web/src/components/next-markdown-content/index.less
Normal file
71
web/src/components/next-markdown-content/index.less
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
.markdownContentWrapper {
|
||||||
|
:global(section.think) {
|
||||||
|
padding-left: 10px;
|
||||||
|
color: #8b8b8b;
|
||||||
|
border-left: 2px solid #d5d3d3;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
:global(blockquote) {
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 4px solid #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.referencePopoverWrapper {
|
||||||
|
max-width: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceChunkImage {
|
||||||
|
width: 10vw;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceInnerChunkImage {
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 6vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceImagePreview {
|
||||||
|
max-width: 45vw;
|
||||||
|
max-height: 45vh;
|
||||||
|
}
|
||||||
|
.chunkContentText {
|
||||||
|
.chunkText;
|
||||||
|
max-height: 45vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.documentLink {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceIcon {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: black;
|
||||||
|
animation: blink 0.6s infinite;
|
||||||
|
vertical-align: text-top;
|
||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileThumbnail {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
284
web/src/components/next-markdown-content/index.tsx
Normal file
284
web/src/components/next-markdown-content/index.tsx
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import Image from '@/components/image';
|
||||||
|
import SvgIcon from '@/components/svg-icon';
|
||||||
|
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
|
||||||
|
import { getExtension } from '@/utils/document-util';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import { visitParents } from 'unist-util-visit-parents';
|
||||||
|
|
||||||
|
import { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||||
|
|
||||||
|
import {
|
||||||
|
preprocessLaTeX,
|
||||||
|
replaceThinkToSection,
|
||||||
|
showImage,
|
||||||
|
} from '@/utils/chat';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { currentReg, replaceTextByOldReg } from '@/pages/chat/utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { pipe } from 'lodash/fp';
|
||||||
|
import { CircleAlert } from 'lucide-react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '../ui/hover-card';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const getChunkIndex = (match: string) => Number(match);
|
||||||
|
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||||
|
function MarkdownContent({
|
||||||
|
reference,
|
||||||
|
clickDocumentButton,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
loading: boolean;
|
||||||
|
reference?: IReferenceObject;
|
||||||
|
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setDocumentIds, data: fileThumbnails } =
|
||||||
|
useFetchDocumentThumbnailsByIds();
|
||||||
|
const contentWithCursor = useMemo(() => {
|
||||||
|
// let text = DOMPurify.sanitize(content);
|
||||||
|
let text = content;
|
||||||
|
if (text === '') {
|
||||||
|
text = t('chat.searching');
|
||||||
|
}
|
||||||
|
const nextText = replaceTextByOldReg(text);
|
||||||
|
return pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
|
||||||
|
}, [content, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const docAggs = reference?.doc_aggs;
|
||||||
|
setDocumentIds(Array.isArray(docAggs) ? docAggs.map((x) => x.doc_id) : []);
|
||||||
|
}, [reference, setDocumentIds]);
|
||||||
|
|
||||||
|
const handleDocumentButtonClick = useCallback(
|
||||||
|
(
|
||||||
|
documentId: string,
|
||||||
|
chunk: IReferenceChunk,
|
||||||
|
isPdf: boolean,
|
||||||
|
documentUrl?: string,
|
||||||
|
) =>
|
||||||
|
() => {
|
||||||
|
if (!isPdf) {
|
||||||
|
if (!documentUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(documentUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
clickDocumentButton?.(documentId, chunk);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clickDocumentButton],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rehypeWrapReference = () => {
|
||||||
|
return function wrapTextTransform(tree: any) {
|
||||||
|
visitParents(tree, 'text', (node, ancestors) => {
|
||||||
|
const latestAncestor = ancestors.at(-1);
|
||||||
|
if (
|
||||||
|
latestAncestor.tagName !== 'custom-typography' &&
|
||||||
|
latestAncestor.tagName !== 'code'
|
||||||
|
) {
|
||||||
|
node.type = 'element';
|
||||||
|
node.tagName = 'custom-typography';
|
||||||
|
node.properties = {};
|
||||||
|
node.children = [{ type: 'text', value: node.value }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReferenceInfo = useCallback(
|
||||||
|
(chunkIndex: number) => {
|
||||||
|
const chunks = reference?.chunks ?? {};
|
||||||
|
const chunkItem = chunks[chunkIndex];
|
||||||
|
|
||||||
|
const documentList = Object.values(reference?.doc_aggs ?? {});
|
||||||
|
const document = documentList.find(
|
||||||
|
(x) => x?.doc_id === chunkItem?.document_id,
|
||||||
|
);
|
||||||
|
const documentId = document?.doc_id;
|
||||||
|
const documentUrl = document?.url;
|
||||||
|
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
|
||||||
|
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
|
||||||
|
const imageId = chunkItem?.image_id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentUrl,
|
||||||
|
fileThumbnail,
|
||||||
|
fileExtension,
|
||||||
|
imageId,
|
||||||
|
chunkItem,
|
||||||
|
documentId,
|
||||||
|
document,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[fileThumbnails, reference],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPopoverContent = useCallback(
|
||||||
|
(chunkIndex: number) => {
|
||||||
|
const {
|
||||||
|
documentUrl,
|
||||||
|
fileThumbnail,
|
||||||
|
fileExtension,
|
||||||
|
imageId,
|
||||||
|
chunkItem,
|
||||||
|
documentId,
|
||||||
|
document,
|
||||||
|
} = getReferenceInfo(chunkIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={chunkItem?.id} className="flex gap-2">
|
||||||
|
{imageId && (
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Image
|
||||||
|
id={imageId}
|
||||||
|
className={styles.referenceChunkImage}
|
||||||
|
></Image>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent>
|
||||||
|
<Image
|
||||||
|
id={imageId}
|
||||||
|
className={cn(styles.referenceImagePreview)}
|
||||||
|
></Image>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
)}
|
||||||
|
<div className={'space-y-2 max-w-[40vw] w-full'}>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(chunkItem?.content ?? ''),
|
||||||
|
}}
|
||||||
|
className={classNames(styles.chunkContentText, 'w-full')}
|
||||||
|
></div>
|
||||||
|
{documentId && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{fileThumbnail ? (
|
||||||
|
<img
|
||||||
|
src={fileThumbnail}
|
||||||
|
alt=""
|
||||||
|
className={styles.fileThumbnail}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SvgIcon
|
||||||
|
name={`file-icon/${fileExtension}`}
|
||||||
|
width={24}
|
||||||
|
></SvgIcon>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={handleDocumentButtonClick(
|
||||||
|
documentId,
|
||||||
|
chunkItem,
|
||||||
|
fileExtension === 'pdf',
|
||||||
|
documentUrl,
|
||||||
|
)}
|
||||||
|
className="text-ellipsis text-wrap"
|
||||||
|
>
|
||||||
|
{document?.doc_name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[getReferenceInfo, handleDocumentButtonClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderReference = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||||
|
const chunkIndex = getChunkIndex(match);
|
||||||
|
|
||||||
|
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
|
||||||
|
getReferenceInfo(chunkIndex);
|
||||||
|
|
||||||
|
const docType = chunkItem?.doc_type;
|
||||||
|
|
||||||
|
return showImage(docType) ? (
|
||||||
|
<Image
|
||||||
|
id={imageId}
|
||||||
|
className={styles.referenceInnerChunkImage}
|
||||||
|
onClick={
|
||||||
|
documentId
|
||||||
|
? handleDocumentButtonClick(
|
||||||
|
documentId,
|
||||||
|
chunkItem,
|
||||||
|
fileExtension === 'pdf',
|
||||||
|
documentUrl,
|
||||||
|
)
|
||||||
|
: () => {}
|
||||||
|
}
|
||||||
|
></Image>
|
||||||
|
) : (
|
||||||
|
<HoverCard key={i}>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<CircleAlert className="size-4 inline-block" />
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent>
|
||||||
|
{renderPopoverContent(chunkIndex)}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return replacedText;
|
||||||
|
},
|
||||||
|
[renderPopoverContent, getReferenceInfo, handleDocumentButtonClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Markdown
|
||||||
|
rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]}
|
||||||
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
className={styles.markdownContentWrapper}
|
||||||
|
components={
|
||||||
|
{
|
||||||
|
'custom-typography': ({ children }: { children: string }) =>
|
||||||
|
renderReference(children),
|
||||||
|
code(props: any) {
|
||||||
|
const { children, className, node, ...rest } = props;
|
||||||
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
return match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
{...rest}
|
||||||
|
PreTag="div"
|
||||||
|
language={match[1]}
|
||||||
|
wrapLongLines
|
||||||
|
>
|
||||||
|
{String(children).replace(/\n$/, '')}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
) : (
|
||||||
|
<code {...rest} className={classNames(className, 'text-wrap')}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{contentWithCursor}
|
||||||
|
</Markdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MarkdownContent);
|
||||||
@ -30,7 +30,6 @@
|
|||||||
.messageTextDark {
|
.messageTextDark {
|
||||||
.chunkText();
|
.chunkText();
|
||||||
.messageTextBase();
|
.messageTextBase();
|
||||||
background-color: #1668dc;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
:global(section.think) {
|
:global(section.think) {
|
||||||
color: rgb(166, 166, 166);
|
color: rgb(166, 166, 166);
|
||||||
@ -41,7 +40,6 @@
|
|||||||
.messageUserText {
|
.messageUserText {
|
||||||
.chunkText();
|
.chunkText();
|
||||||
.messageTextBase();
|
.messageTextBase();
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||||
import { MessageType } from '@/constants/chat';
|
import { MessageType } from '@/constants/chat';
|
||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
@ -17,16 +17,19 @@ import {
|
|||||||
useFetchDocumentThumbnailsByIds,
|
useFetchDocumentThumbnailsByIds,
|
||||||
} from '@/hooks/document-hooks';
|
} from '@/hooks/document-hooks';
|
||||||
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { IMessage } from '@/pages/chat/interface';
|
import { IMessage } from '@/pages/chat/interface';
|
||||||
import MarkdownContent from '@/pages/chat/markdown-content';
|
|
||||||
import { getExtension, isImage } from '@/utils/document-util';
|
import { getExtension, isImage } from '@/utils/document-util';
|
||||||
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
|
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import FileIcon from '../file-icon';
|
import FileIcon from '../file-icon';
|
||||||
import IndentedTreeModal from '../indented-tree/modal';
|
import IndentedTreeModal from '../indented-tree/modal';
|
||||||
import NewDocumentLink from '../new-document-link';
|
import NewDocumentLink from '../new-document-link';
|
||||||
|
import MarkdownContent from '../next-markdown-content';
|
||||||
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';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@ -35,7 +38,7 @@ interface IProps
|
|||||||
IRegenerateMessage,
|
IRegenerateMessage,
|
||||||
PropsWithChildren {
|
PropsWithChildren {
|
||||||
item: IMessage;
|
item: IMessage;
|
||||||
reference: IReference;
|
reference?: IReferenceObject;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
sendLoading?: boolean;
|
sendLoading?: boolean;
|
||||||
visibleAvatar?: boolean;
|
visibleAvatar?: boolean;
|
||||||
@ -48,7 +51,7 @@ interface IProps
|
|||||||
showLoudspeaker?: boolean;
|
showLoudspeaker?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageItem = ({
|
function MessageItem({
|
||||||
item,
|
item,
|
||||||
reference,
|
reference,
|
||||||
loading = false,
|
loading = false,
|
||||||
@ -56,14 +59,13 @@ const MessageItem = ({
|
|||||||
avatarDialog,
|
avatarDialog,
|
||||||
sendLoading = false,
|
sendLoading = false,
|
||||||
clickDocumentButton,
|
clickDocumentButton,
|
||||||
index,
|
|
||||||
removeMessageById,
|
removeMessageById,
|
||||||
regenerateMessage,
|
regenerateMessage,
|
||||||
showLikeButton = true,
|
showLikeButton = true,
|
||||||
showLoudspeaker = true,
|
showLoudspeaker = true,
|
||||||
visibleAvatar = true,
|
visibleAvatar = true,
|
||||||
children,
|
children,
|
||||||
}: IProps) => {
|
}: IProps) {
|
||||||
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;
|
||||||
@ -73,8 +75,10 @@ const MessageItem = ({
|
|||||||
const { visible, hideModal, showModal } = useSetModalState();
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
const [clickedDocumentId, setClickedDocumentId] = useState('');
|
const [clickedDocumentId, setClickedDocumentId] = useState('');
|
||||||
|
|
||||||
const referenceDocumentList = useMemo(() => {
|
const referenceDocuments = useMemo(() => {
|
||||||
return reference?.doc_aggs ?? [];
|
const docs = reference?.doc_aggs ?? {};
|
||||||
|
|
||||||
|
return Object.values(docs);
|
||||||
}, [reference?.doc_aggs]);
|
}, [reference?.doc_aggs]);
|
||||||
|
|
||||||
const handleUserDocumentClick = useCallback(
|
const handleUserDocumentClick = useCallback(
|
||||||
@ -153,16 +157,18 @@ const MessageItem = ({
|
|||||||
{/* <b>{isAssistant ? '' : nickname}</b> */}
|
{/* <b>{isAssistant ? '' : nickname}</b> */}
|
||||||
</Space>
|
</Space>
|
||||||
<div
|
<div
|
||||||
className={
|
className={cn({
|
||||||
isAssistant
|
[theme === 'dark'
|
||||||
? theme === 'dark'
|
? styles.messageTextDark
|
||||||
? styles.messageTextDark
|
: styles.messageText]: isAssistant,
|
||||||
: styles.messageText
|
[styles.messageUserText]: !isAssistant,
|
||||||
: styles.messageUserText
|
'bg-background-card': !isAssistant,
|
||||||
}
|
})}
|
||||||
>
|
>
|
||||||
{item.data ? (
|
{item.data ? (
|
||||||
children
|
children
|
||||||
|
) : sendLoading && isEmpty(item.content) ? (
|
||||||
|
'searching...'
|
||||||
) : (
|
) : (
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -172,32 +178,10 @@ const MessageItem = ({
|
|||||||
></MarkdownContent>
|
></MarkdownContent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isAssistant && referenceDocumentList.length > 0 && (
|
{isAssistant && referenceDocuments.length > 0 && (
|
||||||
<List
|
<ReferenceDocumentList
|
||||||
bordered
|
list={referenceDocuments}
|
||||||
dataSource={referenceDocumentList}
|
></ReferenceDocumentList>
|
||||||
renderItem={(item) => {
|
|
||||||
return (
|
|
||||||
<List.Item>
|
|
||||||
<Flex gap={'small'} align="center">
|
|
||||||
<FileIcon
|
|
||||||
id={item.doc_id}
|
|
||||||
name={item.doc_name}
|
|
||||||
></FileIcon>
|
|
||||||
|
|
||||||
<NewDocumentLink
|
|
||||||
documentId={item.doc_id}
|
|
||||||
documentName={item.doc_name}
|
|
||||||
prefix="document"
|
|
||||||
link={item.url}
|
|
||||||
>
|
|
||||||
{item.doc_name}
|
|
||||||
</NewDocumentLink>
|
|
||||||
</Flex>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{isUser && documentList.length > 0 && (
|
{isUser && documentList.length > 0 && (
|
||||||
<List
|
<List
|
||||||
@ -252,6 +236,6 @@ const MessageItem = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default memo(MessageItem);
|
export default memo(MessageItem);
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Docagg } from '@/interfaces/database/chat';
|
||||||
|
import FileIcon from '../file-icon';
|
||||||
|
import NewDocumentLink from '../new-document-link';
|
||||||
|
|
||||||
|
export function ReferenceDocumentList({ list }: { list: Docagg[] }) {
|
||||||
|
return (
|
||||||
|
<section className="flex gap-3 flex-wrap">
|
||||||
|
{list.map((item) => (
|
||||||
|
<Card key={item.doc_id}>
|
||||||
|
<CardContent className="p-2">
|
||||||
|
<FileIcon id={item.doc_id} name={item.doc_name}></FileIcon>
|
||||||
|
<NewDocumentLink
|
||||||
|
documentId={item.doc_id}
|
||||||
|
documentName={item.doc_name}
|
||||||
|
prefix="document"
|
||||||
|
link={item.url}
|
||||||
|
className="text-text-sub-title-invert"
|
||||||
|
>
|
||||||
|
{item.doc_name}
|
||||||
|
</NewDocumentLink>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { Authorization } from '@/constants/authorization';
|
import { Authorization } from '@/constants/authorization';
|
||||||
|
import { IReferenceObject } from '@/interfaces/database/chat';
|
||||||
import { BeginQuery } from '@/pages/agent/interface';
|
import { BeginQuery } from '@/pages/agent/interface';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { getAuthorization } from '@/utils/authorization-util';
|
import { getAuthorization } from '@/utils/authorization-util';
|
||||||
@ -43,6 +44,10 @@ export interface IMessageData {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMessageEndData {
|
||||||
|
reference: IReferenceObject;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ILogData extends INodeData {
|
export interface ILogData extends INodeData {
|
||||||
logs: {
|
logs: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -58,11 +63,13 @@ export type INodeEvent = IAnswerEvent<INodeData>;
|
|||||||
|
|
||||||
export type IMessageEvent = IAnswerEvent<IMessageData>;
|
export type IMessageEvent = IAnswerEvent<IMessageData>;
|
||||||
|
|
||||||
|
export type IMessageEndEvent = IAnswerEvent<IMessageEndData>;
|
||||||
|
|
||||||
export type IInputEvent = IAnswerEvent<IInputData>;
|
export type IInputEvent = IAnswerEvent<IInputData>;
|
||||||
|
|
||||||
export type ILogEvent = IAnswerEvent<ILogData>;
|
export type ILogEvent = IAnswerEvent<ILogData>;
|
||||||
|
|
||||||
export type IChatEvent = INodeEvent | IMessageEvent;
|
export type IChatEvent = INodeEvent | IMessageEvent | IMessageEndEvent;
|
||||||
|
|
||||||
export type IEventList = Array<IChatEvent>;
|
export type IEventList = Array<IChatEvent>;
|
||||||
|
|
||||||
|
|||||||
@ -96,6 +96,11 @@ export interface IReference {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReferenceObject {
|
||||||
|
chunks: Record<string, IReferenceChunk>;
|
||||||
|
doc_aggs: Record<string, Docagg>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAnswer {
|
export interface IAnswer {
|
||||||
answer: string;
|
answer: string;
|
||||||
reference?: IReference;
|
reference?: IReference;
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import { useParams } from 'umi';
|
|||||||
import DebugContent from '../debug-content';
|
import DebugContent from '../debug-content';
|
||||||
import { BeginQuery } from '../interface';
|
import { BeginQuery } from '../interface';
|
||||||
import { buildBeginQueryWithObject } from '../utils';
|
import { buildBeginQueryWithObject } from '../utils';
|
||||||
import { buildAgentMessageItemReference } from '../utils/chat';
|
|
||||||
|
|
||||||
const AgentChatBox = () => {
|
const AgentChatBox = () => {
|
||||||
const {
|
const {
|
||||||
@ -29,9 +28,9 @@ const AgentChatBox = () => {
|
|||||||
loading,
|
loading,
|
||||||
ref,
|
ref,
|
||||||
derivedMessages,
|
derivedMessages,
|
||||||
reference,
|
|
||||||
stopOutputMessage,
|
stopOutputMessage,
|
||||||
sendFormMessage,
|
sendFormMessage,
|
||||||
|
findReferenceByMessageId,
|
||||||
} = useSendNextMessage();
|
} = useSendNextMessage();
|
||||||
|
|
||||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||||
@ -71,7 +70,7 @@ const AgentChatBox = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-1 flex-col pl-5 h-[90vh]">
|
<section className="flex flex-1 flex-col px-5 h-[90vh]">
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<div>
|
<div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
@ -88,10 +87,7 @@ const AgentChatBox = () => {
|
|||||||
avatar={userInfo.avatar}
|
avatar={userInfo.avatar}
|
||||||
avatarDialog={canvasInfo.avatar}
|
avatarDialog={canvasInfo.avatar}
|
||||||
item={message}
|
item={message}
|
||||||
reference={buildAgentMessageItemReference(
|
reference={findReferenceByMessageId(message.id)}
|
||||||
{ message: derivedMessages, reference },
|
|
||||||
message,
|
|
||||||
)}
|
|
||||||
clickDocumentButton={clickDocumentButton}
|
clickDocumentButton={clickDocumentButton}
|
||||||
index={i}
|
index={i}
|
||||||
showLikeButton={false}
|
showLikeButton={false}
|
||||||
|
|||||||
@ -1,24 +1,18 @@
|
|||||||
import {
|
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from '@/components/ui/sheet';
|
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import AgentChatBox from './box';
|
import AgentChatBox from './box';
|
||||||
|
|
||||||
export function ChatSheet({ hideModal }: IModalProps<any>) {
|
export function ChatSheet({ hideModal }: IModalProps<any>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Sheet open modal={false} onOpenChange={hideModal}>
|
<Sheet open modal={false} onOpenChange={hideModal}>
|
||||||
<SheetTitle className="hidden"></SheetTitle>
|
|
||||||
<SheetContent
|
<SheetContent
|
||||||
className={cn('top-20 p-0')}
|
className={cn('top-20 p-0')}
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<SheetHeader>
|
<div className="pl-5 pt-2">{t('chat.chat')}</div>
|
||||||
<SheetTitle>Are you absolutely sure?</SheetTitle>
|
|
||||||
</SheetHeader>
|
|
||||||
<AgentChatBox></AgentChatBox>
|
<AgentChatBox></AgentChatBox>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { useFetchAgent } from '@/hooks/use-agent-request';
|
|||||||
import {
|
import {
|
||||||
IEventList,
|
IEventList,
|
||||||
IInputEvent,
|
IInputEvent,
|
||||||
|
IMessageEndData,
|
||||||
|
IMessageEndEvent,
|
||||||
IMessageEvent,
|
IMessageEvent,
|
||||||
MessageEventType,
|
MessageEventType,
|
||||||
useSendMessageBySSE,
|
useSendMessageBySSE,
|
||||||
@ -17,7 +19,7 @@ import i18n from '@/locales/config';
|
|||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import trim from 'lodash/trim';
|
import trim from 'lodash/trim';
|
||||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useParams } from 'umi';
|
import { useParams } from 'umi';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { BeginId } from '../constant';
|
import { BeginId } from '../constant';
|
||||||
@ -114,6 +116,9 @@ export const useSendNextMessage = () => {
|
|||||||
const { refetch } = useFetchAgent();
|
const { refetch } = useFetchAgent();
|
||||||
const { addEventList } = useContext(AgentChatLogContext);
|
const { addEventList } = useContext(AgentChatLogContext);
|
||||||
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
||||||
|
const [messageEndEventList, setMessageEndEventList] = useState<
|
||||||
|
IMessageEndEvent[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
|
const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
|
||||||
api.runCanvas,
|
api.runCanvas,
|
||||||
@ -126,13 +131,16 @@ export const useSendNextMessage = () => {
|
|||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
};
|
};
|
||||||
|
|
||||||
params.running_hint_text = i18n.t('flow.runningHintText', {
|
params.running_hint_text = i18n.t('flow.runningHintText', {
|
||||||
defaultValue: 'is running...🕞',
|
defaultValue: 'is running...🕞',
|
||||||
});
|
});
|
||||||
if (message.content) {
|
if (message.content) {
|
||||||
|
const query = getBeginNodeDataQuery();
|
||||||
|
|
||||||
params.query = message.content;
|
params.query = message.content;
|
||||||
// params.message_id = message.id;
|
// params.message_id = message.id;
|
||||||
params.inputs = {}; // begin operator inputs
|
params.inputs = transferInputsArrayToObject(query); // begin operator inputs
|
||||||
}
|
}
|
||||||
const res = await send(params);
|
const res = await send(params);
|
||||||
|
|
||||||
@ -146,7 +154,14 @@ export const useSendNextMessage = () => {
|
|||||||
refetch(); // pull the message list after sending the message successfully
|
refetch(); // pull the message list after sending the message successfully
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[agentId, send, setValue, removeLatestMessage, refetch],
|
[
|
||||||
|
agentId,
|
||||||
|
send,
|
||||||
|
getBeginNodeDataQuery,
|
||||||
|
setValue,
|
||||||
|
removeLatestMessage,
|
||||||
|
refetch,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSendMessage = useCallback(
|
const handleSendMessage = useCallback(
|
||||||
@ -156,6 +171,23 @@ export const useSendNextMessage = () => {
|
|||||||
[sendMessage],
|
[sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const messageEndEvent = answerList.find(
|
||||||
|
(x) => x.event === MessageEventType.MessageEnd,
|
||||||
|
);
|
||||||
|
if (messageEndEvent) {
|
||||||
|
setMessageEndEventList((list) => {
|
||||||
|
const nextList = [...list];
|
||||||
|
if (
|
||||||
|
nextList.every((x) => x.message_id !== messageEndEvent.message_id)
|
||||||
|
) {
|
||||||
|
nextList.push(messageEndEvent as IMessageEndEvent);
|
||||||
|
}
|
||||||
|
return nextList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [addEventList.length, answerList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { content, id } = findMessageFromList(answerList);
|
const { content, id } = findMessageFromList(answerList);
|
||||||
const inputAnswer = findInputFromList(answerList);
|
const inputAnswer = findInputFromList(answerList);
|
||||||
@ -195,11 +227,20 @@ export const useSendNextMessage = () => {
|
|||||||
[addNewestOneQuestion, send],
|
[addNewestOneQuestion, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const findReferenceByMessageId = useCallback(
|
||||||
|
(messageId: string) => {
|
||||||
|
const event = messageEndEventList.find(
|
||||||
|
(item) => item.message_id === messageId,
|
||||||
|
);
|
||||||
|
if (event) {
|
||||||
|
return (event?.data as IMessageEndData)?.reference;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[messageEndEventList],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = getBeginNodeDataQuery();
|
if (prologue) {
|
||||||
if (query.length > 0) {
|
|
||||||
send({ id: agentId, inputs: transferInputsArrayToObject(query) });
|
|
||||||
} else if (prologue) {
|
|
||||||
addNewestOneAnswer({
|
addNewestOneAnswer({
|
||||||
answer: prologue,
|
answer: prologue,
|
||||||
});
|
});
|
||||||
@ -230,5 +271,6 @@ export const useSendNextMessage = () => {
|
|||||||
stopOutputMessage,
|
stopOutputMessage,
|
||||||
send,
|
send,
|
||||||
sendFormMessage,
|
sendFormMessage,
|
||||||
|
findReferenceByMessageId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user