📊 feat: add comprehensive model monitoring dashboard

This commit introduces a complete model monitoring system that provides
real-time insights into model performance and usage statistics.

##  Features Added

### Core Components
- **ModelMonitoringStats**: Four key metric cards displaying total models,
  active models, total requests, and average success rate
- **ModelMonitoringTable**: Interactive table with search, filtering, and
  pagination for detailed model data
- **useModelMonitoring**: Custom hook for data fetching and state management

### Dashboard Integration
- Added new "模型观测" (Model Monitoring) tab to main dashboard
- Integrated monitoring components with existing dashboard layout
- Maintained consistent UI/UX with shadcn/ui design system

### Data Processing
- Smart data aggregation from existing `/api/data/` endpoints
- Automatic calculation of success rates and average metrics
- Support for both admin and user-specific data views

### Interactive Features
- Real-time search by model name
- Business group filtering
- Pagination with 10 items per page
- Color-coded success rate indicators (green >95%, yellow 90-95%, red <90%)
- Refresh capability for up-to-date data

## 🔧 Technical Implementation

### Type Safety
- Added comprehensive TypeScript interfaces in `@/types/api.ts`
- Defined `ModelInfo`, `ModelMonitoringStats`, and related types

### Utility Functions
- Enhanced color utilities with `modelToColor()` for consistent model identification
- Improved formatters for quota, tokens, and percentage display
- Maintained existing utility function architecture

### Architecture
- Follows established patterns from dashboard components
- Reuses existing HTTP client and authentication utilities
- Consistent error handling and loading states

### Code Quality
- All components pass linter checks
- Proper import organization and formatting
- Responsive design for mobile compatibility

## 🎯 User Experience

### Visual Design
- Color-coded model indicators for quick identification
- Success rate visualization with icons and colors
- Clean table layout with proper spacing and typography
- Skeleton loading states for smooth UX

### Functionality
- Search models by name with instant filtering
- Filter by business groups for organized viewing
- Navigate through paginated results efficiently
- Refresh data manually when needed

## 📋 Files Modified

- `web/src/types/api.ts`: Added model monitoring type definitions
- `web/src/features/dashboard/hooks/use-model-monitoring.ts`: Core data hook
- `web/src/features/dashboard/components/model-monitoring-stats.tsx`: Stats cards
- `web/src/features/dashboard/components/model-monitoring-table.tsx`: Data table
- `web/src/features/dashboard/index.tsx`: Dashboard integration
- Various formatting and import organization improvements

This implementation provides a comprehensive solution for model monitoring
that aligns with the existing codebase architecture while delivering
powerful insights into model performance and usage patterns.
This commit is contained in:
t0ng7u
2025-09-26 02:41:46 +08:00
parent 456987a3d4
commit 99fcc354e3
18 changed files with 3606 additions and 230 deletions

270
web/src/lib/comparisons.ts Normal file
View File

