diff --git a/web/src/components/next-markdown-content/index.tsx b/web/src/components/next-markdown-content/index.tsx
index 71d4611b4..cf13b9d02 100644
--- a/web/src/components/next-markdown-content/index.tsx
+++ b/web/src/components/next-markdown-content/index.tsx
@@ -5,7 +5,6 @@ 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';
@@ -18,7 +17,6 @@ import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import {
- currentReg,
preprocessLaTeX,
replaceTextByOldReg,
replaceThinkToSection,
@@ -31,6 +29,11 @@ import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
import { CircleAlert } from 'lucide-react';
+import { ImageCarousel } from '../markdown-content/image-carousel';
+import {
+ groupConsecutiveReferences,
+ shouldShowCarousel,
+} from '../markdown-content/reference-utils';
import { Button } from '../ui/button';
import {
HoverCard,
@@ -39,6 +42,19 @@ import {
} from '../ui/hover-card';
import styles from './index.less';
+// Helper function to convert IReferenceObject to IReference
+const convertReferenceObjectToReference = (
+ referenceObject: IReferenceObject,
+) => {
+ const chunks = Object.values(referenceObject.chunks);
+ const docAggs = Object.values(referenceObject.doc_aggs);
+ return {
+ chunks,
+ doc_aggs: docAggs,
+ total: chunks.length,
+ };
+};
+
const getChunkIndex = (match: string) => Number(match);
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
function MarkdownContent({
@@ -211,47 +227,95 @@ function MarkdownContent({
const renderReference = useCallback(
(text: string) => {
- let replacedText = reactStringReplace(text, currentReg, (match, i) => {
- const chunkIndex = getChunkIndex(match);
+ const groups = groupConsecutiveReferences(text);
+ const elements = [];
+ let lastIndex = 0;
- const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
- getReferenceInfo(chunkIndex);
+ const convertedReference = reference
+ ? convertReferenceObjectToReference(reference)
+ : null;
- const docType = chunkItem?.doc_type;
+ groups.forEach((group, groupIndex) => {
+ if (group[0].start > lastIndex) {
+ elements.push(text.substring(lastIndex, group[0].start));
+ }
- return showImage(docType) ? (
-
- {}
- }
- >
- {imageId}
-
- ) : (
-
-
-
-
-
- {renderPopoverContent(chunkIndex)}
-
-
- );
+ if (
+ convertedReference &&
+ shouldShowCarousel(group, convertedReference)
+ ) {
+ elements.push(
+ ,
+ );
+ } else {
+ group.forEach((ref) => {
+ const chunkIndex = getChunkIndex(ref.id);
+ const {
+ documentUrl,
+ fileExtension,
+ imageId,
+ chunkItem,
+ documentId,
+ } = getReferenceInfo(chunkIndex);
+ const docType = chunkItem?.doc_type;
+
+ if (showImage(docType)) {
+ elements.push(
+
+ {}
+ }
+ />
+ {imageId}
+ ,
+ );
+ } else {
+ elements.push(
+
+
+
+
+
+ {renderPopoverContent(chunkIndex)}
+
+ ,
+ );
+ }
+ });
+ }
+
+ lastIndex = group[group.length - 1].end;
});
- return replacedText;
+ if (lastIndex < text.length) {
+ elements.push(text.substring(lastIndex));
+ }
+
+ return elements;
},
- [renderPopoverContent, getReferenceInfo, handleDocumentButtonClick],
+ [
+ renderPopoverContent,
+ getReferenceInfo,
+ handleDocumentButtonClick,
+ reference,
+ fileThumbnails,
+ ],
);
return (
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index 97a94a137..5116f359a 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -2084,12 +2084,14 @@ Important structured information may include: names, dates, locations, events, k
schema: 'Schema',
response: 'Response',
executionMode: 'Execution mode',
+ executionModeTip:
+ 'Accepted Response: The system returns an acknowledgment immediately after the request is validated, while the workflow continues to execute asynchronously in the background. /Final Response: The system returns a response only after the workflow execution is completed.',
authMethods: 'Authentication methods',
authType: 'Authentication type',
limit: 'Request limit',
per: 'Time period',
maxBodySize: 'Maximum body size',
- ipWhitelist: 'Ip whitelist',
+ ipWhitelist: 'IP whitelist',
tokenHeader: 'Token header',
tokenValue: 'Token value',
username: 'Username',
@@ -2109,6 +2111,8 @@ Important structured information may include: names, dates, locations, events, k
queryParameters: 'Query parameters',
headerParameters: 'Header parameters',
requestBodyParameters: 'Request body parameters',
+ streaming: 'Accepted response',
+ immediately: 'Final response',
},
},
llmTools: {
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 399425391..16bdffb94 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -1835,6 +1835,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
schema: '模式',
response: '响应',
executionMode: '执行模式',
+ executionModeTip:
+ 'Accepted Response:请求校验通过后立即返回接收成功响应,工作流在后台异步执行;Final Response:系统在工作流执行完成后返回最终处理结果',
authMethods: '认证方法',
authType: '认证类型',
limit: '请求限制',
diff --git a/web/src/pages/agent/form/begin-form/webhook/response.tsx b/web/src/pages/agent/form/begin-form/webhook/response.tsx
index 69fb8f09b..a1a26b4f9 100644
--- a/web/src/pages/agent/form/begin-form/webhook/response.tsx
+++ b/web/src/pages/agent/form/begin-form/webhook/response.tsx
@@ -24,9 +24,10 @@ export function WebhookResponse() {
{executionMode === WebhookExecutionMode.Immediately && (
diff --git a/web/src/pages/next-search/markdown-content/index.tsx b/web/src/pages/next-search/markdown-content/index.tsx
index 00594e98f..8934e277a 100644
--- a/web/src/pages/next-search/markdown-content/index.tsx
+++ b/web/src/pages/next-search/markdown-content/index.tsx
@@ -4,9 +4,8 @@ import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import { getExtension } from '@/utils/document-util';
import { InfoCircleOutlined } from '@ant-design/icons';
import DOMPurify from 'dompurify';
-import { memo, useCallback, useEffect, useMemo } from 'react';
+import React, { 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';
@@ -18,8 +17,13 @@ import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
+import ImageCarousel from '@/components/markdown-content/image-carousel';
+import {
+ groupConsecutiveReferences,
+ shouldShowCarousel,
+ type ReferenceGroup,
+} from '@/components/markdown-content/reference-utils';
import {
- currentReg,
preprocessLaTeX,
replaceTextByOldReg,
replaceThinkToSection,
@@ -37,8 +41,6 @@ import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
-const getChunkIndex = (match: string) => Number(match);
-
// Defining Tailwind CSS class name constants
const styles = {
referenceChunkImage: 'w-[10vw] object-contain',
@@ -86,18 +88,11 @@ const MarkdownContent = ({
(
documentId: string,
chunk: IReferenceChunk,
- // isPdf: boolean,
- // documentUrl?: string,
+ isPdf: boolean = false,
+ documentUrl?: string,
) =>
() => {
- // if (!isPdf) {
- // if (!documentUrl) {
- // return;
- // }
- // window.open(documentUrl, '_blank');
- // } else {
clickDocumentButton?.(documentId, chunk);
- // }
},
[clickDocumentButton],
);
@@ -218,43 +213,83 @@ const MarkdownContent = ({
const renderReference = useCallback(
(text: string) => {
- let replacedText = reactStringReplace(text, currentReg, (match, i) => {
- const chunkIndex = getChunkIndex(match);
+ const groups = groupConsecutiveReferences(text);
+ const elements: React.ReactNode[] = [];
+ let lastIndex = 0;
- const { imageId, chunkItem, documentId } = getReferenceInfo(chunkIndex);
+ groups.forEach((group: ReferenceGroup) => {
+ // Add text before the group
+ if (group[0].start > lastIndex) {
+ elements.push(text.slice(lastIndex, group[0].start));
+ }
- const docType = chunkItem?.doc_type;
+ // Determine if this group should be a carousel
+ if (shouldShowCarousel(group, reference)) {
+ // Render carousel for consecutive image group
+ elements.push(
+ ,
+ );
+ } else {
+ // Render individual references in the group
+ group.forEach((ref) => {
+ const chunkIndex = Number(ref.id);
+ const { imageId, chunkItem, documentId } =
+ getReferenceInfo(chunkIndex);
+ const docType = chunkItem?.doc_type;
- return showImage(docType) ? (
- {}
+ if (showImage(docType)) {
+ elements.push(
+ {}
+ }
+ />,
+ );
+ } else {
+ elements.push(
+
+
+
+
+
+ {getPopoverContent(chunkIndex)}
+
+ ,
+ );
}
- >
- ) : (
-
-
-
-
-
- {getPopoverContent(chunkIndex)}
-
-
- );
+ // Add the original reference text
+ elements.push(ref.fullMatch);
+ });
+ }
+
+ lastIndex = group[group.length - 1].end;
});
- return replacedText;
+ // Add any remaining text after the last group
+ if (lastIndex < text.length) {
+ elements.push(text.slice(lastIndex));
+ }
+
+ return elements;
},
- [getPopoverContent, getReferenceInfo, handleDocumentButtonClick],
+ [
+ reference,
+ fileThumbnails,
+ handleDocumentButtonClick,
+ getReferenceInfo,
+ getPopoverContent,
+ ],
);
return (
@@ -265,7 +300,11 @@ const MarkdownContent = ({
components={
{
'custom-typography': ({ children }: { children: string }) =>
- renderReference(children),
+ Array.isArray(renderReference(children))
+ ? renderReference(children).map((element, index) => (
+ {element}
+ ))
+ : renderReference(children),
code(props: any) {
const { children, className, ...rest } = props;
const restProps = omit(rest, 'node');