refertor:使用antdX重构dify聊天渲染组件,到处引用文件

This commit is contained in:
PingChuan
2025-12-05 23:36:12 +08:00
parent 5f9ce2fe9f
commit 7a2a481f44
11 changed files with 469 additions and 91 deletions
+1
View File
@@ -30,6 +30,7 @@ export type {
MessageMore,
Feedbacktype,
ThoughtItem,
RetrieverResource,
// 文件类型
VisionFile,
+39
View File
@@ -28,6 +28,29 @@ export interface ConversationItem {
introduction?: string;
}
/**
* 检索资源类型 - 来自 RAG 的引用内容
*/
export interface RetrieverResource {
position: number;
dataset_id: string;
dataset_name: string;
document_id: string;
document_name: string;
data_source_type: string;
segment_id: string;
retriever_from: string;
score: number;
hit_count: number | null;
word_count: number | null;
segment_position: number | null;
index_node_hash: string | null;
content: string;
page: number | null;
doc_metadata: Record<string, any> | null;
title: string | null;
}
/**
* 聊天消息类型
*/
@@ -45,6 +68,7 @@ export interface ChatItem {
useCurrentUserAvatar?: boolean;
isOpeningStatement?: boolean;
suggestedQuestions?: string[];
retriever_resources?: RetrieverResource[];
}
/**
@@ -336,6 +360,21 @@ export interface MessageEnd {
task_id: string;
conversation_id: string;
message_id: string;
id?: string;
created_at?: number;
metadata?: {
annotation_reply?: any;
retriever_resources?: RetrieverResource[];
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
total_price: string;
currency: string;
latency: number;
};
};
files?: any[];
}
/**
+13 -4
View File
@@ -3,7 +3,7 @@ import { useState } from 'react';
import type { ChatItem, Feedbacktype } from '~/api/dify-chat';
import '../../styles/components/chat-with-llm/chat-message.css';
import { parseMessageContent } from '../../utils/message-parser';
import Markdown from './markdown';
import Markdown, { SourcesPanel } from './markdown';
import ThinkingBlock from './thinking-block';
import ThoughtProcess from './thought-process';
@@ -27,7 +27,7 @@ export default function ChatMessage({
message.feedback?.rating || null
);
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message;
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more, retriever_resources } = message;
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
/**
@@ -70,7 +70,10 @@ export default function ChatMessage({
<div key={index}>
{thought.thought && (
<div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}>
<Markdown content={thought.thought} />
<Markdown
content={thought.thought}
retrieverResources={index === agent_thoughts.length - 1 ? retriever_resources : undefined}
/>
</div>
)}
{thought.tool && (
@@ -98,7 +101,7 @@ export default function ChatMessage({
{/* 实际回复内容 */}
{parsed.response && (
<div className={isResponding ? 'streaming-text' : ''}>
<Markdown content={parsed.response} />
<Markdown content={parsed.response} retrieverResources={retriever_resources} />
</div>
)}
</div>
@@ -184,6 +187,12 @@ export default function ChatMessage({
</Card>
</div>
</div>
{/* 引用来源面板 - 放在气泡外面 */}
{isAnswer && retriever_resources && retriever_resources.length > 0 && (
<div className="sources-panel-wrapper">
<SourcesPanel resources={retriever_resources} />
</div>
)}
</div>
);
}
+2 -1
View File
@@ -227,7 +227,7 @@ export default function Chat() {
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
});
// 添加AI回答
// 添加AI回答(包含检索资源)
newChatList.push({
id: item.id,
content: item.answer,
@@ -235,6 +235,7 @@ export default function Chat() {
feedback: item.feedback,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
retriever_resources: item.retriever_resources || [],
});
});
+118 -69
View File
@@ -1,89 +1,138 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import 'katex/dist/katex.min.css';
import RemarkMath from 'remark-math';
import RemarkBreaks from 'remark-breaks';
import RehypeKatex from 'rehype-katex';
import RemarkGfm from 'remark-gfm';
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[];
}
/**
* Markdown 渲染组件
* 使用 react-markdown 库进行标准 Markdown 解析,支持流式渲染
* 引用索引组件 - 在文本中显示可点击的引用标记
*/
export default function Markdown({ content, className = '' }: MarkdownProps) {
// console.log('🎨 [Markdown] 渲染组件:', {
// contentLength: content?.length || 0,
// contentPreview: content?.substring(0, 100) + (content && content.length > 100 ? '...' : ''),
// className,
// hasContent: !!content
// });
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) {
console.log('⚠️ [Markdown] 内容为空,返回null');
return null;
}
return (
<div className={`markdown-content ${className}`}>
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={{
code({ className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
const isCodeBlock = match;
if (isCodeBlock) {
// 代码块
return (
<pre style={{
backgroundColor: '#f6f8fa',
padding: '16px',
borderRadius: '6px',
overflow: 'auto',
fontSize: '14px',
lineHeight: '1.45',
margin: '1em 0'
}}>
<code style={{
backgroundColor: 'transparent',
padding: '0',
fontSize: 'inherit',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}>
{String(children).replace(/\n$/, '')}
</code>
</pre>
);
} else {
// 内联代码
return (
<code
className={className}
style={{
backgroundColor: 'rgba(175, 184, 193, 0.2)',
padding: '0.2em 0.4em',
borderRadius: '3px',
fontSize: '85%',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}
{...props}
>
{children}
</code>
);
}
},
}}
<XMarkdown
components={customComponents}
paragraphTag="div"
>
{content}
</ReactMarkdown>
</XMarkdown>
</div>
);
}
}
+10 -10
View File
@@ -44,9 +44,9 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
// 主要
// 梅州
'51703': {
baseUrl: 'http://172.16.0.78:8073',
documentUrl: 'http://172.16.0.78:8073/docauditai/',
uploadUrl: 'http://172.16.0.78:8073/admin/documents',
baseUrl: 'http://172.16.0.56:8073',
documentUrl: 'http://172.16.0.56:8073/docauditai/',
uploadUrl: 'http://172.16.0.56:8073/admin/documents',
collaboraUrl: 'http://172.16.0.81:9980',
appUrl: 'http://172.16.0.34:51703',
@@ -121,15 +121,15 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
const configs: Record<string, ApiConfig> = {
// 开发环境
development: {
baseUrl: 'http://172.16.0.58:8073', // FastAPI后端(包含/dify代理)
documentUrl: 'http://172.16.0.58:8073/docauditai/',
uploadUrl: 'http://172.16.0.58:8073/admin/documents',
baseUrl: 'http://172.16.0.56:8073', // FastAPI后端(包含/dify代理)
documentUrl: 'http://172.16.0.56:8073/docauditai/',
uploadUrl: 'http://172.16.0.56:8073/admin/documents',
// baseUrl: 'http://172.16.0.55:8073', // FastAPI后端(包含/dify代理)
// documentUrl: 'http://172.16.0.55:8073/docauditai/',
// uploadUrl: 'http://172.16.0.55:8073/admin/documents',
collaboraUrl: 'http://172.16.0.81:9980',
appUrl: 'http://172.16.0.78:51703',
appUrl: 'http://172.16.0.56:51703',
oauth: {
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
@@ -146,9 +146,9 @@ const configs: Record<string, ApiConfig> = {
// 测试环境
testing: {
baseUrl: 'http://nas.7bm.co:8873', // FastAPI后端(包含/dify代理)
documentUrl: 'http://nas.7bm.co:8873/docauditai/',
uploadUrl: 'http://nas.7bm.co:8873/admin/documents',
baseUrl: 'http://172.16.0.56:8073', // FastAPI后端(包含/dify代理)
documentUrl: 'http://172.16.0.56:8073/docauditai/',
uploadUrl: 'http://172.16.0.56:8073/admin/documents',
collaboraUrl: 'http://10.79.97.17:9980',
appUrl: 'http://10.79.97.17:51703',
oauth: {
+21 -2
View File
@@ -411,8 +411,27 @@ export default function useChatMessage({
},
onMessageEnd: (messageEnd: MessageEnd) => {
// 处理消息结束事件
// console.log('Message ended:', messageEnd);
// 处理消息结束事件 - 获取检索资源
console.log('📋 [useChatMessage] 消息结束事件:', {
messageId: messageEnd.message_id,
hasMetadata: !!messageEnd.metadata,
hasRetrieverResources: !!messageEnd.metadata?.retriever_resources,
resourceCount: messageEnd.metadata?.retriever_resources?.length || 0
});
// 如果有检索资源,更新响应项
if (messageEnd.metadata?.retriever_resources && messageEnd.metadata.retriever_resources.length > 0) {
responseItem.retriever_resources = messageEnd.metadata.retriever_resources;
// 更新聊天列表
updateCurrentQA({
responseItem: { ...responseItem },
questionId,
placeholderAnswerId,
questionItem,
originalResponseId,
});
}
},
onMessageReplace: (messageReplace: MessageReplace) => {
@@ -298,4 +298,201 @@
.message-card.assistant .markdown-content {
background-color: #a4e2ad;
max-width: 65vh;
}
/* ============================================================================ */
/* 引用来源样式 - 绿白主题配色 */
/* ============================================================================ */
/* 普通引用上标 */
.source-ref-plain {
color: #00684a;
font-size: 0.75em;
vertical-align: super;
cursor: default;
}
/* 引用来源面板外层容器 */
.sources-panel-wrapper {
padding-left: 8px;
margin-top: 4px;
margin-bottom: 8px;
}
/* 引用来源面板 - 更紧凑的设计 */
.sources-panel {
padding: 6px 10px;
background: transparent;
border-radius: 4px;
}
.sources-panel-header {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 500;
color: #6b7280;
margin-right: 8px;
}
.sources-panel-header i {
font-size: 12px;
color: #00684a;
}
.sources-panel-list {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
/* 引用项 - 更小更紧凑 */
.source-item {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: #fff;
border: 1px solid #d1e7dd;
border-radius: 12px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
max-width: 200px;
}
.source-item:hover {
border-color: #00684a;
background: #f0fdf4;
}
/* 引用编号 - 使用绿色主题 */
.source-item-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
background: #00684a;
color: #fff;
border-radius: 50%;
font-size: 9px;
font-weight: 600;
flex-shrink: 0;
}
.source-item-name {
color: #374151;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
font-size: 11px;
}
/* 得分显示 - 使用绿色渐变 */
.source-item-score {
flex-shrink: 0;
font-size: 10px;
color: #00684a;
font-weight: 500;
}
/* 引用来源 Tooltip 样式 - 绿色半透明主题 */
.source-tooltip-overlay {
max-width: 480px !important;
}
.source-tooltip-overlay .ant-tooltip-inner {
padding: 12px 14px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 104, 74, 0.25);
}
.source-tooltip {
max-width: 100%;
}
.source-tooltip-header {
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 13px;
font-weight: 600;
color: #fff;
}
.source-tooltip-content {
font-size: 12px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 10px;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
padding-right: 4px;
}
/* 自定义滚动条样式 */
.source-tooltip-content::-webkit-scrollbar {
width: 5px;
}
.source-tooltip-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.source-tooltip-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.35);
border-radius: 3px;
}
.source-tooltip-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.source-tooltip-meta {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* Ant Design X Sources 组件样式覆盖 - 使用绿色主题 */
.markdown-content .ant-x-sources {
display: inline;
}
.markdown-content .ant-x-sources-tag {
font-size: 11px;
padding: 0 3px;
cursor: pointer;
color: #00684a;
background: transparent;
border: none;
}
.markdown-content .ant-x-sources-tag:hover {
color: #005a3f;
text-decoration: underline;
}
/* 响应式调整 */
@media (max-width: 768px) {
.sources-panel-wrapper {
padding-left: 4px;
}
.sources-panel-list {
flex-wrap: wrap;
}
.source-item {
max-width: 180px;
}
}