Files
LangBot/web/src/app/home/monitoring/hooks/useMonitoringData.ts
Guanchao Wang 5d9f6ec763 Feat/monitor (#1928)
* feat: add monitor

* feat: fix tab

* feat: work

* feat: not reliable monitor

* feat: enhance monitoring page layout with integrated filters and refresh button

* feat: add support for runner recording

* feat: add jump button & alignment

* feat: new

* fix: not show query variables in local agent

* fix: pnpm lint and python ruff check

* fix: ruff fromat

* chore: remove unnecessary migration

* style: optimize monitoring page layout and fix sticky filter issues

- Enhanced metric cards with gradient backgrounds and hover effects
- Increased traffic chart height from 200px to 300px
- Adjusted grid layout and spacing for better visual appeal
- Fixed sticky filter area to properly cover parent padding without transparent gaps
- Used negative margins and positioning to eliminate scrolling artifacts
- Matched padding/margins with other pages (pipelines, bots) for consistency
- Removed duplicate title/subtitle from page content
- Added cursor-pointer styling to tab triggers
- Removed border between tab list and tab content

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: apply prettier formatting to monitoring components

- Fixed indentation and spacing in MetricCard.tsx
- Fixed formatting in TrafficChart.tsx
- Applied prettier formatting to page.tsx

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: update HomeSidebar to trigger action on child selection and localize monitoring titles

* refactor: streamline LLM and embedding invocation methods

* feat: add embedding model monitor

* fix: database version

* chore: simplify pnpm-lock.yaml formatting

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 21:08:23 +08:00

353 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from 'react';
import {
FilterState,
MonitoringData,
ModelCall,
LLMCall,
EmbeddingCall,
} from '../types/monitoring';
import { backendClient } from '@/app/infra/http';
/**
* 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: new Date(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: new Date(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: new Date(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:
new Date(session.last_activity).getTime() -
new Date(session.start_time).getTime(),
lastActivity: new Date(session.last_activity),
startTime: new Date(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: new Date(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,
};
}