This commit is contained in:
2026-03-28 00:24:18 -03:00
parent 49da927da0
commit f6ef95ebea
6 changed files with 176 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
import type { DataSource } from 'mpr-ui-framework'
import { usePipelineStore } from '../stores/pipeline'
import { useStageRegistry } from './useStageRegistry'
interface CheckpointFrame {
seq: number
@@ -65,19 +66,17 @@ export function useCheckpointLoader(
currentFrameRef.value = frame.seq
}
const { checkpointStageFor } = useStageRegistry()
// Auto-load checkpoint when entering editor mode
watch(
() => [pipeline.layoutMode, pipeline.editorStage, jobId.value] as const,
([mode, stage, job]) => {
if (mode === 'bbox_editor' && stage && job) {
const stageMap: Record<string, string> = {
detect_edges: 'filter_scenes',
detect_contours: 'detect_edges',
detect_color: 'detect_contours',
merge_regions: 'detect_color',
const cpStage = checkpointStageFor(stage)
if (cpStage) {
loadCheckpoint(job, cpStage)
}
const cpStage = stageMap[stage] ?? 'filter_scenes'
loadCheckpoint(job, cpStage)
}
},
{ immediate: true },

View File

@@ -0,0 +1,79 @@
import { computed } from 'vue'
import { useRegistry } from 'mpr-ui-framework'
export interface StageConfigField {
name: string
type: string
default: unknown
description: string
min: number | null
max: number | null
options: string[] | null
}
export interface StageInfo {
name: string
label: string
description: string
category: string
config_fields: StageConfigField[]
reads: string[]
writes: string[]
}
/**
* Detection-specific stage registry.
*
* Fetches stage definitions from the backend and provides helpers
* for looking up stages, determining editor capabilities, and
* resolving checkpoint-to-editor mappings from IO metadata.
*/
export function useStageRegistry() {
const { data: stages, loading, error, refresh } = useRegistry<StageInfo>('/api/detect/config/stages')
const stageMap = computed(() => {
const map = new Map<string, StageInfo>()
for (const s of stages.value) {
map.set(s.name, s)
}
return map
})
const stageNames = computed(() => stages.value.map(s => s.name))
/**
* Given a stage name, find which preceding stage's checkpoint
* to load for the editor. Uses IO metadata: finds the stage
* whose writes include what this stage reads.
*
* Falls back to the previous stage in the list if IO doesn't resolve.
*/
function checkpointStageFor(stageName: string): string | null {
const idx = stages.value.findIndex(s => s.name === stageName)
if (idx <= 0) return null
// Previous stage in the pipeline order
return stages.value[idx - 1].name
}
/**
* Stages that have config fields (and thus can open a parameter editor).
*/
const editableStages = computed(() =>
stages.value.filter(s => s.config_fields.length > 0).map(s => s.name)
)
function getStage(name: string): StageInfo | undefined {
return stageMap.value.get(name)
}
return {
stages,
loading,
error,
refresh,
stageNames,
editableStages,
getStage,
checkpointStageFor,
}
}

View File

@@ -1,13 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { Panel, GraphRenderer } from 'mpr-ui-framework'
import type { GraphNode, DataSource } from 'mpr-ui-framework'
import { usePipelineStore } from '../stores/pipeline'
const PIPELINE_NODES = [
'extract_frames', 'filter_scenes', 'detect_edges', 'detect_objects', 'preprocess',
'run_ocr', 'match_brands', 'escalate_vlm', 'escalate_cloud', 'compile_report',
]
import { useStageRegistry } from '../composables/useStageRegistry'
const props = defineProps<{
source: DataSource
@@ -15,10 +11,16 @@ const props = defineProps<{
}>()
const pipeline = usePipelineStore()
const { stageNames, editableStages } = useStageRegistry()
const nodes = ref<GraphNode[]>(
PIPELINE_NODES.map((id) => ({ id, status: 'pending' }))
)
const nodes = ref<GraphNode[]>([])
// Initialize nodes from registry when it loads
watch(stageNames, (names) => {
if (names.length > 0 && nodes.value.length === 0) {
nodes.value = names.map((id) => ({ id, status: 'pending' }))
}
}, { immediate: true })
props.source.on<{ nodes: GraphNode[] }>('graph_update', (e) => {
nodes.value = e.nodes
@@ -52,6 +54,7 @@ function onOpenStageEditor(stage: string) {
<Panel title="Pipeline" :status="status">
<GraphRenderer
:nodes="nodes"
:region-stages="editableStages"
@open-region-editor="onOpenRegionEditor"
@open-stage-editor="onOpenStageEditor"
/>

View File

@@ -0,0 +1,77 @@
import { ref, type Ref } from 'vue'
/**
* Generic registry composable — fetches typed data from a URL, caches it,
* exposes it reactively.
*
* Use for any data that is loaded once at app init and rarely changes:
* stage definitions, config schemas, available models, etc.
*
* The registry is shared across all consumers (singleton per URL).
*/
const cache = new Map<string, { data: Ref<any>; loading: Ref<boolean>; error: Ref<string | null>; promise: Promise<void> | null }>()
export function useRegistry<T>(url: string): {
data: Ref<T[]>
loading: Ref<boolean>
error: Ref<string | null>
refresh: () => Promise<void>
} {
if (!cache.has(url)) {
const data = ref<T[]>([]) as Ref<T[]>
const loading = ref(false)
const error = ref<string | null>(null)
const entry = { data, loading, error, promise: null as Promise<void> | null }
cache.set(url, entry)
async function doFetch() {
loading.value = true
error.value = null
try {
const resp = await fetch(url)
if (!resp.ok) {
error.value = `Failed to fetch registry: ${resp.status}`
return
}
data.value = await resp.json()
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
entry.promise = doFetch()
}
const entry = cache.get(url)!
async function refresh() {
const data = entry.data
const loading = entry.loading
const error = entry.error
loading.value = true
error.value = null
try {
const resp = await fetch(url)
if (!resp.ok) {
error.value = `Failed to fetch registry: ${resp.status}`
return
}
data.value = await resp.json()
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
return {
data: entry.data as Ref<T[]>,
loading: entry.loading,
error: entry.error,
refresh,
}
}

View File

@@ -3,6 +3,7 @@ export { DataSource, type DataSourceStatus } from './datasources/DataSource'
export { SSEDataSource } from './datasources/SSEDataSource'
export { StaticDataSource } from './datasources/StaticDataSource'
export { useDataSource } from './composables/useDataSource'
export { useRegistry } from './composables/useRegistry'
// Components
export { default as Panel } from './components/Panel.vue'

View File

@@ -20,9 +20,7 @@ const emit = defineEmits<{
'open-stage-editor': [stage: string]
}>()
const regionStageSet = computed(() => new Set(props.regionStages ?? [
'detect_edges', 'detect_objects', 'run_ocr', 'match_brands', 'escalate_vlm', 'escalate_cloud',
]))
const regionStageSet = computed(() => new Set(props.regionStages ?? []))
const statusColors: Record<string, string> = {
pending: 'var(--status-idle)',