phase 12
This commit is contained in:
@@ -8,6 +8,10 @@ export interface FrameBBox {
|
||||
h: number
|
||||
confidence: number
|
||||
label: string
|
||||
resolved_brand?: string | null
|
||||
source?: string | null
|
||||
stage?: string | null
|
||||
ocr_text?: string | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -46,27 +50,37 @@ function draw() {
|
||||
const bw = box.w * scale
|
||||
const bh = box.h * scale
|
||||
|
||||
// Box outline
|
||||
ctx.strokeStyle = confidenceColor(box.confidence)
|
||||
const color = sourceColor(box)
|
||||
const resolved = box.resolved_brand || box.ocr_text
|
||||
|
||||
// Box outline only — no labels, no percentages
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 2
|
||||
if (!resolved) {
|
||||
ctx.setLineDash([4, 3])
|
||||
}
|
||||
ctx.strokeRect(bx, by, bw, bh)
|
||||
|
||||
// Label background
|
||||
const label = `${box.label} ${(box.confidence * 100).toFixed(0)}%`
|
||||
ctx.font = '11px var(--font-mono)'
|
||||
const metrics = ctx.measureText(label)
|
||||
const labelH = 16
|
||||
ctx.fillStyle = confidenceColor(box.confidence)
|
||||
ctx.fillRect(bx, by - labelH, metrics.width + 8, labelH)
|
||||
|
||||
// Label text
|
||||
ctx.fillStyle = '#000'
|
||||
ctx.fillText(label, bx + 4, by - 4)
|
||||
ctx.setLineDash([])
|
||||
}
|
||||
}
|
||||
img.src = `data:image/jpeg;base64,${props.imageSrc}`
|
||||
}
|
||||
|
||||
const SOURCE_COLORS: Record<string, string> = {
|
||||
yolo: '#f5a623', // yellow — raw detection
|
||||
ocr: '#ff8c42', // orange — text extracted
|
||||
ocr_matched: '#3ecf8e', // green — brand resolved
|
||||
local_vlm: '#4f9cf9', // blue — VLM resolved
|
||||
cloud_llm: '#a78bfa', // purple — cloud resolved
|
||||
unresolved: '#e05252', // red — nothing matched
|
||||
}
|
||||
|
||||
function sourceColor(box: FrameBBox): string {
|
||||
if (box.resolved_brand) return SOURCE_COLORS.ocr_matched
|
||||
if (box.source && box.source in SOURCE_COLORS) return SOURCE_COLORS[box.source]
|
||||
return confidenceColor(box.confidence)
|
||||
}
|
||||
|
||||
function confidenceColor(conf: number): string {
|
||||
if (conf >= 0.7) return 'var(--conf-high)'
|
||||
if (conf >= 0.4) return 'var(--conf-mid)'
|
||||
|
||||
@@ -11,8 +11,19 @@ export interface GraphNode {
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: GraphNode[]
|
||||
/** Stages that have a region editor (bbox/polygon) */
|
||||
regionStages?: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'open-region-editor': [stage: string]
|
||||
'open-stage-editor': [stage: string]
|
||||
}>()
|
||||
|
||||
const regionStageSet = computed(() => new Set(props.regionStages ?? [
|
||||
'detect_objects', 'run_ocr', 'match_brands', 'escalate_vlm', 'escalate_cloud',
|
||||
]))
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'var(--status-idle)',
|
||||
running: 'var(--status-processing)',
|
||||
@@ -23,17 +34,15 @@ const statusColors: Record<string, string> = {
|
||||
const flowNodes = computed(() =>
|
||||
props.nodes.map((n, i) => ({
|
||||
id: n.id,
|
||||
label: n.id.replace(/_/g, ' '),
|
||||
position: { x: 20, y: i * 70 },
|
||||
style: {
|
||||
background: statusColors[n.status] ?? statusColors.pending,
|
||||
color: n.status === 'pending' ? '#ccc' : '#000',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--panel-radius)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
fontWeight: '600',
|
||||
padding: '8px 16px',
|
||||
type: 'stage',
|
||||
position: { x: 20, y: i * 80 },
|
||||
data: {
|
||||
label: n.id.replace(/_/g, ' '),
|
||||
status: n.status,
|
||||
color: statusColors[n.status] ?? statusColors.pending,
|
||||
textColor: n.status === 'pending' ? '#888' : '#000',
|
||||
hasRegionEditor: regionStageSet.value.has(n.id),
|
||||
isRunning: n.status === 'running',
|
||||
},
|
||||
}))
|
||||
)
|
||||
@@ -63,7 +72,38 @@ const flowEdges = computed(() => {
|
||||
:nodes-connectable="false"
|
||||
:zoom-on-scroll="false"
|
||||
:pan-on-scroll="false"
|
||||
/>
|
||||
>
|
||||
<template #node-stage="{ data, id }">
|
||||
<div
|
||||
class="stage-node"
|
||||
:class="{ running: data.isRunning }"
|
||||
:style="{ background: data.color, color: data.textColor }"
|
||||
>
|
||||
<span class="stage-label">{{ data.label }}</span>
|
||||
<span class="stage-actions">
|
||||
<button
|
||||
v-if="data.hasRegionEditor"
|
||||
class="stage-btn region-btn"
|
||||
title="Region editor"
|
||||
@click.stop="emit('open-region-editor', id)"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="5" cy="5" r="3.5"/><line x1="7.5" y1="7.5" x2="11" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="stage-btn config-btn"
|
||||
title="Stage config"
|
||||
@click.stop="emit('open-stage-editor', id)"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="6" cy="6" r="2"/><path d="M6 1v2M6 9v2M1 6h2M9 6h2M2.5 2.5l1.4 1.4M8.1 8.1l1.4 1.4M2.5 9.5l1.4-1.4M8.1 3.9l1.4-1.4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -77,4 +117,66 @@ const flowEdges = computed(() => {
|
||||
.graph-renderer :deep(.vue-flow__background) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Hide default node styling — we use custom template */
|
||||
.graph-renderer :deep(.vue-flow__node-stage) {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.stage-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--panel-radius);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.stage-node.running {
|
||||
animation: node-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.stage-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stage-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.stage-node:hover .stage-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stage-btn {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stage-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@keyframes node-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user