mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
V2 results flow (#4196)
Better tool flow for reusability Pinning Styling of tool flow consumption of files after tooling --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
@@ -1,16 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Box, Button, Stack, Text } from "@mantine/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
@@ -39,154 +33,84 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
useEffect(() => {
|
||||
addPasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [addPasswordParams.parameters, selectedFiles]);
|
||||
}, [addPasswordParams.parameters]);
|
||||
|
||||
const handleAddPassword = async () => {
|
||||
try {
|
||||
await addPasswordOperation.executeOperation(
|
||||
addPasswordParams.fullParameters,
|
||||
selectedFiles
|
||||
);
|
||||
await addPasswordOperation.executeOperation(addPasswordParams.fullParameters, selectedFiles);
|
||||
if (addPasswordOperation.files && onComplete) {
|
||||
onComplete(addPasswordOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t('addPassword.error.failed', 'Add password operation failed'));
|
||||
onError(error instanceof Error ? error.message : t("addPassword.error.failed", "Add password operation failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'addPassword');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "addPassword");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
addPasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('addPassword');
|
||||
setCurrentMode("addPassword");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = addPasswordOperation.files.length > 0 || addPasswordOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const passwordsCollapsed = hasResults;
|
||||
const passwordsCollapsed = !hasFiles || hasResults;
|
||||
const permissionsCollapsed = collapsedPermissions || hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
addPasswordOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: addPasswordOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[addPasswordOperation.files, addPasswordOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('files.title', 'Files')}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('files.selected.single', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('files.selected.multiple', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t('files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Passwords & Encryption Step */}
|
||||
<ToolStep
|
||||
title={t('addPassword.title', 'Passwords & Encryption')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={passwordsCollapsed}
|
||||
isCompleted={passwordsCollapsed}
|
||||
onCollapsedClick={hasResults ? handleSettingsReset : undefined}
|
||||
completedMessage={passwordsCollapsed ? t('addPassword.passwords.completed', 'Passwords configured') : undefined}
|
||||
tooltip={addPasswordTips}
|
||||
>
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("addPassword.passwords.stepTitle", "Passwords & Encryption"),
|
||||
isCollapsed: passwordsCollapsed,
|
||||
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
|
||||
tooltip: addPasswordTips,
|
||||
content: (
|
||||
<AddPasswordSettings
|
||||
parameters={addPasswordParams.parameters}
|
||||
onParameterChange={addPasswordParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Permissions Step */}
|
||||
<ToolStep
|
||||
title={t('changePermissions.title', 'Document Permissions')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={permissionsCollapsed}
|
||||
isCompleted={permissionsCollapsed}
|
||||
onCollapsedClick={hasResults ? handleSettingsReset : () => setCollapsedPermissions(!collapsedPermissions)}
|
||||
tooltip={addPasswordPermissionsTips}
|
||||
>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("changePermissions.title", "Document Permissions"),
|
||||
isCollapsed: permissionsCollapsed,
|
||||
onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedPermissions(!collapsedPermissions),
|
||||
content: (
|
||||
<ChangePermissionsSettings
|
||||
parameters={addPasswordParams.permissions.parameters}
|
||||
onParameterChange={addPasswordParams.permissions.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
<Box mt="md">
|
||||
<OperationButton
|
||||
onClick={handleAddPassword}
|
||||
isLoading={addPasswordOperation.isLoading}
|
||||
disabled={!addPasswordParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t('loading')}
|
||||
submitText={t('addPassword.submit', 'Encrypt')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title={t('results.title', 'Results')}
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{addPasswordOperation.status && (
|
||||
<Text size="sm" c="dimmed">{addPasswordOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={addPasswordOperation.errorMessage}
|
||||
onClose={addPasswordOperation.clearError}
|
||||
/>
|
||||
|
||||
{addPasswordOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={addPasswordOperation.downloadUrl}
|
||||
download={addPasswordOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={addPasswordOperation.isGeneratingThumbnails}
|
||||
title={t('addPassword.results.title', 'Encrypted PDFs')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("addPassword.submit", "Encrypt"),
|
||||
isVisible: !hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleAddPassword,
|
||||
disabled: !addPasswordParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: addPasswordOperation,
|
||||
title: t("addPassword.results.title", "Encrypted PDFs"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default AddPassword;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
|
||||
@@ -34,137 +28,73 @@ const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps
|
||||
useEffect(() => {
|
||||
changePermissionsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [changePermissionsParams.parameters, selectedFiles]);
|
||||
}, [changePermissionsParams.parameters]);
|
||||
|
||||
const handleChangePermissions = async () => {
|
||||
try {
|
||||
await changePermissionsOperation.executeOperation(
|
||||
changePermissionsParams.parameters,
|
||||
selectedFiles
|
||||
);
|
||||
await changePermissionsOperation.executeOperation(changePermissionsParams.parameters, selectedFiles);
|
||||
if (changePermissionsOperation.files && onComplete) {
|
||||
onComplete(changePermissionsOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t('changePermissions.error.failed', 'Change permissions operation failed'));
|
||||
onError(
|
||||
error instanceof Error ? error.message : t("changePermissions.error.failed", "Change permissions operation failed")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'changePermissions');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "changePermissions");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
changePermissionsOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('changePermissions');
|
||||
setCurrentMode("changePermissions");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = changePermissionsOperation.files.length > 0 || changePermissionsOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
changePermissionsOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: changePermissionsOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[changePermissionsOperation.files, changePermissionsOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('files.title', 'Files')}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('files.selected.single', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('files.selected.multiple', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t('files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("changePermissions.title", "Document Permissions"),
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
tooltip: changePermissionsTips,
|
||||
content: (
|
||||
<ChangePermissionsSettings
|
||||
parameters={changePermissionsParams.parameters}
|
||||
onParameterChange={changePermissionsParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Permissions Step */}
|
||||
<ToolStep
|
||||
title={t('changePermissions.title', 'Document Permissions')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? t('changePermissions.completed', 'Permissions changed') : undefined}
|
||||
tooltip={changePermissionsTips}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<ChangePermissionsSettings
|
||||
parameters={changePermissionsParams.parameters}
|
||||
onParameterChange={changePermissionsParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
<OperationButton
|
||||
onClick={handleChangePermissions}
|
||||
isLoading={changePermissionsOperation.isLoading}
|
||||
disabled={!changePermissionsParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t('loading')}
|
||||
submitText={t('changePermissions.submit', 'Change Permissions')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title={t('results.title', 'Results')}
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{changePermissionsOperation.status && (
|
||||
<Text size="sm" c="dimmed">{changePermissionsOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={changePermissionsOperation.errorMessage}
|
||||
onClose={changePermissionsOperation.clearError}
|
||||
/>
|
||||
|
||||
{changePermissionsOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={changePermissionsOperation.downloadUrl}
|
||||
download={changePermissionsOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={changePermissionsOperation.isGeneratingThumbnails}
|
||||
title={t('changePermissions.results.title', 'Modified PDFs')}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("changePermissions.submit", "Change Permissions"),
|
||||
isVisible: !hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleChangePermissions,
|
||||
disabled: !changePermissionsParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: changePermissionsOperation,
|
||||
title: t("changePermissions.results.title", "Modified PDFs"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default ChangePermissions;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
|
||||
@@ -34,138 +28,71 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
useEffect(() => {
|
||||
compressOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [compressParams.parameters, selectedFiles]);
|
||||
}, [compressParams.parameters]);
|
||||
|
||||
const handleCompress = async () => {
|
||||
try {
|
||||
await compressOperation.executeOperation(
|
||||
compressParams.parameters,
|
||||
selectedFiles
|
||||
);
|
||||
await compressOperation.executeOperation(compressParams.parameters, selectedFiles);
|
||||
if (compressOperation.files && onComplete) {
|
||||
onComplete(compressOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : 'Compress operation failed');
|
||||
onError(error instanceof Error ? error.message : "Compress operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'compress');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "compress");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
compressOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('compress');
|
||||
setCurrentMode("compress");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
compressOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: compressOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[compressOperation.files, compressOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? `Selected: ${selectedFiles[0].name}`
|
||||
: `Selected: ${selectedFiles.length} files`
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder="Select a PDF file in the main view to get started"
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles && !hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
tooltip: compressTips,
|
||||
content: (
|
||||
<CompressSettings
|
||||
parameters={compressParams.parameters}
|
||||
onParameterChange={compressParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title="Settings"
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? "Compression completed" : undefined}
|
||||
tooltip={compressTips}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<CompressSettings
|
||||
parameters={compressParams.parameters}
|
||||
onParameterChange={compressParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
<OperationButton
|
||||
onClick={handleCompress}
|
||||
isLoading={compressOperation.isLoading}
|
||||
disabled={!compressParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("loading")}
|
||||
submitText="Compress and Review"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{compressOperation.status && (
|
||||
<Text size="sm" c="dimmed">{compressOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={compressOperation.errorMessage}
|
||||
onClose={compressOperation.clearError}
|
||||
/>
|
||||
|
||||
{compressOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={compressOperation.downloadUrl}
|
||||
download={compressOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={compressOperation.isGeneratingThumbnails}
|
||||
title="Compression Results"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("compress.submit", "Compress"),
|
||||
isVisible: !hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleCompress,
|
||||
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: compressOperation,
|
||||
title: t("compress.title", "Compression Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default Compress;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||
|
||||
@@ -27,15 +21,13 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const convertParams = useConvertParameters();
|
||||
const convertOperation = useConvertOperation();
|
||||
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||
convertParams.getEndpointName()
|
||||
);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(convertParams.getEndpointName());
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -79,133 +71,67 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const handleConvert = async () => {
|
||||
try {
|
||||
await convertOperation.executeOperation(
|
||||
convertParams.parameters,
|
||||
selectedFiles
|
||||
);
|
||||
await convertOperation.executeOperation(convertParams.parameters, selectedFiles);
|
||||
if (convertOperation.files && onComplete) {
|
||||
onComplete(convertOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : 'Convert operation failed');
|
||||
onError(error instanceof Error ? error.message : "Convert operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'convert');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "convert");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
convertOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('convert');
|
||||
setCurrentMode("convert");
|
||||
};
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
convertOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: convertOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[convertOperation.files, convertOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" p="sm">
|
||||
<ToolStep
|
||||
title={t("convert.files", "Files")}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('filesSelected', '{{count}} files selected', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: filesCollapsed,
|
||||
placeholder: t("convert.selectFilesPlaceholder", "Select files in the main view to get started"),
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("convert.settings", "Settings"),
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
content: (
|
||||
<ConvertSettings
|
||||
parameters={convertParams.parameters}
|
||||
onParameterChange={convertParams.updateParameter}
|
||||
getAvailableToExtensions={convertParams.getAvailableToExtensions}
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t("convert.selectFilesPlaceholder", "Select files in the main view to get started")}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
<ToolStep
|
||||
title={t("convert.settings", "Settings")}
|
||||
isVisible={true}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? t("convert.conversionCompleted", "Conversion completed") : undefined}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<ConvertSettings
|
||||
parameters={convertParams.parameters}
|
||||
onParameterChange={convertParams.updateParameter}
|
||||
getAvailableToExtensions={convertParams.getAvailableToExtensions}
|
||||
selectedFiles={selectedFiles}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
{hasFiles && convertParams.parameters.fromExtension && convertParams.parameters.toExtension && (
|
||||
<OperationButton
|
||||
onClick={handleConvert}
|
||||
isLoading={convertOperation.isLoading}
|
||||
disabled={!convertParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("convert.converting", "Converting...")}
|
||||
submitText={t("convert.convertFiles", "Convert Files")}
|
||||
data-testid="convert-button"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
<ToolStep
|
||||
title={t("convert.results", "Results")}
|
||||
isVisible={hasResults}
|
||||
data-testid="conversion-results"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{convertOperation.status && (
|
||||
<Text size="sm" c="dimmed">{convertOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={convertOperation.errorMessage}
|
||||
onClose={convertOperation.clearError}
|
||||
/>
|
||||
|
||||
{convertOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={convertOperation.downloadUrl}
|
||||
download={convertOperation.downloadFilename || t("convert.defaultFilename", "converted_file")}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
data-testid="download-button"
|
||||
>
|
||||
{t("convert.downloadConverted", "Download Converted File")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={convertOperation.isGeneratingThumbnails}
|
||||
title={t("convert.conversionResults", "Conversion Results")}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
</div>
|
||||
);
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("convert.convertFiles", "Convert Files"),
|
||||
loadingText: t("convert.converting", "Converting..."),
|
||||
onClick: handleConvert,
|
||||
isVisible: !hasResults,
|
||||
disabled: !convertParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
testId: "convert-button",
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: convertOperation,
|
||||
title: t("convert.conversionResults", "Conversion Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
testId: "conversion-results",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default Convert;
|
||||
|
||||
@@ -15,12 +15,7 @@ export interface MergePdfPanelProps {
|
||||
updateParams: (newParams: Partial<MergePdfPanelProps["params"]>) => void;
|
||||
}
|
||||
|
||||
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
files,
|
||||
setDownloadUrl,
|
||||
params,
|
||||
updateParams,
|
||||
}) => {
|
||||
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl, params, updateParams }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||
@@ -51,7 +46,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
||||
const actualFile = new File([blob], storedFile.name, {
|
||||
type: storedFile.type,
|
||||
lastModified: storedFile.lastModified
|
||||
lastModified: storedFile.lastModified,
|
||||
});
|
||||
formData.append("fileInput", actualFile);
|
||||
}
|
||||
@@ -83,9 +78,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (index: number) => {
|
||||
setSelectedFiles((prev) =>
|
||||
prev.map((selected, i) => (i === index ? !selected : selected))
|
||||
);
|
||||
setSelectedFiles((prev) => prev.map((selected, i) => (i === index ? !selected : selected)));
|
||||
};
|
||||
|
||||
const selectedCount = selectedFiles.filter(Boolean).length;
|
||||
@@ -96,7 +89,9 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="md" />
|
||||
<Text size="sm" c="dimmed">{t("loading", "Loading...")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("loading", "Loading...")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -112,55 +107,42 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text fw={500} size="lg">{t("merge.header")}</Text>
|
||||
<Stack gap={4}>
|
||||
{files.map((file, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
<Checkbox
|
||||
checked={selectedFiles[index] || false}
|
||||
onChange={() => handleCheckboxChange(index)}
|
||||
/>
|
||||
<Text size="sm">{file.name}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
{selectedCount < 2 && (
|
||||
<Text size="sm" c="red">
|
||||
{t("multiPdfPrompt")}
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleMerge}
|
||||
loading={isLoading}
|
||||
disabled={selectedCount < 2 || isLoading}
|
||||
mt="md"
|
||||
>
|
||||
{t("merge.submit")}
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<Alert color="red" mt="sm">
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
{downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download="merged.pdf"
|
||||
color="green"
|
||||
variant="light"
|
||||
mt="md"
|
||||
>
|
||||
{t("downloadPdf")}
|
||||
</Button>
|
||||
)}
|
||||
<Checkbox
|
||||
label={t("merge.removeCertSign")}
|
||||
checked={removeDuplicates}
|
||||
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
|
||||
/>
|
||||
<Stack>
|
||||
<Text fw={500} size="lg">
|
||||
{t("merge.header")}
|
||||
</Text>
|
||||
<Stack gap={4}>
|
||||
{files.map((file, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
<Checkbox checked={selectedFiles[index] || false} onChange={() => handleCheckboxChange(index)} />
|
||||
<Text size="sm">{file.name}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
{selectedCount < 2 && (
|
||||
<Text size="sm" c="red">
|
||||
{t("multiPdfPrompt")}
|
||||
</Text>
|
||||
)}
|
||||
<Button onClick={handleMerge} loading={isLoading} disabled={selectedCount < 2 || isLoading} mt="md">
|
||||
{t("merge.submit")}
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<Alert color="red" mt="sm">
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
{downloadUrl && (
|
||||
<Button component="a" href={downloadUrl} download="merged.pdf" color="green" variant="light" mt="md">
|
||||
{t("downloadPdf")}
|
||||
</Button>
|
||||
)}
|
||||
<Checkbox
|
||||
label={t("merge.removeCertSign")}
|
||||
checked={removeDuplicates}
|
||||
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Stack, Text, Box } from "@mantine/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
||||
import AdvancedOCRSettings from "../components/tools/ocr/AdvancedOCRSettings";
|
||||
@@ -30,7 +24,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const ocrTips = useOCRTips();
|
||||
|
||||
// Step expansion state management
|
||||
const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files');
|
||||
const [expandedStep, setExpandedStep] = useState<"files" | "settings" | "advanced" | null>("files");
|
||||
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("ocr-pdf");
|
||||
|
||||
@@ -41,11 +35,11 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
useEffect(() => {
|
||||
ocrOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [ocrParams.parameters, selectedFiles]);
|
||||
}, [ocrParams.parameters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFiles.length > 0 && expandedStep === 'files') {
|
||||
setExpandedStep('settings');
|
||||
if (selectedFiles.length > 0 && expandedStep === "files") {
|
||||
setExpandedStep("settings");
|
||||
}
|
||||
}, [selectedFiles.length, expandedStep]);
|
||||
|
||||
@@ -58,161 +52,88 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const handleOCR = async () => {
|
||||
try {
|
||||
await ocrOperation.executeOperation(
|
||||
ocrParams.parameters,
|
||||
selectedFiles
|
||||
);
|
||||
await ocrOperation.executeOperation(ocrParams.parameters, selectedFiles);
|
||||
if (ocrOperation.files && onComplete) {
|
||||
onComplete(ocrOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : 'OCR operation failed');
|
||||
onError(error instanceof Error ? error.message : "OCR operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'ocr');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "ocr");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
ocrOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode("ocr");
|
||||
};
|
||||
|
||||
// Step visibility and collapse logic
|
||||
const filesVisible = true;
|
||||
const settingsVisible = true;
|
||||
const resultsVisible = hasResults;
|
||||
const settingsCollapsed = expandedStep !== "settings";
|
||||
|
||||
const filesCollapsed = expandedStep !== 'files';
|
||||
const settingsCollapsed = expandedStep !== 'settings';
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
ocrOperation.files?.map((file: File, index: number) => ({
|
||||
file,
|
||||
thumbnail: ocrOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[ocrOperation.files, ocrOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
isVisible={filesVisible}
|
||||
isCollapsed={hasFiles ? filesCollapsed : false}
|
||||
isCompleted={hasFiles}
|
||||
onCollapsedClick={undefined}
|
||||
completedMessage={hasFiles && filesCollapsed ?
|
||||
selectedFiles.length === 1
|
||||
? `Selected: ${selectedFiles[0].name}`
|
||||
: `Selected: ${selectedFiles.length} files`
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder="Select a PDF file in the main view to get started"
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: hasFiles || hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: !hasFiles || settingsCollapsed,
|
||||
onCollapsedClick: hasResults
|
||||
? handleSettingsReset
|
||||
: () => {
|
||||
if (!hasFiles) return; // Only allow if files are selected
|
||||
setExpandedStep(expandedStep === "settings" ? null : "settings");
|
||||
},
|
||||
tooltip: ocrTips,
|
||||
content: (
|
||||
<OCRSettings
|
||||
parameters={ocrParams.parameters}
|
||||
onParameterChange={ocrParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title="Settings"
|
||||
isVisible={settingsVisible}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={hasFiles && hasValidSettings}
|
||||
onCollapsedClick={() => {
|
||||
if (!hasFiles) return; // Only allow if files are selected
|
||||
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
|
||||
}}
|
||||
completedMessage={hasFiles && hasValidSettings && settingsCollapsed ? "Basic settings configured" : undefined}
|
||||
tooltip={ocrTips}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<OCRSettings
|
||||
parameters={ocrParams.parameters}
|
||||
onParameterChange={ocrParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Advanced Step */}
|
||||
<ToolStep
|
||||
title="Advanced"
|
||||
isVisible={true}
|
||||
isCollapsed={expandedStep !== 'advanced'}
|
||||
isCompleted={hasFiles && hasResults}
|
||||
onCollapsedClick={() => {
|
||||
if (!hasFiles) return; // Only allow if files are selected
|
||||
setExpandedStep(expandedStep === 'advanced' ? null : 'advanced');
|
||||
}}
|
||||
completedMessage={hasFiles && hasResults && expandedStep !== 'advanced' ? "OCR processing completed" : undefined}
|
||||
>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Advanced",
|
||||
isCollapsed: expandedStep !== "advanced",
|
||||
onCollapsedClick: hasResults
|
||||
? handleSettingsReset
|
||||
: () => {
|
||||
if (!hasFiles) return; // Only allow if files are selected
|
||||
setExpandedStep(expandedStep === "advanced" ? null : "advanced");
|
||||
},
|
||||
content: (
|
||||
<AdvancedOCRSettings
|
||||
advancedOptions={ocrParams.parameters.additionalOptions}
|
||||
ocrRenderType={ocrParams.parameters.ocrRenderType}
|
||||
onParameterChange={ocrParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Process Button - Available after all configuration */}
|
||||
{hasValidSettings && !hasResults && (
|
||||
<Box mt="md">
|
||||
<OperationButton
|
||||
onClick={handleOCR}
|
||||
isLoading={ocrOperation.isLoading}
|
||||
disabled={!ocrParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("loading")}
|
||||
submitText="Process OCR and Review"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={resultsVisible}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{ocrOperation.status && (
|
||||
<Text size="sm" c="dimmed">{ocrOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={ocrOperation.errorMessage}
|
||||
onClose={ocrOperation.clearError}
|
||||
/>
|
||||
|
||||
{ocrOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={ocrOperation.downloadUrl}
|
||||
download={ocrOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={ocrOperation.isGeneratingThumbnails}
|
||||
title="OCR Results"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("ocr.operation.submit", "Process OCR and Review"),
|
||||
loadingText: t("loading"),
|
||||
onClick: handleOCR,
|
||||
isVisible: hasValidSettings && !hasResults,
|
||||
disabled: !ocrParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: ocrOperation,
|
||||
title: t("ocr.results.title", "OCR Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default OCR;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||
|
||||
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
||||
@@ -27,27 +21,22 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const sanitizeOperation = useSanitizeOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||
sanitizeParams.getEndpointName()
|
||||
);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(sanitizeParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
sanitizeOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [sanitizeParams.parameters, selectedFiles]);
|
||||
}, [sanitizeParams.parameters]);
|
||||
|
||||
const handleSanitize = async () => {
|
||||
try {
|
||||
await sanitizeOperation.executeOperation(
|
||||
sanitizeParams.parameters,
|
||||
selectedFiles,
|
||||
);
|
||||
await sanitizeOperation.executeOperation(sanitizeParams.parameters, selectedFiles);
|
||||
if (sanitizeOperation.files && onComplete) {
|
||||
onComplete(sanitizeOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed'));
|
||||
onError(error instanceof Error ? error.message : t("sanitize.error.generic", "Sanitization failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -55,112 +44,54 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const handleSettingsReset = () => {
|
||||
sanitizeOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode("sanitize");
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'sanitize');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "sanitize");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = sanitizeOperation.files.length > 0;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
const filesCollapsed = hasFiles || hasResults;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title={t('sanitize.steps.files', 'Files')}
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
|
||||
: t('filesSelected', 'Selected: {{count}} files', { count: selectedFiles.length })
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder={t('sanitize.files.placeholder', 'Select a PDF file in the main view to get started')}
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: filesCollapsed,
|
||||
placeholder: t("sanitize.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("sanitize.steps.settings", "Settings"),
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
|
||||
content: (
|
||||
<SanitizeSettings
|
||||
parameters={sanitizeParams.parameters}
|
||||
onParameterChange={sanitizeParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title={t('sanitize.steps.settings', 'Settings')}
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? t('sanitize.completed', 'Sanitization completed') : undefined}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<SanitizeSettings
|
||||
parameters={sanitizeParams.parameters}
|
||||
onParameterChange={sanitizeParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
<OperationButton
|
||||
onClick={handleSanitize}
|
||||
isLoading={sanitizeOperation.isLoading}
|
||||
disabled={!sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("loading")}
|
||||
submitText={t("sanitize.submit", "Sanitize PDF")}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title={t('sanitize.steps.results', 'Results')}
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{sanitizeOperation.status && (
|
||||
<Text size="sm" c="dimmed">{sanitizeOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={sanitizeOperation.errorMessage}
|
||||
onClose={sanitizeOperation.clearError}
|
||||
/>
|
||||
|
||||
{sanitizeOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={sanitizeOperation.downloadUrl}
|
||||
download={sanitizeOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{sanitizeOperation.files.length === 1
|
||||
? t("download", "Download")
|
||||
: t("downloadZip", "Download ZIP")
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={sanitizeOperation.files.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: sanitizeOperation.thumbnails[index]
|
||||
}))}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={sanitizeOperation.isGeneratingThumbnails}
|
||||
title={t("sanitize.sanitizationResults", "Sanitization Results")}
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("sanitize.submit", "Sanitize PDF"),
|
||||
isVisible: !hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: handleSanitize,
|
||||
disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: sanitizeOperation,
|
||||
title: t("sanitize.sanitizationResults", "Sanitization Results"),
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default Sanitize;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||
|
||||
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
||||
@@ -26,141 +20,77 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const splitOperation = useSplitOperation();
|
||||
|
||||
// Endpoint validation
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||
splitParams.getEndpointName()
|
||||
);
|
||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(splitParams.getEndpointName());
|
||||
|
||||
useEffect(() => {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}, [splitParams.parameters, selectedFiles]);
|
||||
}, [splitParams.parameters]);
|
||||
|
||||
const handleSplit = async () => {
|
||||
try {
|
||||
await splitOperation.executeOperation(
|
||||
splitParams.parameters,
|
||||
selectedFiles
|
||||
);
|
||||
await splitOperation.executeOperation(splitParams.parameters, selectedFiles);
|
||||
if (splitOperation.files && onComplete) {
|
||||
onComplete(splitOperation.files);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error instanceof Error ? error.message : 'Split operation failed');
|
||||
onError(error instanceof Error ? error.message : "Split operation failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'split');
|
||||
setCurrentMode('viewer');
|
||||
sessionStorage.setItem("previousMode", "split");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('split');
|
||||
setCurrentMode("split");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = splitOperation.downloadUrl !== null;
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
const filesCollapsed = hasFiles || hasResults;
|
||||
const settingsCollapsed = !hasFiles || hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
splitOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: splitOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[splitOperation.files, splitOperation.thumbnails]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ? `Selected: ${selectedFiles[0]?.name}` : undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder="Select a PDF file in the main view to get started"
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles,
|
||||
isCollapsed: filesCollapsed,
|
||||
placeholder: "Select a PDF file in the main view to get started",
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: settingsCollapsed,
|
||||
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
|
||||
content: (
|
||||
<SplitSettings
|
||||
parameters={splitParams.parameters}
|
||||
onParameterChange={splitParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title="Settings"
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? "Split completed" : undefined}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<SplitSettings
|
||||
parameters={splitParams.parameters}
|
||||
onParameterChange={splitParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
{splitParams.parameters.mode && (
|
||||
<OperationButton
|
||||
onClick={handleSplit}
|
||||
isLoading={splitOperation.isLoading}
|
||||
disabled={!splitParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("loading")}
|
||||
submitText={t("split.submit", "Split PDF")}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{splitOperation.status && (
|
||||
<Text size="sm" c="dimmed">{splitOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={splitOperation.errorMessage}
|
||||
onClose={splitOperation.clearError}
|
||||
/>
|
||||
|
||||
{splitOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={splitOperation.downloadUrl}
|
||||
download="split_output.zip"
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={splitOperation.isGeneratingThumbnails}
|
||||
title="Split Results"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("split.submit", "Split PDF"),
|
||||
loadingText: t("loading"),
|
||||
onClick: handleSplit,
|
||||
isVisible: !hasResults,
|
||||
disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: hasResults,
|
||||
operation: splitOperation,
|
||||
title: "Split Results",
|
||||
onFileClick: handleThumbnailClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default Split;
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { BaseToolProps } from '../types/tool';
|
||||
import React, { useEffect } from "react";
|
||||
import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const SwaggerUI: React.FC<BaseToolProps> = () => {
|
||||
useEffect(() => {
|
||||
// Redirect to Swagger UI
|
||||
window.open('/swagger-ui/5.21.0/index.html', '_blank');
|
||||
window.open("/swagger-ui/5.21.0/index.html", "_blank");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<div style={{ textAlign: "center", padding: "2rem" }}>
|
||||
<p>Opening Swagger UI in a new tab...</p>
|
||||
<p>If it didn't open automatically, <a href="/swagger-ui/5.21.0/index.html" target="_blank" rel="noopener noreferrer">click here</a></p>
|
||||
<p>
|
||||
If it didn't open automatically,{" "}
|
||||
<a href="/swagger-ui/5.21.0/index.html" target="_blank" rel="noopener noreferrer">
|
||||
click here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwaggerUI;
|
||||
export default SwaggerUI;
|
||||
|
||||
Reference in New Issue
Block a user