This commit is contained in:
2026-03-23 15:18:23 -03:00
parent 5ed876d694
commit b57da622cb
17 changed files with 554 additions and 103 deletions

View File

@@ -9,6 +9,7 @@ COPY framework/ ./framework/
COPY detection-app/ ./detection-app/
WORKDIR /ui/detection-app
ENV CI=true
RUN pnpm install
EXPOSE 5175

View File

@@ -3,6 +3,7 @@ import { ref } from 'vue'
import { SSEDataSource, Panel, LayoutGrid } from 'mpr-ui-framework'
import 'mpr-ui-framework/src/tokens.css'
import LogPanel from './panels/LogPanel.vue'
import FunnelPanel from './panels/FunnelPanel.vue'
import type { StatsUpdate } from './types/sse-contract'
const jobId = ref(new URLSearchParams(window.location.search).get('job') || 'test-job')
@@ -39,7 +40,7 @@ source.connect()
<span class="job-id">job: {{ jobId }}</span>
</header>
<LayoutGrid :columns="2" :rows="1" gap="var(--space-2)">
<LayoutGrid :columns="2" :rows="2" gap="var(--space-2)">
<Panel title="Stats" :status="status">
<div class="stats" v-if="stats">
<div class="stat" v-for="s in [
@@ -57,6 +58,8 @@ source.connect()
<div v-else class="empty">Waiting for stats...</div>
</Panel>
<FunnelPanel :source="source" :status="status" />
<LogPanel :source="source" :status="status" />
</LayoutGrid>
</div>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Panel } from 'mpr-ui-framework'
import TimeSeriesRenderer from 'mpr-ui-framework/src/renderers/TimeSeriesRenderer.vue'
import type { DataSource } from 'mpr-ui-framework'
import type { StatsUpdate } from '../types/sse-contract'
const props = defineProps<{
source: DataSource
status?: 'idle' | 'live' | 'processing' | 'error'
}>()
// Accumulate stats snapshots over time
const snapshots = ref<{ ts: number; stats: StatsUpdate }[]>([])
const startTime = Date.now() / 1000
props.source.on<StatsUpdate>('stats_update', (e) => {
snapshots.value.push({ ts: Date.now() / 1000 - startTime, stats: e })
})
const series = [
{ label: 'Frames', color: '#4f9cf9' },
{ label: 'After filter', color: '#3ecf8e' },
{ label: 'Regions', color: '#f5a623' },
{ label: 'OCR resolved', color: '#a78bfa' },
]
const chartData = computed(() => {
const timestamps = snapshots.value.map((s) => s.ts)
const frames = snapshots.value.map((s) => s.stats.frames_extracted)
const filtered = snapshots.value.map((s) => s.stats.frames_after_scene_filter)
const regions = snapshots.value.map((s) => s.stats.regions_detected)
const ocr = snapshots.value.map((s) => s.stats.regions_resolved_by_ocr)
return [timestamps, frames, filtered, regions, ocr] as const
})
</script>
<template>
<Panel title="Processing Funnel" :status="status">
<TimeSeriesRenderer
v-if="snapshots.length > 0"
:series="series"
:data="chartData"
:stacked="true"
/>
<div v-else class="empty">Waiting for stats...</div>
</Panel>
</template>
<style scoped>
.empty {
color: var(--text-dim);
padding: var(--space-6);
text-align: center;
}
</style>