### 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)tags/v0.20.0
| .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; | |||||
| } |
| 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); |
| .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); | ||||
| .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; | ||||
| } | } |
| 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, | ||||
| 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; | ||||
| IRegenerateMessage, | IRegenerateMessage, | ||||
| PropsWithChildren { | PropsWithChildren { | ||||
| item: IMessage; | item: IMessage; | ||||
| reference: IReference; | |||||
| reference?: IReferenceObject; | |||||
| loading?: boolean; | loading?: boolean; | ||||
| sendLoading?: boolean; | sendLoading?: boolean; | ||||
| visibleAvatar?: boolean; | visibleAvatar?: boolean; | ||||
| showLoudspeaker?: boolean; | showLoudspeaker?: boolean; | ||||
| } | } | ||||
| const MessageItem = ({ | |||||
| function MessageItem({ | |||||
| item, | item, | ||||
| reference, | reference, | ||||
| loading = false, | loading = false, | ||||
| 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; | ||||
| const { visible, hideModal, showModal } = useSetModalState(); | const { visible, hideModal, showModal } = useSetModalState(); | ||||
| const [clickedDocumentId, setClickedDocumentId] = useState(''); | const [clickedDocumentId, setClickedDocumentId] = useState(''); | ||||
| const referenceDocumentList = useMemo(() => { | |||||
| return reference?.doc_aggs ?? []; | |||||
| const referenceDocuments = useMemo(() => { | |||||
| const docs = reference?.doc_aggs ?? {}; | |||||
| return Object.values(docs); | |||||
| }, [reference?.doc_aggs]); | }, [reference?.doc_aggs]); | ||||
| const handleUserDocumentClick = useCallback( | const handleUserDocumentClick = useCallback( | ||||
| {/* <b>{isAssistant ? '' : nickname}</b> */} | {/* <b>{isAssistant ? '' : nickname}</b> */} | ||||
| </Space> | </Space> | ||||
| <div | <div | ||||
| className={ | |||||
| isAssistant | |||||
| ? theme === 'dark' | |||||
| ? styles.messageTextDark | |||||
| : styles.messageText | |||||
| : styles.messageUserText | |||||
| } | |||||
| className={cn({ | |||||
| [theme === 'dark' | |||||
| ? styles.messageTextDark | |||||
| : styles.messageText]: isAssistant, | |||||
| [styles.messageUserText]: !isAssistant, | |||||
| 'bg-background-card': !isAssistant, | |||||
| })} | |||||
| > | > | ||||
| {item.data ? ( | {item.data ? ( | ||||
| children | children | ||||
| ) : sendLoading && isEmpty(item.content) ? ( | |||||
| 'searching...' | |||||
| ) : ( | ) : ( | ||||
| <MarkdownContent | <MarkdownContent | ||||
| loading={loading} | loading={loading} | ||||
| ></MarkdownContent> | ></MarkdownContent> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| {isAssistant && referenceDocumentList.length > 0 && ( | |||||
| <List | |||||
| bordered | |||||
| dataSource={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> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| {isAssistant && referenceDocuments.length > 0 && ( | |||||
| <ReferenceDocumentList | |||||
| list={referenceDocuments} | |||||
| ></ReferenceDocumentList> | |||||
| )} | )} | ||||
| {isUser && documentList.length > 0 && ( | {isUser && documentList.length > 0 && ( | ||||
| <List | <List | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| }; | |||||
| } | |||||
| export default memo(MessageItem); | export default memo(MessageItem); |
| 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> | |||||
| ); | |||||
| } |
| 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'; | ||||
| 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; | ||||
| 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>; | ||||
| 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; |
| 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 { | ||||
| 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 } = | ||||
| 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}> | ||||
| avatar={userInfo.avatar} | avatar={userInfo.avatar} | ||||
| avatarDialog={canvasInfo.avatar} | avatarDialog={canvasInfo.avatar} | ||||
| item={message} | item={message} | ||||
| reference={buildAgentMessageItemReference( | |||||
| { message: derivedMessages, reference }, | |||||
| message, | |||||
| )} | |||||
| reference={findReferenceByMessageId(message.id)} | |||||
| clickDocumentButton={clickDocumentButton} | clickDocumentButton={clickDocumentButton} | ||||
| index={i} | index={i} | ||||
| showLikeButton={false} | showLikeButton={false} |
| import { | |||||
| Sheet, | |||||
| SheetContent, | |||||
| SheetHeader, | |||||
| SheetTitle, | |||||
| } from '@/components/ui/sheet'; | |||||
| import { Sheet, SheetContent } 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> | |||||
| <SheetTitle>Are you absolutely sure?</SheetTitle> | |||||
| </SheetHeader> | |||||
| <div className="pl-5 pt-2">{t('chat.chat')}</div> | |||||
| <AgentChatBox></AgentChatBox> | <AgentChatBox></AgentChatBox> | ||||
| </SheetContent> | </SheetContent> | ||||
| </Sheet> | </Sheet> |
| import { | import { | ||||
| IEventList, | IEventList, | ||||
| IInputEvent, | IInputEvent, | ||||
| IMessageEndData, | |||||
| IMessageEndEvent, | |||||
| IMessageEvent, | IMessageEvent, | ||||
| MessageEventType, | MessageEventType, | ||||
| useSendMessageBySSE, | useSendMessageBySSE, | ||||
| 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'; | ||||
| 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, | ||||
| 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); | ||||
| 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( | ||||
| [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); | ||||
| [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 (query.length > 0) { | |||||
| send({ id: agentId, inputs: transferInputsArrayToObject(query) }); | |||||
| } else if (prologue) { | |||||
| if (prologue) { | |||||
| addNewestOneAnswer({ | addNewestOneAnswer({ | ||||
| answer: prologue, | answer: prologue, | ||||
| }); | }); | ||||
| stopOutputMessage, | stopOutputMessage, | ||||
| send, | send, | ||||
| sendFormMessage, | sendFormMessage, | ||||
| findReferenceByMessageId, | |||||
| }; | }; | ||||
| }; | }; |