Files
LangBot/web/src/app/home/monitoring/hooks/useMonitoringData.ts
6mvp6 6bb73297e0 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
2026-03-30 00:05:27 +08:00

356 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from 'react';
import {
FilterState,
MonitoringData,
ModelCall,
LLMCall,
EmbeddingCall,
FeedbackRecord,
FeedbackStats,
} from '../types/monitoring';
import { backendClient } from '@/app/infra/http';
import { parseUTCTimestamp } from '../utils/dateUtils';
/**
* Custom hook for fetching and managing monitoring data
*/
export function useMonitoringData(filterState: FilterState) {
const [data, setData] = useState<MonitoringData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Memoize filter parameters to prevent unnecessary re-renders
const selectedBotsStr = useMemo(
() => JSON.stringify(filterState.selectedBots),
[filterState.selectedBots],
);
const selectedPipelinesStr = useMemo(
() => JSON.stringify(filterState.selectedPipelines),
[filterState.selectedPipelines],
);
const customDateRangeStr = useMemo(
() => JSON.stringify(filterState.customDateRange),
[filterState.customDateRange],
);
// Convert time range to datetime strings
const getTimeRange = useCallback(() => {
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]);
// Fetch data based on filters
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const { startTime, endTime } = getTimeRange();
const response = await backendClient.getMonitoringData({
botId:
filterState.selectedBots.length > 0
? filterState.selectedBots
: undefined,
pipelineId:
filterState.selectedPipelines.length > 0
? filterState.selectedPipelines
: undefined,
startTime,
endTime,
limit: 50,
});
// Transform the response to match MonitoringData interface
const transformedData: MonitoringData = {
overview: {
totalMessages: response.overview.total_messages,
llmCalls: response.overview.llm_calls,
embeddingCalls: response.overview.embedding_calls || 0,
modelCalls:
response.overview.model_calls || response.overview.llm_calls,
successRate: response.overview.success_rate,
activeSessions: response.overview.active_sessions,
},
messages: response.messages.map(
(msg: {
id: string;
timestamp: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
message_content: string;
session_id: string;
status: string;
level: string;
platform?: string;
user_id?: string;
runner_name?: string;
variables?: string;
}) => ({
id: msg.id,
timestamp: parseUTCTimestamp(msg.timestamp),
botId: msg.bot_id,
botName: msg.bot_name,
pipelineId: msg.pipeline_id,
pipelineName: msg.pipeline_name,
messageContent: msg.message_content,
sessionId: msg.session_id,
status: msg.status as 'success' | 'error' | 'pending',
level: msg.level as 'info' | 'warning' | 'error' | 'debug',
platform: msg.platform,
userId: msg.user_id,
runnerName: msg.runner_name,
variables: msg.variables,
}),
),
llmCalls: response.llmCalls.map(
(call: {
id: string;
timestamp: string;
model_name: string;
input_tokens: number;
output_tokens: number;
total_tokens: number;
duration: number;
cost?: number;
status: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
error_message?: string;
message_id?: string;
}) => ({
id: call.id,
timestamp: parseUTCTimestamp(call.timestamp),
modelName: call.model_name,
tokens: {
input: call.input_tokens,
output: call.output_tokens,
total: call.total_tokens,
},
duration: call.duration,
cost: call.cost,
status: call.status as 'success' | 'error',
botId: call.bot_id,
botName: call.bot_name,
pipelineId: call.pipeline_id,
pipelineName: call.pipeline_name,
errorMessage: call.error_message,
messageId: call.message_id,
}),
),
embeddingCalls: (response.embeddingCalls || []).map(
(call: {
id: string;
timestamp: string;
model_name: string;
prompt_tokens: number;
total_tokens: number;
duration: number;
input_count: number;
status: string;
error_message?: string;
knowledge_base_id?: string;
query_text?: string;
session_id?: string;
message_id?: string;
call_type?: string;
}) => ({
id: call.id,
timestamp: parseUTCTimestamp(call.timestamp),
modelName: call.model_name,
promptTokens: call.prompt_tokens,
totalTokens: call.total_tokens,
duration: call.duration,
inputCount: call.input_count,
status: call.status as 'success' | 'error',
errorMessage: call.error_message,
knowledgeBaseId: call.knowledge_base_id,
queryText: call.query_text,
sessionId: call.session_id,
messageId: call.message_id,
callType: call.call_type as 'embedding' | 'retrieve' | undefined,
}),
),
// Create merged modelCalls array from llmCalls and embeddingCalls
modelCalls: [] as ModelCall[], // Will be populated after transform
sessions: response.sessions.map(
(session: {
session_id: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
message_count: number;
last_activity: string;
start_time: string;
platform?: string;
user_id?: string;
}) => ({
sessionId: session.session_id,
botId: session.bot_id,
botName: session.bot_name,
pipelineId: session.pipeline_id,
pipelineName: session.pipeline_name,
messageCount: session.message_count,
duration:
parseUTCTimestamp(session.last_activity).getTime() -
parseUTCTimestamp(session.start_time).getTime(),
lastActivity: parseUTCTimestamp(session.last_activity),
startTime: parseUTCTimestamp(session.start_time),
platform: session.platform,
userId: session.user_id,
}),
),
errors: response.errors.map(
(error: {
id: string;
timestamp: string;
error_type: string;
error_message: string;
bot_id: string;
bot_name: string;
pipeline_id: string;
pipeline_name: string;
session_id?: string;
stack_trace?: string;
message_id?: string;
}) => ({
id: error.id,
timestamp: parseUTCTimestamp(error.timestamp),
errorType: error.error_type,
errorMessage: error.error_message,
botId: error.bot_id,
botName: error.bot_name,
pipelineId: error.pipeline_id,
pipelineName: error.pipeline_name,
sessionId: error.session_id,
stackTrace: error.stack_trace,
messageId: error.message_id,
}),
),
totalCount: {
messages: response.totalCount.messages,
llmCalls: response.totalCount.llmCalls,
embeddingCalls: response.totalCount.embeddingCalls || 0,
sessions: response.totalCount.sessions,
errors: response.totalCount.errors,
},
};
// Merge LLM calls and embedding calls into modelCalls
const llmModelCalls: ModelCall[] = transformedData.llmCalls.map(
(call: LLMCall): ModelCall => ({
id: call.id,
timestamp: call.timestamp,
modelName: call.modelName,
modelType: 'llm',
status: call.status,
duration: call.duration,
errorMessage: call.errorMessage,
messageId: call.messageId,
tokens: call.tokens,
cost: call.cost,
botId: call.botId,
botName: call.botName,
pipelineId: call.pipelineId,
pipelineName: call.pipelineName,
}),
);
const embeddingModelCalls: ModelCall[] =
transformedData.embeddingCalls.map(
(call: EmbeddingCall): ModelCall => ({
id: call.id,
timestamp: call.timestamp,
modelName: call.modelName,
modelType: 'embedding',
status: call.status,
duration: call.duration,
errorMessage: call.errorMessage,
messageId: call.messageId,
callType: call.callType,
promptTokens: call.promptTokens,
totalTokens: call.totalTokens,
inputCount: call.inputCount,
knowledgeBaseId: call.knowledgeBaseId,
queryText: call.queryText,
sessionId: call.sessionId,
}),
);
// Combine and sort by timestamp (newest first)
transformedData.modelCalls = [
...llmModelCalls,
...embeddingModelCalls,
].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
setData(transformedData);
} catch (err) {
setError(err as Error);
console.error('Failed to fetch monitoring data:', err);
} finally {
setLoading(false);
}
}, [getTimeRange, filterState.selectedBots, filterState.selectedPipelines]);
// Fetch data when filter state changes
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
selectedBotsStr,
selectedPipelinesStr,
filterState.timeRange,
customDateRangeStr,
]);
// Manual refetch function
const refetch = () => {
fetchData();
};
return {
data,
loading,
error,
refetch,
};
}