feat(training): add document-level training enrollment
- V29 migration: document_training_labels join table
- TrainingLabel enum: KURRENT_RECOGNITION, KURRENT_SEGMENTATION
- Document.trainingLabels @ElementCollection
- DocumentService.addTrainingLabel / removeTrainingLabel
- PATCH /api/documents/{id}/training-labels (WRITE_ALL)
- Auto-enroll on Kurrent OCR trigger (OcrService.startOcr)
- TranscriptionEditView: enrollment chips in panel footer
- JPQL queries updated to use MEMBER OF trainingLabels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import org.raddatz.familienarchiv.exception.ErrorCode;
|
|||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.dto.DocumentSort;
|
import org.raddatz.familienarchiv.dto.DocumentSort;
|
||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
|
import org.raddatz.familienarchiv.model.TrainingLabel;
|
||||||
import org.raddatz.familienarchiv.model.DocumentVersion;
|
import org.raddatz.familienarchiv.model.DocumentVersion;
|
||||||
import org.raddatz.familienarchiv.security.Permission;
|
import org.raddatz.familienarchiv.security.Permission;
|
||||||
import org.raddatz.familienarchiv.security.RequirePermission;
|
import org.raddatz.familienarchiv.security.RequirePermission;
|
||||||
@@ -35,9 +36,11 @@ import org.springframework.core.io.InputStreamResource;
|
|||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
@@ -208,6 +211,29 @@ public class DocumentController {
|
|||||||
return ResponseEntity.ok(DocumentSearchResult.of(results));
|
return ResponseEntity.ok(DocumentSearchResult.of(results));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- TRAINING LABELS ---
|
||||||
|
|
||||||
|
public record TrainingLabelRequest(String label, boolean enrolled) {}
|
||||||
|
|
||||||
|
@PatchMapping("/{id}/training-labels")
|
||||||
|
@RequirePermission(Permission.WRITE_ALL)
|
||||||
|
public ResponseEntity<Void> patchTrainingLabel(
|
||||||
|
@PathVariable UUID id,
|
||||||
|
@RequestBody TrainingLabelRequest req) {
|
||||||
|
TrainingLabel label;
|
||||||
|
try {
|
||||||
|
label = TrainingLabel.valueOf(req.label());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown training label: " + req.label());
|
||||||
|
}
|
||||||
|
if (req.enrolled()) {
|
||||||
|
documentService.addTrainingLabel(id, label);
|
||||||
|
} else {
|
||||||
|
documentService.removeTrainingLabel(id, label);
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// --- VERSIONS ---
|
// --- VERSIONS ---
|
||||||
|
|
||||||
@GetMapping("/{id}/versions")
|
@GetMapping("/{id}/versions")
|
||||||
|
|||||||
@@ -110,4 +110,11 @@ public class Document {
|
|||||||
@JoinTable(name = "document_tags", joinColumns = @JoinColumn(name = "document_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
|
@JoinTable(name = "document_tags", joinColumns = @JoinColumn(name = "document_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Set<Tag> tags = new HashSet<>();
|
private Set<Tag> tags = new HashSet<>();
|
||||||
|
|
||||||
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
|
@CollectionTable(name = "document_training_labels", joinColumns = @JoinColumn(name = "document_id"))
|
||||||
|
@Column(name = "label")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Builder.Default
|
||||||
|
private Set<TrainingLabel> trainingLabels = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
|
public enum TrainingLabel {
|
||||||
|
KURRENT_RECOGNITION,
|
||||||
|
KURRENT_SEGMENTATION
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.repository;
|
|||||||
|
|
||||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -14,4 +15,22 @@ public interface TranscriptionBlockRepository extends JpaRepository<Transcriptio
|
|||||||
Optional<TranscriptionBlock> findByIdAndDocumentId(UUID id, UUID documentId);
|
Optional<TranscriptionBlock> findByIdAndDocumentId(UUID id, UUID documentId);
|
||||||
|
|
||||||
int countByDocumentId(UUID documentId);
|
int countByDocumentId(UUID documentId);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT b FROM TranscriptionBlock b
|
||||||
|
JOIN DocumentAnnotation a ON a.id = b.annotationId
|
||||||
|
JOIN Document d ON d.id = b.documentId
|
||||||
|
WHERE (b.source = 'MANUAL' OR (b.source = 'OCR' AND b.reviewed = true))
|
||||||
|
AND 'KURRENT_RECOGNITION' MEMBER OF d.trainingLabels
|
||||||
|
""")
|
||||||
|
List<TranscriptionBlock> findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT b FROM TranscriptionBlock b
|
||||||
|
JOIN DocumentAnnotation a ON a.id = b.annotationId
|
||||||
|
JOIN Document d ON d.id = b.documentId
|
||||||
|
WHERE b.source = 'MANUAL' AND (b.text IS NULL OR b.text = '')
|
||||||
|
AND 'KURRENT_SEGMENTATION' MEMBER OF d.trainingLabels
|
||||||
|
""")
|
||||||
|
List<TranscriptionBlock> findSegmentationBlocks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.raddatz.familienarchiv.model.Document;
|
|||||||
import org.raddatz.familienarchiv.dto.DocumentSort;
|
import org.raddatz.familienarchiv.dto.DocumentSort;
|
||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
import org.raddatz.familienarchiv.model.ScriptType;
|
import org.raddatz.familienarchiv.model.ScriptType;
|
||||||
|
import org.raddatz.familienarchiv.model.TrainingLabel;
|
||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
import org.raddatz.familienarchiv.model.Tag;
|
import org.raddatz.familienarchiv.model.Tag;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||||
@@ -385,6 +386,20 @@ public class DocumentService {
|
|||||||
documentRepository.save(doc);
|
documentRepository.save(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void addTrainingLabel(UUID documentId, TrainingLabel label) {
|
||||||
|
Document doc = getDocumentById(documentId);
|
||||||
|
doc.getTrainingLabels().add(label);
|
||||||
|
documentRepository.save(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeTrainingLabel(UUID documentId, TrainingLabel label) {
|
||||||
|
Document doc = getDocumentById(documentId);
|
||||||
|
doc.getTrainingLabels().remove(label);
|
||||||
|
documentRepository.save(doc);
|
||||||
|
}
|
||||||
|
|
||||||
public Document getDocumentById(UUID id) {
|
public Document getDocumentById(UUID id) {
|
||||||
return documentRepository.findById(id)
|
return documentRepository.findById(id)
|
||||||
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
|
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ public class OcrService {
|
|||||||
|
|
||||||
if (scriptTypeOverride != null) {
|
if (scriptTypeOverride != null) {
|
||||||
documentService.updateScriptType(documentId, scriptTypeOverride);
|
documentService.updateScriptType(documentId, scriptTypeOverride);
|
||||||
|
if (scriptTypeOverride == ScriptType.HANDWRITING_KURRENT) {
|
||||||
|
documentService.addTrainingLabel(documentId, TrainingLabel.KURRENT_RECOGNITION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OcrJob job = OcrJob.builder()
|
OcrJob job = OcrJob.builder()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE document_training_labels (
|
||||||
|
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||||
|
label VARCHAR(50) NOT NULL,
|
||||||
|
PRIMARY KEY (document_id, label)
|
||||||
|
);
|
||||||
@@ -14,6 +14,7 @@ import org.raddatz.familienarchiv.service.FileService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||||
import org.raddatz.familienarchiv.config.SecurityConfig;
|
import org.raddatz.familienarchiv.config.SecurityConfig;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
|
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
@@ -31,8 +32,10 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
|||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@@ -485,6 +488,58 @@ class DocumentControllerTest {
|
|||||||
.andExpect(jsonPath("$[0].editorName").value("Emma Müller"));
|
.andExpect(jsonPath("$[0].editorName").value("Emma Müller"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── PATCH /api/documents/{id}/training-labels ───────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patchTrainingLabels_returns401_whenUnauthenticated() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}"))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void patchTrainingLabels_returns403_whenMissingWritePermission() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}"))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchTrainingLabels_returns204_whenAddingLabel() throws Exception {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
mockMvc.perform(patch("/api/documents/" + id + "/training-labels")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}"))
|
||||||
|
.andExpect(status().isNoContent());
|
||||||
|
|
||||||
|
verify(documentService).addTrainingLabel(eq(id), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchTrainingLabels_returns204_whenRemovingLabel() throws Exception {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
mockMvc.perform(patch("/api/documents/" + id + "/training-labels")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"label\":\"KURRENT_SEGMENTATION\",\"enrolled\":false}"))
|
||||||
|
.andExpect(status().isNoContent());
|
||||||
|
|
||||||
|
verify(documentService).removeTrainingLabel(eq(id), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchTrainingLabels_returns400_whenUnknownLabel() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"label\":\"UNKNOWN_GARBAGE\",\"enrolled\":true}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
// ─── GET /api/documents/{id}/versions/{versionId} ────────────────────────
|
// ─── GET /api/documents/{id}/versions/{versionId} ────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.raddatz.familienarchiv.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||||
|
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||||
|
import org.raddatz.familienarchiv.model.*;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||||
|
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
|
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||||
|
class TrainingBlockQueryTest {
|
||||||
|
|
||||||
|
@Autowired TranscriptionBlockRepository blockRepository;
|
||||||
|
@Autowired DocumentRepository documentRepository;
|
||||||
|
@Autowired AnnotationRepository annotationRepository;
|
||||||
|
|
||||||
|
private UUID kurrentDocId;
|
||||||
|
private UUID typewriterDocId;
|
||||||
|
private UUID kurrentAnnotationId;
|
||||||
|
private UUID typewriterAnnotationId;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Document kurrentDoc = documentRepository.save(Document.builder()
|
||||||
|
.title("Kurrent Brief")
|
||||||
|
.originalFilename("kurrent.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED)
|
||||||
|
.trainingLabels(new java.util.HashSet<>(Set.of(TrainingLabel.KURRENT_RECOGNITION)))
|
||||||
|
.build());
|
||||||
|
kurrentDocId = kurrentDoc.getId();
|
||||||
|
|
||||||
|
Document typewriterDoc = documentRepository.save(Document.builder()
|
||||||
|
.title("Getippter Brief")
|
||||||
|
.originalFilename("typed.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED)
|
||||||
|
.build());
|
||||||
|
typewriterDocId = typewriterDoc.getId();
|
||||||
|
|
||||||
|
kurrentAnnotationId = annotationRepository.save(annotation(kurrentDocId)).getId();
|
||||||
|
typewriterAnnotationId = annotationRepository.save(annotation(typewriterDocId)).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findEligibleKurrentBlocks_includesManualBlock() {
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.MANUAL, false));
|
||||||
|
|
||||||
|
List<TranscriptionBlock> result = blockRepository.findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).getSource()).isEqualTo(BlockSource.MANUAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findEligibleKurrentBlocks_includesReviewedOcrBlock() {
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.OCR, true));
|
||||||
|
|
||||||
|
List<TranscriptionBlock> result = blockRepository.findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).isReviewed()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findEligibleKurrentBlocks_excludesUnreviewedOcrBlock() {
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.OCR, false));
|
||||||
|
|
||||||
|
List<TranscriptionBlock> result = blockRepository.findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findEligibleKurrentBlocks_excludesNonEnrolledDocument() {
|
||||||
|
blockRepository.save(block(typewriterDocId, typewriterAnnotationId, BlockSource.MANUAL, false));
|
||||||
|
|
||||||
|
List<TranscriptionBlock> result = blockRepository.findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findEligibleKurrentBlocks_returnsAllEligibleAcrossBothSources() {
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.MANUAL, false));
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.OCR, true));
|
||||||
|
blockRepository.save(block(kurrentDocId, kurrentAnnotationId, BlockSource.OCR, false)); // excluded
|
||||||
|
|
||||||
|
List<TranscriptionBlock> result = blockRepository.findEligibleKurrentBlocks();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private DocumentAnnotation annotation(UUID docId) {
|
||||||
|
return DocumentAnnotation.builder()
|
||||||
|
.documentId(docId)
|
||||||
|
.pageNumber(1)
|
||||||
|
.x(0.1).y(0.2).width(0.3).height(0.4)
|
||||||
|
.color("#00C7B1")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TranscriptionBlock block(UUID docId, UUID annotId, BlockSource source, boolean reviewed) {
|
||||||
|
return TranscriptionBlock.builder()
|
||||||
|
.annotationId(annotId)
|
||||||
|
.documentId(docId)
|
||||||
|
.text("Liebe Tante")
|
||||||
|
.sortOrder(0)
|
||||||
|
.source(source)
|
||||||
|
.reviewed(reviewed)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ type Props = {
|
|||||||
onDeleteBlock: (blockId: string) => Promise<void>;
|
onDeleteBlock: (blockId: string) => Promise<void>;
|
||||||
onReviewToggle: (blockId: string) => Promise<void>;
|
onReviewToggle: (blockId: string) => Promise<void>;
|
||||||
onTriggerOcr?: (scriptType: string) => void;
|
onTriggerOcr?: (scriptType: string) => void;
|
||||||
|
canWrite?: boolean;
|
||||||
|
trainingLabels?: string[];
|
||||||
|
onToggleTrainingLabel?: (label: string, enrolled: boolean) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -34,10 +37,14 @@ let {
|
|||||||
onSaveBlock,
|
onSaveBlock,
|
||||||
onDeleteBlock,
|
onDeleteBlock,
|
||||||
onReviewToggle,
|
onReviewToggle,
|
||||||
onTriggerOcr
|
onTriggerOcr,
|
||||||
|
canWrite = false,
|
||||||
|
trainingLabels = [],
|
||||||
|
onToggleTrainingLabel
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let activeBlockId: string | null = $state(null);
|
let activeBlockId: string | null = $state(null);
|
||||||
|
let localLabels: string[] = $derived.by(() => [...trainingLabels]);
|
||||||
|
|
||||||
// Sync: when an annotation is clicked on the PDF, activate the corresponding block
|
// Sync: when an annotation is clicked on the PDF, activate the corresponding block
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -188,7 +195,7 @@ let dropTargetIdx: number | null = $state(null);
|
|||||||
let dragOffsetY: number = $state(0);
|
let dragOffsetY: number = $state(0);
|
||||||
let dragStartY = 0;
|
let dragStartY = 0;
|
||||||
let capturedEl: HTMLElement | null = null;
|
let capturedEl: HTMLElement | null = null;
|
||||||
let listEl: HTMLElement | null = null;
|
let listEl: HTMLElement | null = $state(null);
|
||||||
|
|
||||||
function handleGripDown(e: PointerEvent, blockId: string) {
|
function handleGripDown(e: PointerEvent, blockId: string) {
|
||||||
if (!(e.target as HTMLElement).closest('[data-drag-handle]')) return;
|
if (!(e.target as HTMLElement).closest('[data-drag-handle]')) return;
|
||||||
@@ -240,6 +247,23 @@ function handlePointerUp() {
|
|||||||
capturedEl = null;
|
capturedEl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLabelToggle(label: string) {
|
||||||
|
if (!onToggleTrainingLabel) return;
|
||||||
|
const enrolled = !localLabels.includes(label);
|
||||||
|
// Optimistic update
|
||||||
|
if (enrolled) {
|
||||||
|
localLabels = [...localLabels, label];
|
||||||
|
} else {
|
||||||
|
localLabels = localLabels.filter((l) => l !== label);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await onToggleTrainingLabel(label, enrolled);
|
||||||
|
} catch {
|
||||||
|
// Revert on failure
|
||||||
|
localLabels = [...trainingLabels];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function flushViaBeacon() {
|
function flushViaBeacon() {
|
||||||
for (const [blockId, text] of pendingTexts) {
|
for (const [blockId, text] of pendingTexts) {
|
||||||
clearDebounce(blockId);
|
clearDebounce(blockId);
|
||||||
@@ -390,4 +414,23 @@ $effect(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if canWrite}
|
||||||
|
<div class="border-t border-line px-4 py-3">
|
||||||
|
<p class="mb-2 font-sans text-xs font-medium text-ink-2">Für Training vormerken</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each [{ label: 'KURRENT_RECOGNITION', display: 'Kurrent-Erkennung' }, { label: 'KURRENT_SEGMENTATION', display: 'Segmentierung' }] as chip (chip.label)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => handleLabelToggle(chip.label)}
|
||||||
|
class="rounded-full border px-3 py-1 font-sans text-xs font-medium transition-colors {localLabels.includes(chip.label)
|
||||||
|
? 'border-brand-mint bg-brand-mint text-brand-navy'
|
||||||
|
: 'border-line bg-surface text-ink-3 hover:border-brand-mint hover:text-brand-navy'}"
|
||||||
|
>
|
||||||
|
{chip.display}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,6 +116,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/documents/{documentId}/transcription-blocks/{blockId}/review": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put: operations["reviewBlock"];
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/documents/{documentId}/transcription-blocks/reorder": {
|
"/api/documents/{documentId}/transcription-blocks/reorder": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -212,6 +228,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/ocr/batch": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["triggerBatch"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/notifications/read-all": {
|
"/api/notifications/read-all": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -308,6 +340,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/documents/{documentId}/ocr": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["triggerOcr"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/documents/{documentId}/comments": {
|
"/api/documents/{documentId}/comments": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -628,6 +676,38 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/ocr/jobs/{jobId}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getJobStatus"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/ocr/jobs/{jobId}/progress": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["streamProgress"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/notifications": {
|
"/api/notifications": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -740,6 +820,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/documents/{documentId}/ocr-status": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getDocumentOcrStatus"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/documents/search": {
|
"/api/documents/search": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -940,6 +1036,7 @@ export interface components {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
PersonUpdateDTO: {
|
PersonUpdateDTO: {
|
||||||
|
title?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
@@ -978,6 +1075,8 @@ export interface components {
|
|||||||
receiverIds?: string[];
|
receiverIds?: string[];
|
||||||
tags?: string;
|
tags?: string;
|
||||||
metadataComplete?: boolean;
|
metadataComplete?: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
scriptType?: "UNKNOWN" | "TYPEWRITER" | "HANDWRITING_LATIN" | "HANDWRITING_KURRENT";
|
||||||
};
|
};
|
||||||
Document: {
|
Document: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
@@ -1002,9 +1101,13 @@ export interface components {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
metadataComplete: boolean;
|
metadataComplete: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
scriptType: "UNKNOWN" | "TYPEWRITER" | "HANDWRITING_LATIN" | "HANDWRITING_KURRENT";
|
||||||
receivers?: components["schemas"]["Person"][];
|
receivers?: components["schemas"]["Person"][];
|
||||||
sender?: components["schemas"]["Person"];
|
sender?: components["schemas"]["Person"];
|
||||||
tags?: components["schemas"]["Tag"][];
|
tags?: components["schemas"]["Tag"][];
|
||||||
|
/** @enum {string} */
|
||||||
|
trainingLabels?: ("KURRENT_RECOGNITION" | "KURRENT_SEGMENTATION")[];
|
||||||
};
|
};
|
||||||
UpdateTranscriptionBlockDTO: {
|
UpdateTranscriptionBlockDTO: {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -1021,6 +1124,9 @@ export interface components {
|
|||||||
label?: string;
|
label?: string;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
/** @enum {string} */
|
||||||
|
source: "MANUAL" | "OCR";
|
||||||
|
reviewed: boolean;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
version: number;
|
version: number;
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
@@ -1068,6 +1174,9 @@ export interface components {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
BatchOcrDTO: {
|
||||||
|
documentIds: string[];
|
||||||
|
};
|
||||||
GroupDTO: {
|
GroupDTO: {
|
||||||
name?: string;
|
name?: string;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
@@ -1118,6 +1227,10 @@ export interface components {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
};
|
};
|
||||||
|
TriggerOcrDTO: {
|
||||||
|
/** @enum {string} */
|
||||||
|
scriptType?: "UNKNOWN" | "TYPEWRITER" | "HANDWRITING_LATIN" | "HANDWRITING_KURRENT";
|
||||||
|
};
|
||||||
CreateAnnotationDTO: {
|
CreateAnnotationDTO: {
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
pageNumber?: number;
|
pageNumber?: number;
|
||||||
@@ -1130,6 +1243,7 @@ export interface components {
|
|||||||
/** Format: double */
|
/** Format: double */
|
||||||
height?: number;
|
height?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
polygon?: number[][];
|
||||||
};
|
};
|
||||||
DocumentAnnotation: {
|
DocumentAnnotation: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
@@ -1148,6 +1262,7 @@ export interface components {
|
|||||||
height: number;
|
height: number;
|
||||||
color: string;
|
color: string;
|
||||||
fileHash?: string;
|
fileHash?: string;
|
||||||
|
polygon?: number[][];
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
@@ -1210,6 +1325,8 @@ export interface components {
|
|||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id?: string;
|
id?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
/** Format: int64 */
|
||||||
|
documentCount?: number;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
@@ -1219,14 +1336,37 @@ export interface components {
|
|||||||
alias?: string;
|
alias?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
personType?: string;
|
personType?: string;
|
||||||
|
};
|
||||||
|
OcrJob: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
status: "PENDING" | "RUNNING" | "DONE" | "FAILED";
|
||||||
|
/** Format: int32 */
|
||||||
|
totalDocuments: number;
|
||||||
|
/** Format: int32 */
|
||||||
|
processedDocuments: number;
|
||||||
|
/** Format: int32 */
|
||||||
|
errorCount: number;
|
||||||
|
/** Format: int32 */
|
||||||
|
skippedCount: number;
|
||||||
|
progressMessage?: string;
|
||||||
|
/** Format: uuid */
|
||||||
|
createdBy?: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
SseEmitter: {
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
documentCount?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
PageNotificationDTO: {
|
PageNotificationDTO: {
|
||||||
/** Format: int64 */
|
|
||||||
totalElements?: number;
|
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
|
/** Format: int64 */
|
||||||
|
totalElements?: number;
|
||||||
pageable?: components["schemas"]["PageableObject"];
|
pageable?: components["schemas"]["PageableObject"];
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -1234,10 +1374,10 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
number?: number;
|
number?: number;
|
||||||
sort?: components["schemas"]["SortObject"];
|
sort?: components["schemas"]["SortObject"];
|
||||||
/** Format: int32 */
|
|
||||||
numberOfElements?: number;
|
|
||||||
first?: boolean;
|
first?: boolean;
|
||||||
last?: boolean;
|
last?: boolean;
|
||||||
|
/** Format: int32 */
|
||||||
|
numberOfElements?: number;
|
||||||
empty?: boolean;
|
empty?: boolean;
|
||||||
};
|
};
|
||||||
PageableObject: {
|
PageableObject: {
|
||||||
@@ -1256,10 +1396,6 @@ export interface components {
|
|||||||
empty?: boolean;
|
empty?: boolean;
|
||||||
unsorted?: boolean;
|
unsorted?: boolean;
|
||||||
};
|
};
|
||||||
SseEmitter: {
|
|
||||||
/** Format: int64 */
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
DocumentVersionSummary: {
|
DocumentVersionSummary: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1292,6 +1428,15 @@ export interface components {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
changedAt: string;
|
changedAt: string;
|
||||||
};
|
};
|
||||||
|
OcrStatusDTO: {
|
||||||
|
status?: string;
|
||||||
|
/** Format: uuid */
|
||||||
|
jobId?: string;
|
||||||
|
/** Format: int32 */
|
||||||
|
currentPage?: number;
|
||||||
|
/** Format: int32 */
|
||||||
|
totalPages?: number;
|
||||||
|
};
|
||||||
DocumentSearchResult: {
|
DocumentSearchResult: {
|
||||||
documents?: components["schemas"]["Document"][];
|
documents?: components["schemas"]["Document"][];
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
@@ -1702,6 +1847,29 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
reviewBlock: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
documentId: string;
|
||||||
|
blockId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": components["schemas"]["TranscriptionBlock"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
reorderBlocks: {
|
reorderBlocks: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1914,6 +2082,32 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
triggerBatch: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["BatchOcrDTO"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Accepted */
|
||||||
|
202: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
markAllRead: {
|
markAllRead: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -2124,6 +2318,34 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
triggerOcr: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
documentId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["TriggerOcrDTO"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Accepted */
|
||||||
|
202: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
getDocumentComments: {
|
getDocumentComments: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -2701,6 +2923,50 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
getJobStatus: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
jobId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": components["schemas"]["OcrJob"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
streamProgress: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
jobId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"text/event-stream": components["schemas"]["SseEmitter"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
getNotifications: {
|
getNotifications: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
@@ -2860,6 +3126,28 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
getDocumentOcrStatus: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
documentId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": components["schemas"]["OcrStatusDTO"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
search_1: {
|
search_1: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ async function reviewToggle(blockId: string) {
|
|||||||
transcriptionBlocks = transcriptionBlocks.map((b) => (b.id === blockId ? updated : b));
|
transcriptionBlocks = transcriptionBlocks.map((b) => (b.id === blockId ? updated : b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleTrainingLabel(label: string, enrolled: boolean) {
|
||||||
|
const res = await fetch(`/api/documents/${doc.id}/training-labels`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ label, enrolled })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update training label');
|
||||||
|
}
|
||||||
|
|
||||||
let ocrRunning = $state(false);
|
let ocrRunning = $state(false);
|
||||||
let ocrProgressMessage = $state('');
|
let ocrProgressMessage = $state('');
|
||||||
let ocrErrorMessage = $state('');
|
let ocrErrorMessage = $state('');
|
||||||
@@ -449,11 +458,14 @@ onMount(() => {
|
|||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
storedScriptType={doc.scriptType ?? ''}
|
storedScriptType={doc.scriptType ?? ''}
|
||||||
canRunOcr={canWrite && !!doc.filePath}
|
canRunOcr={canWrite && !!doc.filePath}
|
||||||
|
canWrite={canWrite}
|
||||||
|
trainingLabels={doc.trainingLabels ?? []}
|
||||||
onBlockFocus={handleBlockFocus}
|
onBlockFocus={handleBlockFocus}
|
||||||
onSaveBlock={saveBlock}
|
onSaveBlock={saveBlock}
|
||||||
onDeleteBlock={deleteBlock}
|
onDeleteBlock={deleteBlock}
|
||||||
onReviewToggle={reviewToggle}
|
onReviewToggle={reviewToggle}
|
||||||
onTriggerOcr={triggerOcr}
|
onTriggerOcr={triggerOcr}
|
||||||
|
onToggleTrainingLabel={toggleTrainingLabel}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user