636 lines
19 KiB
Vue
636 lines
19 KiB
Vue
<template>
|
|
<v-card
|
|
color="indigo-darken-1"
|
|
variant="tonal"
|
|
class="h-100 d-flex flex-column"
|
|
>
|
|
<v-card-title class="d-flex align-center justify-space-between">
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-robot" class="mr-2"></v-icon>
|
|
<span class="text-subtitle-1 font-weight-bold">AI Pipeline Monitor</span>
|
|
</div>
|
|
<div class="d-flex align-center">
|
|
<v-chip size="small" color="green" class="mr-2">
|
|
<v-icon icon="mdi-pulse" size="small" class="mr-1"></v-icon>
|
|
Live
|
|
</v-chip>
|
|
<v-chip size="small" :color="connectionStatusColor">
|
|
<v-icon :icon="connectionStatusIcon" size="small" class="mr-1"></v-icon>
|
|
{{ connectionStatusText }}
|
|
</v-chip>
|
|
</div>
|
|
</v-card-title>
|
|
|
|
<v-card-text class="flex-grow-1 pa-0">
|
|
<!-- Connection Status Bar -->
|
|
<div class="px-4 pt-2 pb-1" :class="connectionStatusBarClass">
|
|
<div class="d-flex align-center justify-space-between">
|
|
<div class="text-caption">
|
|
<v-icon :icon="connectionStatusIcon" size="small" class="mr-1"></v-icon>
|
|
{{ connectionStatusMessage }}
|
|
</div>
|
|
<div class="text-caption">
|
|
Polling: {{ pollingInterval / 1000 }}s
|
|
<v-btn
|
|
icon="mdi-refresh"
|
|
size="x-small"
|
|
variant="text"
|
|
class="ml-1"
|
|
@click="forceRefresh"
|
|
:loading="isRefreshing"
|
|
></v-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Robot Status Dashboard -->
|
|
<div class="pa-4">
|
|
<div class="text-subtitle-2 font-weight-medium mb-2">Robot Status Dashboard</div>
|
|
|
|
<!-- Geographical Filter -->
|
|
<div class="mb-3">
|
|
<v-chip-group v-model="selectedRegion" column>
|
|
<v-chip size="small" value="all">All Regions</v-chip>
|
|
<v-chip size="small" value="GB">UK (GB)</v-chip>
|
|
<v-chip size="small" value="EU">Europe</v-chip>
|
|
<v-chip size="small" value="US">North America</v-chip>
|
|
<v-chip size="small" value="OC">Oceania</v-chip>
|
|
</v-chip-group>
|
|
</div>
|
|
|
|
<!-- Robot Status Cards -->
|
|
<v-row dense class="mb-4">
|
|
<v-col v-for="robot in filteredRobots" :key="robot.id" cols="12" sm="6">
|
|
<v-card variant="outlined" class="pa-2">
|
|
<div class="d-flex align-center justify-space-between">
|
|
<div>
|
|
<div class="d-flex align-center">
|
|
<v-icon :icon="robot.icon" size="small" :color="robot.statusColor" class="mr-2"></v-icon>
|
|
<span class="text-caption font-weight-medium">{{ robot.name }}</span>
|
|
</div>
|
|
<div class="text-caption text-grey mt-1">{{ robot.description }}</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-caption" :class="`text-${robot.statusColor}`">{{ robot.status }}</div>
|
|
<div class="text-caption text-grey">{{ robot.region }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<v-progress-linear
|
|
v-if="robot.progress !== undefined"
|
|
:model-value="robot.progress"
|
|
height="6"
|
|
:color="robot.progressColor"
|
|
class="mt-2"
|
|
></v-progress-linear>
|
|
|
|
<!-- Stats -->
|
|
<div class="d-flex justify-space-between mt-2">
|
|
<div class="text-caption">
|
|
<v-icon icon="mdi-check-circle" size="x-small" color="success" class="mr-1"></v-icon>
|
|
{{ robot.successRate }}%
|
|
</div>
|
|
<div class="text-caption">
|
|
<v-icon icon="mdi-alert-circle" size="x-small" color="error" class="mr-1"></v-icon>
|
|
{{ robot.failureRate }}%
|
|
</div>
|
|
<div class="text-caption">
|
|
<v-icon icon="mdi-clock-outline" size="x-small" color="warning" class="mr-1"></v-icon>
|
|
{{ robot.avgTime }}s
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Overall Pipeline Stats -->
|
|
<v-card variant="outlined" class="pa-3 mb-4">
|
|
<div class="text-subtitle-2 font-weight-medium mb-2">Pipeline Overview</div>
|
|
<v-row dense>
|
|
<v-col cols="6" sm="3">
|
|
<div class="text-center">
|
|
<div class="text-h5 font-weight-bold text-primary">{{ pipelineStats.totalProcessed }}</div>
|
|
<div class="text-caption text-grey">Total Processed</div>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="6" sm="3">
|
|
<div class="text-center">
|
|
<div class="text-h5 font-weight-bold text-success">{{ pipelineStats.successRate }}%</div>
|
|
<div class="text-caption text-grey">Success Rate</div>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="6" sm="3">
|
|
<div class="text-center">
|
|
<div class="text-h5 font-weight-bold text-warning">{{ pipelineStats.avgProcessingTime }}s</div>
|
|
<div class="text-caption text-grey">Avg Time</div>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="6" sm="3">
|
|
<div class="text-center">
|
|
<div class="text-h5 font-weight-bold" :class="pipelineStats.queueSize > 100 ? 'text-error' : 'text-info'">{{ pipelineStats.queueSize }}</div>
|
|
<div class="text-caption text-grey">Queue Size</div>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="text-subtitle-2 font-weight-medium mb-2">Recent Activity</div>
|
|
<div class="log-entries-container pa-2" ref="logContainer" style="height: 150px;">
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading && logs.length === 0" class="text-center py-4">
|
|
<v-progress-circular indeterminate color="primary" size="20"></v-progress-circular>
|
|
<div class="text-caption mt-1">Loading AI logs...</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="logs.length === 0" class="text-center py-4">
|
|
<v-icon icon="mdi-robot-off" size="32" color="grey-lighten-1"></v-icon>
|
|
<div class="text-body-2 mt-1">No AI activity</div>
|
|
</div>
|
|
|
|
<!-- Log Entries -->
|
|
<div v-else class="log-entries">
|
|
<div
|
|
v-for="(log, index) in visibleLogs"
|
|
:key="log.id"
|
|
class="log-entry mb-2 pa-2"
|
|
:class="{ 'new-entry': log.isNew }"
|
|
>
|
|
<div class="d-flex align-center">
|
|
<v-icon
|
|
:color="getLogColor(log.type)"
|
|
:icon="getLogIcon(log.type)"
|
|
size="small"
|
|
class="mr-2"
|
|
></v-icon>
|
|
<div class="flex-grow-1">
|
|
<div class="text-caption">{{ log.message }}</div>
|
|
<div class="d-flex align-center mt-1">
|
|
<v-chip size="x-small" :color="getRobotColor(log.robot)" class="mr-1">
|
|
{{ log.robot }}
|
|
</v-chip>
|
|
<span class="text-caption text-grey">{{ formatTime(log.timestamp) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="pa-3">
|
|
<div class="d-flex justify-space-between align-center w-100">
|
|
<div class="text-caption">
|
|
<v-icon icon="mdi-robot" size="small" class="mr-1"></v-icon>
|
|
{{ activeRobots }} active • {{ filteredRobots.length }} filtered
|
|
</div>
|
|
|
|
<div class="d-flex">
|
|
<v-btn
|
|
size="x-small"
|
|
variant="text"
|
|
@click="toggleAutoScroll"
|
|
:color="autoScroll ? 'primary' : 'grey'"
|
|
>
|
|
<v-icon :icon="autoScroll ? 'mdi-pin' : 'mdi-pin-off'" size="small" class="mr-1"></v-icon>
|
|
{{ autoScroll ? 'Auto-scroll' : 'Manual' }}
|
|
</v-btn>
|
|
</div>
|
|
</div>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
|
// Types
|
|
interface AiLogEntry {
|
|
id: string
|
|
timestamp: Date
|
|
message: string
|
|
type: 'info' | 'success' | 'warning' | 'error' | 'gold'
|
|
robot: string
|
|
vehicleId?: string
|
|
status?: string
|
|
details?: string
|
|
isNew?: boolean
|
|
}
|
|
|
|
interface RobotStatus {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
region: string
|
|
status: 'running' | 'idle' | 'error' | 'paused'
|
|
statusColor: string
|
|
icon: string
|
|
progress?: number
|
|
progressColor: string
|
|
successRate: number
|
|
failureRate: number
|
|
avgTime: number
|
|
lastActivity: Date
|
|
}
|
|
|
|
// State
|
|
const logs = ref<AiLogEntry[]>([])
|
|
const isLoading = ref(true)
|
|
const isRefreshing = ref(false)
|
|
const autoScroll = ref(true)
|
|
const pollingInterval = ref(5000) // 5 seconds
|
|
const connectionStatus = ref<'connected' | 'disconnected' | 'error'>('connected')
|
|
const logContainer = ref<HTMLElement | null>(null)
|
|
const selectedRegion = ref('all')
|
|
|
|
// Robot status data
|
|
const robots = ref<RobotStatus[]>([
|
|
{
|
|
id: 'gb-discovery',
|
|
name: 'GB Discovery',
|
|
description: 'UK catalog discovery from MOT CSV',
|
|
region: 'GB',
|
|
status: 'running',
|
|
statusColor: 'success',
|
|
icon: 'mdi-magnify',
|
|
progress: 75,
|
|
progressColor: 'primary',
|
|
successRate: 92,
|
|
failureRate: 3,
|
|
avgTime: 45,
|
|
lastActivity: new Date()
|
|
},
|
|
{
|
|
id: 'gb-hunter',
|
|
name: 'GB Hunter',
|
|
description: 'DVLA API vehicle data fetcher',
|
|
region: 'GB',
|
|
status: 'running',
|
|
statusColor: 'success',
|
|
icon: 'mdi-target',
|
|
progress: 60,
|
|
progressColor: 'indigo',
|
|
successRate: 88,
|
|
failureRate: 5,
|
|
avgTime: 12,
|
|
lastActivity: new Date()
|
|
},
|
|
{
|
|
id: 'nhtsa-fetcher',
|
|
name: 'NHTSA Fetcher',
|
|
description: 'US vehicle specifications',
|
|
region: 'US',
|
|
status: 'idle',
|
|
statusColor: 'warning',
|
|
icon: 'mdi-database-import',
|
|
progress: 0,
|
|
progressColor: 'orange',
|
|
successRate: 95,
|
|
failureRate: 2,
|
|
avgTime: 8,
|
|
lastActivity: new Date(Date.now() - 3600000) // 1 hour ago
|
|
},
|
|
{
|
|
id: 'system-ocr',
|
|
name: 'System OCR',
|
|
description: 'Document processing AI',
|
|
region: 'all',
|
|
status: 'running',
|
|
statusColor: 'success',
|
|
icon: 'mdi-text-recognition',
|
|
progress: 90,
|
|
progressColor: 'green',
|
|
successRate: 85,
|
|
failureRate: 8,
|
|
avgTime: 25,
|
|
lastActivity: new Date()
|
|
},
|
|
{
|
|
id: 'rdw-enricher',
|
|
name: 'RDW Enricher',
|
|
description: 'Dutch vehicle data',
|
|
region: 'EU',
|
|
status: 'error',
|
|
statusColor: 'error',
|
|
icon: 'mdi-alert-circle',
|
|
progress: 30,
|
|
progressColor: 'red',
|
|
successRate: 78,
|
|
failureRate: 15,
|
|
avgTime: 18,
|
|
lastActivity: new Date(Date.now() - 1800000) // 30 minutes ago
|
|
},
|
|
{
|
|
id: 'alchemist-pro',
|
|
name: 'Alchemist Pro',
|
|
description: 'Gold status optimizer',
|
|
region: 'all',
|
|
status: 'running',
|
|
statusColor: 'success',
|
|
icon: 'mdi-star',
|
|
progress: 85,
|
|
progressColor: 'amber',
|
|
successRate: 96,
|
|
failureRate: 1,
|
|
avgTime: 32,
|
|
lastActivity: new Date()
|
|
}
|
|
])
|
|
|
|
// Mock data generator
|
|
const generateMockLog = (): AiLogEntry => {
|
|
const robotList = robots.value.map(r => r.name)
|
|
const types: AiLogEntry['type'][] = ['info', 'success', 'warning', 'error', 'gold']
|
|
const messages = [
|
|
'Vehicle #4521 changed to Gold Status',
|
|
'New vehicle discovered in UK catalog',
|
|
'DVLA API quota limit reached',
|
|
'OCR processing completed for invoice #789',
|
|
'Service validation failed - missing coordinates',
|
|
'Price comparison completed for 15 services',
|
|
'Vehicle technical data enriched successfully',
|
|
'Database synchronization in progress',
|
|
'AI model training completed',
|
|
'Real-time monitoring activated'
|
|
]
|
|
|
|
const robot = robotList[Math.floor(Math.random() * robotList.length)]
|
|
const type = types[Math.floor(Math.random() * types.length)]
|
|
const message = messages[Math.floor(Math.random() * messages.length)]
|
|
|
|
return {
|
|
id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
timestamp: new Date(),
|
|
message,
|
|
type,
|
|
robot,
|
|
vehicleId: type === 'gold' ? `#${Math.floor(Math.random() * 10000)}` : undefined,
|
|
status: type === 'gold' ? 'GOLD' : type === 'success' ? 'SUCCESS' : type === 'error' ? 'FAILED' : 'PROCESSING',
|
|
details: type === 'error' ? 'API timeout after 30 seconds' : undefined,
|
|
isNew: true
|
|
}
|
|
}
|
|
|
|
// Computed properties
|
|
const filteredRobots = computed(() => {
|
|
if (selectedRegion.value === 'all') return robots.value
|
|
return robots.value.filter(robot => robot.region === selectedRegion.value || robot.region === 'all')
|
|
})
|
|
|
|
const visibleLogs = computed(() => {
|
|
// Show latest 5 logs
|
|
return [...logs.value].slice(-5).reverse()
|
|
})
|
|
|
|
const activeRobots = computed(() => {
|
|
return robots.value.filter(r => r.status === 'running').length
|
|
})
|
|
|
|
const pipelineStats = computed(() => {
|
|
const totalRobots = robots.value.length
|
|
const runningRobots = robots.value.filter(r => r.status === 'running').length
|
|
const totalSuccessRate = robots.value.reduce((sum, r) => sum + r.successRate, 0) / totalRobots
|
|
const totalAvgTime = robots.value.reduce((sum, r) => sum + r.avgTime, 0) / totalRobots
|
|
|
|
return {
|
|
totalProcessed: Math.floor(Math.random() * 10000) + 5000,
|
|
successRate: Math.round(totalSuccessRate),
|
|
avgProcessingTime: Math.round(totalAvgTime),
|
|
queueSize: Math.floor(Math.random() * 200),
|
|
runningRobots,
|
|
totalRobots
|
|
}
|
|
})
|
|
|
|
const connectionStatusColor = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case 'connected': return 'green'
|
|
case 'disconnected': return 'orange'
|
|
case 'error': return 'red'
|
|
default: return 'grey'
|
|
}
|
|
})
|
|
|
|
const connectionStatusIcon = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case 'connected': return 'mdi-check-circle'
|
|
case 'disconnected': return 'mdi-alert-circle'
|
|
case 'error': return 'mdi-close-circle'
|
|
default: return 'mdi-help-circle'
|
|
}
|
|
})
|
|
|
|
const connectionStatusText = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case 'connected': return 'Connected'
|
|
case 'disconnected': return 'Disconnected'
|
|
case 'error': return 'Error'
|
|
default: return 'Unknown'
|
|
}
|
|
})
|
|
|
|
const connectionStatusMessage = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case 'connected': return 'Connected to AI logs stream'
|
|
case 'disconnected': return 'Disconnected - using mock data'
|
|
case 'error': return 'Connection error - check API endpoint'
|
|
default: return 'Status unknown'
|
|
}
|
|
})
|
|
|
|
const connectionStatusBarClass = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case 'connected': return 'bg-green-lighten-5'
|
|
case 'disconnected': return 'bg-orange-lighten-5'
|
|
case 'error': return 'bg-red-lighten-5'
|
|
default: return 'bg-grey-lighten-5'
|
|
}
|
|
})
|
|
|
|
// Helper functions
|
|
// Helper functions
|
|
const getLogColor = (type: AiLogEntry['type']) => {
|
|
switch (type) {
|
|
case 'info': return 'blue'
|
|
case 'success': return 'green'
|
|
case 'warning': return 'orange'
|
|
case 'error': return 'red'
|
|
case 'gold': return 'amber'
|
|
default: return 'grey'
|
|
}
|
|
}
|
|
|
|
const getLogIcon = (type: AiLogEntry['type']) => {
|
|
switch (type) {
|
|
case 'info': return 'mdi-information'
|
|
case 'success': return 'mdi-check-circle'
|
|
case 'warning': return 'mdi-alert'
|
|
case 'error': return 'mdi-alert-circle'
|
|
case 'gold': return 'mdi-star'
|
|
default: return 'mdi-help-circle'
|
|
}
|
|
}
|
|
|
|
const getRobotColor = (robotName: string) => {
|
|
const robot = robots.value.find(r => r.name === robotName)
|
|
return robot?.statusColor || 'grey'
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'running': return 'success'
|
|
case 'idle': return 'warning'
|
|
case 'error': return 'error'
|
|
case 'paused': return 'grey'
|
|
default: return 'grey'
|
|
}
|
|
}
|
|
|
|
const formatTime = (timestamp: Date) => {
|
|
const now = new Date()
|
|
const diff = now.getTime() - timestamp.getTime()
|
|
|
|
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 timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
// Data fetching and polling
|
|
const fetchLogs = async () => {
|
|
if (isRefreshing.value) return
|
|
|
|
isRefreshing.value = true
|
|
try {
|
|
// Simulate API call
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
|
// Add new mock log
|
|
const newLog = generateMockLog()
|
|
logs.value.push(newLog)
|
|
|
|
// Keep only last 50 logs
|
|
if (logs.value.length > 50) {
|
|
logs.value = logs.value.slice(-50)
|
|
}
|
|
|
|
// Mark old logs as not new
|
|
setTimeout(() => {
|
|
logs.value.forEach(log => {
|
|
if (log.isNew && Date.now() - log.timestamp.getTime() > 5000) {
|
|
log.isNew = false
|
|
}
|
|
})
|
|
}, 5000)
|
|
|
|
// Update connection status randomly
|
|
if (Math.random() > 0.95) {
|
|
connectionStatus.value = 'disconnected'
|
|
} else if (Math.random() > 0.98) {
|
|
connectionStatus.value = 'error'
|
|
} else {
|
|
connectionStatus.value = 'connected'
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch AI logs:', error)
|
|
connectionStatus.value = 'error'
|
|
} finally {
|
|
isRefreshing.value = false
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const forceRefresh = () => {
|
|
fetchLogs()
|
|
}
|
|
|
|
const toggleAutoScroll = () => {
|
|
autoScroll.value = !autoScroll.value
|
|
}
|
|
|
|
const clearLogs = () => {
|
|
logs.value = []
|
|
}
|
|
|
|
const scrollToBottom = () => {
|
|
if (logContainer.value && autoScroll.value) {
|
|
nextTick(() => {
|
|
logContainer.value!.scrollTop = logContainer.value!.scrollHeight
|
|
})
|
|
}
|
|
}
|
|
|
|
// Polling management
|
|
let pollInterval: number | null = null
|
|
|
|
const startPolling = () => {
|
|
if (pollInterval) clearInterval(pollInterval)
|
|
pollInterval = setInterval(() => {
|
|
fetchLogs()
|
|
scrollToBottom()
|
|
}, pollingInterval.value) as unknown as number
|
|
}
|
|
|
|
const stopPolling = () => {
|
|
if (pollInterval) {
|
|
clearInterval(pollInterval)
|
|
pollInterval = null
|
|
}
|
|
}
|
|
|
|
// Lifecycle hooks
|
|
onMounted(() => {
|
|
// Initial load
|
|
fetchLogs()
|
|
|
|
// Start polling
|
|
startPolling()
|
|
|
|
// Generate initial logs
|
|
for (let i = 0; i < 10; i++) {
|
|
const log = generateMockLog()
|
|
log.timestamp = new Date(Date.now() - (10 - i) * 60000) // Staggered times
|
|
log.isNew = false
|
|
logs.value.push(log)
|
|
}
|
|
|
|
isLoading.value = false
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
stopPolling()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.log-entries-container {
|
|
overflow-y: auto;
|
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
border-radius: 4px;
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.log-entry {
|
|
border-left: 3px solid;
|
|
background-color: rgba(255, 255, 255, 0.02);
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.log-entry:hover {
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.log-entry.new-entry {
|
|
background-color: rgba(33, 150, 243, 0.1);
|
|
border-left-color: #2196f3;
|
|
}
|
|
|
|
.h-100 {
|
|
height: 100%;
|
|
}
|
|
</style>
|