Runs as a Classification-policy pipeline step: it reads a bounded page window, asks the AI
+ * engine to classify the document against the caller's team label set, and stores the engine's JSON
+ * answer — minus the transport-only {@code outcome} field — in the custom Info-dictionary key
+ * {@link PdfMetadataService#CLASSIFICATION_KEY}. Returns the labelled PDF. Not intended for direct
+ * client use.
+ */
+@Slf4j
+@Hidden
+@RestController
+@RequestMapping("/api/v1/ai/tools")
+@Tag(name = "AI Tools", description = "Dispatchable AI-backed tools.")
+public class ClassifyLabelController {
+
+ /** Pages read from each end of the document — mirrors the engine's window. */
+ private static final int WINDOW_PAGES = 2;
+
+ private static final String CLASSIFY_ENDPOINT = "/api/v1/documents/classify";
+
+ private final CustomPDFDocumentFactory pdfDocumentFactory;
+ private final TempFileManager tempFileManager;
+ private final PdfContentExtractor pdfContentExtractor;
+ private final PdfMetadataService pdfMetadataService;
+ private final AiEngineClient aiEngineClient;
+ private final ObjectMapper objectMapper;
+ private final UserServiceInterface userService;
+
+ /**
+ * Present only when the policy subsystem is enabled ({@code policies.enabled}); the store and
+ * team authority are gated on it. Null otherwise, in which case there are no team labels to
+ * classify against and the document is passed through unlabelled.
+ */
+ private final ClassificationLabelStore labelStore;
+
+ private final PolicyManagementAuthority policyManagementAuthority;
+
+ public ClassifyLabelController(
+ CustomPDFDocumentFactory pdfDocumentFactory,
+ TempFileManager tempFileManager,
+ PdfContentExtractor pdfContentExtractor,
+ PdfMetadataService pdfMetadataService,
+ AiEngineClient aiEngineClient,
+ ObjectMapper objectMapper,
+ @Autowired(required = false) UserServiceInterface userService,
+ @Autowired(required = false) ClassificationLabelStore labelStore,
+ @Autowired(required = false) PolicyManagementAuthority policyManagementAuthority) {
+ this.pdfDocumentFactory = pdfDocumentFactory;
+ this.tempFileManager = tempFileManager;
+ this.pdfContentExtractor = pdfContentExtractor;
+ this.pdfMetadataService = pdfMetadataService;
+ this.aiEngineClient = aiEngineClient;
+ this.objectMapper = objectMapper;
+ this.userService = userService;
+ this.labelStore = labelStore;
+ this.policyManagementAuthority = policyManagementAuthority;
+ }
+
+ @PostMapping(value = "/classify-and-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ @Operation(
+ summary = "Classify a PDF and label its metadata",
+ description =
+ "Reads the first two and last two pages, classifies the document via the AI"
+ + " engine, and stores the result in the StirlingPDFClassification"
+ + " metadata field. Dispatched by the Classification policy; not"
+ + " intended for direct client use.")
+ public ResponseEntity {addError}
+ {emptyText ?? t("policies.labels.empty", "No labels yet.")}
+
+ {t("policies.labels.ungrouped", "Ungrouped")}
+
{rows.map((row, idx) => {
const isLast = idx === rows.length - 1;
diff --git a/frontend/editor/src/core/components/shared/FileDocIcon.tsx b/frontend/editor/src/core/components/shared/FileDocIcon.tsx
index 135162c54d..aafaf9aabd 100644
--- a/frontend/editor/src/core/components/shared/FileDocIcon.tsx
+++ b/frontend/editor/src/core/components/shared/FileDocIcon.tsx
@@ -1,3 +1,5 @@
+import { accentColor } from "@app/utils/accentColors";
+
export type FileDocVariant =
| "pdf"
| "spreadsheet"
@@ -7,14 +9,15 @@ export type FileDocVariant =
| "code"
| "generic";
+// Theme-adaptive accent per file type (shared palette; see utils/accentColors.ts).
export const VARIANT_COLORS: Record