Split the results of OCR

This commit is contained in:
pjmalandrino 2026-03-20 21:45:26 +01:00
parent c3c0149532
commit 7bb4f8e298
6 changed files with 180 additions and 15 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -20,7 +20,7 @@
ref="canvasRef"
class="overlay-canvas"
@mousemove="onMouseMove"
@mouseleave="hoveredElement = null"
@mouseleave="hoveredElement = null; emit('highlight-element', -1)"
/>
<div
v-if="hoveredElement"
@ -55,9 +55,13 @@ const props = defineProps({
/** The <img> element to overlay onto */
imageEl: { type: Object, default: null },
/** Page data { page_number, width, height, elements[] } for current page */
pageData: { type: Object, default: null }
pageData: { type: Object, default: null },
/** Index of the element to highlight (from ResultTabs hover) */
highlightedIndex: { type: Number, default: -1 }
})
const emit = defineEmits(['highlight-element'])
const hiddenTypes = reactive(new Set())
const canvasRef = ref(null)
const hoveredElement = ref(null)
@ -97,15 +101,23 @@ function draw() {
const scale = computeScale(img.clientWidth, img.clientHeight, props.pageData.width, props.pageData.height)
// Build a map of content-filtered indices to match ResultTabs ordering
const allElements = (props.pageData.elements || [])
const contentElements = allElements.filter(e => e.content)
for (const el of visibleElements.value) {
const rect = bboxToRect(el.bbox, scale)
const color = ELEMENT_COLORS[el.type] || ELEMENT_COLORS.text
// Check if this element is the highlighted one
const elContentIdx = contentElements.indexOf(el)
const isHighlighted = props.highlightedIndex >= 0 && elContentIdx === props.highlightedIndex
ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.lineWidth = isHighlighted ? 3 : 2
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h)
ctx.fillStyle = color + '20'
ctx.fillStyle = color + (isHighlighted ? '40' : '20')
ctx.fillRect(rect.x, rect.y, rect.w, rect.h)
}
}
@ -121,15 +133,20 @@ function onMouseMove(e) {
const scale = computeScale(img.clientWidth, img.clientHeight, props.pageData.width, props.pageData.height)
const contentElements = (props.pageData.elements || []).filter(e => e.content)
let found = null
let foundIdx = -1
for (const el of visibleElements.value) {
if (pointInRect(mx, my, bboxToRect(el.bbox, scale))) {
found = el
foundIdx = contentElements.indexOf(el)
break
}
}
hoveredElement.value = found
emit('highlight-element', foundIdx)
if (found) {
tooltipStyle.value = {
left: `${Math.min(mx + 12, canvas.width - 250)}px`,
@ -151,7 +168,7 @@ onBeforeUnmount(() => {
})
// Redraw when data or image changes
watch([() => props.pageData, () => props.imageEl, hiddenTypes], () => {
watch([() => props.pageData, () => props.imageEl, () => props.highlightedIndex, hiddenTypes], () => {
nextTick(draw)
})

View file

@ -18,10 +18,42 @@
</div>
<div class="tab-content">
<MarkdownViewer v-if="activeTab === 'text'" :content="pageMarkdown" />
<!-- ELEMENTS VIEW each bbox as a separate card -->
<div v-if="activeTab === 'elements'" class="elements-list">
<div v-if="!currentElements.length" class="elements-empty">
{{ t('results.noElements') }}
</div>
<div
v-for="(el, idx) in currentElements"
:key="idx"
class="element-card"
:class="{ highlighted: highlightedIndex === idx }"
@mouseenter="$emit('highlight-element', idx)"
@mouseleave="$emit('highlight-element', -1)"
>
<div class="element-header">
<span class="element-type" :style="{ color: ELEMENT_COLORS[el.type] || ELEMENT_COLORS.text }">
{{ el.type }}
</span>
<span class="element-level" v-if="el.level">L{{ el.level }}</span>
</div>
<div class="element-content" v-if="el.content">
<MarkdownViewer v-if="el.type === 'table'" :content="el.content" />
<pre v-else-if="el.type === 'code'" class="element-code">{{ el.content }}</pre>
<span v-else>{{ el.content }}</span>
</div>
<div class="element-bbox">
{{ el.bbox.map(v => Math.round(v)).join(', ') }}
</div>
</div>
</div>
<!-- RAW MARKDOWN -->
<div v-else-if="activeTab === 'markdown'" class="raw-markdown">
<pre class="raw-content">{{ pageMarkdown }}</pre>
</div>
<!-- IMAGES -->
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
</div>
</div>
@ -48,16 +80,31 @@ import MarkdownViewer from './MarkdownViewer.vue'
import ImageGallery from './ImageGallery.vue'
import { useI18n } from '../../../shared/i18n.js'
const ELEMENT_COLORS = {
title: '#EF4444',
section_header: '#F97316',
text: '#3B82F6',
table: '#8B5CF6',
picture: '#22C55E',
list: '#06B6D4',
formula: '#EC4899',
code: '#14B8A6',
caption: '#EAB308'
}
const props = defineProps({
currentPage: { type: Number, default: 1 }
currentPage: { type: Number, default: 1 },
highlightedIndex: { type: Number, default: -1 }
})
defineEmits(['highlight-element'])
const store = useAnalysisStore()
const { t } = useI18n()
const activeTab = ref('text')
const activeTab = ref('elements')
const tabs = computed(() => [
{ id: 'text', label: t('results.textResult') },
{ id: 'elements', label: t('results.elements') },
{ id: 'markdown', label: t('results.markdown') },
{ id: 'images', label: t('results.images') }
])
@ -68,11 +115,15 @@ const currentPageData = computed(() => {
return store.currentPages.find(p => p.page_number === props.currentPage) || null
})
const currentElements = computed(() => {
return (currentPageData.value?.elements || []).filter(el => el.content)
})
const currentPageAsArray = computed(() => {
return currentPageData.value ? [currentPageData.value] : []
})
/** Build markdown content from page elements */
/** Build raw markdown content from page elements */
const pageMarkdown = computed(() => {
const page = currentPageData.value
if (!page) return ''
@ -90,7 +141,6 @@ function formatElement(el) {
case 'title':
return `# ${el.content}`
case 'section_header': {
// Use hierarchy level for heading depth (h2-h4)
const depth = Math.min(Math.max(el.level || 2, 2), 4)
return `${'#'.repeat(depth)} ${el.content}`
}
@ -164,9 +214,97 @@ function formatElement(el) {
.tab-content {
flex: 1;
overflow-y: auto;
padding: 16px;
padding: 12px;
}
/* --- Elements list --- */
.elements-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.elements-empty {
text-align: center;
color: var(--text-muted);
padding: 40px;
font-size: 14px;
}
.element-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all var(--transition);
cursor: default;
}
.element-card:hover {
border-color: var(--border-light);
background: var(--bg-elevated);
}
.element-card.highlighted {
border-color: var(--accent);
background: var(--accent-muted);
}
.element-header {
display: flex;
align-items: center;
gap: 8px;
}
.element-type {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.element-level {
font-size: 10px;
font-family: 'IBM Plex Mono', monospace;
color: var(--text-muted);
background: var(--bg-elevated);
padding: 1px 5px;
border-radius: 3px;
}
.element-content {
font-size: 13px;
color: var(--text);
line-height: 1.5;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
}
.element-code {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px;
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
white-space: pre-wrap;
margin: 0;
overflow-x: auto;
}
.element-bbox {
font-family: 'IBM Plex Mono', monospace;
font-size: 10px;
color: var(--text-muted);
}
/* --- Raw markdown --- */
.raw-markdown {
height: 100%;
}
@ -185,6 +323,7 @@ function formatElement(el) {
margin: 0;
}
/* --- Placeholders --- */
.result-placeholder {
display: flex;
flex-direction: column;

View file

@ -127,6 +127,8 @@
ref="bboxOverlayRef"
:image-el="pdfImageRef"
:page-data="currentPageData"
:highlighted-index="highlightedElementIndex"
@highlight-element="highlightedElementIndex = $event"
/>
</div>
</div>
@ -268,7 +270,11 @@
<!-- VERIFIER MODE -->
<div v-if="mode === 'verifier'" class="verify-panel">
<ResultTabs :current-page="currentPage" />
<ResultTabs
:current-page="currentPage"
:highlighted-index="highlightedElementIndex"
@highlight-element="highlightedElementIndex = $event"
/>
</div>
</div>
</div>
@ -295,6 +301,7 @@ const { t } = useI18n()
const mode = ref('configurer')
const currentPage = ref(1)
const visualMode = ref(false)
const highlightedElementIndex = ref(-1)
const pdfImageRef = ref(null)
const bboxOverlayRef = ref(null)

View file

@ -65,10 +65,11 @@ const messages = {
'config.documents': 'Documents',
// Results
'results.textResult': 'Résultat du texte',
'results.elements': 'Éléments',
'results.markdown': 'Markdown',
'results.images': 'Images',
'results.pageOf': 'Page {current} sur {total}',
'results.noElements': 'Aucun élément détecté sur cette page',
'results.noImages': 'Aucune image détectée dans ce document',
'results.noMarkdown': 'Pas de contenu markdown',
'results.runAnalysis': 'Lancez une analyse pour voir les résultats',
@ -154,10 +155,11 @@ const messages = {
'config.imagesScale': 'Images scale',
'config.documents': 'Documents',
'results.textResult': 'Text result',
'results.elements': 'Elements',
'results.markdown': 'Markdown',
'results.images': 'Images',
'results.pageOf': 'Page {current} of {total}',
'results.noElements': 'No elements detected on this page',
'results.noImages': 'No images detected in this document',
'results.noMarkdown': 'No markdown content',
'results.runAnalysis': 'Run an analysis to see results',