feat(feedback): 新增消息反馈功能,支持用户对消息的点赞和点踩
- 在 chat_router 中添加了提交和获取消息反馈的 API 接口 - 在 dashboard_router 中更新统计信息,包含反馈统计数据 - 新增 MessageFeedback 数据模型,存储用户反馈信息 - 前端实现反馈提交和状态获取功能,优化用户体验 - 更新 DashboardView 以展示用户反馈统计信息
This commit is contained in:
parent
a3028d17b5
commit
cdb381b0e9
@ -11,7 +11,7 @@ from langchain_core.messages import AIMessageChunk, HumanMessage, ToolMessage
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.storage.db.models import User
|
||||
from src.storage.db.models import User, MessageFeedback, Message, Conversation
|
||||
from src.storage.conversation import ConversationManager
|
||||
from server.routers.auth_router import get_admin_user
|
||||
from server.utils.auth_middleware import get_db, get_required_user
|
||||
@ -401,6 +401,7 @@ async def get_agent_history(
|
||||
role_type_map = {"user": "human", "assistant": "ai", "tool": "tool", "system": "system"}
|
||||
|
||||
msg_dict = {
|
||||
"id": msg.id, # Include message ID for feedback
|
||||
"type": role_type_map.get(msg.role, msg.role), # human/ai/tool/system
|
||||
"content": msg.content,
|
||||
"created_at": msg.created_at.isoformat() if msg.created_at else None,
|
||||
@ -583,3 +584,112 @@ async def update_thread(
|
||||
"created_at": updated_conv.created_at.isoformat(),
|
||||
"updated_at": updated_conv.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# > === 消息反馈分组 ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class MessageFeedbackRequest(BaseModel):
|
||||
rating: str # 'like' or 'dislike'
|
||||
reason: str | None = None # Optional reason for dislike
|
||||
|
||||
|
||||
class MessageFeedbackResponse(BaseModel):
|
||||
id: int
|
||||
message_id: int
|
||||
rating: str
|
||||
reason: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
@chat.post("/message/{message_id}/feedback", response_model=MessageFeedbackResponse)
|
||||
async def submit_message_feedback(
|
||||
message_id: int,
|
||||
feedback_data: MessageFeedbackRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_required_user),
|
||||
):
|
||||
"""Submit user feedback for a specific message"""
|
||||
try:
|
||||
# Validate rating
|
||||
if feedback_data.rating not in ["like", "dislike"]:
|
||||
raise HTTPException(status_code=422, detail="Rating must be 'like' or 'dislike'")
|
||||
|
||||
# Verify message exists and get conversation to check permissions
|
||||
message = db.query(Message).filter_by(id=message_id).first()
|
||||
|
||||
if not message:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
# Verify user has access to this message (through conversation)
|
||||
conversation = db.query(Conversation).filter_by(id=message.conversation_id).first()
|
||||
if not conversation or conversation.user_id != str(current_user.id):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Check if feedback already exists (user can only submit once)
|
||||
existing_feedback = (
|
||||
db.query(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id)).first()
|
||||
)
|
||||
|
||||
if existing_feedback:
|
||||
raise HTTPException(status_code=409, detail="Feedback already submitted for this message")
|
||||
|
||||
# Create new feedback
|
||||
new_feedback = MessageFeedback(
|
||||
message_id=message_id,
|
||||
user_id=str(current_user.id),
|
||||
rating=feedback_data.rating,
|
||||
reason=feedback_data.reason,
|
||||
)
|
||||
|
||||
db.add(new_feedback)
|
||||
db.commit()
|
||||
db.refresh(new_feedback)
|
||||
|
||||
logger.info(f"User {current_user.id} submitted {feedback_data.rating} feedback for message {message_id}")
|
||||
|
||||
return MessageFeedbackResponse(
|
||||
id=new_feedback.id,
|
||||
message_id=new_feedback.message_id,
|
||||
rating=new_feedback.rating,
|
||||
reason=new_feedback.reason,
|
||||
created_at=new_feedback.created_at.isoformat(),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting message feedback: {e}, {traceback.format_exc()}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
|
||||
|
||||
|
||||
@chat.get("/message/{message_id}/feedback")
|
||||
async def get_message_feedback(
|
||||
message_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_required_user),
|
||||
):
|
||||
"""Get feedback status for a specific message (for current user)"""
|
||||
try:
|
||||
# Get user's feedback for this message
|
||||
feedback = db.query(MessageFeedback).filter_by(message_id=message_id, user_id=str(current_user.id)).first()
|
||||
|
||||
if not feedback:
|
||||
return {"has_feedback": False, "feedback": None}
|
||||
|
||||
return {
|
||||
"has_feedback": True,
|
||||
"feedback": {
|
||||
"id": feedback.id,
|
||||
"rating": feedback.rating,
|
||||
"reason": feedback.reason,
|
||||
"created_at": feedback.created_at.isoformat(),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting message feedback: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get feedback: {str(e)}")
|
||||
|
||||
@ -186,7 +186,7 @@ async def get_dashboard_stats(
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
"""Get dashboard statistics (Admin only)"""
|
||||
from src.storage.db.models import Conversation, Message
|
||||
from src.storage.db.models import Conversation, Message, MessageFeedback
|
||||
|
||||
try:
|
||||
# Basic counts
|
||||
@ -211,6 +211,21 @@ async def get_dashboard_stats(
|
||||
)
|
||||
recent_messages = db.query(func.count(Message.id)).filter(Message.created_at >= yesterday).scalar() or 0
|
||||
|
||||
# Feedback statistics
|
||||
total_feedbacks = db.query(func.count(MessageFeedback.id)).scalar() or 0
|
||||
like_count = db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "like").scalar() or 0
|
||||
dislike_count = (
|
||||
db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "dislike").scalar() or 0
|
||||
)
|
||||
|
||||
# Calculate satisfaction rate
|
||||
satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0
|
||||
|
||||
# Recent feedback (last 24 hours)
|
||||
recent_feedbacks = (
|
||||
db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.created_at >= yesterday).scalar() or 0
|
||||
)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_conversations": total_conversations,
|
||||
@ -224,8 +239,82 @@ async def get_dashboard_stats(
|
||||
"conversations_24h": recent_conversations,
|
||||
"messages_24h": recent_messages,
|
||||
},
|
||||
"feedback_stats": {
|
||||
"total_feedbacks": total_feedbacks,
|
||||
"like_count": like_count,
|
||||
"dislike_count": dislike_count,
|
||||
"satisfaction_rate": satisfaction_rate,
|
||||
"recent_feedbacks_24h": recent_feedbacks,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting dashboard stats: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get dashboard stats: {str(e)}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Feedback Management
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class FeedbackListItem(BaseModel):
|
||||
"""Feedback list item"""
|
||||
|
||||
id: int
|
||||
message_id: int
|
||||
user_id: str
|
||||
rating: str
|
||||
reason: str | None
|
||||
created_at: str
|
||||
message_content: str
|
||||
conversation_title: str | None
|
||||
agent_id: str
|
||||
|
||||
|
||||
@dashboard.get("/feedbacks", response_model=list[FeedbackListItem])
|
||||
async def get_all_feedbacks(
|
||||
rating: str | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
"""Get all feedback records (Admin only)"""
|
||||
from src.storage.db.models import MessageFeedback, Message, Conversation
|
||||
|
||||
try:
|
||||
# Build query with joins
|
||||
query = (
|
||||
db.query(MessageFeedback, Message, Conversation)
|
||||
.join(Message, MessageFeedback.message_id == Message.id)
|
||||
.join(Conversation, Message.conversation_id == Conversation.id)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if rating and rating in ["like", "dislike"]:
|
||||
query = query.filter(MessageFeedback.rating == rating)
|
||||
|
||||
# Order and paginate
|
||||
query = query.order_by(MessageFeedback.created_at.desc()).limit(limit).offset(offset)
|
||||
|
||||
results = query.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": feedback.id,
|
||||
"message_id": feedback.message_id,
|
||||
"user_id": feedback.user_id,
|
||||
"rating": feedback.rating,
|
||||
"reason": feedback.reason,
|
||||
"created_at": feedback.created_at.isoformat(),
|
||||
"message_content": message.content[:100] + ("..." if len(message.content) > 100 else ""),
|
||||
"conversation_title": conversation.title,
|
||||
"agent_id": conversation.agent_id,
|
||||
}
|
||||
for feedback, message, conversation in results
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting feedbacks: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get feedbacks: {str(e)}")
|
||||
|
||||
@ -54,13 +54,6 @@ class Config(SimpleConfig):
|
||||
self.add_item(
|
||||
"content_guard_llm_model", default="siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507", des="内容审查LLM模型"
|
||||
)
|
||||
self.add_item(
|
||||
"enable_web_search",
|
||||
default=False,
|
||||
des=(
|
||||
"是否开启网页搜索(注:现阶段会根据 TAVILY_API_KEY 自动开启,无法手动配置,将会在下个版本移除此配置项)"
|
||||
),
|
||||
)
|
||||
# 默认智能体配置
|
||||
self.add_item("default_agent_id", default="", des="默认智能体ID")
|
||||
# 模型配置
|
||||
@ -68,6 +61,11 @@ class Config(SimpleConfig):
|
||||
## 如果需要自定义本地模型路径,则在 src/.env 中配置 MODEL_DIR
|
||||
self.add_item("model_provider", default="siliconflow", des="模型提供商", choices=list(self.model_names.keys()))
|
||||
self.add_item("model_name", default="zai-org/GLM-4.5", des="模型名称")
|
||||
self.add_item(
|
||||
"fast_model",
|
||||
default="siliconflow/THUDM/GLM-4-9B-0414",
|
||||
des="快速响应模型",
|
||||
)
|
||||
|
||||
self.add_item(
|
||||
"embed_model",
|
||||
@ -238,5 +236,4 @@ class Config(SimpleConfig):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
@ -369,3 +369,31 @@ class OperationLog(Base):
|
||||
"ip_address": self.ip_address,
|
||||
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
||||
}
|
||||
|
||||
|
||||
class MessageFeedback(Base):
|
||||
"""Message feedback table - stores user feedback on AI responses"""
|
||||
|
||||
__tablename__ = "message_feedbacks"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
|
||||
message_id = Column(
|
||||
Integer, ForeignKey("messages.id"), nullable=False, index=True, comment="Message ID being rated"
|
||||
)
|
||||
user_id = Column(String(64), nullable=False, index=True, comment="User ID who provided feedback")
|
||||
rating = Column(String(10), nullable=False, comment="Feedback rating: like or dislike")
|
||||
reason = Column(Text, nullable=True, comment="Optional reason for dislike feedback")
|
||||
created_at = Column(DateTime, default=lambda: dt.datetime.now(dt.UTC), comment="Feedback creation time")
|
||||
|
||||
# Relationships
|
||||
message = relationship("Message", backref="feedbacks")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"message_id": self.message_id,
|
||||
"user_id": self.user_id,
|
||||
"rating": self.rating,
|
||||
"reason": self.reason,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
@ -65,6 +65,23 @@ export const agentApi = {
|
||||
*/
|
||||
getAgentHistory: (agentId, threadId) => apiGet(`/api/chat/agent/${agentId}/history?thread_id=${threadId}`),
|
||||
|
||||
/**
|
||||
* Submit feedback for a message
|
||||
* @param {number} messageId - Message ID
|
||||
* @param {string} rating - 'like' or 'dislike'
|
||||
* @param {string|null} reason - Optional reason for dislike
|
||||
* @returns {Promise} - Feedback response
|
||||
*/
|
||||
submitMessageFeedback: (messageId, rating, reason = null) =>
|
||||
apiPost(`/api/chat/message/${messageId}/feedback`, { rating, reason }),
|
||||
|
||||
/**
|
||||
* Get feedback status for a message
|
||||
* @param {number} messageId - Message ID
|
||||
* @returns {Promise} - Feedback status
|
||||
*/
|
||||
getMessageFeedback: (messageId) => apiGet(`/api/chat/message/${messageId}/feedback`),
|
||||
|
||||
/**
|
||||
* 获取模型提供商的模型列表
|
||||
* @param {string} provider - 模型提供商
|
||||
|
||||
@ -42,6 +42,23 @@ export const dashboardApi = {
|
||||
*/
|
||||
getStats: () => {
|
||||
return apiAdminGet('/api/dashboard/stats')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户反馈列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.rating - 反馈类型过滤 (like/dislike/all)
|
||||
* @param {number} params.limit - 每页数量
|
||||
* @param {number} params.offset - 偏移量
|
||||
* @returns {Promise<Array>} - 反馈列表
|
||||
*/
|
||||
getFeedbacks: (params = {}) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.rating && params.rating !== 'all') queryParams.append('rating', params.rating)
|
||||
if (params.limit) queryParams.append('limit', params.limit)
|
||||
if (params.offset) queryParams.append('offset', params.offset)
|
||||
|
||||
return apiAdminGet(`/api/dashboard/feedbacks?${queryParams.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<div class="refs" v-if="showRefs">
|
||||
<div class="tags">
|
||||
<span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span>
|
||||
<span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span>
|
||||
<span
|
||||
class="item btn"
|
||||
:class="{ 'disabled': feedbackState.hasSubmitted }"
|
||||
@click="likeThisResponse(msg)"
|
||||
:title="feedbackState.hasSubmitted && feedbackState.rating === 'like' ? '已点赞' : '点赞'"
|
||||
>
|
||||
<LikeFilled v-if="feedbackState.rating === 'like'" />
|
||||
<LikeOutlined v-else />
|
||||
</span>
|
||||
<span
|
||||
class="item btn"
|
||||
:class="{ 'disabled': feedbackState.hasSubmitted }"
|
||||
@click="dislikeThisResponse(msg)"
|
||||
:title="feedbackState.hasSubmitted && feedbackState.rating === 'dislike' ? '已点踩' : '点踩'"
|
||||
>
|
||||
<DislikeFilled v-if="feedbackState.rating === 'dislike'" />
|
||||
<DislikeOutlined v-else />
|
||||
</span>
|
||||
<span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)">
|
||||
<BulbOutlined /> {{ getModelName(msg) }}
|
||||
</span>
|
||||
@ -35,10 +51,29 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dislike reason modal -->
|
||||
<a-modal
|
||||
v-model:open="dislikeModalVisible"
|
||||
title="请告诉我们不满意的原因"
|
||||
@ok="submitDislikeFeedback"
|
||||
@cancel="cancelDislike"
|
||||
:confirmLoading="submittingFeedback"
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="dislikeReason"
|
||||
:rows="4"
|
||||
placeholder="您的反馈将帮助我们改进服务(可选)"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
@ -49,8 +84,11 @@ import {
|
||||
BulbOutlined,
|
||||
ReloadOutlined,
|
||||
LikeOutlined,
|
||||
LikeFilled,
|
||||
DislikeOutlined,
|
||||
DislikeFilled,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { agentApi } from '@/apis'
|
||||
|
||||
const emit = defineEmits(['retry', 'openRefs']);
|
||||
const props = defineProps({
|
||||
@ -67,6 +105,18 @@ const props = defineProps({
|
||||
|
||||
const msg = ref(props.message)
|
||||
|
||||
// Feedback state
|
||||
const feedbackState = reactive({
|
||||
hasSubmitted: false,
|
||||
rating: null, // 'like' or 'dislike'
|
||||
reason: null,
|
||||
})
|
||||
|
||||
// Modal state for dislike
|
||||
const dislikeModalVisible = ref(false)
|
||||
const dislikeReason = ref('')
|
||||
const submittingFeedback = ref(false)
|
||||
|
||||
// 使用 useClipboard 实现复制功能
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
@ -140,12 +190,109 @@ const getModelName = (msg) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const likeThisResponse = (msg) => {
|
||||
console.log(msg)
|
||||
// Load existing feedback on mount
|
||||
onMounted(async () => {
|
||||
if (msg.value?.id) {
|
||||
try {
|
||||
const response = await agentApi.getMessageFeedback(msg.value.id)
|
||||
if (response.has_feedback) {
|
||||
feedbackState.hasSubmitted = true
|
||||
feedbackState.rating = response.feedback.rating
|
||||
feedbackState.reason = response.feedback.reason
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load feedback:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle like action
|
||||
const likeThisResponse = async (msg) => {
|
||||
if (feedbackState.hasSubmitted) {
|
||||
message.info('您已经提交过反馈了')
|
||||
return
|
||||
}
|
||||
|
||||
if (!msg?.id) {
|
||||
message.error('无法提交反馈:消息ID不存在')
|
||||
console.error('Message object:', msg)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submittingFeedback.value = true
|
||||
await agentApi.submitMessageFeedback(msg.id, 'like', null)
|
||||
|
||||
feedbackState.hasSubmitted = true
|
||||
feedbackState.rating = 'like'
|
||||
|
||||
message.success('感谢您的反馈!')
|
||||
} catch (error) {
|
||||
console.error('Failed to submit like feedback:', error)
|
||||
if (error.message?.includes('already submitted')) {
|
||||
message.info('您已经提交过反馈了')
|
||||
feedbackState.hasSubmitted = true
|
||||
} else {
|
||||
message.error('提交反馈失败,请稍后重试')
|
||||
}
|
||||
} finally {
|
||||
submittingFeedback.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const dislikeThisResponse = (msg) => {
|
||||
console.log(msg)
|
||||
// Handle dislike action
|
||||
const dislikeThisResponse = async (msg) => {
|
||||
if (feedbackState.hasSubmitted) {
|
||||
message.info('您已经提交过反馈了')
|
||||
return
|
||||
}
|
||||
|
||||
if (!msg?.id) {
|
||||
message.error('无法提交反馈:消息ID不存在')
|
||||
console.error('Message object:', msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Open modal to get reason
|
||||
dislikeModalVisible.value = true
|
||||
}
|
||||
|
||||
// Submit dislike feedback with reason
|
||||
const submitDislikeFeedback = async () => {
|
||||
try {
|
||||
submittingFeedback.value = true
|
||||
await agentApi.submitMessageFeedback(
|
||||
msg.value.id,
|
||||
'dislike',
|
||||
dislikeReason.value || null
|
||||
)
|
||||
|
||||
feedbackState.hasSubmitted = true
|
||||
feedbackState.rating = 'dislike'
|
||||
feedbackState.reason = dislikeReason.value
|
||||
|
||||
dislikeModalVisible.value = false
|
||||
dislikeReason.value = ''
|
||||
|
||||
message.success('感谢您的反馈!')
|
||||
} catch (error) {
|
||||
console.error('Failed to submit dislike feedback:', error)
|
||||
if (error.message?.includes('already submitted')) {
|
||||
message.info('您已经提交过反馈了')
|
||||
feedbackState.hasSubmitted = true
|
||||
dislikeModalVisible.value = false
|
||||
} else {
|
||||
message.error('提交反馈失败,请稍后重试')
|
||||
}
|
||||
} finally {
|
||||
submittingFeedback.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel dislike modal
|
||||
const cancelDislike = () => {
|
||||
dislikeModalVisible.value = false
|
||||
dislikeReason.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -165,6 +312,7 @@ const dislikeThisResponse = (msg) => {
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.btn {
|
||||
cursor: pointer;
|
||||
@ -174,6 +322,16 @@ const dislikeThisResponse = (msg) => {
|
||||
&:active {
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
// Disabled state - when feedback has been submitted
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="calculator-result">
|
||||
<div class="calc-header">
|
||||
<!-- <div class="calc-header">
|
||||
<h4><NumberOutlined /> 计算结果</h4>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="calc-display">
|
||||
<div class="result-container">
|
||||
@ -82,7 +82,7 @@ const formatNumber = (num) => {
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--main-color);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
|
||||
@ -122,19 +122,18 @@ const mainList = [{
|
||||
</div>
|
||||
<div class="nav">
|
||||
<!-- 使用mainList渲染导航项 -->
|
||||
<a-tooltip
|
||||
<RouterLink
|
||||
v-for="(item, index) in mainList"
|
||||
:key="index"
|
||||
placement="right"
|
||||
v-show="!item.hidden">
|
||||
<template #title>{{ item.name }}</template>
|
||||
<RouterLink
|
||||
:to="item.path"
|
||||
class="nav-item"
|
||||
active-class="active">
|
||||
:to="item.path"
|
||||
v-show="!item.hidden"
|
||||
class="nav-item"
|
||||
active-class="active">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>{{ item.name }}</template>
|
||||
<component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" size="22"/>
|
||||
</RouterLink>
|
||||
</a-tooltip>
|
||||
</a-tooltip>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div
|
||||
ref="htmlRefHook"
|
||||
@ -207,7 +206,7 @@ const mainList = [{
|
||||
|
||||
<style lang="less" scoped>
|
||||
// Less 变量定义
|
||||
@header-width: 60px;
|
||||
@header-width: 50px;
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
|
||||
@ -16,6 +16,50 @@
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 用户反馈统计区域 -->
|
||||
<a-card class="feedback-section" title="用户反馈统计" :loading="loading">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="总反馈数"
|
||||
:value="stats.feedback_stats?.total_feedbacks || 0"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="点赞数"
|
||||
:value="stats.feedback_stats?.like_count || 0"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<LikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="点踩数"
|
||||
:value="stats.feedback_stats?.dislike_count || 0"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<DislikeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="满意度"
|
||||
:value="stats.feedback_stats?.satisfaction_rate || 0"
|
||||
suffix="%"
|
||||
:value-style="{ color: stats.feedback_stats?.satisfaction_rate >= 80 ? '#3f8600' : stats.feedback_stats?.satisfaction_rate >= 60 ? '#fa8c16' : '#cf1322' }"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider />
|
||||
<a-button type="primary" @click="showFeedbackList">查看反馈详情</a-button>
|
||||
</a-card>
|
||||
|
||||
<!-- 过滤器区域 -->
|
||||
<a-card class="filter-section" title="筛选条件">
|
||||
<a-row :gutter="16">
|
||||
@ -88,12 +132,57 @@
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 反馈列表模态框 -->
|
||||
<a-modal
|
||||
v-model:open="feedbackModalVisible"
|
||||
title="用户反馈详情"
|
||||
width="1000px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-space style="margin-bottom: 16px">
|
||||
<a-radio-group v-model:value="feedbackFilter" @change="loadFeedbacks">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="like">点赞</a-radio-button>
|
||||
<a-radio-button value="dislike">点踩</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-space>
|
||||
|
||||
<a-table
|
||||
:columns="feedbackColumns"
|
||||
:data-source="feedbacks"
|
||||
:loading="loadingFeedbacks"
|
||||
:pagination="feedbackPagination"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'rating'">
|
||||
<a-tag :color="record.rating === 'like' ? 'green' : 'red'">
|
||||
<template #icon>
|
||||
<LikeOutlined v-if="record.rating === 'like'" />
|
||||
<DislikeOutlined v-else />
|
||||
</template>
|
||||
{{ record.rating === 'like' ? '点赞' : '点踩' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'reason'">
|
||||
<span v-if="record.reason">{{ record.reason }}</span>
|
||||
<span v-else style="color: #999">-</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'created_at'">
|
||||
{{ formatDate(record.created_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { LikeOutlined, DislikeOutlined } from '@ant-design/icons-vue'
|
||||
import { dashboardApi } from '@/apis/dashboard_api'
|
||||
|
||||
// 统计数据
|
||||
@ -102,6 +191,13 @@ const stats = reactive({
|
||||
active_conversations: 0,
|
||||
total_messages: 0,
|
||||
total_users: 0,
|
||||
feedback_stats: {
|
||||
total_feedbacks: 0,
|
||||
like_count: 0,
|
||||
dislike_count: 0,
|
||||
satisfaction_rate: 0,
|
||||
recent_feedbacks_24h: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 过滤器
|
||||
@ -115,6 +211,17 @@ const filters = reactive({
|
||||
const conversations = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 反馈相关
|
||||
const feedbackModalVisible = ref(false)
|
||||
const feedbacks = ref([])
|
||||
const loadingFeedbacks = ref(false)
|
||||
const feedbackFilter = ref('all')
|
||||
const feedbackPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
@ -176,6 +283,49 @@ const columns = [
|
||||
},
|
||||
]
|
||||
|
||||
// 反馈表格列定义
|
||||
const feedbackColumns = [
|
||||
{
|
||||
title: '反馈类型',
|
||||
key: 'rating',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
width: '12%',
|
||||
},
|
||||
{
|
||||
title: '智能体ID',
|
||||
dataIndex: 'agent_id',
|
||||
key: 'agent_id',
|
||||
width: '12%',
|
||||
},
|
||||
{
|
||||
title: '对话标题',
|
||||
dataIndex: 'conversation_title',
|
||||
key: 'conversation_title',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: '消息内容',
|
||||
dataIndex: 'message_content',
|
||||
key: 'message_content',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: '反馈原因',
|
||||
key: 'reason',
|
||||
width: '16%',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'created_at',
|
||||
width: '10%',
|
||||
},
|
||||
]
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
@ -262,6 +412,32 @@ const formatDate = (dateString) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 显示反馈列表
|
||||
const showFeedbackList = () => {
|
||||
feedbackModalVisible.value = true
|
||||
loadFeedbacks()
|
||||
}
|
||||
|
||||
// 加载反馈列表
|
||||
const loadFeedbacks = async () => {
|
||||
loadingFeedbacks.value = true
|
||||
try {
|
||||
const params = {
|
||||
rating: feedbackFilter.value === 'all' ? undefined : feedbackFilter.value,
|
||||
limit: feedbackPagination.pageSize,
|
||||
offset: (feedbackPagination.current - 1) * feedbackPagination.pageSize,
|
||||
}
|
||||
|
||||
const response = await dashboardApi.getFeedbacks(params)
|
||||
feedbacks.value = response
|
||||
} catch (error) {
|
||||
console.error('加载反馈列表失败:', error)
|
||||
message.error('加载反馈列表失败')
|
||||
} finally {
|
||||
loadingFeedbacks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
@ -299,6 +475,12 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.feedback-section {
|
||||
margin-bottom: 24px;
|
||||
background-color: var(--card-bg-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.conversations-section {
|
||||
background-color: var(--card-bg-color);
|
||||
border-radius: 8px;
|
||||
|
||||
@ -25,6 +25,14 @@
|
||||
:model_provider="configStore.config?.model_provider"
|
||||
/>
|
||||
</div>
|
||||
<div class="card card-select">
|
||||
<span class="label">{{ items?.fast_model.des }}</span>
|
||||
<ModelSelectorComponent
|
||||
@select-model="handleFastModelSelect"
|
||||
:model_name="fastModelName"
|
||||
:model_provider="fastModelProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="card card-select">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="label">{{ items?.embed_model.des }}</span>
|
||||
@ -250,6 +258,23 @@ const handleChatModelSelect = ({ provider, name }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const fastModelProvider = computed(() => {
|
||||
const fastModel = configStore.config?.fast_model
|
||||
if (!fastModel) return ''
|
||||
return fastModel.split('/')[0]
|
||||
})
|
||||
|
||||
const fastModelName = computed(() => {
|
||||
const fastModel = configStore.config?.fast_model
|
||||
if (!fastModel) return ''
|
||||
const parts = fastModel.split('/')
|
||||
return parts.slice(1).join('/')
|
||||
})
|
||||
|
||||
const handleFastModelSelect = ({ provider, name }) => {
|
||||
configStore.setConfigValue('fast_model', `${provider}/${name}`)
|
||||
}
|
||||
|
||||
const contentGuardModelProvider = computed(() => {
|
||||
const contentGuardModel = configStore.config?.content_guard_llm_model
|
||||
if (!contentGuardModel) return ''
|
||||
|
||||
Loading…
Reference in New Issue
Block a user