diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index c92c4c56e..b57f6cf59 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -352,6 +352,11 @@ "refreshPage": "Refresh Page" }, "home": { + "compare": { + "tags": "difference,changes,review", + "title": "Compare", + "desc": "Compare two PDF documents and highlight differences" + }, "desc": "Your locally hosted one-stop-shop for all your PDF needs.", "searchBar": "Search for features...", "viewPdf": { @@ -1276,15 +1281,61 @@ "tags": "differentiate,contrast,changes,analysis", "title": "Compare", "header": "Compare PDFs", - "highlightColor": { - "1": "Highlight Color 1:", - "2": "Highlight Color 2:" + "description": "Select the base and comparison PDF to highlight differences.", + "view": { + "title": "Compare view", + "noData": "Run a comparison to view the summary and diff." }, - "document": { - "1": "Document 1", - "2": "Document 2" + "base": { + "label": "Base Document", + "placeholder": "Select a base PDF" + }, + "comparison": { + "label": "Comparison Document", + "placeholder": "Select a comparison PDF" + }, + "cta": "Compare", + "loading": "Comparing...", + "review": { + "title": "Comparison Result", + "actionsHint": "Review the comparison, switch document roles, or export the summary.", + "switchOrder": "Switch order", + "exportSummary": "Export summary" + }, + "addFile": "Add File", + "replaceFile": "Replace File", + "pages": "Pages", + "toggleLayout": "Toggle layout", + "upload": { + "title": "Set up your comparison", + "baseTitle": "Base document", + "baseDescription": "This version acts as the reference for differences.", + "comparisonTitle": "Comparison document", + "comparisonDescription": "Differences from this version will be highlighted.", + "browse": "Browse files", + "selectExisting": "Select existing", + "clearSelection": "Clear selection", + "instructions": "Drag & drop here or use the buttons to choose a file." + }, + "legend": { + "removed": "Removed from base", + "added": "Added in comparison" + }, + "summary": { + "baseHeading": "Base document", + "comparisonHeading": "Comparison document", + "pageLabel": "Page" + }, + "status": { + "extracting": "Extracting text...", + "processing": "Analyzing differences...", + "complete": "Comparison ready" + }, + "error": { + "selectRequired": "Select a base and comparison document.", + "filesMissing": "Unable to locate the selected files. Please re-select them.", + "generic": "Unable to compare these files." }, - "submit": "Compare", "complex": { "message": "One or both of the provided documents are large files, accuracy of comparison may be reduced" }, @@ -1297,7 +1348,15 @@ "text": { "message": "One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison." } - } + }, + "no": { + "text": { + "message": "One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison." + } + }, + "large.file.message": "One or Both of the provided documents are too large to process", + "complex.message": "One or both of the provided documents are large files, accuracy of comparison may be reduced", + "no.text.message": "One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison." }, "certSign": { "tags": "authenticate,PEM,P12,official,encrypt", diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 8d69a9b71..f6eabfb2b 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -79,6 +79,7 @@ export default function Workbench() { switch (currentView) { case "fileEditor": + return ( view.workbenchId === currentView && view.data != null); + + if (customView) { const CustomComponent = customView.component; return ; @@ -152,7 +157,7 @@ export default function Workbench() { return ( = { 'top-right': 'toast-container--top-right', 'bottom-left': 'toast-container--bottom-left', 'bottom-right': 'toast-container--bottom-right', + 'bottom-center': 'toast-container--bottom-center', }; function getToastItemClass(t: ToastInstance): string { @@ -44,7 +45,7 @@ export default function ToastRenderer() { if (!acc[key]) acc[key] = [] as ToastInstance[]; acc[key].push(t); return acc; - }, { 'top-left': [], 'top-right': [], 'bottom-left': [], 'bottom-right': [] }); + }, { 'top-left': [], 'top-right': [], 'bottom-left': [], 'bottom-right': [], 'bottom-center': [] }); return ( <> diff --git a/frontend/src/components/toast/types.ts b/frontend/src/components/toast/types.ts index aeb0c79a5..764e14c34 100644 --- a/frontend/src/components/toast/types.ts +++ b/frontend/src/components/toast/types.ts @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -export type ToastLocation = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +export type ToastLocation = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom-center'; export type ToastAlertType = 'success' | 'error' | 'warning' | 'neutral'; export interface ToastOptions { diff --git a/frontend/src/components/tools/compare/CompareReviewActions.tsx b/frontend/src/components/tools/compare/CompareReviewActions.tsx new file mode 100644 index 000000000..6bfa55a49 --- /dev/null +++ b/frontend/src/components/tools/compare/CompareReviewActions.tsx @@ -0,0 +1,50 @@ +import { Button, Group, Stack, Text } from '@mantine/core'; +import SwapHorizRoundedIcon from '@mui/icons-material/SwapHorizRounded'; +import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded'; +import { useTranslation } from 'react-i18next'; + +interface CompareReviewActionsProps { + onSwitchOrder: () => void; + onDownloadSummary: () => void; + disableDownload?: boolean; + disableSwitch?: boolean; +} + +const CompareReviewActions = ({ + onSwitchOrder, + onDownloadSummary, + disableDownload = false, + disableSwitch = false, +}: CompareReviewActionsProps) => { + const { t } = useTranslation(); + + return ( + + + {t('compare.review.actionsHint', 'Review the comparison, switch document roles, or export the summary.')} + + + + + + + ); +}; + +export default CompareReviewActions; + diff --git a/frontend/src/components/tools/compare/CompareSelectionStep.tsx b/frontend/src/components/tools/compare/CompareSelectionStep.tsx new file mode 100644 index 000000000..01689107a --- /dev/null +++ b/frontend/src/components/tools/compare/CompareSelectionStep.tsx @@ -0,0 +1,105 @@ +import { useMemo } from 'react'; +import { Badge, Card, Group, Select, Stack, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useAllFiles } from '../../../contexts/FileContext'; +import { formatFileSize } from '../../../utils/fileUtils'; +import type { FileId } from '../../../types/file'; + +interface CompareSelectionStepProps { + role: 'base' | 'comparison'; + selectedFileId: FileId | null; + onFileSelect: (fileId: FileId | null) => void; + disabled?: boolean; +} + +export const CompareSelectionStep = ({ + role, + selectedFileId, + onFileSelect, + disabled = false, +}: CompareSelectionStepProps) => { + const { t } = useTranslation(); + const { fileStubs } = useAllFiles(); + + const labels = useMemo(() => { + if (role === 'base') { + return { + title: t('compare.base.label', 'Base document'), + placeholder: t('compare.base.placeholder', 'Select a base PDF'), + }; + } + + return { + title: t('compare.comparison.label', 'Comparison document'), + placeholder: t('compare.comparison.placeholder', 'Select a comparison PDF'), + }; + }, [role, t]); + + const options = useMemo(() => { + return fileStubs + .filter((stub) => stub.type?.includes('pdf') || stub.name.toLowerCase().endsWith('.pdf')) + .map((stub) => ({ + value: stub.id as unknown as string, + label: stub.name, + })); + }, [fileStubs]); + + const selectedStub = useMemo(() => fileStubs.find((stub) => stub.id === selectedFileId), [fileStubs, selectedFileId]); + + const selectValue = selectedFileId ? (selectedFileId as unknown as string) : null; + + // Hide dropdown until there are files in the workbench + if (options.length === 0) { + return ( + + + {t('compare.addFilesHint', 'Add PDFs in the Files step to enable selection.')} + + + ); + } + + return ( + +