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.type }} + + L{{ el.level }} +
+
+ +
{{ el.content }}
+ {{ el.content }} +
+
+ {{ el.bbox.map(v => Math.round(v)).join(', ') }} +
+
+
+ +
{{ pageMarkdown }}
+ +
@@ -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',