mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 14:31:00 +00:00
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
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
ModelCall,
|
||||
LLMCall,
|
||||
EmbeddingCall,
|
||||
FeedbackRecord,
|
||||
FeedbackStats,
|
||||
} from '../types/monitoring';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { parseUTCTimestamp } from '../utils/dateUtils';
|
||||
|
||||
Reference in New Issue
Block a user