Split the results of OCR
This commit is contained in:
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 |
Binary file not shown.
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue