This commit is contained in:
2026-03-26 06:10:19 -03:00
parent 731964ca10
commit e27cb5bcc3
41 changed files with 2079 additions and 95 deletions

View File

@@ -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)'

View File

@@ -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>