diff --git a/web/src/components/edit-tag/index.tsx b/web/src/components/edit-tag/index.tsx index e867185d8..7b0b97372 100644 --- a/web/src/components/edit-tag/index.tsx +++ b/web/src/components/edit-tag/index.tsx @@ -16,7 +16,7 @@ interface EditTagsProps { } const EditTag = React.forwardRef( - ({ value = [], onChange }: EditTagsProps, ref) => { + ({ value = [], onChange }: EditTagsProps) => { const [inputVisible, setInputVisible] = useState(false); const [inputValue, setInputValue] = useState(''); const inputRef = useRef(null); diff --git a/web/src/components/floating-chat-widget-markdown.tsx b/web/src/components/floating-chat-widget-markdown.tsx index d9ac1dc06..58e7bbd04 100644 --- a/web/src/components/floating-chat-widget-markdown.tsx +++ b/web/src/components/floating-chat-widget-markdown.tsx @@ -1,22 +1,32 @@ import Image from '@/components/image'; import SvgIcon from '@/components/svg-icon'; -import { useFetchDocumentThumbnailsByIds, useGetDocumentUrl } from '@/hooks/document-hooks'; +import { + useFetchDocumentThumbnailsByIds, + useGetDocumentUrl, +} from '@/hooks/document-hooks'; import { IReference, IReferenceChunk } from '@/interfaces/database/chat'; -import { preprocessLaTeX, replaceThinkToSection, showImage } from '@/utils/chat'; +import { + preprocessLaTeX, + replaceThinkToSection, + showImage, +} from '@/utils/chat'; import { getExtension } from '@/utils/document-util'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Button, Flex, Popover, Tooltip } from 'antd'; import classNames from 'classnames'; import DOMPurify from 'dompurify'; +import 'katex/dist/katex.min.css'; import { omit } from 'lodash'; import { pipe } from 'lodash/fp'; -import 'katex/dist/katex.min.css'; import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import Markdown from 'react-markdown'; import reactStringReplace from 'react-string-replace'; import SyntaxHighlighter from 'react-syntax-highlighter'; -import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { + oneDark, + oneLight, +} from 'react-syntax-highlighter/dist/esm/styles/prism'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; @@ -39,7 +49,8 @@ const FloatingChatWidgetMarkdown = ({ clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void; }) => { const { t } = useTranslation(); - const { setDocumentIds, data: fileThumbnails } = useFetchDocumentThumbnailsByIds(); + const { setDocumentIds, data: fileThumbnails } = + useFetchDocumentThumbnailsByIds(); const getDocumentUrl = useGetDocumentUrl(); const isDarkTheme = useIsDarkTheme(); @@ -51,23 +62,37 @@ const FloatingChatWidgetMarkdown = ({ useEffect(() => { const docAggs = reference?.doc_aggs; - const docList = Array.isArray(docAggs) ? docAggs : Object.values(docAggs ?? {}); + const docList = Array.isArray(docAggs) + ? docAggs + : Object.values(docAggs ?? {}); setDocumentIds(docList.map((x: any) => x.doc_id).filter(Boolean)); }, [reference, setDocumentIds]); - const handleDocumentButtonClick = useCallback((documentId: string, chunk: IReferenceChunk, isPdf: boolean, documentUrl?: string) => () => { - if (!documentId) return; - if (!isPdf && documentUrl) { - window.open(documentUrl, '_blank'); - } else if (clickDocumentButton) { - clickDocumentButton(documentId, chunk); - } - }, [clickDocumentButton]); + const handleDocumentButtonClick = useCallback( + ( + documentId: string, + chunk: IReferenceChunk, + isPdf: boolean, + documentUrl?: string, + ) => + () => { + if (!documentId) return; + if (!isPdf && documentUrl) { + window.open(documentUrl, '_blank'); + } else if (clickDocumentButton) { + clickDocumentButton(documentId, chunk); + } + }, + [clickDocumentButton], + ); const rehypeWrapReference = () => (tree: any) => { visitParents(tree, 'text', (node, ancestors) => { const latestAncestor = ancestors[ancestors.length - 1]; - if (latestAncestor.tagName !== 'custom-typography' && latestAncestor.tagName !== 'code') { + if ( + latestAncestor.tagName !== 'custom-typography' && + latestAncestor.tagName !== 'code' + ) { node.type = 'element'; node.tagName = 'custom-typography'; node.properties = {}; @@ -76,90 +101,173 @@ const FloatingChatWidgetMarkdown = ({ }); }; - const getReferenceInfo = useCallback((chunkIndex: number) => { - const chunkItem = reference?.chunks?.[chunkIndex]; - if (!chunkItem) return null; - const docAggsArray = Array.isArray(reference?.doc_aggs) ? reference.doc_aggs : Object.values(reference?.doc_aggs ?? {}); - const document = docAggsArray.find((x: any) => x?.doc_id === chunkItem?.document_id) as any; - const documentId = document?.doc_id; - const documentUrl = document?.url ?? (documentId ? getDocumentUrl(documentId) : undefined); - const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; - const fileExtension = documentId ? getExtension(document?.doc_name ?? '') : ''; - return { documentUrl, fileThumbnail, fileExtension, imageId: chunkItem.image_id, chunkItem, documentId, document }; - }, [fileThumbnails, reference, getDocumentUrl]); + const getReferenceInfo = useCallback( + (chunkIndex: number) => { + const chunkItem = reference?.chunks?.[chunkIndex]; + if (!chunkItem) return null; + const docAggsArray = Array.isArray(reference?.doc_aggs) + ? reference.doc_aggs + : Object.values(reference?.doc_aggs ?? {}); + const document = docAggsArray.find( + (x: any) => x?.doc_id === chunkItem?.document_id, + ) as any; + const documentId = document?.doc_id; + const documentUrl = + document?.url ?? (documentId ? getDocumentUrl(documentId) : undefined); + const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; + const fileExtension = documentId + ? getExtension(document?.doc_name ?? '') + : ''; + return { + documentUrl, + fileThumbnail, + fileExtension, + imageId: chunkItem.image_id, + chunkItem, + documentId, + document, + }; + }, + [fileThumbnails, reference, getDocumentUrl], + ); - const getPopoverContent = useCallback((chunkIndex: number) => { - const info = getReferenceInfo(chunkIndex); - - if (!info) { - return
Error: Missing document information.
; - } - - const { documentUrl, fileThumbnail, fileExtension, imageId, chunkItem, documentId, document } = info; - - return ( -
- {imageId && ( - }> - - - )} -
-
- {documentId && ( - - {fileThumbnail ? ( - {document?.doc_name} - ) : ( - - )} - - - - - )} -
-
- ); - }, [getReferenceInfo, handleDocumentButtonClick]); - - const renderReference = useCallback((text: string) => { - return reactStringReplace(text, currentReg, (match, i) => { - const chunkIndex = getChunkIndex(match); + const getPopoverContent = useCallback( + (chunkIndex: number) => { const info = getReferenceInfo(chunkIndex); if (!info) { - return ; + return ( +
+ Error: Missing document information. +
+ ); } - const { imageId, chunkItem, documentId, fileExtension, documentUrl } = info; - - if (showImage(chunkItem?.doc_type)) { - return ; - } + const { + documentUrl, + fileThumbnail, + fileExtension, + imageId, + chunkItem, + documentId, + document, + } = info; return ( - - - + {imageId && ( + + } + > + + + )} +
+
+ {documentId && ( + + {fileThumbnail ? ( + {document?.doc_name} + ) : ( + + )} + + + + + )} +
+ ); - }); - }, [getPopoverContent, getReferenceInfo, handleDocumentButtonClick]); + }, + [getReferenceInfo, handleDocumentButtonClick], + ); + + const renderReference = useCallback( + (text: string) => { + return reactStringReplace(text, currentReg, (match, i) => { + const chunkIndex = getChunkIndex(match); + const info = getReferenceInfo(chunkIndex); + + if (!info) { + return ( + + + + ); + } + + const { imageId, chunkItem, documentId, fileExtension, documentUrl } = + info; + + if (showImage(chunkItem?.doc_type)) { + return ( + + ); + } + + return ( + + + + ); + }); + }, + [getPopoverContent, getReferenceInfo, handleDocumentButtonClick], + ); return (
@@ -167,28 +275,38 @@ const FloatingChatWidgetMarkdown = ({ rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]} remarkPlugins={[remarkGfm, remarkMath]} className="text-sm leading-relaxed space-y-2 prose-sm max-w-full" - 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 ? ( - - {String(children).replace(/\n$/, '')} - - ) : ( - - {children} - - ); - }, - } as any} + components={ + { + 'custom-typography': ({ children }: { children: string }) => + renderReference(children), + code(props: any) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { children, className, node, ...rest } = props; + const match = /language-(\w+)/.exec(className || ''); + return match ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); + }, + } as any + } > {contentWithCursor} @@ -196,4 +314,4 @@ const FloatingChatWidgetMarkdown = ({ ); }; -export default FloatingChatWidgetMarkdown; \ No newline at end of file +export default FloatingChatWidgetMarkdown; diff --git a/web/src/components/floating-chat-widget.tsx b/web/src/components/floating-chat-widget.tsx index 9f7d8cfa5..51aab028a 100644 --- a/web/src/components/floating-chat-widget.tsx +++ b/web/src/components/floating-chat-widget.tsx @@ -1,18 +1,10 @@ import PdfDrawer from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; -import { MessageType, SharedFrom } from '@/constants/chat'; -import { useFetchNextConversationSSE } from '@/hooks/chat-hooks'; -import { useFetchFlowSSE } from '@/hooks/flow-hooks'; +import { MessageType } from '@/constants/chat'; import { useFetchExternalChatInfo } from '@/hooks/use-chat-request'; import i18n from '@/locales/config'; import { MessageCircle, Minimize2, Send, X } from 'lucide-react'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useGetSharedChatSearchParams, useSendSharedMessage, @@ -28,12 +20,7 @@ const FloatingChatWidget = () => { const [isLoaded, setIsLoaded] = useState(false); const messagesEndRef = useRef(null); - const { - sharedId: conversationId, - from, - locale, - visibleAvatar, - } = useGetSharedChatSearchParams(); + const { sharedId: conversationId, locale } = useGetSharedChatSearchParams(); // Check if we're in button-only mode or window-only mode const urlParams = new URLSearchParams(window.location.search); @@ -58,14 +45,6 @@ const FloatingChatWidget = () => { const { data: chatInfo } = useFetchExternalChatInfo(); - const useFetchAvatar = useMemo(() => { - return from === SharedFrom.Agent - ? useFetchFlowSSE - : useFetchNextConversationSSE; - }, [from]); - - const { data: avatarData } = useFetchAvatar(); - const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = useClickDrawer(); @@ -181,6 +160,40 @@ const FloatingChatWidget = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [displayMessages]); + // Render different content based on mode + // Master mode - handles everything and creates second iframe dynamically + useEffect(() => { + if (mode !== 'master') return; + // Create the chat window iframe dynamically when needed + const createChatWindow = () => { + // Check if iframe already exists in parent document + window.parent.postMessage( + { + type: 'CREATE_CHAT_WINDOW', + src: window.location.href.replace('mode=master', 'mode=window'), + }, + '*', + ); + }; + + createChatWindow(); + + // Listen for our own toggle events to show/hide the dynamic iframe + const handleToggle = (e: MessageEvent) => { + if (e.source === window) return; // Ignore our own messages + + const chatWindow = document.getElementById( + 'dynamic-chat-window', + ) as HTMLIFrameElement; + if (chatWindow && e.data.type === 'TOGGLE_CHAT') { + chatWindow.style.display = e.data.isOpen ? 'block' : 'none'; + } + }; + + window.addEventListener('message', handleToggle); + return () => window.removeEventListener('message', handleToggle); + }, [mode]); + // Play sound only when AI response is complete (not streaming chunks) useEffect(() => { if (derivedMessages && derivedMessages.length > 0 && !sendLoading) { @@ -271,41 +284,8 @@ const FloatingChatWidget = () => { const messageCount = displayMessages?.length || 0; - // Render different content based on mode + // Show just the button in master mode if (mode === 'master') { - // Master mode - handles everything and creates second iframe dynamically - useEffect(() => { - // Create the chat window iframe dynamically when needed - const createChatWindow = () => { - // Check if iframe already exists in parent document - window.parent.postMessage( - { - type: 'CREATE_CHAT_WINDOW', - src: window.location.href.replace('mode=master', 'mode=window'), - }, - '*', - ); - }; - - createChatWindow(); - - // Listen for our own toggle events to show/hide the dynamic iframe - const handleToggle = (e: MessageEvent) => { - if (e.source === window) return; // Ignore our own messages - - const chatWindow = document.getElementById( - 'dynamic-chat-window', - ) as HTMLIFrameElement; - if (chatWindow && e.data.type === 'TOGGLE_CHAT') { - chatWindow.style.display = e.data.isOpen ? 'block' : 'none'; - } - }; - - window.addEventListener('message', handleToggle); - return () => window.removeEventListener('message', handleToggle); - }, []); - - // Show just the button in master mode return (
{ />