phase 4
This commit is contained in:
@@ -9,6 +9,7 @@ COPY framework/ ./framework/
|
||||
COPY detection-app/ ./detection-app/
|
||||
|
||||
WORKDIR /ui/detection-app
|
||||
ENV CI=true
|
||||
RUN pnpm install
|
||||
|
||||
EXPOSE 5175
|
||||
|
||||
@@ -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>
|
||||
|
||||
56
ui/detection-app/src/panels/FunnelPanel.vue
Normal file
56
ui/detection-app/src/panels/FunnelPanel.vue
Normal 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>
|
||||
Reference in New Issue
Block a user