feat(wecom): add user feedback support for WeChat Work AI Bot (#2078)

* feat(wecom): add user feedback support for WeChat Work AI Bot

This commit implements user feedback functionality (like/dislike) for
WeChat Work AI Bot conversations, including:

Backend changes:
- Add feedback_id and stream_id fields to WecomBotEvent
- Implement feedback event handling in WecomBotClient (api.py)
- Add StreamSessionManager._feedback_index for feedback_id lookup
- Add on_feedback decorator for custom feedback handlers
- Create MonitoringFeedback entity for database persistence
- Add dbm025 migration for monitoring_feedback table
- Implement FeedbackMonitor helper class
- Update all platform adapters with ap parameter support
- Update botmgr to pass bot_info for monitoring context

Frontend changes:
- Add FeedbackCard and FeedbackList components
- Add useFeedbackData hook for feedback data fetching
- Add feedback tab to monitoring page
- Add feedback types and interfaces
- Add i18n translations (zh-Hans, en-US)

Other changes:
- Update Dockerfile with Chinese mirror for faster builds
- Update docker-compose.yaml with network configuration
- Update .gitignore for docker data and backup files

Note: Known issues that need future improvement:
- feedback_type=3 (cancel) is recorded but not properly handled
- Duplicate feedback records are not deduplicated

* chore: remove unnecessary migration for new table will be created automatically

* chore: ruff format

* chore: prettier

* feat: add feedback handling support across multiple platform adapters

* fix(web): remove unused imports and variables in monitoring module

---------

Co-authored-by: 6mvp6 <13727783693@163.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
6mvp6
2026-03-30 20:23:52 +08:00
committed by GitHub
parent 921d12f596
commit 6e37aae636
18 changed files with 4721 additions and 1110 deletions

View File

@@ -0,0 +1,187 @@
'use client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
ThumbsUp,
ThumbsDown,
TrendingUp,
TrendingDown,
Minus,
} from 'lucide-react';
interface FeedbackCardProps {
title: string;
value: number | string;
subtitle?: string;
icon: React.ReactNode;
trend?: {
value: number;
direction: 'up' | 'down' | 'neutral';
};
variant?: 'default' | 'success' | 'warning' | 'danger';
loading?: boolean;
}
export function FeedbackCard({
title,
value,
subtitle,
icon,
trend,
variant = 'default',
loading = false,
}: FeedbackCardProps) {
const variantStyles = {
default: 'bg-white dark:bg-[#2a2a2e] border-gray-200 dark:border-gray-700',
success:
'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
warning:
'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800',
danger: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800',
};
const iconStyles = {
default: 'text-gray-500 dark:text-gray-400',
success: 'text-green-500 dark:text-green-400',
warning: 'text-yellow-500 dark:text-yellow-400',
danger: 'text-red-500 dark:text-red-400',
};
const trendStyles = {
up: 'text-green-500',
down: 'text-red-500',
neutral: 'text-gray-500',
};
if (loading) {
return (
<div
className={`p-6 rounded-xl border shadow-sm ${variantStyles.default} animate-pulse`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2" />
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-1" />
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24" />
</div>
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-lg" />
</div>
</div>
);
}
return (
<div
className={`p-6 rounded-xl border shadow-sm ${variantStyles[variant]}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
{title}
</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{value}
</p>
{subtitle && (
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
{subtitle}
</p>
)}
{trend && (
<div
className={`flex items-center mt-2 text-sm ${trendStyles[trend.direction]}`}
>
{trend.direction === 'up' && (
<TrendingUp className="w-4 h-4 mr-1" />
)}
{trend.direction === 'down' && (
<TrendingDown className="w-4 h-4 mr-1" />
)}
{trend.direction === 'neutral' && (
<Minus className="w-4 h-4 mr-1" />
)}
<span>
{trend.value > 0 ? '+' : ''}
{trend.value}%
</span>
</div>
)}
</div>
<div
className={`p-3 rounded-lg bg-gray-100 dark:bg-gray-800 ${iconStyles[variant]}`}
>
{icon}
</div>
</div>
</div>
);
}
interface FeedbackStatsProps {
stats: {
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
} | null;
loading?: boolean;
}
export function FeedbackStatsCards({ stats, loading }: FeedbackStatsProps) {
const { t } = useTranslation();
const cards = [
{
title: t('monitoring.feedback.totalFeedback'),
value: stats?.totalFeedback ?? 0,
icon: (
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
),
variant: 'default' as const,
},
{
title: t('monitoring.feedback.totalLikes'),
value: stats?.totalLikes ?? 0,
icon: <ThumbsUp className="w-6 h-6" />,
variant: 'success' as const,
},
{
title: t('monitoring.feedback.totalDislikes'),
value: stats?.totalDislikes ?? 0,
icon: <ThumbsDown className="w-6 h-6" />,
variant: 'danger' as const,
},
{
title: t('monitoring.feedback.satisfactionRate'),
value: stats ? `${stats.satisfactionRate}%` : '0%',
icon: (
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
</svg>
),
variant: (stats && stats.satisfactionRate >= 80
? 'success'
: stats && stats.satisfactionRate >= 50
? 'warning'
: 'danger') as 'default' | 'success' | 'warning' | 'danger',
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{cards.map((card, index) => (
<FeedbackCard
key={index}
title={card.title}
value={card.value}
icon={card.icon}
variant={card.variant}
loading={loading}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,275 @@
'use client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
ThumbsUp,
ThumbsDown,
ChevronRight,
ChevronDown,
ExternalLink,
} from 'lucide-react';
import { FeedbackRecord } from '../types/monitoring';
import { Button } from '@/components/ui/button';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
interface FeedbackListProps {
feedback: FeedbackRecord[];
loading?: boolean;
onViewMessage?: (messageId: string) => void;
}
export function FeedbackList({
feedback,
loading,
onViewMessage,
}: FeedbackListProps) {
const { t } = useTranslation();
const [expandedId, setExpandedId] = React.useState<string | null>(null);
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
if (loading) {
return (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
);
}
if (!feedback || feedback.length === 0) {
return (
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
<svg
className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<p className="text-base font-medium mb-2">
{t('monitoring.feedback.noFeedback')}
</p>
<p className="text-sm">
{t('monitoring.feedback.noFeedbackDescription')}
</p>
</div>
);
}
return (
<div className="space-y-4">
{feedback.map((item) => (
<div
key={item.id}
className={`border rounded-xl overflow-hidden hover:shadow-md transition-all duration-200 ${
item.feedbackType === 'like'
? 'border-green-200 dark:border-green-900'
: 'border-red-200 dark:border-red-900'
}`}
>
{/* Header */}
<div
className={`p-5 cursor-pointer transition-colors ${
item.feedbackType === 'like'
? 'hover:bg-green-50 dark:hover:bg-green-950/50 bg-green-50/50 dark:bg-green-950/30'
: 'hover:bg-red-50 dark:hover:bg-red-950/50 bg-red-50/50 dark:bg-red-950/30'
}`}
onClick={() => toggleExpand(item.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start flex-1">
{/* Expand Icon */}
<div className="mr-3 mt-0.5">
{expandedId === item.id ? (
<ChevronDown
className={`w-5 h-5 ${item.feedbackType === 'like' ? 'text-green-500' : 'text-red-500'}`}
/>
) : (
<ChevronRight
className={`w-5 h-5 ${item.feedbackType === 'like' ? 'text-green-500' : 'text-red-500'}`}
/>
)}
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{/* Feedback Type Icon */}
{item.feedbackType === 'like' ? (
<ThumbsUp className="w-5 h-5 text-green-500" />
) : (
<ThumbsDown className="w-5 h-5 text-red-500" />
)}
<span
className={`text-sm font-medium ${item.feedbackType === 'like' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
>
{item.feedbackType === 'like'
? t('monitoring.feedback.like')
: t('monitoring.feedback.dislike')}
</span>
{item.botName && (
<>
<span className="text-gray-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-400">
{item.botName}
</span>
</>
)}
{item.platform && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
{item.platform}
</span>
)}
</div>
{item.feedbackContent && (
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{item.feedbackContent}
</p>
)}
{item.inaccurateReasons &&
item.inaccurateReasons.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{item.inaccurateReasons.map((reason, idx) => (
<span
key={idx}
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400"
>
{reason}
</span>
))}
</div>
)}
</div>
</div>
{/* Timestamp */}
<div className="flex flex-col items-end gap-2 ml-4">
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
{item.timestamp.toLocaleString()}
</span>
</div>
</div>
</div>
{/* Expanded Details */}
{expandedId === item.id && (
<div
className={`border-t p-5 bg-white dark:bg-gray-900 ${
item.feedbackType === 'like'
? 'border-green-200 dark:border-green-900'
: 'border-red-200 dark:border-red-900'
}`}
>
<div className="space-y-4 pl-8 border-l-2 border-gray-200 dark:border-gray-700 ml-4">
{/* Context Info */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('monitoring.feedback.contextInfo')}
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
{item.botName && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.messageList.bot')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.botName}
</div>
</div>
)}
{item.pipelineName && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.messageList.pipeline')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.pipelineName}
</div>
</div>
)}
{item.sessionId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.sessions.sessionId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.sessionId}
</div>
</div>
)}
{item.userId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.userId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.userId}
</div>
</div>
)}
{item.messageId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.messageId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate flex items-center gap-1">
<span className="truncate">{item.messageId}</span>
{onViewMessage && (
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-xs shrink-0"
onClick={(e) => {
e.stopPropagation();
onViewMessage(item.messageId!);
}}
>
<ExternalLink className="w-3 h-3" />
</Button>
)}
</div>
</div>
)}
{item.streamId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.streamId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.streamId}
</div>
</div>
)}
</div>
</div>
{/* Feedback Content */}
{item.feedbackContent && (
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('monitoring.feedback.feedbackContent')}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap">
{item.feedbackContent}
</p>
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,192 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { httpClient } from '@/app/infra/http';
import { FeedbackRecord, FeedbackStats } from '../types/monitoring';
interface UseFeedbackDataParams {
botIds?: string[];
pipelineIds?: string[];
startTime?: string;
endTime?: string;
feedbackType?: 'like' | 'dislike';
limit?: number;
offset?: number;
}
interface RawFeedbackRecord {
id: string;
timestamp: string;
feedback_id: string;
feedback_type: number;
feedback_content?: string;
inaccurate_reasons?: string;
bot_id?: string;
bot_name?: string;
pipeline_id?: string;
pipeline_name?: string;
session_id?: string;
message_id?: string;
stream_id?: string;
user_id?: string;
platform?: string;
}
interface RawFeedbackStats {
total_feedback: number;
total_likes: number;
total_dislikes: number;
satisfaction_rate: number;
by_bot?: Array<{
bot_id: string;
bot_name: string;
total: number;
likes: number;
dislikes: number;
}>;
}
/**
* Custom hook for fetching and managing feedback data
*/
export function useFeedbackData(params: UseFeedbackDataParams = {}) {
const [feedback, setFeedback] = useState<FeedbackRecord[]>([]);
const [stats, setStats] = useState<FeedbackStats | null>(null);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const paramsStr = useMemo(() => JSON.stringify(params), [params]);
const fetchStats = useCallback(async () => {
try {
const queryParams = new URLSearchParams();
if (params.botIds) {
params.botIds.forEach((id) => queryParams.append('botId', id));
}
if (params.pipelineIds) {
params.pipelineIds.forEach((id) =>
queryParams.append('pipelineId', id),
);
}
if (params.startTime) {
queryParams.append('startTime', params.startTime);
}
if (params.endTime) {
queryParams.append('endTime', params.endTime);
}
const result = await httpClient.get<RawFeedbackStats>(
`/api/v1/monitoring/feedback/stats?${queryParams.toString()}`,
);
if (result) {
setStats({
totalFeedback: result.total_feedback,
totalLikes: result.total_likes,
totalDislikes: result.total_dislikes,
satisfactionRate: result.satisfaction_rate,
byBot: result.by_bot?.map((bot) => ({
botId: bot.bot_id,
botName: bot.bot_name,
totalFeedback: bot.total,
totalLikes: bot.likes,
totalDislikes: bot.dislikes,
satisfactionRate:
bot.total > 0 ? Math.round((bot.likes / bot.total) * 100) : 0,
})),
});
}
} catch (err) {
console.error('Failed to fetch feedback stats:', err);
}
}, [params.botIds, params.pipelineIds, params.startTime, params.endTime]);
const fetchFeedback = useCallback(async () => {
setLoading(true);
setError(null);
try {
const queryParams = new URLSearchParams();
if (params.botIds) {
params.botIds.forEach((id) => queryParams.append('botId', id));
}
if (params.pipelineIds) {
params.pipelineIds.forEach((id) =>
queryParams.append('pipelineId', id),
);
}
if (params.startTime) {
queryParams.append('startTime', params.startTime);
}
if (params.endTime) {
queryParams.append('endTime', params.endTime);
}
if (params.feedbackType) {
queryParams.append(
'feedbackType',
params.feedbackType === 'like' ? '1' : '2',
);
}
if (params.limit) {
queryParams.append('limit', params.limit.toString());
}
if (params.offset) {
queryParams.append('offset', params.offset.toString());
}
const result = await httpClient.get<{
feedback: RawFeedbackRecord[];
total: number;
}>(`/api/v1/monitoring/feedback?${queryParams.toString()}`);
if (result) {
const transformedFeedback: FeedbackRecord[] = result.feedback.map(
(item) => ({
id: item.id,
timestamp: new Date(item.timestamp),
feedbackId: item.feedback_id,
feedbackType: item.feedback_type === 1 ? 'like' : 'dislike',
feedbackContent: item.feedback_content,
inaccurateReasons: item.inaccurate_reasons
? JSON.parse(item.inaccurate_reasons)
: undefined,
botId: item.bot_id,
botName: item.bot_name,
pipelineId: item.pipeline_id,
pipelineName: item.pipeline_name,
sessionId: item.session_id,
messageId: item.message_id,
streamId: item.stream_id,
userId: item.user_id,
platform: item.platform,
}),
);
setFeedback(transformedFeedback);
setTotal(result.total);
}
} catch (err) {
setError(err as Error);
console.error('Failed to fetch feedback:', err);
} finally {
setLoading(false);
}
}, [params]);
const refetch = useCallback(() => {
fetchStats();
fetchFeedback();
}, [fetchStats, fetchFeedback]);
useEffect(() => {
refetch();
}, [paramsStr]);
return {
feedback,
stats,
total,
loading,
error,
refetch,
};
}

View File

@@ -1,6 +1,6 @@
'use client';
import React, { Suspense, useState } from 'react';
import React, { Suspense, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
@@ -10,8 +10,11 @@ import MonitoringFilters from './components/filters/MonitoringFilters';
import { ExportDropdown } from './components/ExportDropdown';
import { useMonitoringFilters } from './hooks/useMonitoringFilters';
import { useMonitoringData } from './hooks/useMonitoringData';
import { useFeedbackData } from './hooks/useFeedbackData';
import { MessageDetailsCard } from './components/MessageDetailsCard';
import { MessageContentRenderer } from './components/MessageContentRenderer';
import { FeedbackStatsCards } from './components/FeedbackCard';
import { FeedbackList } from './components/FeedbackList';
import { MessageDetails } from './types/monitoring';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
@@ -68,6 +71,64 @@ function MonitoringPageContent() {
useMonitoringFilters();
const { data, loading, refetch } = useMonitoringData(filterState);
// Get time range for feedback data
const feedbackTimeRange = useMemo(() => {
const now = new Date();
let startTime: Date | null = null;
switch (filterState.timeRange) {
case 'lastHour':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
break;
case 'last6Hours':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case 'last24Hours':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case 'last7Days':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'last30Days':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'custom':
if (filterState.customDateRange) {
startTime = filterState.customDateRange.from;
}
break;
}
const endTime =
filterState.timeRange === 'custom' && filterState.customDateRange
? filterState.customDateRange.to
: now;
return {
startTime: startTime?.toISOString(),
endTime: endTime.toISOString(),
};
}, [filterState.timeRange, filterState.customDateRange]);
// Feedback data hook
const {
feedback: feedbackList,
stats: feedbackStats,
loading: feedbackLoading,
} = useFeedbackData({
botIds:
filterState.selectedBots.length > 0
? filterState.selectedBots
: undefined,
pipelineIds:
filterState.selectedPipelines.length > 0
? filterState.selectedPipelines
: undefined,
startTime: feedbackTimeRange.startTime,
endTime: feedbackTimeRange.endTime,
limit: 50,
});
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
null,
);
@@ -249,6 +310,9 @@ function MonitoringPageContent() {
<TabsTrigger value="modelCalls" className="px-6 py-2">
{t('monitoring.tabs.modelCalls')}
</TabsTrigger>
<TabsTrigger value="feedback" className="px-6 py-2">
{t('monitoring.tabs.feedback')}
</TabsTrigger>
<TabsTrigger value="errors" className="px-6 py-2">
{t('monitoring.tabs.errors')}
</TabsTrigger>
@@ -609,6 +673,38 @@ function MonitoringPageContent() {
</div>
</TabsContent>
<TabsContent value="feedback" className="p-6 m-0">
<div>
{loading && (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
{!loading && (
<>
{/* Feedback Stats Cards */}
<div className="mb-6">
<FeedbackStatsCards
stats={feedbackStats}
loading={feedbackLoading}
/>
</div>
{/* Feedback List */}
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('monitoring.feedback.feedbackList')}
</h3>
<FeedbackList
feedback={feedbackList}
loading={feedbackLoading}
onViewMessage={jumpToMessage}
/>
</>
)}
</div>
</TabsContent>
<TabsContent value="errors" className="p-6 m-0">
<div>
{loading && (

View File

@@ -162,6 +162,39 @@ export interface DateRange {
to: Date;
}
export interface FeedbackRecord {
id: string;
timestamp: Date;
feedbackId: string;
feedbackType: 'like' | 'dislike';
feedbackContent?: string;
inaccurateReasons?: string[];
botId?: string;
botName?: string;
pipelineId?: string;
pipelineName?: string;
sessionId?: string;
messageId?: string;
streamId?: string;
userId?: string;
platform?: string;
}
export interface FeedbackStats {
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
byBot?: Array<{
botId: string;
botName: string;
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
}>;
}
export interface MonitoringData {
overview: OverviewMetrics;
messages: MonitoringMessage[];
@@ -170,11 +203,14 @@ export interface MonitoringData {
modelCalls: ModelCall[];
sessions: SessionInfo[];
errors: ErrorLog[];
feedback?: FeedbackRecord[];
feedbackStats?: FeedbackStats;
totalCount: {
messages: number;
llmCalls: number;
embeddingCalls: number;
sessions: number;
errors: number;
feedback?: number;
};
}