feat(feedback): 新增消息反馈功能,支持用户对消息的点赞和点踩

- 在 chat_router 中添加了提交和获取消息反馈的 API 接口
- 在 dashboard_router 中更新统计信息,包含反馈统计数据
- 新增 MessageFeedback 数据模型,存储用户反馈信息
- 前端实现反馈提交和状态获取功能,优化用户体验
- 更新 DashboardView 以展示用户反馈统计信息
This commit is contained in:
Wenjie Zhang 2025-10-05 14:50:34 +08:00
parent a3028d17b5
commit cdb381b0e9
11 changed files with 653 additions and 31 deletions

View File

@ -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)}")

View File

@ -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)}")

View File

@ -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()

View File

@ -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,
}

View File

@ -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 - 模型提供商

View File

@ -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()}`)
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 ''