diff --git a/app/components/dify-chat/chat-message.tsx b/app/components/dify-chat/chat-message.tsx
index 8e5207d..4c0f6c3 100644
--- a/app/components/dify-chat/chat-message.tsx
+++ b/app/components/dify-chat/chat-message.tsx
@@ -1,4 +1,5 @@
-import { Button, Card, Spin } from 'antd';
+import { LikeOutlined, LikeFilled, DislikeOutlined, DislikeFilled, CopyOutlined } from '@ant-design/icons';
+import { Button, Card, Spin, Tooltip } from 'antd';
import { useState } from 'react';
import type { ChatItem, Feedbacktype } from '~/api/dify-chat';
import '../../styles/components/chat-with-llm/chat-message.css';
@@ -26,6 +27,20 @@ export default function ChatMessage({
const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
message.feedback?.rating || null
);
+ const [copied, setCopied] = useState(false);
+
+ /**
+ * 处理复制
+ */
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(content);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('复制失败:', err);
+ }
+ };
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more, retriever_resources } = message;
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
@@ -168,6 +183,13 @@ export default function ChatMessage({
);
}
+ // 判断是否可以显示反馈按钮(AI回答、非占位符、非正在响应)
+ const canShowFeedback = isAnswer &&
+ !id.startsWith('placeholder-') &&
+ !id.startsWith('opening-') &&
+ !id.startsWith('response-') &&
+ !isResponding;
+
return (
@@ -184,6 +206,36 @@ export default function ChatMessage({
)}
+ {/* 反馈按钮 - 仅在AI回答且非占位符时显示 */}
+ {canShowFeedback && (
+
+
+ }
+ onClick={handleCopy}
+ className={copied ? 'message-copy-btn copied' : 'message-copy-btn'}
+ />
+
+
+ : }
+ onClick={() => handleFeedback('like')}
+ className={feedback === 'like' ? 'feedback-active-like' : ''}
+ />
+ : }
+ onClick={() => handleFeedback('dislike')}
+ className={feedback === 'dislike' ? 'feedback-active-dislike' : ''}
+ />
+
+
+ )}
diff --git a/app/routes/api.messages.$messageId.feedbacks.tsx b/app/routes/api.messages.$messageId.feedbacks.tsx
index 4200f53..09ff907 100644
--- a/app/routes/api.messages.$messageId.feedbacks.tsx
+++ b/app/routes/api.messages.$messageId.feedbacks.tsx
@@ -16,7 +16,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
// 检查 JWT 是否存在
if (!frontendJWT) {
- console.error('❌ [API] Message Feedback - JWT不存在');
+ console.error('[API] Message Feedback - JWT不存在');
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{
@@ -40,21 +40,21 @@ export async function action({ request, params }: ActionFunctionArgs) {
const body = await request.json();
const { rating } = body;
- console.log('👍 [API] Message Feedback - 提交反馈:', {
+ console.log('[API] Message Feedback - 提交反馈:', {
messageId,
rating,
});
const result = await difyClient.updateMessageFeedback(messageId, rating, frontendJWT);
- console.log('✅ [API] Message Feedback - Success');
+ console.log('[API] Message Feedback - Success');
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
- console.error('❌ [API] Message Feedback - Error:', error.message);
+ console.error('[API] Message Feedback - Error:', error.message);
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
return new Response(
JSON.stringify({ error: error.message || 'Failed to submit feedback' }),
diff --git a/app/styles/components/chat-with-llm/chat-message.css b/app/styles/components/chat-with-llm/chat-message.css
index 5cca393..0eb3d98 100644
--- a/app/styles/components/chat-with-llm/chat-message.css
+++ b/app/styles/components/chat-with-llm/chat-message.css
@@ -106,6 +106,65 @@
gap: 8px;
}
+/* 消息反馈按钮 */
+.message-feedback {
+ display: flex;
+ gap: 4px;
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
+ justify-content: space-between;
+ align-items: center;
+}
+
+.message-copy-btn {
+ color: #52c41a !important;
+ transition: all 0.2s ease;
+}
+
+.message-copy-btn:hover {
+ color: #00684a !important;
+ background: rgba(0, 104, 74, 0.1) !important;
+}
+
+.message-copy-btn.copied {
+ color: #00684a !important;
+}
+
+.feedback-actions-right {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.message-feedback .ant-btn {
+ color: #8c8c8c;
+ transition: all 0.2s ease;
+}
+
+.message-feedback .ant-btn:hover {
+ color: #52c41a;
+ background: rgba(24, 144, 255, 0.1);
+}
+
+.message-feedback .feedback-active-like {
+ color: #52c41a !important;
+}
+
+.message-feedback .feedback-active-like:hover {
+ color: #52c41a !important;
+ background: rgba(82, 196, 26, 0.1);
+}
+
+.message-feedback .feedback-active-dislike {
+ color: #ff4d4f !important;
+}
+
+.message-feedback .feedback-active-dislike:hover {
+ color: #ff4d4f !important;
+ background: rgba(255, 77, 79, 0.1);
+}
+
/* 建议问题 */
.suggested-questions {
margin-top: 16px;