Files
leaudit-platform-frontend/app/components/dify-chat/markdown.tsx
T

139 lines
4.6 KiB
TypeScript

import { Sources } from '@ant-design/x';
import XMarkdown, { type ComponentProps } from '@ant-design/x-markdown';
import { Tooltip } from 'antd';
import React, { useMemo } from 'react';
import type { RetrieverResource } from '~/api/dify-chat';
import '../../styles/components/chat-with-llm/markdown.css';
interface MarkdownProps {
content: string;
className?: string;
retrieverResources?: RetrieverResource[];
}
/**
* 引用索引组件 - 在文本中显示可点击的引用标记
*/
const SourceRefComponent = React.memo(({
children,
resources
}: ComponentProps & { resources?: RetrieverResource[] }) => {
const refNumber = parseInt(`${children}` || '0', 10);
// 如果没有资源数据或引用号无效,只显示上标数字
if (!resources || resources.length === 0 || refNumber <= 0) {
return <sup className="source-ref-plain">[{children}]</sup>;
}
// 查找对应的资源
const resource = resources.find(r => r.position === refNumber);
if (!resource) {
return <sup className="source-ref-plain">[{children}]</sup>;
}
// 构建引用项列表 - 显示完整内容
const items = resources.map((r) => ({
title: `${r.position}. ${r.document_name}`,
key: r.position,
description: r.content || '',
}));
return (
<Sources
activeKey={refNumber}
title={`[${children}]`}
items={items}
inline={true}
/>
);
});
SourceRefComponent.displayName = 'SourceRefComponent';
/**
* 引用来源面板组件 - 在消息下方显示所有引用(独立导出)
*/
export const SourcesPanel = React.memo(({ resources }: { resources: RetrieverResource[] }) => {
if (!resources || resources.length === 0) {
return null;
}
return (
<div className="sources-panel">
<div className="sources-panel-header">
<i className="ri-file-text-line"></i>
<span></span>
</div>
<div className="sources-panel-list">
{resources.map((resource) => (
<Tooltip
key={`${resource.segment_id}-${resource.position}`}
title={
<div className="source-tooltip">
<div className="source-tooltip-header">
<strong>{resource.document_name}</strong>
</div>
<div className="source-tooltip-content">
{resource.content}
</div>
<div className="source-tooltip-meta">
<span>: {(resource.score * 100).toFixed(0)}%</span>
<span style={{ marginLeft: 12 }}>: {resource.dataset_name}</span>
</div>
</div>
}
placement="topLeft"
autoAdjustOverflow={false}
color="rgba(0, 104, 74, 0.92)"
classNames={{ root: 'source-tooltip-overlay' }}
>
<div className="source-item">
<span className="source-item-number">{resource.position}</span>
<span className="source-item-name">{resource.document_name}</span>
<span className="source-item-score">
{(resource.score * 100).toFixed(0)}%
</span>
</div>
</Tooltip>
))}
</div>
</div>
);
});
SourcesPanel.displayName = 'SourcesPanel';
/**
* Markdown 渲染组件
* 使用 @ant-design/x-markdown 进行 Markdown 解析,支持流式渲染
*/
export default function Markdown({
content,
className = '',
retrieverResources
}: MarkdownProps) {
// 创建自定义的 sup 组件,传入引用资源
const customComponents = useMemo(() => {
return {
sup: (props: ComponentProps) => (
<SourceRefComponent {...props} resources={retrieverResources} />
),
};
}, [retrieverResources]);
if (!content) {
return null;
}
return (
<div className={`markdown-content ${className}`}>
<XMarkdown
components={customComponents}
paragraphTag="div"
>
{content}
</XMarkdown>
</div>
);
}