139 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
}
|