Files
leaudit-platform-frontend/app/components/chat/chat-message.tsx
T
TanWenyan c93a87a65e 优化思考模式AI回复的UI显示,支持<think>标签解析
新增功能:
1. 创建消息解析工具 message-parser.ts
   - 解析 <think> 标签,提取思考过程
   - 分离思考内容和实际回复

2. 创建思考过程展示组件 thinking-block.tsx
   - 可折叠/展开的思考过程区域
   - 参考 GPT-5 和 Claude 网页版设计
   - 默认折叠,点击展开查看详细思考过程

3. 修改聊天消息组件 chat-message.tsx
   - 集成思考过程解析和展示
   - 思考过程单独显示在顶部
   - 实际回复正常显示在下方

4. 新增样式 thinking-block.css
   - 契合当前淡绿色(#a4e2ad)配色方案
   - 渐变背景和流畅动画效果
   - 灯泡图标标识思考过程
   - 完整的响应式设计

UI效果:
- 思考过程:淡绿色渐变背景,可折叠区域
- 实际回复:正常Markdown渲染
- 交互流畅:展开/折叠动画平滑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 15:21:27 +08:00

191 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { Button, Card, Spin } from 'antd';
import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat';
import { CHAT_CONFIG } from '../../config/chat';
import Markdown from './markdown';
import ThoughtProcess from './thought-process';
import ThinkingBlock from './thinking-block';
import { parseMessageContent } from '../../utils/message-parser';
import { Dayjs } from 'dayjs';
import '../../styles/components/chat-with-llm/chat-message.css';
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>
);
}