phase 7
This commit is contained in:
@@ -2,6 +2,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'mpr-ui-framework'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
|
import { useStageRegistry } from './useStageRegistry'
|
||||||
|
|
||||||
interface CheckpointFrame {
|
interface CheckpointFrame {
|
||||||
seq: number
|
seq: number
|
||||||
@@ -65,19 +66,17 @@ export function useCheckpointLoader(
|
|||||||
currentFrameRef.value = frame.seq
|
currentFrameRef.value = frame.seq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { checkpointStageFor } = useStageRegistry()
|
||||||
|
|
||||||
// Auto-load checkpoint when entering editor mode
|
// Auto-load checkpoint when entering editor mode
|
||||||
watch(
|
watch(
|
||||||
() => [pipeline.layoutMode, pipeline.editorStage, jobId.value] as const,
|
() => [pipeline.layoutMode, pipeline.editorStage, jobId.value] as const,
|
||||||
([mode, stage, job]) => {
|
([mode, stage, job]) => {
|
||||||
if (mode === 'bbox_editor' && stage && job) {
|
if (mode === 'bbox_editor' && stage && job) {
|
||||||
const stageMap: Record<string, string> = {
|
const cpStage = checkpointStageFor(stage)
|
||||||
detect_edges: 'filter_scenes',
|
if (cpStage) {
|
||||||
detect_contours: 'detect_edges',
|
loadCheckpoint(job, cpStage)
|
||||||
detect_color: 'detect_contours',
|
|
||||||
merge_regions: 'detect_color',
|
|
||||||
}
|
}
|
||||||
const cpStage = stageMap[stage] ?? 'filter_scenes'
|
|
||||||
loadCheckpoint(job, cpStage)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ 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">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { Panel, GraphRenderer } from 'mpr-ui-framework'
|
import { Panel, GraphRenderer } from 'mpr-ui-framework'
|
||||||
import type { GraphNode, DataSource } from 'mpr-ui-framework'
|
import type { GraphNode, DataSource } from 'mpr-ui-framework'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
|
import { useStageRegistry } from '../composables/useStageRegistry'
|
||||||
const PIPELINE_NODES = [
|
|
||||||
'extract_frames', 'filter_scenes', 'detect_edges', 'detect_objects', 'preprocess',
|
|
||||||
'run_ocr', 'match_brands', 'escalate_vlm', 'escalate_cloud', 'compile_report',
|
|
||||||
]
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
source: DataSource
|
source: DataSource
|
||||||
@@ -15,10 +11,16 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const pipeline = usePipelineStore()
|
const pipeline = usePipelineStore()
|
||||||
|
const { stageNames, editableStages } = useStageRegistry()
|
||||||
|
|
||||||
const nodes = ref<GraphNode[]>(
|
const nodes = ref<GraphNode[]>([])
|
||||||
PIPELINE_NODES.map((id) => ({ id, status: 'pending' }))
|
|
||||||
)
|
// 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) => {
|
props.source.on<{ nodes: GraphNode[] }>('graph_update', (e) => {
|
||||||
nodes.value = e.nodes
|
nodes.value = e.nodes
|
||||||
@@ -52,6 +54,7 @@ function onOpenStageEditor(stage: string) {
|
|||||||
<Panel title="Pipeline" :status="status">
|
<Panel title="Pipeline" :status="status">
|
||||||
<GraphRenderer
|
<GraphRenderer
|
||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
|
:region-stages="editableStages"
|
||||||
@open-region-editor="onOpenRegionEditor"
|
@open-region-editor="onOpenRegionEditor"
|
||||||
@open-stage-editor="onOpenStageEditor"
|
@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 { SSEDataSource } from './datasources/SSEDataSource'
|
||||||
export { StaticDataSource } from './datasources/StaticDataSource'
|
export { StaticDataSource } from './datasources/StaticDataSource'
|
||||||
export { useDataSource } from './composables/useDataSource'
|
export { useDataSource } from './composables/useDataSource'
|
||||||
|
export { useRegistry } from './composables/useRegistry'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
export { default as Panel } from './components/Panel.vue'
|
export { default as Panel } from './components/Panel.vue'
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ const emit = defineEmits<{
|
|||||||
'open-stage-editor': [stage: string]
|
'open-stage-editor': [stage: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const regionStageSet = computed(() => new Set(props.regionStages ?? [
|
const regionStageSet = computed(() => new Set(props.regionStages ?? []))
|
||||||
'detect_edges', 'detect_objects', 'run_ocr', 'match_brands', 'escalate_vlm', 'escalate_cloud',
|
|
||||||
]))
|
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
pending: 'var(--status-idle)',
|
pending: 'var(--status-idle)',
|
||||||
|
|||||||
Reference in New Issue
Block a user