@@ -0,0 +1,270 @@
/**
* 对象比较和差异检测工具函数
*/
export interface PropertyChange {
key: string
oldValue: any
newValue: any
}
/**
* 比较两个对象的属性,找出有变化的属性
* @param oldObject 旧对象
* @param newObject 新对象
* @returns 包含变化属性信息的数组
*/
export function compareObjects(
oldObject: Record<string, any>,
newObject: Record<string, any>
): PropertyChange[] {
const changedProperties: PropertyChange[] = []
// 比较两个对象的属性
for (const key in oldObject) {
if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
if (oldObject[key] !== newObject[key]) {
changedProperties.push({
key: key,
oldValue: oldObject[key],
newValue: newObject[key],
})
}
}
}
// 检查新对象中新增的属性
for (const key in newObject) {
if (newObject.hasOwnProperty(key) && !oldObject.hasOwnProperty(key)) {
changedProperties.push({
key: key,
oldValue: undefined,
newValue: newObject[key],
})
}
}
return changedProperties
}
/**
* 深度比较两个对象是否相等
* @param obj1 对象1
* @param obj2 对象2
* @returns 是否相等
*/
export function deepEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) return true
if (obj1 == null || obj2 == null) return false
if (typeof obj1 !== typeof obj2) return false
if (typeof obj1 !== 'object') return obj1 === obj2
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
for (const key of keys1) {
if (!keys2.includes(key)) return false
if (!deepEqual(obj1[key], obj2[key])) return false
}
return true
}
/**
* 获取对象的差异
* @param source 源对象
* @param target 目标对象
* @returns 差异对象
*/
export function getDifference(
source: Record<string, any>,
target: Record<string, any>
): Record<string, any> {
const diff: Record<string, any> = {}
// 检查修改和新增的属性
for (const key in target) {
if (!deepEqual(source[key], target[key])) {
diff[key] = target[key]
}
}
// 检查删除的属性设为undefined
for (const key in source) {
if (!(key in target)) {
diff[key] = undefined
}
}
return diff
}
/**
* 合并对象(深度合并)
* @param target 目标对象
* @param sources 源对象数组
* @returns 合并后的对象
*/
export function deepMerge(target: any, ...sources: any[]): any {
if (!sources.length) return target
const source = sources.shift()
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} })
deepMerge(target[key], source[key])
} else {
Object.assign(target, { [key]: source[key] })
}
}
}
return deepMerge(target, ...sources)
}
/**
* 检查是否为对象
* @param item 检查项
* @returns 是否为对象
*/
function isObject(item: any): boolean {
return item && typeof item === 'object' && !Array.isArray(item)
}
/**
* 克隆对象(深度克隆)
* @param obj 要克隆的对象
* @returns 克隆后的对象
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj.getTime()) as T
if (obj instanceof Array) {
return obj.map((item) => deepClone(item)) as T
}
if (typeof obj === 'object') {
const cloned = {} as T
Object.keys(obj).forEach((key) => {
;(cloned as any)[key] = deepClone((obj as any)[key])
})
return cloned
}
return obj
}
/**
* 检查对象是否为空
* @param obj 对象
* @returns 是否为空
*/
export function isEmpty(obj: any): boolean {
if (obj == null) return true
if (Array.isArray(obj)) return obj.length === 0
if (typeof obj === 'object') return Object.keys(obj).length === 0
if (typeof obj === 'string') return obj.trim().length === 0
return false
}
/**
* 选择对象的特定属性
* @param obj 源对象
* @param keys 要选择的属性键
* @returns 包含选定属性的新对象
*/
export function pick<T extends Record<string, any>, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>
keys.forEach((key) => {
if (key in obj) {
result[key] = obj[key]
}
})
return result
}
/**
* 省略对象的特定属性
* @param obj 源对象
* @param keys 要省略的属性键
* @returns 省略指定属性后的新对象
*/
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const result = { ...obj } as any
keys.forEach((key) => {
delete result[key]
})
return result
}
/**
* 扁平化嵌套对象
* @param obj 嵌套对象
* @param prefix 键前缀
* @returns 扁平化后的对象
*/
export function flatten(
obj: Record<string, any>,
prefix: string = ''
): Record<string, any> {
let flattened: Record<string, any> = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}.${key}` : key
if (
typeof obj[key] === 'object' &&
obj[key] !== null &&
!Array.isArray(obj[key])
) {
Object.assign(flattened, flatten(obj[key], newKey))
} else {
flattened[newKey] = obj[key]
}
}
}
return flattened
}
/**
* 反扁平化对象
* @param obj 扁平化的对象
* @returns 嵌套对象
*/
export function unflatten(obj: Record<string, any>): Record<string, any> {
const result: Record<string, any> = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const keys = key.split('.')
let current = result
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
if (!(k in current)) {
current[k] = {}
}
current = current[k]
}
current[keys[keys.length - 1]] = obj[key]
}
}
return result
}