Merge pull request #150 from scub-france/feature/edit-chunk-text
feat(chunking): inline chunk text editing
This commit is contained in:
commit
7864a2c9e0
13 changed files with 467 additions and 3 deletions
|
|
@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|||
|
||||
### Added
|
||||
|
||||
- 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
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from api.schemas import (
|
|||
ChunkResponse,
|
||||
CreateAnalysisRequest,
|
||||
RechunkRequest,
|
||||
UpdateChunkTextRequest,
|
||||
)
|
||||
from services.analysis_service import AnalysisService
|
||||
|
||||
|
|
@ -110,6 +111,28 @@ async def rechunk_analysis(
|
|||
]
|
||||
|
||||
|
||||
@router.patch("/{job_id}/chunks/{chunk_index}", response_model=list[ChunkResponse])
|
||||
async def update_chunk_text(
|
||||
job_id: str, chunk_index: int, body: UpdateChunkTextRequest, service: ServiceDep
|
||||
) -> list[ChunkResponse]:
|
||||
"""Update the text of a single chunk by index."""
|
||||
try:
|
||||
chunks = await service.update_chunk_text(job_id, chunk_index, body.text)
|
||||
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),
|
||||
)
|
||||
for c in chunks
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/{job_id}", status_code=204)
|
||||
async def delete_analysis(job_id: str, service: ServiceDep) -> None:
|
||||
"""Delete an analysis job."""
|
||||
|
|
|
|||
|
|
@ -158,6 +158,11 @@ class ChunkResponse(_CamelModel):
|
|||
source_page: int | None = None
|
||||
token_count: int = 0
|
||||
bboxes: list[ChunkBboxResponse] = []
|
||||
modified: bool = False
|
||||
|
||||
|
||||
class UpdateChunkTextRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class CreateAnalysisRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -160,6 +160,28 @@ class AnalysisService:
|
|||
|
||||
return chunks
|
||||
|
||||
async def update_chunk_text(self, job_id: str, chunk_index: int, text: str) -> list[dict]:
|
||||
"""Update the text of a single 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]["text"] = text
|
||||
chunks[chunk_index]["modified"] = 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,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.models import AnalysisStatus
|
||||
from domain.services import extract_html_body, merge_results
|
||||
from domain.value_objects import ConversionResult, PageDetail
|
||||
from services.analysis_service import AnalysisConfig, AnalysisService, _count_pdf_pages
|
||||
|
|
@ -515,3 +517,80 @@ class TestBatchedConversion:
|
|||
assert result is None
|
||||
# Only first batch should have been converted
|
||||
assert converter.convert.call_count == 1
|
||||
|
||||
|
||||
class TestUpdateChunkText:
|
||||
"""Tests for AnalysisService.update_chunk_text."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chunk_text_success(self):
|
||||
chunks = [
|
||||
{"text": "original", "headings": [], "sourcePage": 1, "tokenCount": 5, "bboxes": []},
|
||||
{"text": "second", "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.update_chunk_text("j1", 0, "updated text")
|
||||
|
||||
assert result[0]["text"] == "updated text"
|
||||
assert result[0]["modified"] is True
|
||||
assert result[1]["text"] == "second"
|
||||
assert result[1].get("modified", False) is False
|
||||
repo.update_chunks.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chunk_text_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.update_chunk_text("missing", 0, "text")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chunk_text_not_completed(self):
|
||||
job = MagicMock()
|
||||
job.status = AnalysisStatus.RUNNING
|
||||
|
||||
repo = MagicMock()
|
||||
repo.find_by_id = AsyncMock(return_value=job)
|
||||
service = _make_service(analysis_repo=repo)
|
||||
|
||||
with pytest.raises(ValueError, match="not completed"):
|
||||
await service.update_chunk_text("j1", 0, "text")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chunk_text_index_out_of_range(self):
|
||||
chunks = [
|
||||
{"text": "only one", "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.update_chunk_text("j1", 5, "text")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chunk_text_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.update_chunk_text("j1", 0, "text")
|
||||
|
|
|
|||
|
|
@ -278,6 +278,70 @@ class TestCreateAnalysisWithChunking:
|
|||
assert chunks[0]["text"] == "chunk1"
|
||||
|
||||
|
||||
class TestUpdateChunkTextEndpoint:
|
||||
def test_update_chunk_text_success(self, client, mock_analysis_service):
|
||||
updated_chunks = [
|
||||
{
|
||||
"text": "updated text",
|
||||
"headings": ["H1"],
|
||||
"sourcePage": 1,
|
||||
"tokenCount": 10,
|
||||
"bboxes": [],
|
||||
"modified": True,
|
||||
},
|
||||
{
|
||||
"text": "chunk2",
|
||||
"headings": [],
|
||||
"sourcePage": 2,
|
||||
"tokenCount": 20,
|
||||
"bboxes": [],
|
||||
"modified": False,
|
||||
},
|
||||
]
|
||||
mock_analysis_service.update_chunk_text = AsyncMock(return_value=updated_chunks)
|
||||
|
||||
resp = client.patch(
|
||||
"/api/analyses/j1/chunks/0",
|
||||
json={"text": "updated text"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 2
|
||||
assert data[0]["text"] == "updated text"
|
||||
assert data[0]["modified"] is True
|
||||
assert data[1]["modified"] is False
|
||||
|
||||
def test_update_chunk_text_invalid_index(self, client, mock_analysis_service):
|
||||
mock_analysis_service.update_chunk_text = AsyncMock(
|
||||
side_effect=ValueError("Chunk index out of range: 99"),
|
||||
)
|
||||
resp = client.patch(
|
||||
"/api/analyses/j1/chunks/99",
|
||||
json={"text": "new"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_update_chunk_text_not_completed(self, client, mock_analysis_service):
|
||||
mock_analysis_service.update_chunk_text = AsyncMock(
|
||||
side_effect=ValueError("Analysis is not completed: j1"),
|
||||
)
|
||||
resp = client.patch(
|
||||
"/api/analyses/j1/chunks/0",
|
||||
json={"text": "new"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_update_chunk_text_not_found(self, client, mock_analysis_service):
|
||||
mock_analysis_service.update_chunk_text = AsyncMock(
|
||||
side_effect=ValueError("Analysis not found: j1"),
|
||||
)
|
||||
resp = client.patch(
|
||||
"/api/analyses/j1/chunks/0",
|
||||
json={"text": "new"},
|
||||
)
|
||||
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 } from './api'
|
||||
import { rechunkAnalysis, updateChunkText } from './api'
|
||||
|
||||
vi.mock('../../shared/api/http', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
|
|
@ -25,4 +25,19 @@ describe('chunking API', () => {
|
|||
})
|
||||
expect(result).toEqual(chunks)
|
||||
})
|
||||
|
||||
it('updateChunkText sends PATCH to chunk endpoint', async () => {
|
||||
const chunks = [
|
||||
{ text: 'updated', headings: [], sourcePage: 1, tokenCount: 10, bboxes: [], modified: true },
|
||||
]
|
||||
apiFetch.mockResolvedValue(chunks)
|
||||
|
||||
const result = await updateChunkText('job-1', 0, 'updated')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses/job-1/chunks/0', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ text: 'updated' }),
|
||||
})
|
||||
expect(result).toEqual(chunks)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,3 +7,10 @@ export function rechunkAnalysis(jobId: string, chunkingOptions: ChunkingOptions)
|
|||
body: JSON.stringify({ chunkingOptions }),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateChunkText(jobId: string, chunkIndex: number, text: string): Promise<Chunk[]> {
|
||||
return apiFetch<Chunk[]>(`/api/analyses/${jobId}/chunks/${chunkIndex}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useChunkingStore } from './store'
|
|||
|
||||
vi.mock('./api', () => ({
|
||||
rechunkAnalysis: vi.fn(),
|
||||
updateChunkText: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as api from './api'
|
||||
|
|
@ -17,6 +18,7 @@ describe('useChunkingStore', () => {
|
|||
it('starts with default state', () => {
|
||||
const store = useChunkingStore()
|
||||
expect(store.rechunking).toBe(false)
|
||||
expect(store.saving).toBe(false)
|
||||
expect(store.error).toBeNull()
|
||||
})
|
||||
|
||||
|
|
@ -62,4 +64,46 @@ describe('useChunkingStore', () => {
|
|||
expect(store.rechunking).toBe(false)
|
||||
expect(store.error).toBe('fail')
|
||||
})
|
||||
|
||||
it('updateChunkText calls API and returns chunks', async () => {
|
||||
const chunks = [
|
||||
{ text: 'updated', headings: [], sourcePage: 1, tokenCount: 5, bboxes: [], modified: true },
|
||||
]
|
||||
vi.mocked(api.updateChunkText).mockResolvedValue(chunks)
|
||||
|
||||
const store = useChunkingStore()
|
||||
const result = await store.updateChunkText('j1', 0, 'updated')
|
||||
|
||||
expect(api.updateChunkText).toHaveBeenCalledWith('j1', 0, 'updated')
|
||||
expect(result).toEqual(chunks)
|
||||
expect(store.saving).toBe(false)
|
||||
})
|
||||
|
||||
it('updateChunkText sets saving during execution', async () => {
|
||||
let resolve: (v: any) => void
|
||||
vi.mocked(api.updateChunkText).mockImplementation(
|
||||
() =>
|
||||
new Promise((r) => {
|
||||
resolve = r
|
||||
}),
|
||||
)
|
||||
|
||||
const store = useChunkingStore()
|
||||
const promise = store.updateChunkText('j1', 0, 'updated')
|
||||
|
||||
expect(store.saving).toBe(true)
|
||||
resolve!([])
|
||||
await promise
|
||||
expect(store.saving).toBe(false)
|
||||
})
|
||||
|
||||
it('updateChunkText handles errors', async () => {
|
||||
vi.mocked(api.updateChunkText).mockRejectedValue(new Error('save failed'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useChunkingStore()
|
||||
await expect(store.updateChunkText('j1', 0, 'text')).rejects.toThrow('save failed')
|
||||
expect(store.saving).toBe(false)
|
||||
expect(store.error).toBe('save failed')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as api from './api'
|
|||
|
||||
export const useChunkingStore = defineStore('chunking', () => {
|
||||
const rechunking = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function rechunk(jobId: string, chunkingOptions: ChunkingOptions): Promise<Chunk[]> {
|
||||
|
|
@ -21,5 +22,23 @@ export const useChunkingStore = defineStore('chunking', () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { rechunking, error, rechunk }
|
||||
async function updateChunkText(
|
||||
jobId: string,
|
||||
chunkIndex: number,
|
||||
text: string,
|
||||
): Promise<Chunk[]> {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
return await api.updateChunkText(jobId, chunkIndex, text)
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message || 'Failed to update chunk'
|
||||
console.error('Failed to update chunk', e)
|
||||
throw e
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { rechunking, saving, error, rechunk, updateChunkText }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -102,11 +102,64 @@
|
|||
{{ chunk.tokenCount }} tokens
|
||||
</span>
|
||||
<span class="chunk-page" v-if="chunk.sourcePage"> p.{{ chunk.sourcePage }} </span>
|
||||
<span v-if="chunk.modified" class="chunk-modified" data-e2e="chunk-modified">
|
||||
{{ t('chunking.modified') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="editingIdx !== globalIndex(localIdx)"
|
||||
class="chunk-edit-icon"
|
||||
data-e2e="chunk-edit-btn"
|
||||
:title="t('chunking.edit')"
|
||||
@click.stop="startEdit(globalIndex(localIdx), chunk.text)"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14">
|
||||
<path
|
||||
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<div class="chunk-text" data-e2e="chunk-text">{{ chunk.text }}</div>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<div v-if="editingIdx === globalIndex(localIdx)" class="chunk-edit">
|
||||
<textarea
|
||||
ref="editTextarea"
|
||||
class="chunk-edit-textarea"
|
||||
data-e2e="chunk-edit-textarea"
|
||||
v-model="editText"
|
||||
rows="6"
|
||||
/>
|
||||
<div class="chunk-edit-actions">
|
||||
<button
|
||||
class="chunk-edit-btn save"
|
||||
data-e2e="chunk-edit-save"
|
||||
:disabled="chunkingStore.saving"
|
||||
@click="saveEdit(globalIndex(localIdx))"
|
||||
>
|
||||
{{ chunkingStore.saving ? t('chunking.saving') : t('chunking.save') }}
|
||||
</button>
|
||||
<button
|
||||
class="chunk-edit-btn cancel"
|
||||
data-e2e="chunk-edit-cancel"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ t('chunking.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Read mode -->
|
||||
<div
|
||||
v-else
|
||||
class="chunk-text"
|
||||
data-e2e="chunk-text"
|
||||
@dblclick="startEdit(globalIndex(localIdx), chunk.text)"
|
||||
>
|
||||
{{ chunk.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -177,6 +230,32 @@ function globalIndex(localIdx: number): number {
|
|||
return (pagination.page.value - 1) * pagination.pageSize.value + localIdx
|
||||
}
|
||||
|
||||
const editingIdx = ref(-1)
|
||||
const editText = ref('')
|
||||
|
||||
function startEdit(idx: number, text: string) {
|
||||
editingIdx.value = idx
|
||||
editText.value = text
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingIdx.value = -1
|
||||
editText.value = ''
|
||||
}
|
||||
|
||||
async function saveEdit(chunkIndex: number) {
|
||||
if (!props.analysisId) return
|
||||
const allChunks = props.chunks
|
||||
const originalText = allChunks[chunkIndex]?.text
|
||||
if (editText.value === originalText) {
|
||||
cancelEdit()
|
||||
return
|
||||
}
|
||||
await chunkingStore.updateChunkText(props.analysisId, chunkIndex, editText.value)
|
||||
emit('rechunked')
|
||||
cancelEdit()
|
||||
}
|
||||
|
||||
const hoveredChunkIdx = ref(-1)
|
||||
|
||||
function onChunkHover(chunk: Chunk, localIdx: number) {
|
||||
|
|
@ -425,6 +504,100 @@ async function doRechunk() {
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chunk-modified {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.chunk-edit-icon {
|
||||
margin-left: auto;
|
||||
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;
|
||||
}
|
||||
|
||||
.chunk-edit-icon:hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.chunk-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chunk-edit-textarea {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
padding: 8px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chunk-edit-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.chunk-edit-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chunk-edit-btn {
|
||||
padding: 4px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chunk-edit-btn.save {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chunk-edit-btn.save:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chunk-edit-btn.cancel {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chunk-edit-btn.cancel:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chunk-text {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
|
|
@ -433,6 +606,7 @@ async function doRechunk() {
|
|||
word-break: break-word;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.chunk-empty {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ const messages: Messages = {
|
|||
'chunking.chunks': 'chunks',
|
||||
'chunking.noChunks': 'Lancez le chunking pour préparer les segments.',
|
||||
'chunking.noChunksOnPage': 'Aucun chunk sur cette page.',
|
||||
'chunking.edit': 'Modifier',
|
||||
'chunking.save': 'Enregistrer',
|
||||
'chunking.saving': 'Enregistrement...',
|
||||
'chunking.cancel': 'Annuler',
|
||||
'chunking.modified': 'modifié',
|
||||
'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.',
|
||||
|
||||
|
|
@ -245,6 +250,11 @@ const messages: Messages = {
|
|||
'chunking.chunks': 'chunks',
|
||||
'chunking.noChunks': 'Run chunking to prepare segments.',
|
||||
'chunking.noChunksOnPage': 'No chunks on this page.',
|
||||
'chunking.edit': 'Edit',
|
||||
'chunking.save': 'Save',
|
||||
'chunking.saving': 'Saving...',
|
||||
'chunking.cancel': 'Cancel',
|
||||
'chunking.modified': 'modified',
|
||||
'chunking.batchNotice':
|
||||
'Chunking is not available for this analysis. Large documents processed in batch mode do not generate the internal structure required for chunking.',
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export interface Chunk {
|
|||
sourcePage: number | null
|
||||
tokenCount: number
|
||||
bboxes: ChunkBbox[]
|
||||
modified?: boolean
|
||||
}
|
||||
|
||||
export interface PageElement {
|
||||
|
|
|
|||
Loading…
Reference in a new issue