feat:前端新增初版知识库管理页面

This commit is contained in:
PingChuan
2025-11-30 19:27:01 +08:00
parent 9614899171
commit c94cc00138
40 changed files with 3034 additions and 1024 deletions
+189
View File
@@ -0,0 +1,189 @@
import { Button, Card, Spin } from 'antd';
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 ThinkingBlock from './thinking-block';
import ThoughtProcess from './thought-process';
interface ChatMessageProps {
message: ChatItem;
onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
isResponding?: boolean;
onRegenerate?: (messageId: string) => void;
}
/**
* 聊天消息组件
*/
export default function ChatMessage({
message,
onFeedback,
isResponding = false,
onRegenerate
}: ChatMessageProps) {
const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
message.feedback?.rating || null
);
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message;
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
/**
* 处理反馈
*/
const handleFeedback = async (type: 'like' | 'dislike') => {
if (!id || id.startsWith('placeholder-') || id.startsWith('question-') || id.startsWith('opening-'))
return;
// 如果已经选择了相同的反馈,则取消选择
const newFeedback = feedback === type ? null : type;
setFeedback(newFeedback);
await onFeedback?.(id, {
rating: newFeedback,
});
};
/**
* 渲染AI回答内容
*/
const renderAnswerContent = () => {
// console.log('🎨 渲染AI回答内容:', { content, isResponding, isAgentMode });
// 如果正在响应且没有内容
if (isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) {
return (
<div className="responding-indicator">
<Spin size="small" />
<span>AI ...</span>
</div>
);
}
// Agent模式(有思考过程)
if (isAgentMode) {
return (
<div className="space-y-3">
{agent_thoughts?.map((thought, index) => (
<div key={index}>
{thought.thought && (
<div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}>
<Markdown content={thought.thought} />
</div>
)}
{thought.tool && (
<ThoughtProcess
thought={thought}
isFinished={!!thought.observation || !isResponding}
/>
)}
</div>
))}
</div>
);
}
// 普通模式 - 解析内容,检查是否包含思考过程
const parsed = parseMessageContent(content);
return (
<div>
{/* 思考过程区域 */}
{parsed.hasThinking && (
<ThinkingBlock content={parsed.thinking} defaultExpanded={false} />
)}
{/* 实际回复内容 */}
{parsed.response && (
<div className={isResponding ? 'streaming-text' : ''}>
<Markdown content={parsed.response} />
</div>
)}
</div>
);
};
/**
* 渲染建议问题
*/
const renderSuggestedQuestions = () => {
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
return (
<div className="suggested-questions">
<div className="text-sm text-gray-500 mb-2"></div>
<div className="flex flex-wrap gap-2">
{suggestedQuestions.map((question, index) => (
<Button
key={index}
size="small"
type="dashed"
className="question-button text-left"
onClick={() => {
// 这里可以添加点击建议问题的处理逻辑
// console.log('Suggested question clicked:', question);
}}
>
{question}
</Button>
))}
</div>
</div>
);
};
/**
* 渲染消息元信息
*/
const renderMessageMeta = () => {
if (!more) return null;
return (
<div className="message-timestamp">
{more.time && <span>: {more.time}</span>}
{more.tokens && <span className="ml-2">Token: {more.tokens}</span>}
{more.latency && <span className="ml-2">: {more.latency}</span>}
</div>
);
};
// 如果是开场白,特殊处理
if (isOpeningStatement) {
return (
<div className="chat-message">
<Card className="message-card assistant" size="small">
<div className="flex items-start gap-3">
<div className="flex-1">
<Markdown content={content} />
{renderSuggestedQuestions()}
</div>
</div>
</Card>
</div>
);
}
return (
<div className="chat-message">
<div className={`flex ${isAnswer ? 'justify-start' : 'justify-end'} mb-2`}>
<div className={`message-card ${isAnswer ? 'assistant' : 'user'}`}>
<Card size="small" className="shadow-sm">
<div className="flex items-start gap-2">
{/* 消息内容 */}
<div className="flex-1 min-w-0">
{isAnswer ? renderAnswerContent() : (
<div>
<Markdown content={content} />
{/* {renderImages(message_files)} */}
</div>
)}
</div>
</div>
</Card>
</div>
</div>
</div>
);
}