feat: 更新时间范围为14天和14小时,优化满意度计算逻辑
This commit is contained in:
parent
0835ab708e
commit
71cf6830e4
@ -8,7 +8,6 @@
|
|||||||
## Bugs
|
## Bugs
|
||||||
|
|
||||||
- [ ] 前端工具调用渲染出现问题
|
- [ ] 前端工具调用渲染出现问题
|
||||||
- [ ] 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。
|
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
@ -44,3 +43,4 @@
|
|||||||
- [x] upload 接口会阻塞主进程
|
- [x] upload 接口会阻塞主进程
|
||||||
- [x] LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
|
- [x] LightRAG 知识库查看不了解析后的文本,偶然出现,未复现
|
||||||
- [x] LightRAG 知识库应该可以支持修改 LLM
|
- [x] LightRAG 知识库应该可以支持修改 LLM
|
||||||
|
- [x] 智能体的加载状态有问题:(1)智能体加载没有动画;(2)切换对话和加载中,使用同一个loading状态。
|
||||||
@ -540,7 +540,7 @@ async def get_agent_analytics(
|
|||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
|
|
||||||
satisfaction_rate = round((positive_feedbacks / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0
|
satisfaction_rate = round((positive_feedbacks / total_feedbacks * 100), 2) if total_feedbacks > 0 else 100
|
||||||
|
|
||||||
agent_satisfaction.append(
|
agent_satisfaction.append(
|
||||||
{"agent_id": agent_id, "satisfaction_rate": satisfaction_rate, "total_feedbacks": total_feedbacks}
|
{"agent_id": agent_id, "satisfaction_rate": satisfaction_rate, "total_feedbacks": total_feedbacks}
|
||||||
@ -624,7 +624,7 @@ async def get_dashboard_stats(
|
|||||||
like_count = db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "like").scalar() or 0
|
like_count = db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "like").scalar() or 0
|
||||||
|
|
||||||
# Calculate satisfaction rate
|
# Calculate satisfaction rate
|
||||||
satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0
|
satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 100
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_conversations": total_conversations,
|
"total_conversations": total_conversations,
|
||||||
@ -738,7 +738,7 @@ class TimeSeriesStats(BaseModel):
|
|||||||
@dashboard.get("/stats/calls/timeseries", response_model=TimeSeriesStats)
|
@dashboard.get("/stats/calls/timeseries", response_model=TimeSeriesStats)
|
||||||
async def get_call_timeseries_stats(
|
async def get_call_timeseries_stats(
|
||||||
type: str = "models", # models/agents/tokens/tools
|
type: str = "models", # models/agents/tokens/tools
|
||||||
time_range: str = "7days", # 7hours/7days/7weeks
|
time_range: str = "14days", # 14hours/14days/14weeks
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
@ -750,24 +750,24 @@ async def get_call_timeseries_stats(
|
|||||||
now = utc_now()
|
now = utc_now()
|
||||||
local_now = shanghai_now()
|
local_now = shanghai_now()
|
||||||
|
|
||||||
if time_range == "7hours":
|
if time_range == "14hours":
|
||||||
intervals = 7
|
intervals = 14
|
||||||
# 包含当前小时:从6小时前开始
|
# 包含当前小时:从13小时前开始
|
||||||
start_time = now - timedelta(hours=intervals - 1)
|
start_time = now - timedelta(hours=intervals - 1)
|
||||||
group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(Message.created_at, "+8 hours"))
|
group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(Message.created_at, "+8 hours"))
|
||||||
base_local_time = ensure_shanghai(start_time)
|
base_local_time = ensure_shanghai(start_time)
|
||||||
elif time_range == "7weeks":
|
elif time_range == "14weeks":
|
||||||
intervals = 7
|
intervals = 14
|
||||||
# 包含当前周:从6周前开始,并对齐到当周周一 00:00
|
# 包含当前周:从13周前开始,并对齐到当周周一 00:00
|
||||||
local_start = local_now - timedelta(weeks=intervals - 1)
|
local_start = local_now - timedelta(weeks=intervals - 1)
|
||||||
local_start = local_start - timedelta(days=local_start.weekday())
|
local_start = local_start - timedelta(days=local_start.weekday())
|
||||||
local_start = local_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
local_start = local_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
start_time = local_start.astimezone(UTC)
|
start_time = local_start.astimezone(UTC)
|
||||||
group_format = func.strftime("%Y-%W", func.datetime(Message.created_at, "+8 hours"))
|
group_format = func.strftime("%Y-%W", func.datetime(Message.created_at, "+8 hours"))
|
||||||
base_local_time = local_start
|
base_local_time = local_start
|
||||||
else: # 7days (default)
|
else: # 14days (default)
|
||||||
intervals = 7
|
intervals = 14
|
||||||
# 包含当前天:从6天前开始
|
# 包含当前天:从13天前开始
|
||||||
start_time = now - timedelta(days=intervals - 1)
|
start_time = now - timedelta(days=intervals - 1)
|
||||||
group_format = func.strftime("%Y-%m-%d", func.datetime(Message.created_at, "+8 hours"))
|
group_format = func.strftime("%Y-%m-%d", func.datetime(Message.created_at, "+8 hours"))
|
||||||
base_local_time = ensure_shanghai(start_time)
|
base_local_time = ensure_shanghai(start_time)
|
||||||
@ -790,11 +790,11 @@ async def get_call_timeseries_stats(
|
|||||||
elif type == "agents":
|
elif type == "agents":
|
||||||
# 智能体调用统计(基于对话更新时间,按智能体分组)
|
# 智能体调用统计(基于对话更新时间,按智能体分组)
|
||||||
# 为对话创建独立的时间格式化器
|
# 为对话创建独立的时间格式化器
|
||||||
if time_range == "7hours":
|
if time_range == "14hours":
|
||||||
conv_group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(Conversation.updated_at, "+8 hours"))
|
conv_group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(Conversation.updated_at, "+8 hours"))
|
||||||
elif time_range == "7weeks":
|
elif time_range == "14weeks":
|
||||||
conv_group_format = func.strftime("%Y-%W", func.datetime(Conversation.updated_at, "+8 hours"))
|
conv_group_format = func.strftime("%Y-%W", func.datetime(Conversation.updated_at, "+8 hours"))
|
||||||
else: # 7days
|
else: # 14days
|
||||||
conv_group_format = func.strftime("%Y-%m-%d", func.datetime(Conversation.updated_at, "+8 hours"))
|
conv_group_format = func.strftime("%Y-%m-%d", func.datetime(Conversation.updated_at, "+8 hours"))
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
@ -855,11 +855,11 @@ async def get_call_timeseries_stats(
|
|||||||
elif type == "tools":
|
elif type == "tools":
|
||||||
# 工具调用统计(按工具名称分组)
|
# 工具调用统计(按工具名称分组)
|
||||||
# 为工具调用创建独立的时间格式化器
|
# 为工具调用创建独立的时间格式化器
|
||||||
if time_range == "7hours":
|
if time_range == "14hours":
|
||||||
tool_group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(ToolCall.created_at, "+8 hours"))
|
tool_group_format = func.strftime("%Y-%m-%d %H:00", func.datetime(ToolCall.created_at, "+8 hours"))
|
||||||
elif time_range == "7weeks":
|
elif time_range == "14weeks":
|
||||||
tool_group_format = func.strftime("%Y-%W", func.datetime(ToolCall.created_at, "+8 hours"))
|
tool_group_format = func.strftime("%Y-%W", func.datetime(ToolCall.created_at, "+8 hours"))
|
||||||
else: # 7days
|
else: # 14days
|
||||||
tool_group_format = func.strftime("%Y-%m-%d", func.datetime(ToolCall.created_at, "+8 hours"))
|
tool_group_format = func.strftime("%Y-%m-%d", func.datetime(ToolCall.created_at, "+8 hours"))
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
@ -908,7 +908,7 @@ async def get_call_timeseries_stats(
|
|||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
date_key = result.date
|
date_key = result.date
|
||||||
if time_range == "7weeks":
|
if time_range == "14weeks":
|
||||||
date_key = normalize_week_key(date_key)
|
date_key = normalize_week_key(date_key)
|
||||||
category = getattr(result, "category", "unknown")
|
category = getattr(result, "category", "unknown")
|
||||||
count = result.count
|
count = result.count
|
||||||
@ -923,17 +923,17 @@ async def get_call_timeseries_stats(
|
|||||||
# 从起始点开始(北京时间)
|
# 从起始点开始(北京时间)
|
||||||
current_time = base_local_time
|
current_time = base_local_time
|
||||||
|
|
||||||
if time_range == "7hours":
|
if time_range == "14hours":
|
||||||
delta = timedelta(hours=1)
|
delta = timedelta(hours=1)
|
||||||
elif time_range == "7weeks":
|
elif time_range == "14weeks":
|
||||||
delta = timedelta(weeks=1)
|
delta = timedelta(weeks=1)
|
||||||
else:
|
else:
|
||||||
delta = timedelta(days=1)
|
delta = timedelta(days=1)
|
||||||
|
|
||||||
for i in range(intervals):
|
for i in range(intervals):
|
||||||
if time_range == "7hours":
|
if time_range == "14hours":
|
||||||
date_key = current_time.strftime("%Y-%m-%d %H:00")
|
date_key = current_time.strftime("%Y-%m-%d %H:00")
|
||||||
elif time_range == "7weeks":
|
elif time_range == "14weeks":
|
||||||
iso_year, iso_week, _ = current_time.isocalendar()
|
iso_year, iso_week, _ = current_time.isocalendar()
|
||||||
date_key = f"{iso_year}-{iso_week:02d}"
|
date_key = f"{iso_year}-{iso_week:02d}"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -123,10 +123,10 @@ export const dashboardApi = {
|
|||||||
/**
|
/**
|
||||||
* 获取调用统计时间序列数据
|
* 获取调用统计时间序列数据
|
||||||
* @param {string} type - 数据类型 (models/agents/tokens/tools)
|
* @param {string} type - 数据类型 (models/agents/tokens/tools)
|
||||||
* @param {string} timeRange - 时间范围 (7hours/7days/7weeks)
|
* @param {string} timeRange - 时间范围 (14hours/14days/14weeks)
|
||||||
* @returns {Promise<Object>} - 时间序列统计数据
|
* @returns {Promise<Object>} - 时间序列统计数据
|
||||||
*/
|
*/
|
||||||
getCallTimeseries: (type = 'models', timeRange = '7days') => {
|
getCallTimeseries: (type = 'models', timeRange = '14days') => {
|
||||||
return apiAdminGet(`/api/dashboard/stats/calls/timeseries?type=${type}&time_range=${timeRange}`)
|
return apiAdminGet(`/api/dashboard/stats/calls/timeseries?type=${type}&time_range=${timeRange}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,9 +45,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLoadingThreads || isLoadingMessages" class="chat-loading">
|
<!-- 加载状态:新建对话或加载消息 -->
|
||||||
<LoadingOutlined />
|
<div v-if="chatState.creatingNewChat" class="chat-loading">
|
||||||
<span>正在加载历史记录...</span>
|
<div class="loading-spinner"></div>
|
||||||
|
<span>正在创建新对话...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isLoadingMessages" class="chat-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span>正在加载消息...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!conversations.length" class="chat-examples">
|
<div v-else-if="!conversations.length" class="chat-examples">
|
||||||
@ -304,7 +309,7 @@ const isStreaming = computed(() => {
|
|||||||
const threadState = currentThreadState.value;
|
const threadState = currentThreadState.value;
|
||||||
return threadState ? threadState.isStreaming : false;
|
return threadState ? threadState.isStreaming : false;
|
||||||
});
|
});
|
||||||
const isProcessing = computed(() => isStreaming.value || chatState.creatingNewChat);
|
const isProcessing = computed(() => isStreaming.value);
|
||||||
const isSmallContainer = computed(() => uiState.containerWidth <= 520);
|
const isSmallContainer = computed(() => uiState.containerWidth <= 520);
|
||||||
const isMediumContainer = computed(() => uiState.containerWidth <= 768);
|
const isMediumContainer = computed(() => uiState.containerWidth <= 768);
|
||||||
|
|
||||||
@ -1218,10 +1223,23 @@ watch(conversations, () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
animation: slideInUp 0.5s ease-out;
|
animation: slideInUp 0.5s ease-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: 8px;
|
|
||||||
color: var(--gray-700);
|
color: var(--gray-700);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--gray-200);
|
||||||
|
border-top-color: var(--main-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,12 +51,12 @@ const props = defineProps({
|
|||||||
// state
|
// state
|
||||||
const callStatsData = ref(null)
|
const callStatsData = ref(null)
|
||||||
const callStatsLoading = ref(false)
|
const callStatsLoading = ref(false)
|
||||||
const callTimeRange = ref('7days')
|
const callTimeRange = ref('14days')
|
||||||
const callDataType = ref('models')
|
const callDataType = ref('models')
|
||||||
const timeRangeOptions = [
|
const timeRangeOptions = [
|
||||||
{ value: '7hours', label: '近7小时' },
|
{ value: '14hours', label: '近14小时' },
|
||||||
{ value: '7days', label: '近7天' },
|
{ value: '14days', label: '近14天' },
|
||||||
{ value: '7weeks', label: '近7周' },
|
{ value: '14weeks', label: '近14周' },
|
||||||
]
|
]
|
||||||
const dataTypeOptions = [
|
const dataTypeOptions = [
|
||||||
{ value: 'models', label: '模型调用' },
|
{ value: 'models', label: '模型调用' },
|
||||||
@ -64,6 +64,27 @@ const dataTypeOptions = [
|
|||||||
{ value: 'tokens', label: 'Token消耗' },
|
{ value: 'tokens', label: 'Token消耗' },
|
||||||
{ value: 'tools', label: '工具调用' },
|
{ value: 'tools', label: '工具调用' },
|
||||||
]
|
]
|
||||||
|
const isTokenView = computed(() => callDataType.value === 'tokens')
|
||||||
|
|
||||||
|
const formatTokenValue = (value) => {
|
||||||
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||||
|
return '0M'
|
||||||
|
}
|
||||||
|
const millionValue = value / 1_000_000
|
||||||
|
const absMillion = Math.abs(millionValue)
|
||||||
|
const decimalPlaces = absMillion >= 100 ? 0 : absMillion >= 10 ? 1 : 2
|
||||||
|
return `${millionValue.toFixed(decimalPlaces)}M`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatValueForDisplay = (value) => {
|
||||||
|
if (isTokenView.value) {
|
||||||
|
return formatTokenValue(value)
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value.toLocaleString()
|
||||||
|
}
|
||||||
|
return (value ?? 0).toString()
|
||||||
|
}
|
||||||
|
|
||||||
const switchTimeRange = (val) => {
|
const switchTimeRange = (val) => {
|
||||||
if (callTimeRange.value === val) return
|
if (callTimeRange.value === val) return
|
||||||
@ -131,9 +152,9 @@ const renderCallStatsChart = () => {
|
|||||||
|
|
||||||
const xAxisData = data.map(item => {
|
const xAxisData = data.map(item => {
|
||||||
const date = item.date
|
const date = item.date
|
||||||
if (callTimeRange.value === '7hours') {
|
if (callTimeRange.value === '14hours') {
|
||||||
return date.split(' ')[1]
|
return date.split(' ')[1]
|
||||||
} else if (callTimeRange.value === '7weeks') {
|
} else if (callTimeRange.value === '14weeks') {
|
||||||
return `第${date.split('-')[1]}周`
|
return `第${date.split('-')[1]}周`
|
||||||
} else {
|
} else {
|
||||||
return date.split('-').slice(1).join('-')
|
return date.split('-').slice(1).join('-')
|
||||||
@ -171,7 +192,11 @@ const renderCallStatsChart = () => {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLabel: { color: '#6b7280', fontSize: 12 },
|
axisLabel: {
|
||||||
|
color: '#6b7280',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: (value) => (isTokenView.value ? formatTokenValue(value) : value),
|
||||||
|
},
|
||||||
splitLine: { lineStyle: { color: '#f3f4f6' } }
|
splitLine: { lineStyle: { color: '#f3f4f6' } }
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -181,16 +206,18 @@ const renderCallStatsChart = () => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
textStyle: { color: '#374151', fontSize: 12 },
|
textStyle: { color: '#374151', fontSize: 12 },
|
||||||
formatter: (params) => {
|
formatter: (params) => {
|
||||||
|
if (!params?.length) return ''
|
||||||
let total = 0
|
let total = 0
|
||||||
let result = `${params[0].name}<br/>`
|
let result = `${params[0].name}<br/>`
|
||||||
params.forEach(param => {
|
params.forEach(param => {
|
||||||
total += param.value
|
total += param.value
|
||||||
const truncatedName = truncateLegend(param.seriesName)
|
const truncatedName = truncateLegend(param.seriesName)
|
||||||
result += `<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color}\"></span>`
|
result += `<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color}\"></span>`
|
||||||
result += `${truncatedName}: ${param.value}<br/>`
|
result += `${truncatedName}: ${formatValueForDisplay(param.value)}<br/>`
|
||||||
})
|
})
|
||||||
const labelMap = { models: '模型调用', agents: '智能体调用', tokens: 'Token消耗', tools: '工具调用' }
|
const labelMap = { models: '模型调用', agents: '智能体调用', tokens: 'Token消耗', tools: '工具调用' }
|
||||||
return `<div style=\"font-weight:bold;margin-bottom:5px\">${labelMap[callDataType.value]}</div>${result}<strong>总计: ${total}</strong>`
|
const formattedTotal = formatValueForDisplay(total)
|
||||||
|
return `<div style=\"font-weight:bold;margin-bottom:5px\">${labelMap[callDataType.value]}</div>${result}<strong>总计: ${formattedTotal}</strong>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user