phase cv 0
This commit is contained in:
@@ -10,7 +10,9 @@ import BrandTablePanel from './panels/BrandTablePanel.vue'
|
||||
import TimelinePanel from './panels/TimelinePanel.vue'
|
||||
import CostStatsPanel from './panels/CostStatsPanel.vue'
|
||||
import SourceSelector from './panels/SourceSelector.vue'
|
||||
import StageConfigSliders from './components/StageConfigSliders.vue'
|
||||
import type { StatsUpdate, RunContext } from './types/sse-contract'
|
||||
import type { FrameOverlay } from 'mpr-ui-framework/src/renderers/FrameRenderer.vue'
|
||||
import { usePipelineStore } from './stores/pipeline'
|
||||
|
||||
const pipeline = usePipelineStore()
|
||||
@@ -21,9 +23,10 @@ const stats = ref<StatsUpdate | null>(null)
|
||||
const runContext = ref<RunContext | null>(null)
|
||||
const status = ref<'idle' | 'live' | 'processing' | 'error'>('idle')
|
||||
const logPanel = ref<{ clear: () => void } | null>(null)
|
||||
const sseConnected = ref(false)
|
||||
|
||||
// No job selected → open source selector
|
||||
if (!jobParam) {
|
||||
// No job selected and no hash route → open source selector
|
||||
if (!jobParam && !window.location.hash.replace(/^#\/?/, '')) {
|
||||
pipeline.openSourceSelector()
|
||||
}
|
||||
|
||||
@@ -35,7 +38,6 @@ const source = new SSEDataSource({
|
||||
|
||||
source.on<StatsUpdate>('stats_update', (e) => {
|
||||
stats.value = e
|
||||
// Capture run context from first event that carries it
|
||||
if (!runContext.value && e.run_id) {
|
||||
runContext.value = {
|
||||
run_id: (e as any).run_id,
|
||||
@@ -53,7 +55,7 @@ source.on<{ report?: { status?: string, error?: string } }>('job_complete', (e)
|
||||
|
||||
// Resizable splits
|
||||
const pipelineWidth = ref(320)
|
||||
const detectionsFlex = ref(3) // ratio for detections vs stats
|
||||
const detectionsFlex = ref(3)
|
||||
const viewerHeight = ref(240)
|
||||
const timelineFlex = ref(1)
|
||||
const tableFlex = ref(1)
|
||||
@@ -82,11 +84,19 @@ const statusMap: Record<string, 'idle' | 'live' | 'processing' | 'error'> = {
|
||||
live: 'live',
|
||||
error: 'error',
|
||||
}
|
||||
const checkStatus = () => { status.value = statusMap[source.status.value] ?? 'idle' }
|
||||
const checkStatus = () => {
|
||||
if (sseConnected.value) {
|
||||
status.value = statusMap[source.status.value] ?? 'idle'
|
||||
}
|
||||
}
|
||||
setInterval(checkStatus, 500)
|
||||
|
||||
if (jobId.value) {
|
||||
// Only connect SSE for live pipeline runs (no hash route = dashboard mode)
|
||||
// Scenario URLs use hash routing and load from checkpoint instead
|
||||
const isScenarioMode = pipeline.isEditing || pipeline.layoutMode !== 'normal'
|
||||
if (jobId.value && !isScenarioMode) {
|
||||
source.connect()
|
||||
sseConnected.value = true
|
||||
}
|
||||
|
||||
async function stopPipeline() {
|
||||
@@ -96,6 +106,37 @@ async function stopPipeline() {
|
||||
} catch { /* ignore — UI will see the cancel event via SSE */ }
|
||||
}
|
||||
|
||||
// Current frame image (base64) — tracked for the editor's direct GPU calls
|
||||
const currentFrameImage = ref<string | null>(null)
|
||||
const currentFrameRef = ref<number | null>(null)
|
||||
|
||||
source.on<{ frame_ref: number; jpeg_b64: string }>('frame_update', (e) => {
|
||||
currentFrameImage.value = e.jpeg_b64
|
||||
currentFrameRef.value = e.frame_ref
|
||||
})
|
||||
|
||||
// Debug overlays from replay-stage results
|
||||
const editorOverlays = ref<FrameOverlay[]>([])
|
||||
|
||||
function onReplayResult(result: {
|
||||
debug?: Record<string, { edge_overlay_b64: string; lines_overlay_b64: string; horizontal_count: number; pair_count: number }>
|
||||
}) {
|
||||
const overlays: FrameOverlay[] = []
|
||||
if (result.debug) {
|
||||
// Take first frame's debug data (editor shows one frame at a time)
|
||||
const firstDebug = Object.values(result.debug)[0]
|
||||
if (firstDebug) {
|
||||
if (firstDebug.edge_overlay_b64) {
|
||||
overlays.push({ src: firstDebug.edge_overlay_b64, label: 'Canny edges', visible: true, opacity: 0.4 })
|
||||
}
|
||||
if (firstDebug.lines_overlay_b64) {
|
||||
overlays.push({ src: firstDebug.lines_overlay_b64, label: 'Hough lines', visible: true, opacity: 0.5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
editorOverlays.value = overlays
|
||||
}
|
||||
|
||||
function onJobStarted(newJobId: string) {
|
||||
jobId.value = newJobId
|
||||
// Reset UI state
|
||||
@@ -113,7 +154,7 @@ function onJobStarted(newJobId: string) {
|
||||
source.disconnect()
|
||||
source.setUrl(`/api/detect/stream/${newJobId}`)
|
||||
source.connect()
|
||||
// Switch to normal layout (reset sets it to normal already)
|
||||
sseConnected.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -131,7 +172,7 @@ function onJobStarted(newJobId: string) {
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="jobId && (status === 'live' || status === 'processing')"
|
||||
v-if="sseConnected && (status === 'live' || status === 'processing')"
|
||||
class="header-btn stop-btn"
|
||||
title="Stop pipeline"
|
||||
@click="stopPipeline"
|
||||
@@ -198,11 +239,17 @@ function onJobStarted(newJobId: string) {
|
||||
<Panel :title="`Region Editor — ${pipeline.editorStage?.replace(/_/g, ' ')}`" :status="status">
|
||||
<div class="editor-placeholder">
|
||||
<div class="editor-frame">
|
||||
<FramePanel :source="source" :status="status" />
|
||||
<FramePanel :source="source" :status="status" :overlays="editorOverlays" />
|
||||
</div>
|
||||
<div class="editor-tools">
|
||||
<p>Stage: <strong>{{ pipeline.editorStage }}</strong></p>
|
||||
<p>Draw polygons to define regions</p>
|
||||
<StageConfigSliders
|
||||
v-if="pipeline.editorStage"
|
||||
:stage="pipeline.editorStage"
|
||||
:job-id="jobId"
|
||||
:frame-image="currentFrameImage"
|
||||
:frame-ref="currentFrameRef"
|
||||
@replay-result="onReplayResult"
|
||||
/>
|
||||
<button class="editor-close" @click="pipeline.closeEditor()">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,10 +280,24 @@ function onJobStarted(newJobId: string) {
|
||||
<!-- Bottom bar: Log or Blob viewer depending on mode -->
|
||||
<div class="log-row">
|
||||
<template v-if="pipeline.layoutMode === 'bbox_editor'">
|
||||
<Panel :title="`Blobs — ${pipeline.editorStage?.replace(/_/g, ' ')}`" :status="status">
|
||||
<div class="blob-viewer">
|
||||
<div class="blob-placeholder">
|
||||
Blob viewer: crops, preprocessed images, OCR results for {{ pipeline.editorStage }}
|
||||
<Panel :title="`Debug Overlays — ${pipeline.editorStage?.replace(/_/g, ' ')}`" :status="status">
|
||||
<div class="overlay-controls">
|
||||
<template v-if="editorOverlays.length > 0">
|
||||
<label v-for="(overlay, idx) in editorOverlays" :key="idx" class="overlay-toggle">
|
||||
<input type="checkbox" v-model="overlay.visible" />
|
||||
<span class="overlay-label">{{ overlay.label }}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0" max="1" step="0.05"
|
||||
:value="overlay.opacity ?? 0.5"
|
||||
@input="(e) => overlay.opacity = Number((e.target as HTMLInputElement).value)"
|
||||
class="opacity-slider"
|
||||
/>
|
||||
<span class="opacity-value">{{ Math.round((overlay.opacity ?? 0.5) * 100) }}%</span>
|
||||
</label>
|
||||
</template>
|
||||
<div v-else class="blob-placeholder">
|
||||
Run analysis with debug enabled to see edge and line overlays
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -473,11 +534,6 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.blob-viewer {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.blob-placeholder {
|
||||
padding: var(--space-4);
|
||||
color: var(--text-dim);
|
||||
@@ -485,6 +541,42 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.overlay-controls {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overlay-toggle input[type="checkbox"] {
|
||||
accent-color: #00bcd4;
|
||||
}
|
||||
|
||||
.overlay-label {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.opacity-slider {
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.opacity-value {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/* Source selector */
|
||||
.source-selector {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user