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:
parent
102a11765a
commit
ace37429bd
2 changed files with 106 additions and 31 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue