mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Review step
This commit is contained in:
parent
1b14717257
commit
ee6f7a2939
@ -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 (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{emptyMessage}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box mt="lg" p="md" style={{ backgroundColor: 'var(--mantine-color-gray-0)', borderRadius: 8 }} data-testid="results-preview-container">
|
|
||||||
{title && (
|
|
||||||
<Text fw={500} size="md" mb="sm" data-testid="results-preview-title">
|
|
||||||
{title} ({files.length} files)
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isGeneratingThumbnails ? (
|
|
||||||
<Center p="lg" data-testid="results-preview-loading">
|
|
||||||
<Stack align="center" gap="sm">
|
|
||||||
<Loader size="sm" />
|
|
||||||
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<Grid data-testid="results-preview-grid">
|
|
||||||
{files.map((result, index) => (
|
|
||||||
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
|
||||||
<Paper
|
|
||||||
p="xs"
|
|
||||||
withBorder
|
|
||||||
onClick={() => 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'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
{result.thumbnail ? (
|
|
||||||
<Image
|
|
||||||
src={result.thumbnail}
|
|
||||||
alt={`Preview of ${result.file.name}`}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '9rem',
|
|
||||||
objectFit: 'contain'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text size="xs" c="dimmed">No preview</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c="dimmed"
|
|
||||||
mt="xs"
|
|
||||||
style={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
title={result.file.name}
|
|
||||||
>
|
|
||||||
{result.file.name}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{formatSize(result.file.size)}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResultsPreview;
|
|
245
frontend/src/components/tools/shared/ReviewPanel.tsx
Normal file
245
frontend/src/components/tools/shared/ReviewPanel.tsx
Normal file
@ -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 (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{emptyMessage}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGeneratingThumbnails) {
|
||||||
|
return (
|
||||||
|
<Center p="lg" data-testid="review-panel-loading">
|
||||||
|
<Stack align="center" gap="sm">
|
||||||
|
<Loader size="sm" />
|
||||||
|
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFile = files[currentIndex];
|
||||||
|
if (!currentFile) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p="sm" style={{ backgroundColor: 'var(--mantine-color-gray-1)', borderRadius: 8, maxWidth: '100%' }} data-testid="review-panel-container">
|
||||||
|
|
||||||
|
{/* File name at the top */}
|
||||||
|
<Box mb="sm" style={{ minHeight: '3rem', display: 'flex', alignItems: 'flex-start' }}>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={500}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
lineHeight: 1.4
|
||||||
|
}}
|
||||||
|
title={currentFile.file.name}
|
||||||
|
>
|
||||||
|
{currentFile.file.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex gap="md" align="flex-start" style={{ minHeight: '120px', maxWidth: '100%' }} data-testid={`review-panel-item-${currentIndex}`}>
|
||||||
|
{/* Preview on the left */}
|
||||||
|
<Box style={{
|
||||||
|
flex: '0 0 100px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '120px',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
cursor: onFileClick ? 'pointer' : 'default',
|
||||||
|
transition: 'opacity 0.2s ease',
|
||||||
|
}}
|
||||||
|
onClick={() => 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 ? (
|
||||||
|
<Image
|
||||||
|
src={currentFile.thumbnail}
|
||||||
|
alt={`Preview of ${currentFile.file.name}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100px',
|
||||||
|
maxHeight: '120px',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100px',
|
||||||
|
height: '120px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'var(--mantine-color-gray-1)',
|
||||||
|
borderRadius: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="xs" c="dimmed">No preview</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hover overlay with eye icon */}
|
||||||
|
{onFileClick && (
|
||||||
|
<Box
|
||||||
|
className="hover-overlay"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
borderRadius: 4,
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'opacity 0.2s ease',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisibilityIcon style={{ color: 'white', fontSize: '1.5rem' }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Metadata on the right */}
|
||||||
|
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Stack gap="2px">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatSize(currentFile.file.size)}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{currentFile.file.type || 'Unknown'}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatDate(new Date(currentFile.file.lastModified))}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Navigation controls */}
|
||||||
|
{files.length > 1 && (
|
||||||
|
<Stack align="center" gap="xs" mt="xs">
|
||||||
|
<Group justify="center" gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePrevious}
|
||||||
|
disabled={files.length <= 1}
|
||||||
|
data-testid="review-panel-prev"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon style={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
{files.map((_, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: index === currentIndex
|
||||||
|
? 'var(--mantine-color-blue-6)'
|
||||||
|
: 'var(--mantine-color-gray-4)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background-color 0.2s ease'
|
||||||
|
}}
|
||||||
|
onClick={() => setCurrentIndex(index)}
|
||||||
|
data-testid={`review-panel-dot-${index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={files.length <= 1}
|
||||||
|
data-testid="review-panel-next"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon style={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{currentIndex + 1} of {files.length}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReviewPanel;
|
@ -5,7 +5,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
|
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
|
||||||
import { createFilesToolStep, FilesToolStepProps } from './createFilesToolStep';
|
import { createFilesToolStep, FilesToolStepProps } from './createFilesToolStep';
|
||||||
import { createResultsToolStep, ResultsToolStepProps } from './createResultsToolStep';
|
import { createReviewToolStep, ReviewToolStepProps } from './createReviewToolStep';
|
||||||
|
|
||||||
interface ToolStepContextType {
|
interface ToolStepContextType {
|
||||||
visibleStepCount: number;
|
visibleStepCount: number;
|
||||||
@ -22,6 +22,8 @@ export interface ToolStepProps {
|
|||||||
helpText?: string;
|
helpText?: string;
|
||||||
showNumber?: boolean;
|
showNumber?: boolean;
|
||||||
_stepNumber?: number; // Internal prop set by ToolStepContainer
|
_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?: {
|
tooltip?: {
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
tips?: TooltipTip[];
|
tips?: TooltipTip[];
|
||||||
@ -73,6 +75,7 @@ const ToolStep = ({
|
|||||||
helpText,
|
helpText,
|
||||||
showNumber,
|
showNumber,
|
||||||
_stepNumber,
|
_stepNumber,
|
||||||
|
_noPadding,
|
||||||
tooltip
|
tooltip
|
||||||
}: ToolStepProps) => {
|
}: ToolStepProps) => {
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
@ -132,7 +135,7 @@ const ToolStep = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Stack gap="md" pl="md">
|
<Stack gap="md" pl={_noPadding ? 0 : "md"}>
|
||||||
{helpText && (
|
{helpText && (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{helpText}
|
{helpText}
|
||||||
@ -176,17 +179,20 @@ export function createToolSteps() {
|
|||||||
return createFilesToolStep(create, props);
|
return createFilesToolStep(create, props);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createResultsStep = <TParams = any>(props: ResultsToolStepProps<TParams>): React.ReactElement => {
|
const createReviewStep = <TParams = any>(props: ReviewToolStepProps<TParams>): React.ReactElement => {
|
||||||
return createResultsToolStep(create, props);
|
return createReviewToolStep(create, props);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVisibleCount = () => {
|
const getVisibleCount = () => {
|
||||||
return steps.filter(step =>
|
return steps.filter(step => {
|
||||||
(step.props as ToolStepProps).isVisible !== false
|
const props = step.props as ToolStepProps;
|
||||||
).length;
|
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
|
// Context provider wrapper for tools using the factory
|
||||||
@ -196,8 +202,10 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
React.Children.forEach(children, (child) => {
|
React.Children.forEach(children, (child) => {
|
||||||
if (React.isValidElement(child) && child.type === ToolStep) {
|
if (React.isValidElement(child) && child.type === ToolStep) {
|
||||||
const isVisible = (child.props as ToolStepProps).isVisible !== false;
|
const props = child.props as ToolStepProps;
|
||||||
if (isVisible) count++;
|
const isVisible = props.isVisible !== false;
|
||||||
|
const excludeFromCount = props._excludeFromCount === true;
|
||||||
|
if (isVisible && !excludeFromCount) count++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return count;
|
return count;
|
||||||
@ -215,5 +223,5 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type { FilesToolStepProps } from './createFilesToolStep';
|
export type { FilesToolStepProps } from './createFilesToolStep';
|
||||||
export type { ResultsToolStepProps } from './createResultsToolStep';
|
export type { ReviewToolStepProps } from './createReviewToolStep';
|
||||||
export default ToolStep;
|
export default ToolStep;
|
||||||
|
@ -3,19 +3,19 @@ import { Button, Stack, Text } from '@mantine/core';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import ErrorNotification from './ErrorNotification';
|
import ErrorNotification from './ErrorNotification';
|
||||||
import ResultsPreview from './ResultsPreview';
|
import ReviewPanel from './ReviewPanel';
|
||||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||||
|
|
||||||
export interface ResultsToolStepProps<TParams = any> {
|
export interface ReviewToolStepProps<TParams = any> {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
operation: ToolOperationHook<TParams>;
|
operation: ToolOperationHook<TParams>;
|
||||||
title?: string;
|
title?: string;
|
||||||
onFileClick?: (file: File) => void;
|
onFileClick?: (file: File) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createResultsToolStep<TParams = any>(
|
export function createReviewToolStep<TParams = any>(
|
||||||
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
||||||
props: ResultsToolStepProps<TParams>
|
props: ReviewToolStepProps<TParams>
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { operation } = props;
|
const { operation } = props;
|
||||||
@ -25,10 +25,12 @@ export function createResultsToolStep<TParams = any>(
|
|||||||
thumbnail: operation.thumbnails[index]
|
thumbnail: operation.thumbnails[index]
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
return createStep("Results", {
|
return createStep("Review", {
|
||||||
isVisible: props.isVisible
|
isVisible: props.isVisible,
|
||||||
|
_excludeFromCount: true,
|
||||||
|
_noPadding: true
|
||||||
}, (
|
}, (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" >
|
||||||
{operation.status && (
|
{operation.status && (
|
||||||
<Text size="sm" c="dimmed">{operation.status}</Text>
|
<Text size="sm" c="dimmed">{operation.status}</Text>
|
||||||
)}
|
)}
|
||||||
@ -38,28 +40,28 @@ export function createResultsToolStep<TParams = any>(
|
|||||||
onClose={operation.clearError}
|
onClose={operation.clearError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{operation.downloadUrl && (
|
{previewFiles.length > 0 && (
|
||||||
|
<ReviewPanel
|
||||||
|
files={previewFiles}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
isGeneratingThumbnails={operation.isGeneratingThumbnails}
|
||||||
|
title={props.title || "Review"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{operation.downloadUrl && (
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={operation.downloadUrl}
|
href={operation.downloadUrl}
|
||||||
download={operation.downloadFilename}
|
download={operation.downloadFilename}
|
||||||
leftSection={<DownloadIcon />}
|
leftSection={<DownloadIcon />}
|
||||||
color="green"
|
color="blue"
|
||||||
fullWidth
|
fullWidth
|
||||||
mb="md"
|
mb="md"
|
||||||
>
|
>
|
||||||
{t("download", "Download")}
|
{t("download", "Download")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{previewFiles.length > 0 && (
|
|
||||||
<ResultsPreview
|
|
||||||
files={previewFiles}
|
|
||||||
onFileClick={props.onFileClick}
|
|
||||||
isGeneratingThumbnails={operation.isGeneratingThumbnails}
|
|
||||||
title={props.title || "Results"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
));
|
));
|
||||||
}
|
}
|
@ -35,7 +35,7 @@ export interface ExecuteButtonConfig {
|
|||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultsStepConfig {
|
export interface ReviewStepConfig {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
operation: ToolOperationHook<any>;
|
operation: ToolOperationHook<any>;
|
||||||
title: string;
|
title: string;
|
||||||
@ -47,7 +47,7 @@ export interface ToolFlowConfig {
|
|||||||
files: FilesStepConfig;
|
files: FilesStepConfig;
|
||||||
steps: MiddleStepConfig[];
|
steps: MiddleStepConfig[];
|
||||||
executeButton?: ExecuteButtonConfig;
|
executeButton?: ExecuteButtonConfig;
|
||||||
results: ResultsStepConfig;
|
review: ReviewStepConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,7 +81,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||||
<OperationButton
|
<OperationButton
|
||||||
onClick={config.executeButton.onClick}
|
onClick={config.executeButton.onClick}
|
||||||
isLoading={config.results.operation.isLoading}
|
isLoading={config.review.operation.isLoading}
|
||||||
disabled={config.executeButton.disabled}
|
disabled={config.executeButton.disabled}
|
||||||
loadingText={config.executeButton.loadingText}
|
loadingText={config.executeButton.loadingText}
|
||||||
submitText={config.executeButton.text}
|
submitText={config.executeButton.text}
|
||||||
@ -89,12 +89,12 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results Step */}
|
{/* Review Step */}
|
||||||
{steps.createResultsStep({
|
{steps.createReviewStep({
|
||||||
isVisible: config.results.isVisible,
|
isVisible: config.review.isVisible,
|
||||||
operation: config.results.operation,
|
operation: config.review.operation,
|
||||||
title: config.results.title,
|
title: config.review.title,
|
||||||
onFileClick: config.results.onFileClick
|
onFileClick: config.review.onFileClick
|
||||||
})}
|
})}
|
||||||
</ToolStepProvider>
|
</ToolStepProvider>
|
||||||
);
|
);
|
||||||
|
@ -64,11 +64,11 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const settingsCollapsed = !hasFiles || hasResults;
|
const settingsCollapsed = !hasFiles || hasResults;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
<Stack gap="sm" p="sm" style={{ height: '100vh', overflow: 'auto' }}>
|
||||||
{createToolFlow({
|
{createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
isCollapsed: hasFiles
|
isCollapsed: hasFiles && !hasResults,
|
||||||
},
|
},
|
||||||
steps: [{
|
steps: [{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@ -90,7 +90,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onClick: handleCompress,
|
onClick: handleCompress,
|
||||||
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
|
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
|
||||||
},
|
},
|
||||||
results: {
|
review: {
|
||||||
isVisible: hasResults,
|
isVisible: hasResults,
|
||||||
operation: compressOperation,
|
operation: compressOperation,
|
||||||
title: t("compress.title", "Compression Results"),
|
title: t("compress.title", "Compression Results"),
|
||||||
|
@ -101,8 +101,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
|
<Stack gap="sm" p="sm" style={{ height: '100vh', overflow: 'auto' }}>
|
||||||
<Stack gap="sm" p="sm">
|
|
||||||
{createToolFlow({
|
{createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
@ -131,7 +130,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
|
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
testId: "convert-button"
|
testId: "convert-button"
|
||||||
},
|
},
|
||||||
results: {
|
review: {
|
||||||
isVisible: hasResults,
|
isVisible: hasResults,
|
||||||
operation: convertOperation,
|
operation: convertOperation,
|
||||||
title: t("convert.conversionResults", "Conversion Results"),
|
title: t("convert.conversionResults", "Conversion Results"),
|
||||||
@ -140,7 +139,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,11 +84,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const settingsCollapsed = expandedStep !== 'settings';
|
const settingsCollapsed = expandedStep !== 'settings';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
<Stack gap="sm" p="sm" style={{ height: '100vh', overflow: 'auto' }}>
|
||||||
{createToolFlow({
|
{createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
isCollapsed: hasFiles && filesCollapsed,
|
isCollapsed: hasFiles && !hasResults && filesCollapsed,
|
||||||
},
|
},
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
@ -131,7 +131,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
isVisible: hasValidSettings && !hasResults,
|
isVisible: hasValidSettings && !hasResults,
|
||||||
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled
|
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled
|
||||||
},
|
},
|
||||||
results: {
|
review: {
|
||||||
isVisible: hasResults,
|
isVisible: hasResults,
|
||||||
operation: ocrOperation,
|
operation: ocrOperation,
|
||||||
title: t("ocr.results.title", "OCR Results"),
|
title: t("ocr.results.title", "OCR Results"),
|
||||||
|
@ -61,7 +61,7 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = sanitizeOperation.files.length > 0;
|
const hasResults = sanitizeOperation.files.length > 0;
|
||||||
const filesCollapsed = hasFiles;
|
const filesCollapsed = hasFiles || hasResults;
|
||||||
const settingsCollapsed = !hasFiles || hasResults;
|
const settingsCollapsed = !hasFiles || hasResults;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -86,11 +86,12 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
}],
|
}],
|
||||||
executeButton: {
|
executeButton: {
|
||||||
text: t("sanitize.submit", "Sanitize PDF"),
|
text: t("sanitize.submit", "Sanitize PDF"),
|
||||||
|
isVisible: !hasResults,
|
||||||
loadingText: t("loading"),
|
loadingText: t("loading"),
|
||||||
onClick: handleSanitize,
|
onClick: handleSanitize,
|
||||||
disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled
|
disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled
|
||||||
},
|
},
|
||||||
results: {
|
review: {
|
||||||
isVisible: hasResults,
|
isVisible: hasResults,
|
||||||
operation: sanitizeOperation,
|
operation: sanitizeOperation,
|
||||||
title: t("sanitize.sanitizationResults", "Sanitization Results"),
|
title: t("sanitize.sanitizationResults", "Sanitization Results"),
|
||||||
|
@ -90,7 +90,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
isVisible: !hasResults,
|
isVisible: !hasResults,
|
||||||
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled
|
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled
|
||||||
},
|
},
|
||||||
results: {
|
review: {
|
||||||
isVisible: hasResults,
|
isVisible: hasResults,
|
||||||
operation: splitOperation,
|
operation: splitOperation,
|
||||||
title: "Split Results",
|
title: "Split Results",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user