Merge pull request #17 from scub-france/split-rendering
Split the results of OCR
This commit is contained in:
commit
9fe05756e8
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"
|
ref="canvasRef"
|
||||||
class="overlay-canvas"
|
class="overlay-canvas"
|
||||||
@mousemove="onMouseMove"
|
@mousemove="onMouseMove"
|
||||||
@mouseleave="hoveredElement = null"
|
@mouseleave="hoveredElement = null; emit('highlight-element', -1)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="hoveredElement"
|
v-if="hoveredElement"
|
||||||
|
|
@ -55,9 +55,13 @@ const props = defineProps({
|
||||||
/** The <img> element to overlay onto */
|
/** The <img> element to overlay onto */
|
||||||
imageEl: { type: Object, default: null },
|
imageEl: { type: Object, default: null },
|
||||||
/** Page data { page_number, width, height, elements[] } for current page */
|
/** 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 hiddenTypes = reactive(new Set())
|
||||||
const canvasRef = ref(null)
|
const canvasRef = ref(null)
|
||||||
const hoveredElement = 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)
|
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) {
|
for (const el of visibleElements.value) {
|
||||||
const rect = bboxToRect(el.bbox, scale)
|
const rect = bboxToRect(el.bbox, scale)
|
||||||
const color = ELEMENT_COLORS[el.type] || ELEMENT_COLORS.text
|
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.strokeStyle = color
|
||||||
ctx.lineWidth = 2
|
ctx.lineWidth = isHighlighted ? 3 : 2
|
||||||
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h)
|
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)
|
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 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 found = null
|
||||||
|
let foundIdx = -1
|
||||||
for (const el of visibleElements.value) {
|
for (const el of visibleElements.value) {
|
||||||
if (pointInRect(mx, my, bboxToRect(el.bbox, scale))) {
|
if (pointInRect(mx, my, bboxToRect(el.bbox, scale))) {
|
||||||
found = el
|
found = el
|
||||||
|
foundIdx = contentElements.indexOf(el)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hoveredElement.value = found
|
hoveredElement.value = found
|
||||||
|
emit('highlight-element', foundIdx)
|
||||||
if (found) {
|
if (found) {
|
||||||
tooltipStyle.value = {
|
tooltipStyle.value = {
|
||||||
left: `${Math.min(mx + 12, canvas.width - 250)}px`,
|
left: `${Math.min(mx + 12, canvas.width - 250)}px`,
|
||||||
|
|
@ -151,7 +168,7 @@ onBeforeUnmount(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Redraw when data or image changes
|
// Redraw when data or image changes
|
||||||
watch([() => props.pageData, () => props.imageEl, hiddenTypes], () => {
|
watch([() => props.pageData, () => props.imageEl, () => props.highlightedIndex, hiddenTypes], () => {
|
||||||
nextTick(draw)
|
nextTick(draw)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,42 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content">
|
<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">
|
<div v-else-if="activeTab === 'markdown'" class="raw-markdown">
|
||||||
<pre class="raw-content">{{ pageMarkdown }}</pre>
|
<pre class="raw-content">{{ pageMarkdown }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- IMAGES -->
|
||||||
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
|
<ImageGallery v-else-if="activeTab === 'images'" :pages="currentPageAsArray" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,16 +80,31 @@ import MarkdownViewer from './MarkdownViewer.vue'
|
||||||
import ImageGallery from './ImageGallery.vue'
|
import ImageGallery from './ImageGallery.vue'
|
||||||
import { useI18n } from '../../../shared/i18n.js'
|
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({
|
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 store = useAnalysisStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const activeTab = ref('text')
|
const activeTab = ref('elements')
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => [
|
||||||
{ id: 'text', label: t('results.textResult') },
|
{ id: 'elements', label: t('results.elements') },
|
||||||
{ id: 'markdown', label: t('results.markdown') },
|
{ id: 'markdown', label: t('results.markdown') },
|
||||||
{ id: 'images', label: t('results.images') }
|
{ id: 'images', label: t('results.images') }
|
||||||
])
|
])
|
||||||
|
|
@ -68,11 +115,15 @@ const currentPageData = computed(() => {
|
||||||
return store.currentPages.find(p => p.page_number === props.currentPage) || null
|
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(() => {
|
const currentPageAsArray = computed(() => {
|
||||||
return currentPageData.value ? [currentPageData.value] : []
|
return currentPageData.value ? [currentPageData.value] : []
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Build markdown content from page elements */
|
/** Build raw markdown content from page elements */
|
||||||
const pageMarkdown = computed(() => {
|
const pageMarkdown = computed(() => {
|
||||||
const page = currentPageData.value
|
const page = currentPageData.value
|
||||||
if (!page) return ''
|
if (!page) return ''
|
||||||
|
|
@ -90,7 +141,6 @@ function formatElement(el) {
|
||||||
case 'title':
|
case 'title':
|
||||||
return `# ${el.content}`
|
return `# ${el.content}`
|
||||||
case 'section_header': {
|
case 'section_header': {
|
||||||
// Use hierarchy level for heading depth (h2-h4)
|
|
||||||
const depth = Math.min(Math.max(el.level || 2, 2), 4)
|
const depth = Math.min(Math.max(el.level || 2, 2), 4)
|
||||||
return `${'#'.repeat(depth)} ${el.content}`
|
return `${'#'.repeat(depth)} ${el.content}`
|
||||||
}
|
}
|
||||||
|
|
@ -164,9 +214,97 @@ function formatElement(el) {
|
||||||
.tab-content {
|
.tab-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
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 {
|
.raw-markdown {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +323,7 @@ function formatElement(el) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Placeholders --- */
|
||||||
.result-placeholder {
|
.result-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,8 @@
|
||||||
ref="bboxOverlayRef"
|
ref="bboxOverlayRef"
|
||||||
:image-el="pdfImageRef"
|
:image-el="pdfImageRef"
|
||||||
:page-data="currentPageData"
|
:page-data="currentPageData"
|
||||||
|
:highlighted-index="highlightedElementIndex"
|
||||||
|
@highlight-element="highlightedElementIndex = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -268,7 +270,11 @@
|
||||||
|
|
||||||
<!-- VERIFIER MODE -->
|
<!-- VERIFIER MODE -->
|
||||||
<div v-if="mode === 'verifier'" class="verify-panel">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -295,6 +301,7 @@ const { t } = useI18n()
|
||||||
const mode = ref('configurer')
|
const mode = ref('configurer')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const visualMode = ref(false)
|
const visualMode = ref(false)
|
||||||
|
const highlightedElementIndex = ref(-1)
|
||||||
const pdfImageRef = ref(null)
|
const pdfImageRef = ref(null)
|
||||||
const bboxOverlayRef = ref(null)
|
const bboxOverlayRef = ref(null)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,11 @@ const messages = {
|
||||||
'config.documents': 'Documents',
|
'config.documents': 'Documents',
|
||||||
|
|
||||||
// Results
|
// Results
|
||||||
'results.textResult': 'Résultat du texte',
|
'results.elements': 'Éléments',
|
||||||
'results.markdown': 'Markdown',
|
'results.markdown': 'Markdown',
|
||||||
'results.images': 'Images',
|
'results.images': 'Images',
|
||||||
'results.pageOf': 'Page {current} sur {total}',
|
'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.noImages': 'Aucune image détectée dans ce document',
|
||||||
'results.noMarkdown': 'Pas de contenu markdown',
|
'results.noMarkdown': 'Pas de contenu markdown',
|
||||||
'results.runAnalysis': 'Lancez une analyse pour voir les résultats',
|
'results.runAnalysis': 'Lancez une analyse pour voir les résultats',
|
||||||
|
|
@ -154,10 +155,11 @@ const messages = {
|
||||||
'config.imagesScale': 'Images scale',
|
'config.imagesScale': 'Images scale',
|
||||||
'config.documents': 'Documents',
|
'config.documents': 'Documents',
|
||||||
|
|
||||||
'results.textResult': 'Text result',
|
'results.elements': 'Elements',
|
||||||
'results.markdown': 'Markdown',
|
'results.markdown': 'Markdown',
|
||||||
'results.images': 'Images',
|
'results.images': 'Images',
|
||||||
'results.pageOf': 'Page {current} of {total}',
|
'results.pageOf': 'Page {current} of {total}',
|
||||||
|
'results.noElements': 'No elements detected on this page',
|
||||||
'results.noImages': 'No images detected in this document',
|
'results.noImages': 'No images detected in this document',
|
||||||
'results.noMarkdown': 'No markdown content',
|
'results.noMarkdown': 'No markdown content',
|
||||||
'results.runAnalysis': 'Run an analysis to see results',
|
'results.runAnalysis': 'Run an analysis to see results',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue