import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { DownloadIcon, EditIcon, ArrowLeftIcon, ArrowRightIcon, ChevronsRightIcon, AnalyzeIcon, WindowMaximizeIcon, TextScanIcon, } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; import { formatFileSize } from '../utils/format'; import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; import { useAssetNavigator } from '../hooks/useAssetNavigator'; import PreviewZoomOverlay from './PreviewZoomOverlay'; import { openOcrTextInNewTab } from '../utils/ocr'; const MAX_PREVIEW_STACK_ITEMS = 15; const derivePreviewOrientation = (metadata) => { const width = Number(metadata?.width); const height = Number(metadata?.height); if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { return width >= height ? 'landscape' : 'portrait'; } return 'landscape'; }; const sortCorrespondents = (entries = []) => entries .map((entry) => ({ id: entry.id, name: entry.name || '', count: entry.count, })) .sort((a, b) => (a.name || '').localeCompare(b.name || '')); const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
{entries.length ? ( entries.map((entry, index) => { const key = entry.id ?? `${entry.name}-${index}`; return ( {entry.name} {showCount && entry.count ? ` (${entry.count})` : ''} {onRemove ? ( ) : null} ); }) ) : ( No correspondents yet. )}
); const TagSection = ({ title, tags = [], onRemove, onAdd, emptyMessage = 'No tags yet.', addPlaceholder = 'Add or create tag', addButtonLabel = 'Add', datalistId, datalistOptions = [], className, }) => (
{title}
{tags.length ? ( tags.map((tag) => { const key = tag.id ?? tag.label; const style = getTagColorStyle(tag.color); const removable = Boolean(onRemove); const className = removable ? 'badge tag-chip tag-chip--removable' : 'badge tag-chip'; return ( {tag.label} {removable ? ( ) : null} ); }) ) : ( {emptyMessage} )}
{onAdd ? (
{ event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onAdd({ value, input }); }} > {datalistId ? ( {datalistOptions.map((option) => ( ) : null}
) : null}
); const CorrespondentSection = ({ title, entries = [], onRemove, onAdd, showCount = false, addPlaceholder = 'Add or create correspondent', addButtonLabel = 'Add', datalistId, datalistOptions = [], className, }) => (
{title}
{onAdd ? (
{ event.preventDefault(); const form = event.currentTarget; const nameInput = form.elements.correspondent; const value = nameInput.value.trim(); if (!value) return; onAdd({ name: value, input: nameInput }); form.reset(); }} > {datalistId ? ( {datalistOptions.map((name) => ( ) : null}
) : null}
); const computeStackAngle = (docId, index) => { if (index === 0) return 0; let hash = 0; const source = docId || `stack-${index}`; for (let i = 0; i < source.length; i += 1) { hash = (hash * 31 + source.charCodeAt(i)) % 997; } const magnitude = Math.max(3, (hash % 13) + 3); const sign = index % 2 === 0 ? 1 : -1; return magnitude * sign; }; const PreviewStack = ({ items = [], maxItems = MAX_PREVIEW_STACK_ITEMS, emptyMessage = 'Preview unavailable', onItemActivate, onOpenPreview, onZoomPreview, }) => { const limited = useMemo(() => items.slice(0, maxItems), [items, maxItems]); const hasMultiple = limited.length > 1; const preparedItems = useMemo( () => limited.map((entry, index) => ({ entry, angle: index === 0 ? 0 : computeStackAngle(entry.id, index), })), [limited], ); if (!limited.length) { return {emptyMessage}; } return (
{preparedItems.map(({ entry, angle }, index) => { const transform = hasMultiple ? `translate(-50%, -50%) rotate(${angle}deg)` : 'translate(-50%, -50%)'; const isFront = index === 0; return (
{entry.alt { event.stopPropagation(); if (isFront) { if (onZoomPreview) { onZoomPreview(entry); } else if (onOpenPreview) { onOpenPreview(entry.id); } else if (onItemActivate) { onItemActivate(entry.id); } } else if (onItemActivate) { onItemActivate(entry.id); } }} onKeyDown={(event) => { if (!onItemActivate && !onOpenPreview && !onZoomPreview) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); if (isFront) { if (onZoomPreview) { onZoomPreview(entry); } else if (onOpenPreview) { onOpenPreview(entry.id); } else { onItemActivate?.(entry.id); } } else { onItemActivate?.(entry.id); } } }} />
); })}
); }; const DetailPanel = ({ selectedDocuments = [], tags = [], tagLookupById = new Map(), onTagAdd, onTagRemove, onRegenerateThumbnails, onOpenPreview, onBulkTagAdd, onBulkTagRemove, onBulkReanalyze, onBulkCorrespondentAdd, onBulkCorrespondentRemove, onPromoteSelection, onUpdateTitle = async () => false, ensureAssetUrl = null, getDocumentAsset = () => null, ensurePreviewData = () => Promise.resolve(), correspondents = [], onCorrespondentAdd, onCorrespondentRemove, resolveApiPath, onFolderNavigate = null, resolveFolderPath = null, onClose = () => {}, }) => { const selectedCount = selectedDocuments.length; const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null; const singleDocId = singleDoc?.id || null; const selectionKey = useMemo( () => selectedDocuments.map((doc) => doc?.id ?? '').join('|'), [selectedDocuments], ); const singleDownloadHref = useMemo(() => { if (!singleDoc) return null; const downloadPath = singleDoc.current_version?.download_path; if (!downloadPath || !resolveApiPath) { return null; } return resolveApiPath(downloadPath); }, [singleDoc, resolveApiPath]); const [titleEditDocId, setTitleEditDocId] = useState(null); const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); const [titleError, setTitleError] = useState(null); const [zoomedPreview, setZoomedPreview] = useState(null); const bulkDocumentIds = useMemo( () => selectedDocuments.map((doc) => doc?.id).filter(Boolean), [selectedDocuments], ); useEffect(() => { if (!singleDoc) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); return; } if (titleEditDocId && titleEditDocId !== singleDoc.id) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); } }, [singleDoc, titleEditDocId]); useEffect(() => { setZoomedPreview(null); }, [selectionKey]); const startTitleEdit = useCallback(() => { if (!singleDoc) return; setTitleEditDocId(singleDoc.id); setTitleDraft(singleDoc.title || singleDoc.original_name || ''); setTitleError(null); }, [singleDoc]); const cancelTitleEdit = useCallback(() => { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); }, []); const submitTitleEdit = useCallback( async (event) => { event.preventDefault(); if (!singleDoc) return; const trimmed = titleDraft.trim(); if (!trimmed) { setTitleError('Title cannot be empty.'); return; } setTitleSaving(true); try { const ok = await onUpdateTitle(singleDoc.id, trimmed); if (ok) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); } else { setTitleError('Failed to update title.'); } } finally { setTitleSaving(false); } }, [singleDoc, titleDraft, onUpdateTitle], ); const handlePreviewActivate = useCallback( (docId) => { if (!docId) return; onPromoteSelection?.(docId); }, [onPromoteSelection], ); const hasOcrAsset = useMemo( () => Boolean(singleDoc && getDocumentAsset(singleDoc, 'ocr-text')), [singleDoc, getDocumentAsset], ); const openOcr = useCallback(async () => { if (!singleDoc) { return; } try { await openOcrTextInNewTab({ document: singleDoc, ensurePreviewData, getDocumentAsset, ensureAssetUrl, }); } catch (error) { /* noop */ } }, [singleDoc, ensurePreviewData, getDocumentAsset, ensureAssetUrl]); const singlePreviewNavigator = useAssetNavigator({ document: singleDoc, assetType: 'preview', ensureAssetUrl, getAsset: getDocumentAsset, prefetch: 3, }); const makePreviewItem = useCallback( (doc, ordinal = 1) => { if (!doc) return null; const asset = getDocumentAsset(doc, 'preview'); const assetView = createAssetView(asset); const object = assetView.getObject(ordinal); let url = object?.url || null; if (!url) { url = resolveDocumentAssetUrl(doc, 'preview', { ensureAssetUrl, getAsset: getDocumentAsset, ensureOptions: { start: ordinal, limit: 1 }, objectOrdinal: ordinal, }); } if (!url) { return null; } const metadata = object?.metadata || assetView.getPrimaryMetadata() || {}; const orientation = derivePreviewOrientation(metadata); return { id: doc.id, url, orientation, alt: doc.title, }; }, [ensureAssetUrl, getDocumentAsset], ); const stackDocuments = useMemo(() => { if (!selectedDocuments.length) return []; const seen = new Set(); const ordered = []; for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) { const doc = selectedDocuments[index]; if (!doc?.id || seen.has(doc.id)) continue; seen.add(doc.id); ordered.push(doc); if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) { break; } } return ordered; }, [selectedDocuments]); const stackTopDocument = stackDocuments[0] || null; const stackTopDocId = stackTopDocument?.id || null; const stackPreviewNavigator = useAssetNavigator({ document: stackTopDocument, assetType: 'preview', ensureAssetUrl, getAsset: getDocumentAsset, prefetch: 3, }); const singlePreviewItems = useMemo(() => { if (!singleDoc) return []; const url = singlePreviewNavigator.currentUrl; if (!url) { return []; } const orientation = derivePreviewOrientation(singlePreviewNavigator.currentMetadata); return [ { id: singleDoc.id, url, orientation, alt: singleDoc.title, }, ]; }, [singleDoc, singlePreviewNavigator.currentUrl, singlePreviewNavigator.currentMetadata]); const stackPreviews = useMemo(() => { if (!stackDocuments.length) { return []; } return stackDocuments .map((doc) => { if (!doc) return null; if (stackTopDocument && doc.id === stackTopDocument.id) { const url = stackPreviewNavigator.currentUrl; if (!url) { return null; } const orientation = derivePreviewOrientation(stackPreviewNavigator.currentMetadata); return { id: doc.id, url, orientation, alt: doc.title, }; } return makePreviewItem(doc, 1); }) .filter(Boolean); }, [ stackDocuments, stackTopDocument, stackPreviewNavigator.currentUrl, stackPreviewNavigator.currentMetadata, makePreviewItem, ]); const singleCardinality = singlePreviewNavigator.cardinality; const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0); const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl); const topDocId = stackTopDocument?.id || null; const topCardinality = stackPreviewNavigator.cardinality; const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0); const topHasPreview = Boolean(stackPreviewNavigator.currentUrl); const bulkTagUnion = useMemo(() => { if (!selectedDocuments.length) return []; const tagMap = new Map(); selectedDocuments.forEach((doc) => { (doc.tags || []).forEach((tag) => { const label = (tag?.label || '').trim(); if (!label) return; if (!tagMap.has(label)) { const fallback = tagLookupById.get(tag.id) || {}; tagMap.set(label, { id: tag.id, label, color: tag.color || fallback.color, }); } }); }); return [...tagMap.values()].sort((a, b) => a.label.localeCompare(b.label)); }, [selectedDocuments, tagLookupById]); const stackTotalSizeBytes = useMemo(() => { if (!stackPreviews.length) return 0; const byId = new Map(selectedDocuments.map((doc) => [doc.id, doc])); return stackPreviews.reduce((sum, item) => { const source = byId.get(item.id); const bytes = source?.current_version?.size_bytes; return sum + (typeof bytes === 'number' ? bytes : 0); }, 0); }, [stackPreviews, selectedDocuments]); const availableCorrespondents = useMemo( () => (Array.isArray(correspondents) ? correspondents : []), [correspondents], ); const correspondentOptions = useMemo(() => { const seen = new Set(); return availableCorrespondents .map((entry) => (entry?.name || '').trim()) .filter((name) => { if (!name) return false; const lower = name.toLowerCase(); if (seen.has(lower)) { return false; } seen.add(lower); return true; }); }, [availableCorrespondents]); const singleCorrespondents = useMemo(() => { if (!singleDoc) return []; return sortCorrespondents(singleDoc.correspondents || []); }, [singleDoc]); const singleFolderPath = useMemo(() => { if (!singleDoc?.folder_id) { return null; } if (typeof resolveFolderPath !== 'function') { return null; } const segments = resolveFolderPath(singleDoc.folder_id); if (!Array.isArray(segments) || !segments.some((segment) => segment?.id && segment.id !== 'root')) { return null; } return segments; }, [singleDoc?.folder_id, resolveFolderPath]); const bulkCorrespondents = useMemo(() => { if (selectedDocuments.length <= 1) { const doc = selectedDocuments[0]; return doc ? sortCorrespondents(doc.correspondents || []) : []; } const map = new Map(); selectedDocuments.forEach((doc) => { if (!doc?.id) return; (doc.correspondents || []).forEach((entry) => { if (!entry?.id) return; if (!map.has(entry.id)) { map.set(entry.id, { id: entry.id, name: entry.name || '', documentIds: new Set(), }); } map.get(entry.id).documentIds.add(doc.id); }); }); return [...map.values()] .map((entry) => ({ id: entry.id, name: entry.name, documentIds: [...entry.documentIds], count: entry.documentIds.size, })) .sort((a, b) => (a.name || '').localeCompare(b.name || '')); }, [selectedDocuments]); const handleBulkCorrespondentRemove = useCallback( (entry) => { if (!entry?.id) return; if (onBulkCorrespondentRemove) { return onBulkCorrespondentRemove({ assignments: [ { correspondent_id: entry.id, }, ], documentIds: entry.documentIds, }); } if (!onCorrespondentRemove) return; const targets = entry.documentIds && entry.documentIds.length ? entry.documentIds : selectedDocuments .filter((doc) => (doc.correspondents || []).some((item) => item.id === entry.id)) .map((doc) => doc.id); return Promise.all( targets.map((documentId) => onCorrespondentRemove({ documentId, correspondentId: entry.id, }), ), ).catch(() => {}); }, [onBulkCorrespondentRemove, onCorrespondentRemove, selectedDocuments], ); const openZoomPreview = useCallback((config) => { if (!config) return; setZoomedPreview({ mode: config.mode, docId: config.docId ?? null, }); }, []); const closeZoomPreview = useCallback(() => { setZoomedPreview(null); }, []); const handleSingleZoom = useCallback( (entry) => { if (!singleHasPreview) return; const targetId = entry?.id ?? singleDocId; if (!targetId) return; openZoomPreview({ mode: 'single', docId: targetId }); }, [openZoomPreview, singleHasPreview, singleDocId], ); const handleStackZoom = useCallback( (entry) => { if (!stackTopDocId || entry?.id !== stackTopDocId) return; if (!topHasPreview) return; openZoomPreview({ mode: 'stack', docId: stackTopDocId }); }, [openZoomPreview, stackTopDocId, topHasPreview], ); const zoomDisplay = useMemo(() => { if (!zoomedPreview) { return null; } if ( zoomedPreview.mode === 'single' && singleDocId && singleDoc && singleHasPreview && zoomedPreview.docId === singleDocId ) { return { url: singlePreviewNavigator.currentUrl, alt: singleDoc.title, canGoPrev: singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev), canGoNext: singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext), goPrev: singlePreviewNavigator.goPrev, goNext: singlePreviewNavigator.goNext, }; } if ( zoomedPreview.mode === 'stack' && stackTopDocId && stackTopDocument && zoomedPreview.docId === stackTopDocId && topHasPreview ) { return { url: stackPreviewNavigator.currentUrl, alt: stackTopDocument.title, canGoPrev: topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoPrev), canGoNext: topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoNext), goPrev: stackPreviewNavigator.goPrev, goNext: stackPreviewNavigator.goNext, }; } return null; }, [ zoomedPreview, singleDoc, singleDocId, singleHasPreview, singlePreviewNavigator.currentUrl, singlePreviewNavigator.canGoPrev, singlePreviewNavigator.canGoNext, singlePreviewNavigator.goPrev, singlePreviewNavigator.goNext, singleEffectiveCardinality, stackTopDocument, stackTopDocId, topHasPreview, stackPreviewNavigator.currentUrl, stackPreviewNavigator.canGoPrev, stackPreviewNavigator.canGoNext, stackPreviewNavigator.goPrev, stackPreviewNavigator.goNext, topEffectiveCardinality, ]); useEffect(() => { if (zoomedPreview && !zoomDisplay) { setZoomedPreview(null); } }, [zoomedPreview, zoomDisplay]); const { documentId: singleNavigatorDocId, asset: singleNavigatorAsset, ordinal: singleNavigatorOrdinal, canGoPrev: singleNavigatorCanGoPrev, canGoNext: singleNavigatorCanGoNext, cardinality: singleNavigatorCardinality, } = singlePreviewNavigator; const { documentId: stackNavigatorDocId, asset: stackNavigatorAsset, ordinal: stackNavigatorOrdinal, canGoPrev: stackNavigatorCanGoPrev, canGoNext: stackNavigatorCanGoNext, cardinality: stackNavigatorCardinality, } = stackPreviewNavigator; useEffect(() => { if (typeof ensureAssetUrl !== 'function') { return; } const warmNavigator = (navigator) => { const { documentId, asset, ordinal, canGoPrev, canGoNext, cardinality, } = navigator; if (!documentId || !asset || !Number.isFinite(ordinal)) { return; } const requests = []; if (canGoPrev) { const prevOrdinal = Math.max(1, ordinal - 1); if (!cardinality || prevOrdinal <= cardinality) { requests.push( ensureAssetUrl(documentId, asset, { start: prevOrdinal, limit: 1, objectOrdinal: prevOrdinal, }), ); } } if (canGoNext) { const nextOrdinal = ordinal + 1; if (!cardinality || nextOrdinal <= cardinality) { requests.push( ensureAssetUrl(documentId, asset, { start: nextOrdinal, limit: 1, objectOrdinal: nextOrdinal, }), ); } } requests.forEach((promise) => promise?.catch?.(() => {})); }; const singleWarmState = { documentId: singleNavigatorDocId, asset: singleNavigatorAsset, ordinal: singleNavigatorOrdinal, canGoPrev: singleNavigatorCanGoPrev, canGoNext: singleNavigatorCanGoNext, cardinality: singleNavigatorCardinality, }; const stackWarmState = { documentId: stackNavigatorDocId, asset: stackNavigatorAsset, ordinal: stackNavigatorOrdinal, canGoPrev: stackNavigatorCanGoPrev, canGoNext: stackNavigatorCanGoNext, cardinality: stackNavigatorCardinality, }; warmNavigator(singleWarmState); warmNavigator(stackWarmState); }, [ ensureAssetUrl, singleNavigatorDocId, singleNavigatorAsset, singleNavigatorOrdinal, singleNavigatorCanGoPrev, singleNavigatorCanGoNext, singleNavigatorCardinality, stackNavigatorDocId, stackNavigatorAsset, stackNavigatorOrdinal, stackNavigatorCanGoPrev, stackNavigatorCanGoNext, stackNavigatorCardinality, ]); const renderSingle = () => { if (!singleDoc) { return

Select a document to view metadata, tags and actions.

; } const displayName = singleDoc.title || singleDoc.original_name; const isEditingTitle = titleEditDocId === singleDoc.id; const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0; const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—'; const issuedAt = singleDoc.issued_at ? new Date(singleDoc.issued_at).toLocaleString() : '—'; const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : []; const pageCountRaw = singleDoc.current_version?.metadata?.page_count; const pageCountValue = typeof pageCountRaw === 'number' ? pageCountRaw : pageCountRaw != null && pageCountRaw !== '' ? Number.parseInt(pageCountRaw, 10) : null; const hasPageCount = Number.isFinite(pageCountValue) && pageCountValue >= 0; const metadata = singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null; const effectiveCardinality = singleEffectiveCardinality; const canGoPrev = singlePreviewNavigator.canGoPrev; const canGoNext = singlePreviewNavigator.canGoNext; const hasPreviewImage = singleHasPreview; const interceptNavPointer = (event) => { event.preventDefault(); event.stopPropagation(); }; return ( <>
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
) : null}
{isEditingTitle ? (
{ setTitleDraft(event.target.value); if (titleError) { setTitleError(null); } }} onKeyDown={(event) => { if (event.key === 'Escape') { event.preventDefault(); cancelTitleEdit(); } }} aria-label="Document title" autoFocus />
) : ( <>

{displayName}

)}
{titleError ?
{titleError}
: null}
Uploaded:{' '} {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
Size:{' '} {sizeLabel}
Type: {singleDoc.content_type || 'Unknown'}
Issued: {issuedAt}
{hasPageCount ? (
Pages: {pageCountValue}
) : null} {singleFolderPath?.length ? (
Folder:{' '} {singleFolderPath.map((segment, index) => { const label = segment?.name || '…'; const targetId = segment?.id || null; const key = `${targetId || label}-${index}`; const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function'; const href = !isClickable ? null : targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`; return ( {index > 0 ? / : null} {isClickable ? ( { if ( event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) { return; } event.preventDefault(); event.stopPropagation(); onFolderNavigate(targetId); }} > {label} ) : ( {label} )} ); })}
) : null}
Original filename:{' '} {singleDoc.original_name}
({ id: tag.id, label: tag.label, color: tag.color || tagLookupById.get(tag.id)?.color, }))} onRemove={(tag) => onTagRemove(singleDoc.id, tag.id)} onAdd={({ value, input }) => onTagAdd(singleDoc, value, input)} datalistId="tag-catalog-single" datalistOptions={tags} /> onCorrespondentRemove?.({ documentId: singleDoc.id, correspondentId: entry.id, }) } onAdd={({ name, input }) => onCorrespondentAdd?.({ document: singleDoc, name, input, }) } datalistId="correspondent-catalog-single" datalistOptions={correspondentOptions} /> {metadata && (
Metadata
{JSON.stringify(metadata, null, 2)}
)} ); }; const renderBulk = () => { const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`; const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—'; const topDocIdLocal = topDocId; const topCardinalityLocal = topEffectiveCardinality; const topHasPreview = Boolean(stackPreviewNavigator.currentUrl); const topCanGoPrev = stackPreviewNavigator.canGoPrev; const topCanGoNext = stackPreviewNavigator.canGoNext; const interceptTopNavPointer = (event) => { event.preventDefault(); event.stopPropagation(); }; const documentIds = bulkDocumentIds; return ( <>
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
) : null}

{countLabel}

Total size (stack): {sizeLabel}
onBulkTagRemove?.({ label: tag.label, documentIds })} onAdd={({ value, input }) => onBulkTagAdd?.({ label: value, input, documentIds }) } addPlaceholder="Add tag to selection" addButtonLabel="Add tag" datalistId="tag-catalog-bulk" datalistOptions={tags} className="bulk-tags" /> onBulkCorrespondentAdd?.({ name, input, documentIds }) } addPlaceholder="Add correspondent to selection" datalistId="correspondent-catalog-bulk" datalistOptions={correspondentOptions} showCount className="bulk-correspondents" /> ); }; const isBulkSelection = selectedCount > 1; const showOcrAction = Boolean(singleDoc && hasOcrAsset); return ( <> ); }; export default DetailPanel;