Merge pull request #151 from scub-france/feature/delete-chunk
feat(chunking): soft-delete chunk with confirmation dialog
This commit is contained in:
commit
da6ebec6dd
13 changed files with 383 additions and 5 deletions
|
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|||
|
||||
- Inline chunk text editing: double-click or edit button to modify chunk text, with save/cancel and "modified" badge
|
||||
- Docker Compose dev stack (`docker-compose.dev.yml`) with OpenSearch, Dashboards, hot-reload backend and Vite frontend
|
||||
- Soft-delete chunks: delete button with confirmation dialog, chunks hidden from UI but preserved in data
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,28 @@ async def update_chunk_text(
|
|||
token_count=c.get("tokenCount", 0),
|
||||
bboxes=[ChunkBboxResponse(page=b["page"], bbox=b["bbox"]) for b in c.get("bboxes", [])],
|
||||
modified=c.get("modified", False),
|
||||
deleted=c.get("deleted", False),
|
||||
)
|
||||
for c in chunks
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/{job_id}/chunks/{chunk_index}", response_model=list[ChunkResponse])
|
||||
async def delete_chunk(job_id: str, chunk_index: int, service: ServiceDep) -> list[ChunkResponse]:
|
||||
"""Soft-delete a chunk by index (marks it as deleted)."""
|
||||
try:
|
||||
chunks = await service.delete_chunk(job_id, chunk_index)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
return [
|
||||
ChunkResponse(
|
||||
text=c["text"],
|
||||
headings=c.get("headings", []),
|
||||
source_page=c.get("sourcePage"),
|
||||
token_count=c.get("tokenCount", 0),
|
||||
bboxes=[ChunkBboxResponse(page=b["page"], bbox=b["bbox"]) for b in c.get("bboxes", [])],
|
||||
modified=c.get("modified", False),
|
||||
deleted=c.get("deleted", False),
|
||||
)
|
||||
for c in chunks
|
||||
]
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ class ChunkResponse(_CamelModel):
|
|||
token_count: int = 0
|
||||
bboxes: list[ChunkBboxResponse] = []
|
||||
modified: bool = False
|
||||
deleted: bool = False
|
||||
|
||||
|
||||
class UpdateChunkTextRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -182,6 +182,27 @@ class AnalysisService:
|
|||
|
||||
return chunks
|
||||
|
||||
async def delete_chunk(self, job_id: str, chunk_index: int) -> list[dict]:
|
||||
"""Soft-delete a chunk by index. Returns the full updated chunks list."""
|
||||
job = await self._analysis_repo.find_by_id(job_id)
|
||||
if not job:
|
||||
raise ValueError(f"Analysis not found: {job_id}")
|
||||
if job.status != AnalysisStatus.COMPLETED:
|
||||
raise ValueError(f"Analysis is not completed: {job_id}")
|
||||
if not job.chunks_json:
|
||||
raise ValueError(f"No chunks available: {job_id}")
|
||||
|
||||
chunks = json.loads(job.chunks_json)
|
||||
if chunk_index < 0 or chunk_index >= len(chunks):
|
||||
raise ValueError(f"Chunk index out of range: {chunk_index}")
|
||||
|
||||
chunks[chunk_index]["deleted"] = True
|
||||
|
||||
chunks_json = json.dumps(chunks)
|
||||
await self._analysis_repo.update_chunks(job_id, chunks_json)
|
||||
|
||||
return chunks
|
||||
|
||||
async def _run_batched_conversion(
|
||||
self,
|
||||
job_id: str,
|
||||
|
|
|
|||
|
|
@ -594,3 +594,64 @@ class TestUpdateChunkText:
|
|||
|
||||
with pytest.raises(ValueError, match="No chunks available"):
|
||||
await service.update_chunk_text("j1", 0, "text")
|
||||
|
||||
|
||||
class TestDeleteChunk:
|
||||
"""Tests for AnalysisService.delete_chunk."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_chunk_success(self):
|
||||
chunks = [
|
||||
{"text": "chunk1", "headings": [], "sourcePage": 1, "tokenCount": 5, "bboxes": []},
|
||||
{"text": "chunk2", "headings": [], "sourcePage": 2, "tokenCount": 3, "bboxes": []},
|
||||
]
|
||||
job = MagicMock()
|
||||
job.status = AnalysisStatus.COMPLETED
|
||||
job.chunks_json = json.dumps(chunks)
|
||||
|
||||
repo = MagicMock()
|
||||
repo.find_by_id = AsyncMock(return_value=job)
|
||||
repo.update_chunks = AsyncMock(return_value=True)
|
||||
|
||||
service = _make_service(analysis_repo=repo)
|
||||
result = await service.delete_chunk("j1", 0)
|
||||
|
||||
assert result[0]["deleted"] is True
|
||||
assert result[1].get("deleted", False) is False
|
||||
repo.update_chunks.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_chunk_not_found(self):
|
||||
repo = MagicMock()
|
||||
repo.find_by_id = AsyncMock(return_value=None)
|
||||
service = _make_service(analysis_repo=repo)
|
||||
|
||||
with pytest.raises(ValueError, match="Analysis not found"):
|
||||
await service.delete_chunk("missing", 0)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_chunk_index_out_of_range(self):
|
||||
chunks = [{"text": "only", "headings": [], "sourcePage": 1, "tokenCount": 5, "bboxes": []}]
|
||||
job = MagicMock()
|
||||
job.status = AnalysisStatus.COMPLETED
|
||||
job.chunks_json = json.dumps(chunks)
|
||||
|
||||
repo = MagicMock()
|
||||
repo.find_by_id = AsyncMock(return_value=job)
|
||||
service = _make_service(analysis_repo=repo)
|
||||
|
||||
with pytest.raises(ValueError, match="out of range"):
|
||||
await service.delete_chunk("j1", 5)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_chunk_no_chunks(self):
|
||||
job = MagicMock()
|
||||
job.status = AnalysisStatus.COMPLETED
|
||||
job.chunks_json = None
|
||||
|
||||
repo = MagicMock()
|
||||
repo.find_by_id = AsyncMock(return_value=job)
|
||||
service = _make_service(analysis_repo=repo)
|
||||
|
||||
with pytest.raises(ValueError, match="No chunks available"):
|
||||
await service.delete_chunk("j1", 0)
|
||||
|
|
|
|||
|
|
@ -342,6 +342,50 @@ class TestUpdateChunkTextEndpoint:
|
|||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestDeleteChunkEndpoint:
|
||||
def test_delete_chunk_success(self, client, mock_analysis_service):
|
||||
updated_chunks = [
|
||||
{
|
||||
"text": "chunk1",
|
||||
"headings": [],
|
||||
"sourcePage": 1,
|
||||
"tokenCount": 10,
|
||||
"bboxes": [],
|
||||
"deleted": True,
|
||||
},
|
||||
{
|
||||
"text": "chunk2",
|
||||
"headings": [],
|
||||
"sourcePage": 2,
|
||||
"tokenCount": 20,
|
||||
"bboxes": [],
|
||||
"deleted": False,
|
||||
},
|
||||
]
|
||||
mock_analysis_service.delete_chunk = AsyncMock(return_value=updated_chunks)
|
||||
|
||||
resp = client.delete("/api/analyses/j1/chunks/0")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 2
|
||||
assert data[0]["deleted"] is True
|
||||
assert data[1]["deleted"] is False
|
||||
|
||||
def test_delete_chunk_invalid_index(self, client, mock_analysis_service):
|
||||
mock_analysis_service.delete_chunk = AsyncMock(
|
||||
side_effect=ValueError("Chunk index out of range: 99"),
|
||||
)
|
||||
resp = client.delete("/api/analyses/j1/chunks/99")
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_delete_chunk_not_completed(self, client, mock_analysis_service):
|
||||
mock_analysis_service.delete_chunk = AsyncMock(
|
||||
side_effect=ValueError("Analysis is not completed: j1"),
|
||||
)
|
||||
resp = client.delete("/api/analyses/j1/chunks/0")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestRechunkEndpoint:
|
||||
def test_rechunk_success(self, client, mock_analysis_service):
|
||||
mock_analysis_service.rechunk = AsyncMock(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { rechunkAnalysis, updateChunkText } from './api'
|
||||
import { rechunkAnalysis, updateChunkText, deleteChunk } from './api'
|
||||
|
||||
vi.mock('../../shared/api/http', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
|
|
@ -40,4 +40,18 @@ describe('chunking API', () => {
|
|||
})
|
||||
expect(result).toEqual(chunks)
|
||||
})
|
||||
|
||||
it('deleteChunk sends DELETE to chunk endpoint', async () => {
|
||||
const chunks = [
|
||||
{ text: 'chunk1', headings: [], sourcePage: 1, tokenCount: 10, bboxes: [], deleted: true },
|
||||
]
|
||||
apiFetch.mockResolvedValue(chunks)
|
||||
|
||||
const result = await deleteChunk('job-1', 0)
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses/job-1/chunks/0', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
expect(result).toEqual(chunks)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,3 +14,9 @@ export function updateChunkText(jobId: string, chunkIndex: number, text: string)
|
|||
body: JSON.stringify({ text }),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteChunk(jobId: string, chunkIndex: number): Promise<Chunk[]> {
|
||||
return apiFetch<Chunk[]>(`/api/analyses/${jobId}/chunks/${chunkIndex}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useChunkingStore } from './store'
|
|||
vi.mock('./api', () => ({
|
||||
rechunkAnalysis: vi.fn(),
|
||||
updateChunkText: vi.fn(),
|
||||
deleteChunk: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as api from './api'
|
||||
|
|
@ -19,6 +20,7 @@ describe('useChunkingStore', () => {
|
|||
const store = useChunkingStore()
|
||||
expect(store.rechunking).toBe(false)
|
||||
expect(store.saving).toBe(false)
|
||||
expect(store.deleting).toBe(false)
|
||||
expect(store.error).toBeNull()
|
||||
})
|
||||
|
||||
|
|
@ -106,4 +108,46 @@ describe('useChunkingStore', () => {
|
|||
expect(store.saving).toBe(false)
|
||||
expect(store.error).toBe('save failed')
|
||||
})
|
||||
|
||||
it('deleteChunk calls API and returns chunks', async () => {
|
||||
const chunks = [
|
||||
{ text: 'chunk1', headings: [], sourcePage: 1, tokenCount: 5, bboxes: [], deleted: true },
|
||||
]
|
||||
vi.mocked(api.deleteChunk).mockResolvedValue(chunks)
|
||||
|
||||
const store = useChunkingStore()
|
||||
const result = await store.deleteChunk('j1', 0)
|
||||
|
||||
expect(api.deleteChunk).toHaveBeenCalledWith('j1', 0)
|
||||
expect(result).toEqual(chunks)
|
||||
expect(store.deleting).toBe(false)
|
||||
})
|
||||
|
||||
it('deleteChunk sets deleting during execution', async () => {
|
||||
let resolve: (v: any) => void
|
||||
vi.mocked(api.deleteChunk).mockImplementation(
|
||||
() =>
|
||||
new Promise((r) => {
|
||||
resolve = r
|
||||
}),
|
||||
)
|
||||
|
||||
const store = useChunkingStore()
|
||||
const promise = store.deleteChunk('j1', 0)
|
||||
|
||||
expect(store.deleting).toBe(true)
|
||||
resolve!([])
|
||||
await promise
|
||||
expect(store.deleting).toBe(false)
|
||||
})
|
||||
|
||||
it('deleteChunk handles errors', async () => {
|
||||
vi.mocked(api.deleteChunk).mockRejectedValue(new Error('delete failed'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useChunkingStore()
|
||||
await expect(store.deleteChunk('j1', 0)).rejects.toThrow('delete failed')
|
||||
expect(store.deleting).toBe(false)
|
||||
expect(store.error).toBe('delete failed')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import * as api from './api'
|
|||
export const useChunkingStore = defineStore('chunking', () => {
|
||||
const rechunking = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function rechunk(jobId: string, chunkingOptions: ChunkingOptions): Promise<Chunk[]> {
|
||||
|
|
@ -40,5 +41,19 @@ export const useChunkingStore = defineStore('chunking', () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { rechunking, saving, error, rechunk, updateChunkText }
|
||||
async function deleteChunk(jobId: string, chunkIndex: number): Promise<Chunk[]> {
|
||||
deleting.value = true
|
||||
error.value = null
|
||||
try {
|
||||
return await api.deleteChunk(jobId, chunkIndex)
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message || 'Failed to delete chunk'
|
||||
console.error('Failed to delete chunk', e)
|
||||
throw e
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { rechunking, saving, deleting, error, rechunk, updateChunkText, deleteChunk }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
<!-- Chunks list -->
|
||||
<div class="chunk-results" data-e2e="chunk-results" v-if="pageChunks.length">
|
||||
<div class="chunk-summary" data-e2e="chunk-summary">
|
||||
{{ pagination.totalItems.value }} {{ t('chunking.chunks') }}
|
||||
{{ activeChunks.length }} {{ t('chunking.chunks') }}
|
||||
</div>
|
||||
<div class="chunk-list">
|
||||
<div
|
||||
|
|
@ -118,6 +118,20 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="chunk-delete-icon"
|
||||
data-e2e="chunk-delete-btn"
|
||||
:title="t('chunking.delete')"
|
||||
@click.stop="confirmDelete(globalIndex(localIdx))"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chunk-headings" v-if="chunk.headings.length">
|
||||
<span class="chunk-heading" v-for="h in chunk.headings" :key="h">{{ h }}</span>
|
||||
|
|
@ -164,6 +178,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<div v-if="deleteConfirmIdx !== -1" class="chunk-confirm-overlay" data-e2e="chunk-confirm">
|
||||
<div class="chunk-confirm-dialog">
|
||||
<p class="chunk-confirm-text">{{ t('chunking.deleteConfirm') }}</p>
|
||||
<div class="chunk-confirm-actions">
|
||||
<button
|
||||
class="chunk-confirm-btn danger"
|
||||
data-e2e="chunk-confirm-yes"
|
||||
:disabled="chunkingStore.deleting"
|
||||
@click="doDelete"
|
||||
>
|
||||
{{ chunkingStore.deleting ? t('chunking.deleting') : t('chunking.delete') }}
|
||||
</button>
|
||||
<button
|
||||
class="chunk-confirm-btn cancel"
|
||||
data-e2e="chunk-confirm-no"
|
||||
@click="deleteConfirmIdx = -1"
|
||||
>
|
||||
{{ t('chunking.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chunk-empty" v-else-if="!chunkingStore.rechunking">
|
||||
<p>
|
||||
{{ chunks.length ? t('chunking.noChunksOnPage') : t('chunking.noChunks') }}
|
||||
|
|
@ -223,11 +261,29 @@ const isBatchedAnalysis = computed(() => {
|
|||
return props.analysisStatus === 'COMPLETED' && !props.hasDocumentJson
|
||||
})
|
||||
|
||||
const pageChunks = computed(() => props.chunks.filter((c) => c.sourcePage === props.currentPage))
|
||||
const activeChunks = computed(() => props.chunks.filter((c) => !c.deleted))
|
||||
const pageChunks = computed(() =>
|
||||
activeChunks.value.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
|
||||
const pageLocalIdx = (pagination.page.value - 1) * pagination.pageSize.value + localIdx
|
||||
const pageChunk = pageChunks.value[pageLocalIdx]
|
||||
return props.chunks.indexOf(pageChunk)
|
||||
}
|
||||
|
||||
const deleteConfirmIdx = ref(-1)
|
||||
|
||||
function confirmDelete(chunkIndex: number) {
|
||||
deleteConfirmIdx.value = chunkIndex
|
||||
}
|
||||
|
||||
async function doDelete() {
|
||||
if (!props.analysisId || deleteConfirmIdx.value === -1) return
|
||||
await chunkingStore.deleteChunk(props.analysisId, deleteConfirmIdx.value)
|
||||
deleteConfirmIdx.value = -1
|
||||
emit('rechunked')
|
||||
}
|
||||
|
||||
const editingIdx = ref(-1)
|
||||
|
|
@ -282,6 +338,7 @@ async function doRechunk() {
|
|||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chunk-config {
|
||||
|
|
@ -529,6 +586,19 @@ async function doRechunk() {
|
|||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.chunk-delete-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.chunk-card:hover .chunk-edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
@ -538,6 +608,15 @@ async function doRechunk() {
|
|||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.chunk-card:hover .chunk-delete-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chunk-delete-icon:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.chunk-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -598,6 +677,67 @@ async function doRechunk() {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.chunk-confirm-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chunk-confirm-dialog {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
max-width: 300px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.chunk-confirm-text {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chunk-confirm-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chunk-confirm-btn {
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chunk-confirm-btn.danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chunk-confirm-btn.danger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chunk-confirm-btn.cancel {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chunk-confirm-btn.cancel:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chunk-text {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ const messages: Messages = {
|
|||
'chunking.saving': 'Enregistrement...',
|
||||
'chunking.cancel': 'Annuler',
|
||||
'chunking.modified': 'modifié',
|
||||
'chunking.delete': 'Supprimer',
|
||||
'chunking.deleting': 'Suppression...',
|
||||
'chunking.deleteConfirm':
|
||||
'Supprimer ce chunk ? Il sera marqué comme supprimé jusqu\u2019à la prochaine synchronisation.',
|
||||
'chunking.batchNotice':
|
||||
'Le chunking n\u2019est pas disponible pour cette analyse. Les documents volumineux trait\u00e9s par batch ne g\u00e9n\u00e8rent pas la structure interne n\u00e9cessaire au d\u00e9coupage.',
|
||||
|
||||
|
|
@ -255,6 +259,10 @@ const messages: Messages = {
|
|||
'chunking.saving': 'Saving...',
|
||||
'chunking.cancel': 'Cancel',
|
||||
'chunking.modified': 'modified',
|
||||
'chunking.delete': 'Delete',
|
||||
'chunking.deleting': 'Deleting...',
|
||||
'chunking.deleteConfirm':
|
||||
'Delete this chunk? It will be marked as deleted until the next sync.',
|
||||
'chunking.batchNotice':
|
||||
'Chunking is not available for this analysis. Large documents processed in batch mode do not generate the internal structure required for chunking.',
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface Chunk {
|
|||
tokenCount: number
|
||||
bboxes: ChunkBbox[]
|
||||
modified?: boolean
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
export interface PageElement {
|
||||
|
|
|
|||
Loading…
Reference in a new issue