diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 333f6ca8b..e9a536532 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1318,6 +1318,25 @@ "header": "Auto Rename PDF", "submit": "Auto Rename" }, + "autoRename": { + "title": "Auto Rename PDF File", + "description": "This tool will automatically rename PDF files based on their content. It analyses the document to find the most suitable title from the text.", + "submit": "Auto Rename", + "settings": { + "title": "Auto-Rename Settings", + "useFirstTextAsFallback": "Use first text as fallback", + "useFirstTextAsFallbackDesc": "If no suitable title is found, use the first text in the document" + }, + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst auto-renaming the PDF." + }, + "results": { + "title": "Auto-Rename Results" + } + }, "adjust-contrast": { "tags": "color-correction,tune,modify,enhance,colour-correction" }, diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 0638d78d3..f6db6a843 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1045,6 +1045,25 @@ "header": "Auto Rename PDF", "submit": "Auto Rename" }, + "autoRename": { + "title": "Auto Rename PDF File", + "description": "This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text.", + "submit": "Auto Rename", + "settings": { + "title": "Auto-Rename Settings", + "useFirstTextAsFallback": "Use first text as fallback", + "useFirstTextAsFallbackDesc": "If no suitable title is found, use the first text in the document" + }, + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while auto-renaming the PDF." + }, + "results": { + "title": "Auto-Rename Results" + } + }, "adjust-contrast": { "tags": "color-correction,tune,modify,enhance" }, diff --git a/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx b/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx new file mode 100644 index 000000000..a069879d2 --- /dev/null +++ b/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { AutoRenameParameters } from '../../../hooks/tools/autoRename/useAutoRenameParameters'; + +interface AutoRenameSettingsProps { + parameters: AutoRenameParameters; + onParameterChange: (parameter: K, value: AutoRenameParameters[K]) => void; + disabled?: boolean; +} + +const AutoRenameSettings: React.FC = ({ + parameters, + onParameterChange, // Used for parameter changes + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('autoRename.description', 'This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text.')} +

+
+ ); +}; + +export default AutoRenameSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 359cf6d35..412ab118a 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -14,6 +14,7 @@ import Repair from '../tools/Repair'; import SingleLargePage from '../tools/SingleLargePage'; import UnlockPdfForms from '../tools/UnlockPdfForms'; import RemoveCertificateSign from '../tools/RemoveCertificateSign'; +import AutoRename from '../tools/AutoRename'; @@ -356,11 +357,13 @@ export function useFlatToolRegistry(): ToolRegistry { "auto-rename-pdf-file": { icon: match_word, name: t("home.auto-rename.title", "Auto Rename PDF File"), - component: null, + component: AutoRename, view: "format", description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.AUTOMATION + subcategory: SubcategoryId.AUTOMATION, + maxFiles: -1, + endpoints: ["auto-rename"] }, "auto-split-pages": { icon: split_scene_right, diff --git a/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts b/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts new file mode 100644 index 000000000..ded3da8d5 --- /dev/null +++ b/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { AutoRenameParameters } from './useAutoRenameParameters'; + +export const useAutoRenameOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: AutoRenameParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + formData.append("useFirstTextAsFallback", parameters.useFirstTextAsFallback.toString()); + return formData; + }; + + return useToolOperation({ + operationType: 'autoRename', + endpoint: '/api/v1/misc/auto-rename', + buildFormData, + filePrefix: '', // Not used when preserveBackendFilename is true + multiFileEndpoint: false, + preserveBackendFilename: true, // Use filename from backend response headers + getErrorMessage: createStandardErrorHandler(t('autoRename.error.failed', 'An error occurred while auto-renaming the PDF.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts b/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts new file mode 100644 index 000000000..ede570c33 --- /dev/null +++ b/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts @@ -0,0 +1,19 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface AutoRenameParameters extends BaseParameters { + useFirstTextAsFallback: boolean; +} + +export const defaultParameters: AutoRenameParameters = { + useFirstTextAsFallback: false, +}; + +export type AutoRenameParametersHook = BaseParametersHook; + +export const useAutoRenameParameters = (): AutoRenameParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'auto-rename', + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/shared/useToolApiCalls.ts b/frontend/src/hooks/tools/shared/useToolApiCalls.ts index ec0f398aa..67e3e6c15 100644 --- a/frontend/src/hooks/tools/shared/useToolApiCalls.ts +++ b/frontend/src/hooks/tools/shared/useToolApiCalls.ts @@ -8,6 +8,7 @@ export interface ApiCallsConfig { buildFormData: (params: TParams, file: File) => FormData; filePrefix: string; responseHandler?: ResponseHandler; + preserveBackendFilename?: boolean; } export const useToolApiCalls = () => { @@ -46,7 +47,8 @@ export const useToolApiCalls = () => { response.data, [file], config.filePrefix, - config.responseHandler + config.responseHandler, + config.preserveBackendFilename ? response.headers : undefined ); processedFiles.push(...responseFiles); diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index 623b93d84..b2e3479e4 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -49,6 +49,13 @@ export interface ToolOperationConfig { */ multiFileEndpoint?: boolean; + /** + * Whether to preserve the filename provided by the backend in response headers. + * When true, ignores filePrefix and uses the filename from Content-Disposition header. + * Useful for tools like auto-rename where the backend determines the final filename. + */ + preserveBackendFilename?: boolean; + /** How to handle API responses (e.g., ZIP extraction, single file response) */ responseHandler?: ResponseHandler; @@ -172,7 +179,8 @@ export const useToolOperation = ( endpoint: config.endpoint, buildFormData: config.buildFormData as (params: TParams, file: File) => FormData, filePrefix: config.filePrefix, - responseHandler: config.responseHandler + responseHandler: config.responseHandler, + preserveBackendFilename: config.preserveBackendFilename }; processedFiles = await processFiles( params, diff --git a/frontend/src/tools/AutoRename.tsx b/frontend/src/tools/AutoRename.tsx new file mode 100644 index 000000000..473b672df --- /dev/null +++ b/frontend/src/tools/AutoRename.tsx @@ -0,0 +1,80 @@ +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import { createToolFlow } from "../components/tools/shared/createToolFlow"; + +import { useAutoRenameParameters } from "../hooks/tools/autoRename/useAutoRenameParameters"; +import { useAutoRenameOperation } from "../hooks/tools/autoRename/useAutoRenameOperation"; +import { BaseToolProps } from "../types/tool"; + +const AutoRename = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const autoRenameParams = useAutoRenameParameters(); + const autoRenameOperation = useAutoRenameOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(autoRenameParams.getEndpointName()); + + useEffect(() => { + autoRenameOperation.resetResults(); + onPreviewFile?.(null); + }, [autoRenameParams.parameters]); + + const handleAutoRename = async () => { + try { + await autoRenameOperation.executeOperation(autoRenameParams.parameters, selectedFiles); + if (autoRenameOperation.files && onComplete) { + onComplete(autoRenameOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("autoRename.error.failed", "Auto-rename operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "auto-rename-pdf-file"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + autoRenameOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("auto-rename-pdf-file"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = autoRenameOperation.files.length > 0 || autoRenameOperation.downloadUrl !== null; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + placeholder: t("autoRename.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [], + executeButton: { + text: t("autoRename.submit", "Auto-Rename"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleAutoRename, + disabled: !autoRenameParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: autoRenameOperation, + title: t("autoRename.results.title", "Auto-Rename Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default AutoRename; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index d9dde75b7..3d5729bf8 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -22,7 +22,8 @@ export type ModeType = | 'single-large-page' | 'repair' | 'unlockPdfForms' - | 'removeCertificateSign'; + | 'removeCertificateSign' + | 'auto-rename-pdf-file'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; diff --git a/frontend/src/utils/fileResponseUtils.ts b/frontend/src/utils/fileResponseUtils.ts index 6e4422099..c4cb660e6 100644 --- a/frontend/src/utils/fileResponseUtils.ts +++ b/frontend/src/utils/fileResponseUtils.ts @@ -10,7 +10,15 @@ export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => { const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (match && match[1]) { - return match[1].replace(/['"]/g, ''); + const filename = match[1].replace(/['"]/g, ''); + // Decode URL-encoded characters (e.g., %20 for spaces) + try { + return decodeURIComponent(filename); + } catch (error) { + // If decoding fails, return the original filename + console.warn('Failed to decode filename:', filename, error); + return filename; + } } return null; }; diff --git a/frontend/src/utils/toolResponseProcessor.ts b/frontend/src/utils/toolResponseProcessor.ts index fe2f11242..683f8cd79 100644 --- a/frontend/src/utils/toolResponseProcessor.ts +++ b/frontend/src/utils/toolResponseProcessor.ts @@ -1,23 +1,40 @@ // Note: This utility should be used with useToolResources for ZIP operations +import { getFilenameFromHeaders } from './fileResponseUtils'; export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise | File[]; /** * Processes a blob response into File(s). * - If a tool-specific responseHandler is provided, it is used. + * - If responseHeaders provided and contains Content-Disposition, uses that filename. * - Otherwise, create a single file using the filePrefix + original name. */ export async function processResponse( blob: Blob, originalFiles: File[], filePrefix: string, - responseHandler?: ResponseHandler + responseHandler?: ResponseHandler, + responseHeaders?: Record ): Promise { if (responseHandler) { const out = await responseHandler(blob, originalFiles); return Array.isArray(out) ? out : [out as unknown as File]; } + // Check if we should use the backend-provided filename from headers + // Only when responseHeaders are explicitly provided (indicating the operation requested this) + if (responseHeaders) { + const contentDisposition = responseHeaders['content-disposition']; + const backendFilename = getFilenameFromHeaders(contentDisposition); + if (backendFilename) { + const type = blob.type || responseHeaders['content-type'] || 'application/octet-stream'; + return [new File([blob], backendFilename, { type })]; + } + // If preserveBackendFilename was requested but no Content-Disposition header found, + // fall back to default behavior (this handles cases where backend doesn't set the header) + } + + // Default behavior: use filePrefix + original name const original = originalFiles[0]?.name ?? 'result.pdf'; const name = `${filePrefix}${original}`; const type = blob.type || 'application/octet-stream';