Files
akmon/uni_modules/ak-ai-news/components/AINewsDashboard.vue
2026-01-20 08:04:15 +08:00

899 lines
21 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Real-time AI News System Monitoring Dashboard -->
<template>
<view class="dashboard-container">
<!-- Header -->
<view class="dashboard-header">
<text class="dashboard-title">AI News System Dashboard</text>
<view class="status-indicator" :class="systemHealth.status">
<text class="status-text">{{ systemHealth.status.toUpperCase() }}</text>
<text class="health-score">{{ systemHealth.score }}/100</text>
</view>
</view>
<!-- Quick Stats -->
<view class="quick-stats">
<view class="stat-card">
<text class="stat-value">{{ formatNumber(stats.requests.total) }}</text>
<text class="stat-label">Total Requests</text>
<text class="stat-change" :class="getChangeClass(requestsChange)">
{{ formatChange(requestsChange) }}
</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ (stats.requests.successRate * 100).toFixed(1) }}%</text>
<text class="stat-label">Success Rate</text>
<text class="stat-change" :class="getChangeClass(successRateChange)">
{{ formatChange(successRateChange) }}
</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.timing.averageLatency.toFixed(0) }}ms</text>
<text class="stat-label">Avg Latency</text>
<text class="stat-change" :class="getChangeClass(-latencyChange)">
{{ formatChange(latencyChange) }}
</text>
</view>
<view class="stat-card">
<text class="stat-value">${{ stats.costs.total.toFixed(2) }}</text>
<text class="stat-label">Total Cost</text>
<text class="stat-change" :class="getChangeClass(-costChange)">
${{ costChange.toFixed(2) }}
</text>
</view>
</view>
<!-- Health Checks -->
<view class="health-section">
<text class="section-title">System Health Checks</text>
<view class="health-checks">
<view class="health-check"
v-for="(check, key) in systemHealth.checks"
:key="key"
:class="getHealthCheckClass(key, check)">
<text class="check-name">{{ formatCheckName(key) }}</text>
<text class="check-value">{{ formatCheckValue(key, check) }}</text>
<view class="check-indicator" :class="getHealthCheckClass(key, check)"></view>
</view>
</view>
</view>
<!-- Active Alerts -->
<view class="alerts-section" v-if="systemHealth.alerts.length > 0">
<text class="section-title">Active Alerts ({{ systemHealth.alerts.length }})</text>
<scroll-view class="alerts-list" scroll-y="true">
<view class="alert-item"
v-for="alert in systemHealth.alerts"
:key="alert.id"
:class="alert.severity">
<view class="alert-header">
<text class="alert-severity">{{ alert.severity.toUpperCase() }}</text>
<text class="alert-time">{{ formatTime(alert.timestamp) }}</text>
</view>
<text class="alert-message">{{ alert.message }}</text>
<text class="alert-source">Source: {{ alert.source }}</text>
</view>
</scroll-view>
</view>
<!-- Performance Charts -->
<view class="charts-section">
<text class="section-title">Performance Trends</text>
<!-- Response Time Chart -->
<view class="chart-container">
<text class="chart-title">Response Time (Last Hour)</text>
<view class="chart-area">
<canvas class="chart-canvas"
canvas-id="responseTimeChart"
@touchstart="onChartTouch"
@touchend="onChartTouchEnd"></canvas>
</view>
</view>
<!-- Request Volume Chart -->
<view class="chart-container">
<text class="chart-title">Request Volume</text>
<view class="chart-area">
<canvas class="chart-canvas"
canvas-id="requestVolumeChart"></canvas>
</view>
</view>
<!-- Cost Distribution -->
<view class="chart-container">
<text class="chart-title">Cost by Provider</text>
<view class="cost-breakdown">
<view class="cost-item" v-for="(cost, provider) in stats.costs.byProvider" :key="provider">
<view class="cost-bar-container">
<text class="provider-name">{{ provider }}</text>
<view class="cost-bar">
<view class="cost-fill"
:style="{ width: (cost / stats.costs.total * 100) + '%' }"></view>
</view>
<text class="cost-value">${{ cost.toFixed(2) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Optimization Recommendations -->
<view class="recommendations-section" v-if="recommendations.length > 0">
<text class="section-title">Optimization Recommendations</text>
<scroll-view class="recommendations-list" scroll-y="true">
<view class="recommendation-item"
v-for="(rec, index) in recommendations"
:key="index"
:class="rec.priority">
<view class="rec-header">
<text class="rec-type">{{ rec.type.toUpperCase() }}</text>
<text class="rec-priority">{{ rec.priority.toUpperCase() }}</text>
</view>
<text class="rec-description">{{ rec.description }}</text>
<view class="rec-impact">
<text class="impact-title">Expected Impact:</text>
<text v-if="rec.expectedImpact.performanceGain" class="impact-item">
🚀 {{ rec.expectedImpact.performanceGain }}
</text>
<text v-if="rec.expectedImpact.costSaving" class="impact-item">
💰 {{ rec.expectedImpact.costSaving }}
</text>
<text v-if="rec.expectedImpact.reliabilityImprovement" class="impact-item">
🛡 {{ rec.expectedImpact.reliabilityImprovement }}
</text>
</view>
<view class="rec-actions">
<button class="btn-apply" @click="applyRecommendation(rec)" :disabled="rec.applying">
{{ rec.applying ? 'Applying...' : 'Apply' }}
</button>
<button class="btn-dismiss" @click="dismissRecommendation(rec)">
Dismiss
</button>
</view>
</view>
</scroll-view>
</view>
<!-- Control Panel -->
<view class="controls-section">
<text class="section-title">Controls</text>
<view class="control-buttons">
<button class="control-btn" @click="refreshData" :disabled="isRefreshing">
{{ isRefreshing ? 'Refreshing...' : 'Refresh Data' }}
</button>
<button class="control-btn" @click="exportData">
Export Metrics
</button>
<button class="control-btn" @click="toggleAutoRefresh">
{{ autoRefresh ? 'Stop Auto-Refresh' : 'Start Auto-Refresh' }}
</button>
<button class="control-btn danger" @click="clearAlerts">
Clear Alerts
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
import {
AIPerformanceMonitor,
type PerformanceStats,
type SystemHealth,
type OptimizationRecommendation,
defaultPerformanceConfig
} from '../services/AIPerformanceMonitor.uts'
// Reactive data
const monitor = new AIPerformanceMonitor(defaultPerformanceConfig)
const stats = ref<PerformanceStats>({
timeRange: { start: 0, end: 0, duration: 0 },
requests: { total: 0, successful: 0, failed: 0, successRate: 0 },
timing: { averageLatency: 0, medianLatency: 0, p95Latency: 0, p99Latency: 0 },
costs: { total: 0, average: 0, byProvider: {} },
cache: { hitRate: 0, totalRequests: 0, hits: 0, misses: 0 },
errors: { byType: {}, byProvider: {}, topErrors: [] }
})
const systemHealth = ref<SystemHealth>({
status: 'healthy',
score: 100,
checks: {
apiConnectivity: true,
memoryUsage: 0,
errorRate: 0,
responseTime: 0,
costBudget: 0,
cacheEfficiency: 0
},
alerts: []
})
const recommendations = ref<OptimizationRecommendation[]>([])
const isRefreshing = ref(false)
const autoRefresh = ref(true)
let refreshInterval: number | null = null
// Previous values for change calculation
const previousStats = ref<PerformanceStats | null>(null)
// Computed properties for changes
const requestsChange = computed(() => {
if (!previousStats.value) return 0
return stats.value.requests.total - previousStats.value.requests.total
})
const successRateChange = computed(() => {
if (!previousStats.value) return 0
return stats.value.requests.successRate - previousStats.value.requests.successRate
})
const latencyChange = computed(() => {
if (!previousStats.value) return 0
return stats.value.timing.averageLatency - previousStats.value.timing.averageLatency
})
const costChange = computed(() => {
if (!previousStats.value) return 0
return stats.value.costs.total - previousStats.value.costs.total
})
// Lifecycle hooks
onMounted(async () => {
console.log('🚀 Starting monitoring dashboard...')
monitor.startMonitoring()
await refreshData()
if (autoRefresh.value) {
startAutoRefresh()
}
})
onUnmounted(() => {
console.log('🛑 Stopping monitoring dashboard...')
stopAutoRefresh()
monitor.stopMonitoring()
})
// Methods
const refreshData = async () => {
if (isRefreshing.value) return
isRefreshing.value = true
try {
// Store previous stats for change calculation
previousStats.value = { ...stats.value }
// Get fresh data
const now = Date.now()
const oneHourAgo = now - 3600000
stats.value = monitor.getPerformanceStats(oneHourAgo, now)
systemHealth.value = monitor.getSystemHealth()
recommendations.value = monitor.getOptimizationRecommendations()
console.log('📊 Dashboard data refreshed')
} catch (error) {
console.error('❌ Failed to refresh dashboard data:', error)
} finally {
isRefreshing.value = false
}
}
const startAutoRefresh = () => {
if (refreshInterval) return
refreshInterval = setInterval(async () => {
await refreshData()
}, 30000) // Refresh every 30 seconds
console.log('🔄 Auto-refresh started')
}
const stopAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
console.log('⏹️ Auto-refresh stopped')
}
}
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
const applyRecommendation = async (rec: OptimizationRecommendation) => {
rec.applying = true
try {
const result = await monitor.applyOptimizations([rec])
if (result.applied > 0) {
console.log('✅ Recommendation applied successfully')
// Remove from list
const index = recommendations.value.indexOf(rec)
if (index > -1) {
recommendations.value.splice(index, 1)
}
} else {
console.error('❌ Failed to apply recommendation')
}
} catch (error) {
console.error('💥 Error applying recommendation:', error)
} finally {
rec.applying = false
}
}
const dismissRecommendation = (rec: OptimizationRecommendation) => {
const index = recommendations.value.indexOf(rec)
if (index > -1) {
recommendations.value.splice(index, 1)
}
}
const exportData = () => {
try {
const data = monitor.exportPerformanceData('json')
// In real implementation, this would trigger a download
console.log('📤 Exporting performance data...')
console.log(data)
// For uni-app, you might want to save to local storage or share
uni.setStorageSync('ai-news-performance-data', data)
uni.showToast({
title: 'Data exported to storage',
icon: 'success'
})
} catch (error) {
console.error('❌ Failed to export data:', error)
uni.showToast({
title: 'Export failed',
icon: 'error'
})
}
}
const clearAlerts = () => {
systemHealth.value.alerts = []
uni.showToast({
title: 'Alerts cleared',
icon: 'success'
})
}
// Utility methods
const formatNumber = (num: number): string => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'
}
return num.toString()
}
const formatChange = (change: number): string => {
if (change === 0) return '0'
const sign = change > 0 ? '+' : ''
return `${sign}${change.toFixed(1)}`
}
const getChangeClass = (change: number): string => {
if (change > 0) return 'positive'
if (change < 0) return 'negative'
return 'neutral'
}
const formatCheckName = (key: string): string => {
return key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())
}
const formatCheckValue = (key: string, value: any): string => {
switch (key) {
case 'apiConnectivity':
return value ? 'Connected' : 'Disconnected'
case 'memoryUsage':
return (value * 100).toFixed(1) + '%'
case 'errorRate':
return (value * 100).toFixed(2) + '%'
case 'responseTime':
return value.toFixed(0) + 'ms'
case 'costBudget':
return (value * 100).toFixed(1) + '%'
case 'cacheEfficiency':
return (value * 100).toFixed(1) + '%'
default:
return String(value)
}
}
const getHealthCheckClass = (key: string, value: any): string => {
switch (key) {
case 'apiConnectivity':
return value ? 'healthy' : 'critical'
case 'memoryUsage':
return value > 0.8 ? 'critical' : value > 0.6 ? 'warning' : 'healthy'
case 'errorRate':
return value > 0.1 ? 'critical' : value > 0.05 ? 'warning' : 'healthy'
case 'responseTime':
return value > 5000 ? 'critical' : value > 3000 ? 'warning' : 'healthy'
case 'costBudget':
return value > 0.9 ? 'critical' : value > 0.7 ? 'warning' : 'healthy'
case 'cacheEfficiency':
return value < 0.3 ? 'warning' : value < 0.5 ? 'healthy' : 'excellent'
default:
return 'neutral'
}
}
const formatTime = (timestamp: number): string => {
const now = Date.now()
const diff = now - timestamp
if (diff < 60000) return 'Just now'
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'
return new Date(timestamp).toLocaleDateString()
}
// Chart event handlers
const onChartTouch = (e: any) => {
console.log('Chart touched:', e)
}
const onChartTouchEnd = (e: any) => {
console.log('Chart touch ended:', e)
}
</script>
<style scoped>
.dashboard-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.dashboard-title {
font-size: 24px;
font-weight: bold;
color: #333;
}
.status-indicator {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 20px;
border-radius: 8px;
min-width: 100px;
}
.status-indicator.healthy {
background-color: #e8f5e8;
border: 2px solid #4caf50;
}
.status-indicator.warning {
background-color: #fff3e0;
border: 2px solid #ff9800;
}
.status-indicator.critical {
background-color: #ffebee;
border: 2px solid #f44336;
}
.status-text {
font-weight: bold;
font-size: 14px;
}
.health-score {
font-size: 18px;
font-weight: bold;
margin-top: 4px;
}
.quick-stats {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.stat-value {
display: block;
font-size: 28px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.stat-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stat-change {
display: block;
font-size: 12px;
font-weight: bold;
}
.stat-change.positive {
color: #4caf50;
}
.stat-change.negative {
color: #f44336;
}
.stat-change.neutral {
color: #666;
}
.health-section,
.alerts-section,
.charts-section,
.recommendations-section,
.controls-section {
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.section-title {
display: block;
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.health-checks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.health-check {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #ddd;
}
.health-check.healthy {
background-color: #e8f5e8;
border-left-color: #4caf50;
}
.health-check.warning {
background-color: #fff3e0;
border-left-color: #ff9800;
}
.health-check.critical {
background-color: #ffebee;
border-left-color: #f44336;
}
.check-name {
font-weight: bold;
color: #333;
}
.check-value {
color: #666;
}
.alerts-list {
max-height: 300px;
}
.alert-item {
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #ddd;
}
.alert-item.info {
background-color: #e3f2fd;
border-left-color: #2196f3;
}
.alert-item.warning {
background-color: #fff3e0;
border-left-color: #ff9800;
}
.alert-item.error {
background-color: #ffebee;
border-left-color: #f44336;
}
.alert-item.critical {
background-color: #fce4ec;
border-left-color: #e91e63;
}
.alert-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.alert-severity {
font-weight: bold;
font-size: 12px;
}
.alert-time {
font-size: 12px;
color: #666;
}
.alert-message {
display: block;
margin-bottom: 4px;
color: #333;
}
.alert-source {
display: block;
font-size: 12px;
color: #666;
}
.chart-container {
margin-bottom: 30px;
}
.chart-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.chart-area {
height: 200px;
position: relative;
}
.chart-canvas {
width: 100%;
height: 100%;
}
.cost-breakdown {
display: flex;
flex-direction: column;
gap: 10px;
}
.cost-item {
display: flex;
align-items: center;
gap: 15px;
}
.cost-bar-container {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.provider-name {
min-width: 80px;
font-weight: bold;
color: #333;
}
.cost-bar {
flex: 1;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.cost-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #2196f3);
transition: width 0.3s ease;
}
.cost-value {
min-width: 60px;
text-align: right;
font-weight: bold;
color: #333;
}
.recommendations-list {
max-height: 400px;
}
.recommendation-item {
padding: 15px;
margin-bottom: 15px;
border-radius: 8px;
border-left: 4px solid #ddd;
}
.recommendation-item.low {
background-color: #f9f9f9;
border-left-color: #9e9e9e;
}
.recommendation-item.medium {
background-color: #fff3e0;
border-left-color: #ff9800;
}
.recommendation-item.high {
background-color: #ffebee;
border-left-color: #f44336;
}
.recommendation-item.critical {
background-color: #fce4ec;
border-left-color: #e91e63;
}
.rec-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.rec-type {
font-weight: bold;
font-size: 12px;
padding: 4px 8px;
background-color: #e0e0e0;
border-radius: 4px;
}
.rec-priority {
font-weight: bold;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
}
.rec-description {
display: block;
margin-bottom: 10px;
color: #333;
line-height: 1.4;
}
.rec-impact {
margin-bottom: 15px;
}
.impact-title {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.impact-item {
display: block;
margin-bottom: 2px;
color: #666;
font-size: 14px;
}
.rec-actions {
display: flex;
gap: 10px;
}
.btn-apply,
.btn-dismiss {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
}
.btn-apply {
background-color: #4caf50;
color: white;
}
.btn-apply:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.btn-dismiss {
background-color: #f5f5f5;
color: #333;
}
.control-buttons {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.control-btn {
padding: 12px 24px;
border: 2px solid #2196f3;
background-color: #2196f3;
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.control-btn:hover {
background-color: #1976d2;
border-color: #1976d2;
}
.control-btn:disabled {
background-color: #ccc;
border-color: #ccc;
cursor: not-allowed;
}
.control-btn.danger {
background-color: #f44336;
border-color: #f44336;
}
.control-btn.danger:hover {
background-color: #d32f2f;
border-color: #d32f2f;
}
</style>