diff --git a/docs/screenshots/DS-execution-result.png b/docs/screenshots/DS-execution-result.png
index 4b0876d..1500ae4 100644
Binary files a/docs/screenshots/DS-execution-result.png and b/docs/screenshots/DS-execution-result.png differ
diff --git a/document-parser/data/docling_studio.db b/document-parser/data/docling_studio.db
index 84ff90a..ef5cdbb 100644
Binary files a/document-parser/data/docling_studio.db and b/document-parser/data/docling_studio.db differ
diff --git a/frontend/src/features/analysis/ui/BboxOverlay.vue b/frontend/src/features/analysis/ui/BboxOverlay.vue
index a3badf7..edfd17c 100644
--- a/frontend/src/features/analysis/ui/BboxOverlay.vue
+++ b/frontend/src/features/analysis/ui/BboxOverlay.vue
@@ -20,7 +20,7 @@
ref="canvasRef"
class="overlay-canvas"
@mousemove="onMouseMove"
- @mouseleave="hoveredElement = null"
+ @mouseleave="hoveredElement = null; emit('highlight-element', -1)"
/>
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)
})
diff --git a/frontend/src/features/analysis/ui/ResultTabs.vue b/frontend/src/features/analysis/ui/ResultTabs.vue
index 3a0ed8f..1a3a9e7 100644
--- a/frontend/src/features/analysis/ui/ResultTabs.vue
+++ b/frontend/src/features/analysis/ui/ResultTabs.vue
@@ -18,10 +18,42 @@
-
+
+
+
+ {{ t('results.noElements') }}
+
+
+
+
+
+
{{ el.content }}
+
{{ el.content }}
+
+
+ {{ el.bbox.map(v => Math.round(v)).join(', ') }}
+
+
+
+
+
+
+
@@ -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;
diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue
index 78c26c2..8aa4c5e 100644
--- a/frontend/src/pages/StudioPage.vue
+++ b/frontend/src/pages/StudioPage.vue
@@ -127,6 +127,8 @@
ref="bboxOverlayRef"
:image-el="pdfImageRef"
:page-data="currentPageData"
+ :highlighted-index="highlightedElementIndex"
+ @highlight-element="highlightedElementIndex = $event"
/>
@@ -268,7 +270,11 @@
-
+
@@ -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)
diff --git a/frontend/src/shared/i18n.js b/frontend/src/shared/i18n.js
index bb7446a..16d5ce6 100644
--- a/frontend/src/shared/i18n.js
+++ b/frontend/src/shared/i18n.js
@@ -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',