From bece37e6c8676ca04e8a96239d6efe8b335a4bc2 Mon Sep 17 00:00:00 2001 From: Adrian Gora <47756404+adagora@users.noreply.github.com> Date: Sun, 28 Sep 2025 07:58:10 +0200 Subject: [PATCH] Fix: floating widget match style with original one (#10317) ### What problem does this PR solve? These changes are intended to implement the remaining functionalities of the fullscreen widget. The question arises: how to display document prieview of PDFs in this floating widget? - simply enlarge the widget window - implement zoom in/out - render outside the iframe? ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- .../floating-chat-widget-markdown.less | 58 ++++ .../floating-chat-widget-markdown.tsx | 199 ++++++++++++ web/src/components/floating-chat-widget.tsx | 293 ++++++++++-------- web/src/components/pdf-drawer/index.tsx | 9 +- 4 files changed, 429 insertions(+), 130 deletions(-) create mode 100644 web/src/components/floating-chat-widget-markdown.less create mode 100644 web/src/components/floating-chat-widget-markdown.tsx diff --git a/web/src/components/floating-chat-widget-markdown.less b/web/src/components/floating-chat-widget-markdown.less new file mode 100644 index 000000000..5df14edb6 --- /dev/null +++ b/web/src/components/floating-chat-widget-markdown.less @@ -0,0 +1,58 @@ +/* floating-chat-widget-markdown.less */ + +.widget-citation-popover { + max-width: 90vw; + /* Use viewport width for better responsiveness */ + width: max-content; + + .ant-popover-inner { + max-height: 400px; + overflow-y: auto; + } + + .ant-popover-inner-content { + padding: 12px; + } +} + +/* Responsive breakpoints for popover width */ +@media (min-width: 480px) { + .widget-citation-popover { + max-width: 360px; + } +} + +.widget-citation-content { + + p, + div, + span, + button { + word-break: break-word; + overflow-wrap: break-word; + white-space: normal; + } +} + +.floating-chat-widget { + + /* General styles for markdown content within the widget */ + p, + div, + ul, + ol, + blockquote { + line-height: 1.6; + } + + /* Enhanced image styles */ + img, + .ant-image, + .ant-image-img { + max-width: 100% !important; + height: auto !important; + border-radius: 8px; + margin: 8px 0 !important; + display: inline-block !important; + } +} \ No newline at end of file diff --git a/web/src/components/floating-chat-widget-markdown.tsx b/web/src/components/floating-chat-widget-markdown.tsx new file mode 100644 index 000000000..d9ac1dc06 --- /dev/null +++ b/web/src/components/floating-chat-widget-markdown.tsx @@ -0,0 +1,199 @@ +import Image from '@/components/image'; +import SvgIcon from '@/components/svg-icon'; +import { useFetchDocumentThumbnailsByIds, useGetDocumentUrl } from '@/hooks/document-hooks'; +import { IReference, IReferenceChunk } from '@/interfaces/database/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 { 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 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 { currentReg, replaceTextByOldReg } from '../pages/next-chats/utils'; +import styles from './floating-chat-widget-markdown.less'; +import { useIsDarkTheme } from './theme-provider'; + +const getChunkIndex = (match: string) => Number(match.replace(/\[|\]/g, '')); + +const FloatingChatWidgetMarkdown = ({ + reference, + clickDocumentButton, + content, +}: { + content: string; + loading: boolean; + reference: IReference; + clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void; +}) => { + const { t } = useTranslation(); + const { setDocumentIds, data: fileThumbnails } = useFetchDocumentThumbnailsByIds(); + const getDocumentUrl = useGetDocumentUrl(); + const isDarkTheme = useIsDarkTheme(); + + const contentWithCursor = useMemo(() => { + let text = content === '' ? t('chat.searching') : content; + const nextText = replaceTextByOldReg(text); + return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + }, [content, t]); + + useEffect(() => { + const docAggs = reference?.doc_aggs; + 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 rehypeWrapReference = () => (tree: any) => { + visitParents(tree, 'text', (node, ancestors) => { + const latestAncestor = ancestors[ancestors.length - 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 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
+ {children}
+
+ );
+ },
+ } as any}
+ >
+ {contentWithCursor}
+