phase 12
This commit is contained in:
@@ -10,6 +10,9 @@ import BrandTablePanel from './panels/BrandTablePanel.vue'
|
||||
import TimelinePanel from './panels/TimelinePanel.vue'
|
||||
import CostStatsPanel from './panels/CostStatsPanel.vue'
|
||||
import type { StatsUpdate, RunContext } from './types/sse-contract'
|
||||
import { usePipelineStore } from './stores/pipeline'
|
||||
|
||||
const pipeline = usePipelineStore()
|
||||
|
||||
const jobId = ref(new URLSearchParams(window.location.search).get('job') || 'test-job')
|
||||
const stats = ref<StatsUpdate | null>(null)
|
||||
@@ -89,56 +92,99 @@ source.connect()
|
||||
</div>
|
||||
<ResizeHandle direction="horizontal" @resize="onPipelineResize" />
|
||||
|
||||
<!-- Right area: interactive panels -->
|
||||
<!-- Right area: mode-dependent content -->
|
||||
<div class="content-col">
|
||||
<!-- Row 1: Frame viewer + Funnel -->
|
||||
<div class="viewer-row" :style="{ height: viewerHeight + 'px' }">
|
||||
<FramePanel :source="source" :status="status" />
|
||||
<FunnelPanel :source="source" :status="status" />
|
||||
</div>
|
||||
<ResizeHandle direction="vertical" @resize="onViewerResize" />
|
||||
|
||||
<!-- Row 2: Detections + Stats side by side -->
|
||||
<div class="detections-stats-row">
|
||||
<div class="detections-col" :style="{ flex: detectionsFlex }">
|
||||
<Panel title="Detections" :status="status">
|
||||
<div class="detections-stack">
|
||||
<div class="timeline-section" :style="{ flex: timelineFlex }">
|
||||
<TimelinePanel :source="source" :status="status" :embedded="true" />
|
||||
</div>
|
||||
<ResizeHandle direction="vertical" @resize="onTimelineResize" />
|
||||
<div class="table-section" :style="{ flex: tableFlex }">
|
||||
<BrandTablePanel :source="source" :status="status" :embedded="true" />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<!-- === NORMAL MODE === -->
|
||||
<template v-if="pipeline.layoutMode === 'normal'">
|
||||
<div class="viewer-row" :style="{ height: viewerHeight + 'px' }">
|
||||
<FramePanel :source="source" :status="status" />
|
||||
<FunnelPanel :source="source" :status="status" />
|
||||
</div>
|
||||
<ResizeHandle direction="horizontal" @resize="onDetectionsResize" />
|
||||
<div class="stats-col">
|
||||
<Panel title="Pipeline" :status="status">
|
||||
<div class="pipeline-stats">
|
||||
<div class="stat" v-for="s in [
|
||||
{ label: 'Frames', value: stats?.frames_extracted ?? '—' },
|
||||
{ label: 'After filter', value: stats?.frames_after_scene_filter ?? '—' },
|
||||
{ label: 'Regions', value: stats?.regions_detected ?? '—' },
|
||||
{ label: 'OCR resolved', value: stats?.regions_resolved_by_ocr ?? '—' },
|
||||
{ label: 'VLM escalated', value: stats?.regions_escalated_to_local_vlm ?? '—' },
|
||||
{ label: 'Cloud escalated', value: stats?.regions_escalated_to_cloud_llm ?? '—' },
|
||||
]" :key="s.label">
|
||||
<span class="label">{{ s.label }}</span>
|
||||
<span class="value">{{ s.value }}</span>
|
||||
<ResizeHandle direction="vertical" @resize="onViewerResize" />
|
||||
|
||||
<div class="detections-stats-row">
|
||||
<div class="detections-col" :style="{ flex: detectionsFlex }">
|
||||
<Panel title="Detections" :status="status">
|
||||
<div class="detections-stack">
|
||||
<div class="timeline-section" :style="{ flex: timelineFlex }">
|
||||
<TimelinePanel :source="source" :status="status" :embedded="true" />
|
||||
</div>
|
||||
<ResizeHandle direction="vertical" @resize="onTimelineResize" />
|
||||
<div class="table-section" :style="{ flex: tableFlex }">
|
||||
<BrandTablePanel :source="source" :status="status" :embedded="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<CostStatsPanel :source="source" :status="status" />
|
||||
</Panel>
|
||||
</div>
|
||||
<ResizeHandle direction="horizontal" @resize="onDetectionsResize" />
|
||||
<div class="stats-col">
|
||||
<Panel title="Pipeline" :status="status">
|
||||
<div class="pipeline-stats">
|
||||
<div class="stat" v-for="s in [
|
||||
{ label: 'Frames', value: stats?.frames_extracted ?? '—' },
|
||||
{ label: 'After filter', value: stats?.frames_after_scene_filter ?? '—' },
|
||||
{ label: 'Regions', value: stats?.regions_detected ?? '—' },
|
||||
{ label: 'OCR resolved', value: stats?.regions_resolved_by_ocr ?? '—' },
|
||||
{ label: 'VLM escalated', value: stats?.regions_escalated_to_local_vlm ?? '—' },
|
||||
{ label: 'Cloud escalated', value: stats?.regions_escalated_to_cloud_llm ?? '—' },
|
||||
]" :key="s.label">
|
||||
<span class="label">{{ s.label }}</span>
|
||||
<span class="value">{{ s.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<CostStatsPanel :source="source" :status="status" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- === BBOX EDITOR MODE === -->
|
||||
<template v-else-if="pipeline.layoutMode === 'bbox_editor'">
|
||||
<Panel :title="`Region Editor — ${pipeline.editorStage?.replace(/_/g, ' ')}`" :status="status">
|
||||
<div class="editor-placeholder">
|
||||
<div class="editor-frame">
|
||||
<FramePanel :source="source" :status="status" />
|
||||
</div>
|
||||
<div class="editor-tools">
|
||||
<p>Stage: <strong>{{ pipeline.editorStage }}</strong></p>
|
||||
<p>Draw polygons to define regions</p>
|
||||
<button class="editor-close" @click="pipeline.closeEditor()">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<!-- === STAGE EDITOR MODE === -->
|
||||
<template v-else-if="pipeline.layoutMode === 'stage_editor'">
|
||||
<Panel :title="`Stage Config — ${pipeline.editorStage?.replace(/_/g, ' ')}`" :status="status">
|
||||
<div class="editor-placeholder">
|
||||
<div class="editor-config">
|
||||
<p>Stage: <strong>{{ pipeline.editorStage }}</strong></p>
|
||||
<p>Config fields will be auto-generated from stage registry</p>
|
||||
<button class="editor-close" @click="pipeline.closeEditor()">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Log (full width) -->
|
||||
<!-- Bottom bar: Log or Blob viewer depending on mode -->
|
||||
<div class="log-row">
|
||||
<LogPanel :source="source" :status="status" />
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<template v-else>
|
||||
<LogPanel :source="source" :status="status" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -290,4 +336,67 @@ header h1 { font-size: var(--font-size-lg); font-weight: 600; }
|
||||
}
|
||||
|
||||
.empty { color: var(--text-dim); padding: var(--space-6); text-align: center; }
|
||||
|
||||
/* Editor placeholders */
|
||||
.editor-placeholder {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.editor-frame {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.editor-tools {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-3);
|
||||
background: var(--surface-2);
|
||||
border-radius: var(--panel-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.editor-config {
|
||||
padding: var(--space-4);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.editor-close {
|
||||
background: var(--surface-3);
|
||||
border: 1px solid var(--surface-3);
|
||||
border-radius: 4px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.editor-close:hover {
|
||||
background: var(--status-error);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.blob-viewer {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.blob-placeholder {
|
||||
padding: var(--space-4);
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user