diff --git a/frontend/src/components/tools/shared/ResultsPreview.tsx b/frontend/src/components/tools/shared/ResultsPreview.tsx
deleted file mode 100644
index 9bddb5dc9..000000000
--- a/frontend/src/components/tools/shared/ResultsPreview.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Grid, Paper, Box, Image, Text, Loader, Stack, Center } from '@mantine/core';
-
-export interface ResultFile {
- file: File;
- thumbnail?: string;
-}
-
-export interface ResultsPreviewProps {
- files: ResultFile[];
- isGeneratingThumbnails?: boolean;
- onFileClick?: (file: File) => void;
- title?: string;
- emptyMessage?: string;
- loadingMessage?: string;
-}
-
-const ResultsPreview = ({
- files,
- isGeneratingThumbnails = false,
- onFileClick,
- title,
- emptyMessage = "No files to preview",
- loadingMessage = "Generating previews..."
-}: ResultsPreviewProps) => {
- const formatSize = (size: number) => {
- if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
- if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
- return `${size} B`;
- };
-
- if (files.length === 0 && !isGeneratingThumbnails) {
- return (
-
- {emptyMessage}
-
- );
- }
-
- return (
-
- {title && (
-
- {title} ({files.length} files)
-
- )}
-
- {isGeneratingThumbnails ? (
-
-
-
- {loadingMessage}
-
-
- ) : (
-
- {files.map((result, index) => (
-
- onFileClick?.(result.file)}
- data-testid={`results-preview-thumbnail-${index}`}
- style={{
- textAlign: 'center',
- height: '10rem',
- width:'5rem',
- display: 'flex',
- flexDirection: 'column',
- cursor: onFileClick ? 'pointer' : 'default',
- transition: 'all 0.2s ease'
- }}
- >
-
- {result.thumbnail ? (
-
- ) : (
- No preview
- )}
-
-
- {result.file.name}
-
-
- {formatSize(result.file.size)}
-
-
-
- ))}
-
- )}
-
- );
-}
-
-export default ResultsPreview;
diff --git a/frontend/src/components/tools/shared/ReviewPanel.tsx b/frontend/src/components/tools/shared/ReviewPanel.tsx
new file mode 100644
index 000000000..451ef4e9b
--- /dev/null
+++ b/frontend/src/components/tools/shared/ReviewPanel.tsx
@@ -0,0 +1,245 @@
+import React, { useState } from 'react';
+import { Paper, Box, Image, Text, Loader, Stack, Center, Group, ActionIcon, Flex } from '@mantine/core';
+import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import ChevronRightIcon from '@mui/icons-material/ChevronRight';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+
+export interface ReviewFile {
+ file: File;
+ thumbnail?: string;
+}
+
+export interface ReviewPanelProps {
+ files: ReviewFile[];
+ isGeneratingThumbnails?: boolean;
+ onFileClick?: (file: File) => void;
+ title?: string;
+ emptyMessage?: string;
+ loadingMessage?: string;
+}
+
+const ReviewPanel = ({
+ files,
+ isGeneratingThumbnails = false,
+ onFileClick,
+ title,
+ emptyMessage = "No files to preview",
+ loadingMessage = "Generating previews..."
+}: ReviewPanelProps) => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ const formatSize = (size: number) => {
+ if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
+ if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
+ return `${size} B`;
+ };
+
+ const formatDate = (date: Date) => {
+ return new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ }).format(date);
+ };
+
+ const handlePrevious = () => {
+ setCurrentIndex((prev) => (prev === 0 ? files.length - 1 : prev - 1));
+ };
+
+ const handleNext = () => {
+ setCurrentIndex((prev) => (prev === files.length - 1 ? 0 : prev + 1));
+ };
+
+ if (files.length === 0 && !isGeneratingThumbnails) {
+ return (
+
+ {emptyMessage}
+
+ );
+ }
+
+ if (isGeneratingThumbnails) {
+ return (
+
+
+
+ {loadingMessage}
+
+
+ );
+ }
+
+ const currentFile = files[currentIndex];
+ if (!currentFile) return null;
+
+ return (
+
+
+ {/* File name at the top */}
+
+
+ {currentFile.file.name}
+
+
+
+
+ {/* Preview on the left */}
+
+ onFileClick?.(currentFile.file)}
+ onMouseEnter={(e) => {
+ if (onFileClick) {
+ const overlay = e.currentTarget.querySelector('.hover-overlay');
+ if (overlay) overlay.style.opacity = '1';
+ }
+ }}
+ onMouseLeave={(e) => {
+ const overlay = e.currentTarget.querySelector('.hover-overlay');
+ if (overlay) overlay.style.opacity = '0';
+ }}
+ >
+ {currentFile.thumbnail ? (
+
+ ) : (
+
+ No preview
+
+ )}
+
+ {/* Hover overlay with eye icon */}
+ {onFileClick && (
+
+
+
+ )}
+
+
+
+ {/* Metadata on the right */}
+
+
+
+ {formatSize(currentFile.file.size)}
+
+
+ {currentFile.file.type || 'Unknown'}
+
+
+ {formatDate(new Date(currentFile.file.lastModified))}
+
+
+
+
+
+ {/* Navigation controls */}
+ {files.length > 1 && (
+
+
+
+
+
+
+
+ {files.map((_, index) => (
+ setCurrentIndex(index)}
+ data-testid={`review-panel-dot-${index}`}
+ />
+ ))}
+
+
+
+
+
+
+
+
+ {currentIndex + 1} of {files.length}
+
+
+ )}
+
+ );
+};
+
+export default ReviewPanel;
diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx
index 053392c2a..314f534ce 100644
--- a/frontend/src/components/tools/shared/ToolStep.tsx
+++ b/frontend/src/components/tools/shared/ToolStep.tsx
@@ -5,7 +5,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { Tooltip } from '../../shared/Tooltip';
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
import { createFilesToolStep, FilesToolStepProps } from './createFilesToolStep';
-import { createResultsToolStep, ResultsToolStepProps } from './createResultsToolStep';
+import { createReviewToolStep, ReviewToolStepProps } from './createReviewToolStep';
interface ToolStepContextType {
visibleStepCount: number;
@@ -22,6 +22,8 @@ export interface ToolStepProps {
helpText?: string;
showNumber?: boolean;
_stepNumber?: number; // Internal prop set by ToolStepContainer
+ _excludeFromCount?: boolean; // Internal prop to exclude from visible count calculation
+ _noPadding?: boolean; // Internal prop to exclude from default left padding
tooltip?: {
content?: React.ReactNode;
tips?: TooltipTip[];
@@ -73,6 +75,7 @@ const ToolStep = ({
helpText,
showNumber,
_stepNumber,
+ _noPadding,
tooltip
}: ToolStepProps) => {
if (!isVisible) return null;
@@ -132,7 +135,7 @@ const ToolStep = ({
{!isCollapsed && (
-
+
{helpText && (
{helpText}
@@ -176,17 +179,20 @@ export function createToolSteps() {
return createFilesToolStep(create, props);
};
- const createResultsStep = (props: ResultsToolStepProps): React.ReactElement => {
- return createResultsToolStep(create, props);
+ const createReviewStep = (props: ReviewToolStepProps): React.ReactElement => {
+ return createReviewToolStep(create, props);
};
const getVisibleCount = () => {
- return steps.filter(step =>
- (step.props as ToolStepProps).isVisible !== false
- ).length;
+ return steps.filter(step => {
+ const props = step.props as ToolStepProps;
+ const isVisible = props.isVisible !== false;
+ const excludeFromCount = props._excludeFromCount === true;
+ return isVisible && !excludeFromCount;
+ }).length;
};
- return { create, createFilesStep, createResultsStep, getVisibleCount, steps };
+ return { create, createFilesStep, createReviewStep, getVisibleCount, steps };
}
// Context provider wrapper for tools using the factory
@@ -196,8 +202,10 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
let count = 0;
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.type === ToolStep) {
- const isVisible = (child.props as ToolStepProps).isVisible !== false;
- if (isVisible) count++;
+ const props = child.props as ToolStepProps;
+ const isVisible = props.isVisible !== false;
+ const excludeFromCount = props._excludeFromCount === true;
+ if (isVisible && !excludeFromCount) count++;
}
});
return count;
@@ -215,5 +223,5 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
}
export type { FilesToolStepProps } from './createFilesToolStep';
-export type { ResultsToolStepProps } from './createResultsToolStep';
+export type { ReviewToolStepProps } from './createReviewToolStep';
export default ToolStep;
diff --git a/frontend/src/components/tools/shared/createResultsToolStep.tsx b/frontend/src/components/tools/shared/createReviewToolStep.tsx
similarity index 76%
rename from frontend/src/components/tools/shared/createResultsToolStep.tsx
rename to frontend/src/components/tools/shared/createReviewToolStep.tsx
index fe774cba6..faea34023 100644
--- a/frontend/src/components/tools/shared/createResultsToolStep.tsx
+++ b/frontend/src/components/tools/shared/createReviewToolStep.tsx
@@ -3,32 +3,34 @@ import { Button, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import DownloadIcon from '@mui/icons-material/Download';
import ErrorNotification from './ErrorNotification';
-import ResultsPreview from './ResultsPreview';
+import ReviewPanel from './ReviewPanel';
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
-export interface ResultsToolStepProps {
+export interface ReviewToolStepProps {
isVisible: boolean;
operation: ToolOperationHook;
title?: string;
onFileClick?: (file: File) => void;
}
-export function createResultsToolStep(
+export function createReviewToolStep(
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
- props: ResultsToolStepProps
+ props: ReviewToolStepProps
): React.ReactElement {
const { t } = useTranslation();
const { operation } = props;
-
+
const previewFiles = operation.files?.map((file, index) => ({
file,
thumbnail: operation.thumbnails[index]
})) || [];
- return createStep("Results", {
- isVisible: props.isVisible
+ return createStep("Review", {
+ isVisible: props.isVisible,
+ _excludeFromCount: true,
+ _noPadding: true
}, (
-
+
{operation.status && (
{operation.status}
)}
@@ -38,28 +40,28 @@ export function createResultsToolStep(
onClose={operation.clearError}
/>
- {operation.downloadUrl && (
+ {previewFiles.length > 0 && (
+
+ )}
+
+ {operation.downloadUrl && (
}
- color="green"
+ color="blue"
fullWidth
mb="md"
>
{t("download", "Download")}
)}
-
- {previewFiles.length > 0 && (
-
- )}
));
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx
index fd9eabff1..27496b441 100644
--- a/frontend/src/components/tools/shared/createToolFlow.tsx
+++ b/frontend/src/components/tools/shared/createToolFlow.tsx
@@ -35,7 +35,7 @@ export interface ExecuteButtonConfig {
testId?: string;
}
-export interface ResultsStepConfig {
+export interface ReviewStepConfig {
isVisible: boolean;
operation: ToolOperationHook;
title: string;
@@ -47,7 +47,7 @@ export interface ToolFlowConfig {
files: FilesStepConfig;
steps: MiddleStepConfig[];
executeButton?: ExecuteButtonConfig;
- results: ResultsStepConfig;
+ review: ReviewStepConfig;
}
/**
@@ -81,7 +81,7 @@ export function createToolFlow(config: ToolFlowConfig) {
{config.executeButton && config.executeButton.isVisible !== false && (
)}
- {/* Results Step */}
- {steps.createResultsStep({
- isVisible: config.results.isVisible,
- operation: config.results.operation,
- title: config.results.title,
- onFileClick: config.results.onFileClick
+ {/* Review Step */}
+ {steps.createReviewStep({
+ isVisible: config.review.isVisible,
+ operation: config.review.operation,
+ title: config.review.title,
+ onFileClick: config.review.onFileClick
})}
);
diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx
index b727dbc87..893f7485d 100644
--- a/frontend/src/tools/Compress.tsx
+++ b/frontend/src/tools/Compress.tsx
@@ -64,11 +64,11 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const settingsCollapsed = !hasFiles || hasResults;
return (
-
+
{createToolFlow({
files: {
selectedFiles,
- isCollapsed: hasFiles
+ isCollapsed: hasFiles && !hasResults,
},
steps: [{
title: "Settings",
@@ -90,7 +90,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
onClick: handleCompress,
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
},
- results: {
+ review: {
isVisible: hasResults,
operation: compressOperation,
title: t("compress.title", "Compression Results"),
diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx
index 669643566..2b9c9fe8d 100644
--- a/frontend/src/tools/Convert.tsx
+++ b/frontend/src/tools/Convert.tsx
@@ -101,8 +101,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
};
return (
-
-
+
{createToolFlow({
files: {
selectedFiles,
@@ -131,7 +130,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
testId: "convert-button"
},
- results: {
+ review: {
isVisible: hasResults,
operation: convertOperation,
title: t("convert.conversionResults", "Conversion Results"),
@@ -140,7 +139,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
}
})}
-
);
};
diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx
index b5e80d8bf..7fc2cd457 100644
--- a/frontend/src/tools/OCR.tsx
+++ b/frontend/src/tools/OCR.tsx
@@ -84,11 +84,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const settingsCollapsed = expandedStep !== 'settings';
return (
-
+
{createToolFlow({
files: {
selectedFiles,
- isCollapsed: hasFiles && filesCollapsed,
+ isCollapsed: hasFiles && !hasResults && filesCollapsed,
},
steps: [
{
@@ -131,7 +131,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isVisible: hasValidSettings && !hasResults,
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled
},
- results: {
+ review: {
isVisible: hasResults,
operation: ocrOperation,
title: t("ocr.results.title", "OCR Results"),
diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx
index 19ec669c0..6e2e34559 100644
--- a/frontend/src/tools/Sanitize.tsx
+++ b/frontend/src/tools/Sanitize.tsx
@@ -61,7 +61,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const hasFiles = selectedFiles.length > 0;
const hasResults = sanitizeOperation.files.length > 0;
- const filesCollapsed = hasFiles;
+ const filesCollapsed = hasFiles || hasResults;
const settingsCollapsed = !hasFiles || hasResults;
return (
@@ -86,11 +86,12 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
}],
executeButton: {
text: t("sanitize.submit", "Sanitize PDF"),
+ isVisible: !hasResults,
loadingText: t("loading"),
onClick: handleSanitize,
disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled
},
- results: {
+ review: {
isVisible: hasResults,
operation: sanitizeOperation,
title: t("sanitize.sanitizationResults", "Sanitization Results"),
diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx
index f480451d9..2b4ff1fe9 100644
--- a/frontend/src/tools/Split.tsx
+++ b/frontend/src/tools/Split.tsx
@@ -90,7 +90,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isVisible: !hasResults,
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled
},
- results: {
+ review: {
isVisible: hasResults,
operation: splitOperation,
title: "Split Results",