mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-03 08:47:48 +08:00
Fix: code supports matplotlib (#13724)
### What problem does this PR solve? Code as "final" node:  Code as "mid" node:  ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -211,7 +211,7 @@ const MarkdownContent = ({
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
|
||||
return (
|
||||
@ -242,9 +242,7 @@ const MarkdownContent = ({
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
components={
|
||||
{
|
||||
p: ({ children, node, ...props }: any) => (
|
||||
<p {...props}>{children}</p>
|
||||
),
|
||||
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
||||
'custom-typography': ({ children }: { children: string }) =>
|
||||
renderReference(children),
|
||||
code(props: any) {
|
||||
|
||||
@ -79,3 +79,27 @@
|
||||
display: inline-block;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.artifactImageWrapper {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.artifactImage {
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.artifactDownload {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #1677ff;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ 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 { downloadFileFromBlob } from '@/utils/file-util';
|
||||
import request from '@/utils/request';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
@ -38,9 +40,120 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '../ui/hover-card';
|
||||
import message from '../ui/message';
|
||||
import styles from './index.module.less';
|
||||
|
||||
const getChunkIndex = (match: string) => parseCitationIndex(match);
|
||||
|
||||
const isArtifactUrl = (url?: string) =>
|
||||
Boolean(url && url.includes('/document/artifact/'));
|
||||
|
||||
const fetchArtifactBlob = async (url: string): Promise<Blob> => {
|
||||
const response = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
return response.data as Blob;
|
||||
};
|
||||
|
||||
const getArtifactName = (url?: string, fallback?: string) =>
|
||||
fallback || url?.split('/').pop()?.split('?')[0] || 'artifact';
|
||||
|
||||
function ArtifactLink({
|
||||
href,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const handleClick = useCallback(
|
||||
async (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const blob = await fetchArtifactBlob(href);
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
window.open(objectUrl, '_blank', 'noopener,noreferrer');
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60 * 1000);
|
||||
} catch {
|
||||
message.error('Failed to open artifact');
|
||||
}
|
||||
},
|
||||
[href],
|
||||
);
|
||||
|
||||
return (
|
||||
<a href={href} className={className} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactImage({
|
||||
src,
|
||||
alt,
|
||||
downloadLabel,
|
||||
}: {
|
||||
src: string;
|
||||
alt?: string;
|
||||
downloadLabel: string;
|
||||
}) {
|
||||
const [imageSrc, setImageSrc] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let objectUrl = '';
|
||||
let active = true;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const blob = await fetchArtifactBlob(src);
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
if (active) {
|
||||
setImageSrc(objectUrl);
|
||||
}
|
||||
} catch {
|
||||
message.error('Failed to load artifact image');
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [alt, src]);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
try {
|
||||
const blob = await fetchArtifactBlob(src);
|
||||
downloadFileFromBlob(blob, getArtifactName(src, alt));
|
||||
} catch {
|
||||
message.error('Failed to download artifact');
|
||||
}
|
||||
}, [alt, src]);
|
||||
|
||||
return (
|
||||
<span className={styles.artifactImageWrapper}>
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt={alt || ''} className={styles.artifactImage} />
|
||||
) : (
|
||||
<span className={styles.artifactImage} />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.artifactDownload}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{downloadLabel}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||
function MarkdownContent({
|
||||
reference,
|
||||
@ -213,7 +326,7 @@ function MarkdownContent({
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
|
||||
return (
|
||||
@ -244,11 +357,44 @@ function MarkdownContent({
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
components={
|
||||
{
|
||||
p: ({ children, node, ...props }: any) => (
|
||||
<p {...props}>{children}</p>
|
||||
),
|
||||
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
||||
'custom-typography': ({ children }: { children: string }) =>
|
||||
renderReference(children),
|
||||
a({ href, children, ...props }: any) {
|
||||
if (isArtifactUrl(href)) {
|
||||
return (
|
||||
<ArtifactLink href={href} className={styles.artifactDownload}>
|
||||
{children}
|
||||
</ArtifactLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={href} {...omit(props, 'node')}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
img({ src, alt, ...props }: any) {
|
||||
if (isArtifactUrl(src)) {
|
||||
return (
|
||||
<ArtifactImage
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
downloadLabel={t('common.download')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={styles.artifactImageWrapper}>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className={styles.artifactImage}
|
||||
{...omit(props, 'node')}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
code(props: any) {
|
||||
const { children, className, ...rest } = props;
|
||||
const restProps = omit(rest, 'node');
|
||||
|
||||
@ -42,6 +42,12 @@ const options = [
|
||||
].map((x) => ({ value: x, label: x }));
|
||||
|
||||
const DynamicFieldName = 'outputs';
|
||||
const CodeSystemOutputs = {
|
||||
content: {
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
function CodeForm({ node }: INextOperatorForm) {
|
||||
const formData = node?.data.form as ICodeForm;
|
||||
@ -159,7 +165,12 @@ function CodeForm({ node }: INextOperatorForm) {
|
||||
)}
|
||||
</FormWrapper>
|
||||
<div className="p-5">
|
||||
<Output list={buildOutputList(formData.outputs)}></Output>
|
||||
<Output
|
||||
list={buildOutputList({
|
||||
...(formData?.outputs ?? {}),
|
||||
...CodeSystemOutputs,
|
||||
})}
|
||||
></Output>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -61,13 +61,28 @@ export function buildSecondaryOutputOptions(
|
||||
}));
|
||||
}
|
||||
|
||||
function getNodeOutputs(x: BaseNode) {
|
||||
const outputs = x.data.form?.outputs ?? {};
|
||||
if (x.data.label !== Operator.Code) {
|
||||
return outputs;
|
||||
}
|
||||
|
||||
return {
|
||||
...outputs,
|
||||
content: outputs.content ?? {
|
||||
type: JsonSchemaDataType.String,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOutputOptions(x: BaseNode) {
|
||||
return {
|
||||
label: x.data.name,
|
||||
value: x.id,
|
||||
title: x.data.name,
|
||||
options: buildSecondaryOutputOptions(
|
||||
x.data.form.outputs,
|
||||
getNodeOutputs(x),
|
||||
x.id,
|
||||
x.data.name,
|
||||
<OperatorIcon name={x.data.label as Operator} />,
|
||||
@ -83,7 +98,7 @@ export function buildNodeOutputOptions({
|
||||
nodeIds: string[];
|
||||
}) {
|
||||
const nodeWithOutputList = nodes.filter(
|
||||
(x) => nodeIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs),
|
||||
(x) => nodeIds.some((y) => y === x.id) && !isEmpty(getNodeOutputs(x)),
|
||||
);
|
||||
|
||||
return nodeWithOutputList.map((x) => buildOutputOptions(x));
|
||||
@ -114,7 +129,7 @@ export function buildChildOutputOptions({
|
||||
nodeId?: string;
|
||||
}) {
|
||||
const nodeWithOutputList = nodes.filter(
|
||||
(x) => x.parentId === nodeId && !isEmpty(x.data?.form?.outputs),
|
||||
(x) => x.parentId === nodeId && !isEmpty(getNodeOutputs(x)),
|
||||
);
|
||||
|
||||
return nodeWithOutputList.map((x) => buildOutputOptions(x));
|
||||
|
||||
Reference in New Issue
Block a user