Improve Prepare mode with page filtering, collapsible config, and bbox overlay

Filter chunks by current PDF page for consistency with Verify mode.
Add collapsible chunking config section and paginated chunk list.
Show BboxOverlay in Prepare mode so users see element bounding boxes.
Fix scroll containment on prepare panel.
This commit is contained in:
Pier-Jean Malandrino 2026-04-02 13:41:59 +02:00
parent 102a11765a
commit ace37429bd
2 changed files with 106 additions and 31 deletions

View file

@ -1,10 +1,24 @@
<template> <template>
<div class="chunk-panel"> <div class="chunk-panel">
<!-- Chunking config --> <!-- Chunking config collapsible -->
<div class="chunk-config"> <div class="chunk-config">
<div class="config-section"> <button class="config-toggle" @click="configOpen = !configOpen">
<label class="config-label">{{ t('chunking.settings') }}</label> <svg
class="config-chevron"
:class="{ open: configOpen }"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
<span class="config-label">{{ t('chunking.settings') }}</span>
</button>
<div v-if="configOpen" class="config-body">
<div class="config-row"> <div class="config-row">
<label class="config-label-sm">{{ t('chunking.chunkerType') }}</label> <label class="config-label-sm">{{ t('chunking.chunkerType') }}</label>
<select class="config-select" v-model="options.chunker_type"> <select class="config-select" v-model="options.chunker_type">
@ -40,31 +54,31 @@
<span class="toggle-text">{{ t('chunking.repeatTableHeader') }}</span> <span class="toggle-text">{{ t('chunking.repeatTableHeader') }}</span>
</label> </label>
</div> </div>
</div>
<button <button
class="chunk-btn primary" class="chunk-btn primary"
:disabled="!canRechunk || analysisStore.rechunking" :disabled="!canRechunk || analysisStore.rechunking"
@click="doRechunk" @click="doRechunk"
> >
<div v-if="analysisStore.rechunking" class="spinner-sm" /> <div v-if="analysisStore.rechunking" class="spinner-sm" />
{{ analysisStore.rechunking ? t('chunking.chunking') : t('chunking.run') }} {{ analysisStore.rechunking ? t('chunking.chunking') : t('chunking.run') }}
</button> </button>
</div>
</div> </div>
<!-- Chunks list --> <!-- Chunks list -->
<div class="chunk-results" v-if="analysisStore.currentChunks.length"> <div class="chunk-results" v-if="pageChunks.length">
<div class="chunk-summary"> <div class="chunk-summary">
{{ analysisStore.currentChunks.length }} {{ t('chunking.chunks') }} {{ pagination.totalItems.value }} {{ t('chunking.chunks') }}
</div> </div>
<div class="chunk-list"> <div class="chunk-list">
<div <div
class="chunk-card" class="chunk-card"
v-for="(chunk, idx) in analysisStore.currentChunks" v-for="(chunk, localIdx) in pagination.paginatedItems.value"
:key="idx" :key="globalIndex(localIdx)"
> >
<div class="chunk-header"> <div class="chunk-header">
<span class="chunk-index">#{{ idx + 1 }}</span> <span class="chunk-index">#{{ globalIndex(localIdx) + 1 }}</span>
<span class="chunk-tokens" v-if="chunk.tokenCount"> <span class="chunk-tokens" v-if="chunk.tokenCount">
{{ chunk.tokenCount }} tokens {{ chunk.tokenCount }} tokens
</span> </span>
@ -81,20 +95,43 @@
</div> </div>
<div class="chunk-empty" v-else-if="!analysisStore.rechunking"> <div class="chunk-empty" v-else-if="!analysisStore.rechunking">
<p>{{ t('chunking.noChunks') }}</p> <p>
{{
analysisStore.currentChunks.length
? t('chunking.noChunksOnPage')
: t('chunking.noChunks')
}}
</p>
</div> </div>
<!-- Pagination -->
<PaginationBar
:page="pagination.page.value"
:page-count="pagination.pageCount.value"
:page-size="pagination.pageSize.value"
@update:page="pagination.goTo($event)"
@update:page-size="pagination.setPageSize($event)"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, computed } from 'vue' import { ref, reactive, computed } from 'vue'
import { useAnalysisStore } from '../../analysis/store' import { useAnalysisStore } from '../../analysis/store'
import { useI18n } from '../../../shared/i18n' import { useI18n } from '../../../shared/i18n'
import { usePagination } from '../../../shared/composables/usePagination'
import { PaginationBar } from '../../../shared/ui'
import type { ChunkingOptions } from '../../../shared/types' import type { ChunkingOptions } from '../../../shared/types'
const props = defineProps<{
currentPage: number
}>()
const analysisStore = useAnalysisStore() const analysisStore = useAnalysisStore()
const { t } = useI18n() const { t } = useI18n()
const configOpen = ref(true)
const options = reactive<Required<ChunkingOptions>>({ const options = reactive<Required<ChunkingOptions>>({
chunker_type: 'hybrid', chunker_type: 'hybrid',
max_tokens: 512, max_tokens: 512,
@ -107,6 +144,15 @@ const canRechunk = computed(() => {
return analysis?.status === 'COMPLETED' && analysis.hasDocumentJson return analysis?.status === 'COMPLETED' && analysis.hasDocumentJson
}) })
const pageChunks = computed(() =>
analysisStore.currentChunks.filter((c) => c.sourcePage === props.currentPage),
)
const pagination = usePagination(pageChunks, { pageSize: 20 })
function globalIndex(localIdx: number): number {
return (pagination.page.value - 1) * pagination.pageSize.value + localIdx
}
async function doRechunk() { async function doRechunk() {
if (!analysisStore.currentAnalysis) return if (!analysisStore.currentAnalysis) return
await analysisStore.rechunk(analysisStore.currentAnalysis.id, { ...options }) await analysisStore.rechunk(analysisStore.currentAnalysis.id, { ...options })
@ -122,17 +168,35 @@ async function doRechunk() {
} }
.chunk-config { .chunk-config {
padding: 16px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
display: flex; flex-shrink: 0;
flex-direction: column;
gap: 12px;
} }
.config-section { .config-toggle {
display: flex; display: flex;
flex-direction: column; align-items: center;
gap: 8px; gap: 6px;
width: 100%;
padding: 10px 16px;
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
}
.config-toggle:hover {
color: var(--text);
}
.config-chevron {
width: 14px;
height: 14px;
transition: transform 0.2s;
transform: rotate(0deg);
}
.config-chevron.open {
transform: rotate(90deg);
} }
.config-label { .config-label {
@ -140,7 +204,13 @@ async function doRechunk() {
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: var(--text-secondary); }
.config-body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px 16px;
} }
.config-label-sm { .config-label-sm {
@ -225,6 +295,7 @@ async function doRechunk() {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 6px; gap: 6px;
margin-top: 4px;
} }
.chunk-btn.primary { .chunk-btn.primary {
@ -241,6 +312,7 @@ async function doRechunk() {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
min-height: 0;
} }
.chunk-summary { .chunk-summary {
@ -331,6 +403,8 @@ async function doRechunk() {
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
</style> </style>

View file

@ -136,7 +136,7 @@
@load="onPdfImageLoad" @load="onPdfImageLoad"
/> />
<BboxOverlay <BboxOverlay
v-if="visualMode && hasAnalysisResults" v-if="(visualMode || mode === 'preparer') && hasAnalysisResults"
ref="bboxOverlayRef" ref="bboxOverlayRef"
:image-el="pdfImageRef" :image-el="pdfImageRef"
:page-data="currentPageData" :page-data="currentPageData"
@ -292,7 +292,7 @@
<!-- PREPARER MODE (feature-flipped) --> <!-- PREPARER MODE (feature-flipped) -->
<div v-if="mode === 'preparer' && chunkingEnabled" class="prepare-panel"> <div v-if="mode === 'preparer' && chunkingEnabled" class="prepare-panel">
<ChunkPanel /> <ChunkPanel :current-page="currentPage" />
</div> </div>
</div> </div>
</div> </div>
@ -1136,7 +1136,8 @@ onBeforeUnmount(() => {
} }
/* Verify panel */ /* Verify panel */
.verify-panel { .verify-panel,
.prepare-panel {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;