phase 7
This commit is contained in:
@@ -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,20 +66,18 @@ 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 = stageMap[stage] ?? 'filter_scenes'
|
||||
const cpStage = checkpointStageFor(stage)
|
||||
if (cpStage) {
|
||||
loadCheckpoint(job, cpStage)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
79
ui/detection-app/src/composables/useStageRegistry.ts
Normal file
79
ui/detection-app/src/composables/useStageRegistry.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
77
ui/framework/src/composables/useRegistry.ts
Normal file
77
ui/framework/src/composables/useRegistry.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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)',
|
||||
|
||||
Reference in New Issue
Block a user