From 06c6b969925c2dd8ee54df245f63c7988fb526bc Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 6 Nov 2025 11:49:42 +0000 Subject: [PATCH] Fix first batch of `any` types --- .../configSections/AdminGeneralSection.tsx | 2 +- .../tools/automate/AutomationSelection.tsx | 2 +- .../tools/automate/useSavedAutomations.ts | 4 +- .../tools/automate/useSuggestedAutomations.ts | 14 ++- frontend/src/core/hooks/useAdminSettings.ts | 46 +++++--- frontend/src/core/hooks/useToolParameters.ts | 8 +- frontend/src/core/services/errorUtils.ts | 37 ++++-- .../core/services/googleDrivePickerService.ts | 85 +++++++++++--- .../src/core/services/httpErrorHandler.ts | 105 +++++++++++++----- .../src/core/services/pdfWorkerManager.ts | 34 +++--- .../services/signatureDetectionService.ts | 44 ++++++-- .../src/core/services/specialErrorToasts.ts | 7 +- frontend/src/core/types/automation.ts | 18 ++- frontend/src/core/utils/automationExecutor.ts | 100 +++++++++++++---- .../src/core/utils/automationFileProcessor.ts | 55 +++++++-- frontend/src/core/utils/downloadUtils.ts | 2 +- .../src/core/utils/settingsPendingHelper.ts | 47 +++++--- frontend/src/core/utils/toolErrorHandler.ts | 38 ++++--- .../src/core/utils/toolResponseProcessor.ts | 10 +- frontend/src/global.d.ts | 8 +- 20 files changed, 472 insertions(+), 194 deletions(-) diff --git a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx index c17c88f6a..249dc62b2 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx @@ -131,7 +131,7 @@ export default function AdminGeneralSection() { } return { - sectionData: {}, + sectionData: settings, deltaSettings }; } diff --git a/frontend/src/core/components/tools/automate/AutomationSelection.tsx b/frontend/src/core/components/tools/automate/AutomationSelection.tsx index 1b53ff900..949547289 100644 --- a/frontend/src/core/components/tools/automate/AutomationSelection.tsx +++ b/frontend/src/core/components/tools/automate/AutomationSelection.tsx @@ -76,7 +76,7 @@ export default function AutomationSelection({ key={automation.id} title={automation.name} description={automation.description} - badgeIcon={automation.icon} + badgeIcon={automation.iconComponent} operations={automation.operations.map(op => op.operation)} onClick={() => onRun(automation)} showMenu={true} diff --git a/frontend/src/core/hooks/tools/automate/useSavedAutomations.ts b/frontend/src/core/hooks/tools/automate/useSavedAutomations.ts index a4bba4857..7e82a08bf 100644 --- a/frontend/src/core/hooks/tools/automate/useSavedAutomations.ts +++ b/frontend/src/core/hooks/tools/automate/useSavedAutomations.ts @@ -46,7 +46,7 @@ export function useSavedAutomations() { const { automationStorage } = await import('@app/services/automationStorage'); // Map suggested automation icons to MUI icon keys - const getIconKey = (_suggestedIcon: {id: string}): string => { + const getIconKey = (): string => { // Check the automation ID or name to determine the appropriate icon switch (suggestedAutomation.id) { case 'secure-pdf-ingestion': @@ -65,7 +65,7 @@ export function useSavedAutomations() { const savedAutomation = { name: suggestedAutomation.name, description: suggestedAutomation.description, - icon: getIconKey(suggestedAutomation.icon), + icon: suggestedAutomation.icon ?? getIconKey(), operations: suggestedAutomation.operations }; diff --git a/frontend/src/core/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/core/hooks/tools/automate/useSuggestedAutomations.ts index 378380050..fef5f6b03 100644 --- a/frontend/src/core/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/core/hooks/tools/automate/useSuggestedAutomations.ts @@ -13,7 +13,7 @@ const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1. export function useSuggestedAutomations(): SuggestedAutomation[] { const { t } = useTranslation(); - const suggestedAutomations = useMemo(() => { +const suggestedAutomations = useMemo(() => { const now = new Date().toISOString(); return [ { @@ -65,7 +65,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { ], createdAt: now, updatedAt: now, - icon: SecurityIcon, + icon: 'SecurityIcon', + iconComponent: SecurityIcon, }, { id: "email-preparation", @@ -112,7 +113,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { ], createdAt: now, updatedAt: now, - icon: CompressIcon, + icon: 'CompressIcon', + iconComponent: CompressIcon, }, { id: "secure-workflow", @@ -151,7 +153,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { ], createdAt: now, updatedAt: now, - icon: SecurityIcon, + icon: 'SecurityIcon', + iconComponent: SecurityIcon, }, { id: "process-images", @@ -185,7 +188,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { ], createdAt: now, updatedAt: now, - icon: StarIcon, + icon: 'StarIcon', + iconComponent: StarIcon, }, ]; }, [t]); diff --git a/frontend/src/core/hooks/useAdminSettings.ts b/frontend/src/core/hooks/useAdminSettings.ts index 00c3af432..6432fe31d 100644 --- a/frontend/src/core/hooks/useAdminSettings.ts +++ b/frontend/src/core/hooks/useAdminSettings.ts @@ -1,8 +1,12 @@ import { useState } from 'react'; import apiClient from '@app/services/apiClient'; -import { mergePendingSettings, isFieldPending, hasPendingChanges } from '@app/utils/settingsPendingHelper'; +import { mergePendingSettings, isFieldPending, hasPendingChanges, SettingsWithPending } from '@app/utils/settingsPendingHelper'; -interface UseAdminSettingsOptions { +type SettingsRecord = Record; +type CombinedSettings = T & SettingsRecord; +type PendingSettings = SettingsWithPending> & CombinedSettings; + +interface UseAdminSettingsOptions { sectionName: string; /** * Optional transformer to combine data from multiple endpoints. @@ -14,14 +18,14 @@ interface UseAdminSettingsOptions { * Returns an object with sectionData and optionally deltaSettings. */ saveTransformer?: (settings: T) => { - sectionData: any; - deltaSettings?: Record; + sectionData: T; + deltaSettings?: SettingsRecord; }; } -interface UseAdminSettingsReturn { +interface UseAdminSettingsReturn { settings: T; - rawSettings: any; + rawSettings: PendingSettings | null; loading: boolean; saving: boolean; setSettings: (settings: T) => void; @@ -39,14 +43,14 @@ interface UseAdminSettingsReturn { * const { settings, setSettings, saveSettings, isFieldPending } = useAdminSettings({ * sectionName: 'legal' * }); - */ -export function useAdminSettings( +*/ +export function useAdminSettings( options: UseAdminSettingsOptions ): UseAdminSettingsReturn { const { sectionName, fetchTransformer, saveTransformer } = options; const [settings, setSettings] = useState({} as T); - const [rawSettings, setRawSettings] = useState(null); + const [rawSettings, setRawSettings] = useState | null>(null); const [originalSettings, setOriginalSettings] = useState({} as T); // Track original active values const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -55,15 +59,15 @@ export function useAdminSettings( try { setLoading(true); - let rawData: any; + let rawData: PendingSettings; if (fetchTransformer) { // Use custom fetch logic for complex sections - rawData = await fetchTransformer(); + rawData = (await fetchTransformer()) as PendingSettings; } else { // Simple single-endpoint fetch const response = await apiClient.get(`/api/v1/admin/settings/section/${sectionName}`); - rawData = response.data || {}; + rawData = (response.data || {}) as PendingSettings; } console.log(`[useAdminSettings:${sectionName}] Raw response:`, JSON.stringify(rawData, null, 2)); @@ -77,7 +81,7 @@ export function useAdminSettings( console.log(`[useAdminSettings:${sectionName}] Original active settings:`, JSON.stringify(activeOnly, null, 2)); // Merge pending changes into settings for display - const mergedSettings = mergePendingSettings(rawData); + const mergedSettings = mergePendingSettings(rawData) as unknown as CombinedSettings; console.log(`[useAdminSettings:${sectionName}] Merged settings:`, JSON.stringify(mergedSettings, null, 2)); setSettings(mergedSettings as T); @@ -94,7 +98,10 @@ export function useAdminSettings( setSaving(true); // Compute delta: only include fields that changed from original - const delta = computeDelta(originalSettings, settings); + const delta = computeDelta( + originalSettings as SettingsRecord, + settings as SettingsRecord + ); console.log(`[useAdminSettings:${sectionName}] Delta (changed fields):`, JSON.stringify(delta, null, 2)); if (Object.keys(delta).length === 0) { @@ -107,7 +114,10 @@ export function useAdminSettings( const { sectionData, deltaSettings } = saveTransformer(settings); // Save section data (with delta applied) - const sectionDelta = computeDelta(originalSettings, sectionData); + const sectionDelta = computeDelta( + originalSettings as SettingsRecord, + sectionData as unknown as SettingsRecord + ); if (Object.keys(sectionDelta).length > 0) { await apiClient.put(`/api/v1/admin/settings/section/${sectionName}`, sectionDelta); } @@ -148,8 +158,8 @@ export function useAdminSettings( * Compute delta between original and current settings. * Returns only fields that have changed. */ -function computeDelta(original: any, current: any): any { - const delta: any = {}; +function computeDelta(original: SettingsRecord, current: SettingsRecord): SettingsRecord { + const delta: SettingsRecord = {}; for (const key in current) { if (!Object.prototype.hasOwnProperty.call(current, key)) continue; @@ -182,7 +192,7 @@ function computeDelta(original: any, current: any): any { /** * Check if value is a plain object (not array, not null, not Date, etc.) */ -function isPlainObject(value: any): boolean { +function isPlainObject(value: unknown): value is SettingsRecord { return ( value !== null && typeof value === 'object' && diff --git a/frontend/src/core/hooks/useToolParameters.ts b/frontend/src/core/hooks/useToolParameters.ts index 1afd66835..f39331817 100644 --- a/frontend/src/core/hooks/useToolParameters.ts +++ b/frontend/src/core/hooks/useToolParameters.ts @@ -4,14 +4,14 @@ import { useCallback, useMemo } from 'react'; -type ToolParameterValues = Record; +type ToolParameterValues = Record; /** * Register tool parameters and get current values */ export function useToolParameters( _toolName: string, - _parameters: Record + _parameters: Record ): [ToolParameterValues, (updates: Partial) => void] { // Return empty values and noop updater @@ -24,10 +24,10 @@ export function useToolParameters( /** * Hook for managing a single tool parameter */ -export function useToolParameter( +export function useToolParameter( toolName: string, paramName: string, - definition: any + definition: unknown ): [T, (value: T) => void] { const [allParams, updateParams] = useToolParameters(toolName, { [paramName]: definition }); diff --git a/frontend/src/core/services/errorUtils.ts b/frontend/src/core/services/errorUtils.ts index b95e3dfd5..75872dd4c 100644 --- a/frontend/src/core/services/errorUtils.ts +++ b/frontend/src/core/services/errorUtils.ts @@ -4,23 +4,35 @@ export const FILE_EVENTS = { const UUID_REGEX = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g; -export function tryParseJson(input: unknown): T | undefined { +export function tryParseJson(input: unknown): T | undefined { if (typeof input !== 'string') return input as T | undefined; try { return JSON.parse(input) as T; } catch { return undefined; } } -export async function normalizeAxiosErrorData(data: any): Promise { +type TextProducer = { text: () => Promise }; + +function hasTextMethod(value: unknown): value is TextProducer { + return typeof value === 'object' && value !== null && 'text' in value && typeof (value as { text?: unknown }).text === 'function'; +} + +export async function normalizeAxiosErrorData(data: unknown): Promise { if (!data) return undefined; - if (typeof data?.text === 'function') { + if (hasTextMethod(data)) { const text = await data.text(); return tryParseJson(text) ?? text; } return data; } -export function extractErrorFileIds(payload: any): string[] | undefined { +function hasErrorFileIds(payload: unknown): payload is { errorFileIds?: unknown } { + return typeof payload === 'object' && payload !== null && 'errorFileIds' in payload; +} + +export function extractErrorFileIds(payload: unknown): string[] | undefined { if (!payload) return undefined; - if (Array.isArray(payload?.errorFileIds)) return payload.errorFileIds as string[]; + if (hasErrorFileIds(payload) && Array.isArray(payload.errorFileIds)) { + return payload.errorFileIds.filter((value): value is string => typeof value === 'string'); + } if (typeof payload === 'string') { const matches = payload.match(UUID_REGEX); if (matches && matches.length > 0) return Array.from(new Set(matches)); @@ -33,15 +45,22 @@ export function broadcastErroredFiles(fileIds: string[]) { window.dispatchEvent(new CustomEvent(FILE_EVENTS.markError, { detail: { fileIds } })); } +type Sized = { size?: number }; + +function getFileSize(file: Sized | File | null | undefined): number | undefined { + if (!file) return undefined; + if (file instanceof File) return file.size; + const { size } = file; + return typeof size === 'number' ? size : undefined; +} + export function isZeroByte(file: File | { size?: number } | null | undefined): boolean { - if (!file) return true; - const size = (file as any).size; + const size = getFileSize(file); return typeof size === 'number' ? size <= 0 : true; } export function isEmptyOutput(files: File[] | null | undefined): boolean { if (!files || files.length === 0) return true; - return files.every(f => (f as any)?.size === 0); + return files.every(file => getFileSize(file) === 0); } - diff --git a/frontend/src/core/services/googleDrivePickerService.ts b/frontend/src/core/services/googleDrivePickerService.ts index d2956c38a..ca101093d 100644 --- a/frontend/src/core/services/googleDrivePickerService.ts +++ b/frontend/src/core/services/googleDrivePickerService.ts @@ -19,6 +19,29 @@ interface PickerOptions { mimeTypes?: string | null; } +type GoogleTokenResponse = { + access_token?: string; + error?: string; +}; + +type GoogleTokenClient = { + callback: (response: GoogleTokenResponse) => void; + requestAccessToken: (options?: { prompt?: string }) => void; +}; + +type PickerResponseObject = google.picker.ResponseObject; + +interface PickerDocument { + name?: string; + mimeType?: string; + lastModified?: number; + [key: string]: unknown; +} + +type DriveFileResponse = { + body: string; +}; + // Expandable mime types for Google Picker const expandableMimeTypes: Record = { 'image/*': ['image/jpeg', 'image/png', 'image/svg+xml'], @@ -51,7 +74,7 @@ function fileInputToGooglePickerMimeTypes(accept?: string): string | null { class GoogleDrivePickerService { private config: GoogleDriveConfig | null = null; - private tokenClient: any = null; + private tokenClient: GoogleTokenClient | null = null; private accessToken: string | null = null; private gapiLoaded = false; private gisLoaded = false; @@ -112,7 +135,7 @@ class GoogleDrivePickerService { client_id: this.config.clientId, scope: SCOPES, callback: () => {}, // Will be overridden during picker creation - }); + }) as GoogleTokenClient; this.gisLoaded = true; } @@ -142,13 +165,14 @@ class GoogleDrivePickerService { return; } - this.tokenClient.callback = (response: any) => { - if (response.error !== undefined) { + this.tokenClient.callback = (response: GoogleTokenResponse) => { + if (typeof response.error === 'string') { reject(new Error(response.error)); return; } - if(response.access_token == null){ - reject(new Error("No acces token in response")); + if (!response.access_token) { + reject(new Error('No access token in response')); + return; } this.accessToken = response.access_token; @@ -192,7 +216,9 @@ class GoogleDrivePickerService { .setOAuthToken(this.accessToken) .addView(view1) .addView(view2) - .setCallback((data: any) => this.pickerCallback(data, resolve, reject)); + .setCallback((data: PickerResponseObject) => { + void this.pickerCallback(data, resolve, reject); + }); if (options.multiple) { builder.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED); @@ -207,30 +233,55 @@ class GoogleDrivePickerService { * Handle picker selection callback */ private async pickerCallback( - data: any, + data: PickerResponseObject, resolve: (files: File[]) => void, reject: (error: Error) => void ): Promise { if (data.action === window.google.picker.Action.PICKED) { try { + const documentKey = window.google.picker.Response.DOCUMENTS; + const responseData = data as unknown as Record; + const documents = responseData[documentKey]; + if (!Array.isArray(documents)) { + reject(new Error('Picker response missing documents')); + return; + } + const files = await Promise.all( - data[window.google.picker.Response.DOCUMENTS].map(async (pickedFile: any) => { - const fileId = pickedFile[window.google.picker.Document.ID]; + (documents as PickerDocument[]).map(async (pickedFile) => { + const record = pickedFile as PickerDocument; + const fileIdValue = record[window.google.picker.Document.ID]; + if (typeof fileIdValue !== 'string') { + throw new Error('Invalid Google Drive file identifier'); + } + const res = await window.gapi.client.drive.files.get({ - fileId: fileId, + fileId: fileIdValue, alt: 'media', }); + const driveResponse = res as DriveFileResponse; + if (typeof driveResponse.body !== 'string') { + throw new Error('Unexpected Google Drive file response'); + } // Convert response body to File object - const file = new File( - [new Uint8Array(res.body.length).map((_: any, i: number) => res.body.charCodeAt(i))], - pickedFile.name, + const buffer = new Uint8Array(driveResponse.body.length); + for (let i = 0; i < driveResponse.body.length; i += 1) { + buffer[i] = driveResponse.body.charCodeAt(i); + } + + const nameValue = record.name; + const mimeTypeValue = record.mimeType; + const lastModifiedValue = record.lastModified; + + return new File( + [buffer], + typeof nameValue === 'string' ? nameValue : 'drive-file', { - type: pickedFile.mimeType, - lastModified: pickedFile.lastModified, + type: typeof mimeTypeValue === 'string' ? mimeTypeValue : 'application/octet-stream', + lastModified: typeof lastModifiedValue === 'number' ? lastModifiedValue : Date.now(), } ); - return file; }) ); diff --git a/frontend/src/core/services/httpErrorHandler.ts b/frontend/src/core/services/httpErrorHandler.ts index fa5160adf..d785ccf03 100644 --- a/frontend/src/core/services/httpErrorHandler.ts +++ b/frontend/src/core/services/httpErrorHandler.ts @@ -1,5 +1,6 @@ // frontend/src/services/httpErrorHandler.ts import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig } from 'axios'; import { alert } from '@app/components/toast'; import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } from '@app/services/errorUtils'; import { showSpecialErrorToast } from '@app/services/specialErrorToasts'; @@ -29,33 +30,59 @@ function titleForStatus(status?: number): string { return 'Request failed'; } -function extractAxiosErrorMessage(error: any): { title: string; body: string } { - if (axios.isAxiosError(error)) { +type AxiosErrorData = { + message?: string; + errorFileIds?: string[]; + [key: string]: unknown; +}; + +function isErrorWithMessage(error: unknown): error is { message: string } { + return typeof error === 'object' && error !== null && 'message' in error && typeof (error as { message?: unknown }).message === 'string'; +} + +function extractAxiosErrorMessage(error: unknown): { title: string; body: string } { + if (axios.isAxiosError(error)) { const status = error.response?.status; - const _statusText = error.response?.statusText || ''; - let parsed: any = undefined; const raw = error.response?.data; + let parsed: AxiosErrorData | string | undefined; + if (typeof raw === 'string') { - try { parsed = JSON.parse(raw); } catch { /* keep as string */ } + try { + parsed = JSON.parse(raw) as AxiosErrorData; + } catch { + parsed = raw; + } } else { - parsed = raw; + parsed = raw as AxiosErrorData | undefined; } + const extractIds = (): string[] | undefined => { - if (Array.isArray(parsed?.errorFileIds)) return parsed.errorFileIds as string[]; - const rawText = typeof raw === 'string' ? raw : ''; - const uuidMatches = rawText.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g); - return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const idsCandidate = (parsed as AxiosErrorData).errorFileIds; + if (Array.isArray(idsCandidate)) { + return idsCandidate.filter((id): id is string => typeof id === 'string'); + } + } + if (typeof raw === 'string') { + const uuidMatches = raw.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g); + return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined; + } + return undefined; }; const body = ((): string => { - const data = parsed; - if (!data) return typeof raw === 'string' ? raw : ''; + if (!parsed) return typeof raw === 'string' ? raw : ''; + if (typeof parsed === 'string') return parsed; const ids = extractIds(); if (ids && ids.length > 0) return `Failed files: ${ids.join(', ')}`; - if (data?.message) return data.message as string; - if (typeof raw === 'string') return raw; - try { return JSON.stringify(data); } catch { return ''; } + if (parsed.message) return parsed.message; + try { + return JSON.stringify(parsed); + } catch { + return ''; + } })(); + const ids = extractIds(); const title = titleForStatus(status); if (ids && ids.length > 0) { @@ -69,12 +96,16 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } { const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body; return { title, body: bodyMsg }; } + try { - const msg = (error?.message || String(error)) as string; - return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg }; - } catch (e) { - // ignore extraction errors - console.debug('extractAxiosErrorMessage', e); + if (isErrorWithMessage(error)) { + const msg = error.message; + return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg }; + } + const fallbackMessage = String(error); + return { title: 'Network error', body: isUnhelpfulMessage(fallbackMessage) ? FRIENDLY_FALLBACK : fallbackMessage }; + } catch (parseError) { + console.debug('extractAxiosErrorMessage', parseError); return { title: 'Network error', body: FRIENDLY_FALLBACK }; } } @@ -87,16 +118,35 @@ const SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate * Handles HTTP errors with toast notifications and file error broadcasting * Returns true if the error should be suppressed (deduplicated), false otherwise */ -export async function handleHttpError(error: any): Promise { +type ErrorWithConfig = { + config?: (AxiosRequestConfig & { suppressErrorToast?: boolean }) | null; + response?: { + status?: number; + data?: unknown; + }; +}; + +function toAxiosError(error: unknown): AxiosError | undefined { + return axios.isAxiosError(error) ? error : undefined; +} + +export async function handleHttpError(error: unknown): Promise { + const axiosError = toAxiosError(error); + const errorWithConfig = (axiosError ?? (typeof error === 'object' && error !== null ? (error as ErrorWithConfig) : undefined)); + // Check if this error should skip the global toast (component will handle it) - if (error?.config?.suppressErrorToast === true) { + const suppressToast = + !!errorWithConfig?.config && + typeof errorWithConfig.config === 'object' && + (errorWithConfig.config as { suppressErrorToast?: boolean }).suppressErrorToast === true; + if (suppressToast) { return false; // Don't show global toast, but continue rejection } // Compute title/body (friendly) from the error object const { title, body } = extractAxiosErrorMessage(error); // Normalize response data ONCE, reuse for both ID extraction and special-toast matching - const raw = (error?.response?.data) as any; + const raw = errorWithConfig?.response?.data; let normalized: unknown = raw; try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); } @@ -111,8 +161,11 @@ export async function handleHttpError(error: any): Promise { } // 2) Generic-vs-special dedupe by endpoint - const url: string | undefined = error?.config?.url; - const status: number | undefined = error?.response?.status; + const url = + errorWithConfig?.config && typeof errorWithConfig.config === 'object' + ? (errorWithConfig.config as AxiosRequestConfig).url + : undefined; + const status: number | undefined = errorWithConfig?.response?.status; const now = Date.now(); const isSpecial = status === 422 || @@ -148,4 +201,4 @@ export async function handleHttpError(error: any): Promise { } return false; // Error was handled with toast, continue normal rejection -} \ No newline at end of file +} diff --git a/frontend/src/core/services/pdfWorkerManager.ts b/frontend/src/core/services/pdfWorkerManager.ts index 13f18d023..a905083d7 100644 --- a/frontend/src/core/services/pdfWorkerManager.ts +++ b/frontend/src/core/services/pdfWorkerManager.ts @@ -6,6 +6,7 @@ */ import { GlobalWorkerOptions, getDocument, PDFDocumentProxy } from 'pdfjs-dist/legacy/build/pdf.mjs'; +import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'; class PDFWorkerManager { private static instance: PDFWorkerManager; @@ -57,32 +58,25 @@ class PDFWorkerManager { } // Normalize input data to PDF.js format - let pdfData: any; + let params: DocumentInitParameters; if (data instanceof ArrayBuffer || data instanceof Uint8Array) { - pdfData = { data }; + params = { data }; } else if (typeof data === 'string') { - pdfData = data; // URL string + params = { url: data }; } else if (data && typeof data === 'object' && 'data' in data) { - pdfData = data; // Already in {data: ArrayBuffer} format + const buffer = (data as { data: ArrayBuffer }).data; + params = { data: buffer }; } else { - pdfData = data; // Pass through as-is + params = (data as DocumentInitParameters) ?? {}; } - const loadingTask = getDocument( - typeof pdfData === 'string' ? { - url: pdfData, - disableAutoFetch: options.disableAutoFetch ?? true, - disableStream: options.disableStream ?? true, - stopAtErrors: options.stopAtErrors ?? false, - verbosity: options.verbosity ?? 0 - } : { - ...pdfData, - disableAutoFetch: options.disableAutoFetch ?? true, - disableStream: options.disableStream ?? true, - stopAtErrors: options.stopAtErrors ?? false, - verbosity: options.verbosity ?? 0 - } - ); + const loadingTask = getDocument({ + ...params, + disableAutoFetch: options.disableAutoFetch ?? true, + disableStream: options.disableStream ?? true, + stopAtErrors: options.stopAtErrors ?? false, + verbosity: options.verbosity ?? 0 + }); try { const pdf = await loadingTask.promise; diff --git a/frontend/src/core/services/signatureDetectionService.ts b/frontend/src/core/services/signatureDetectionService.ts index 44a12be92..891aab826 100644 --- a/frontend/src/core/services/signatureDetectionService.ts +++ b/frontend/src/core/services/signatureDetectionService.ts @@ -4,10 +4,25 @@ * without needing to make API calls */ -// PDF.js types (simplified) +import React from 'react'; +import type { + DocumentInitParameters, + PDFDocumentLoadingTask, + PDFDocumentProxy, + PDFPageProxy +} from 'pdfjs-dist/types/src/display/api'; + +type PdfMetadataResult = Awaited>; +type PdfMetadataInfo = PdfMetadataResult['info']; +type PdfMetadata = PdfMetadataResult['metadata']; + +interface PdfjsLib { + getDocument: (src?: string | URL | Uint8Array | ArrayBuffer | DocumentInitParameters) => PDFDocumentLoadingTask; +} + declare global { interface Window { - pdfjsLib?: any; + pdfjsLib?: PdfjsLib; } } @@ -39,17 +54,18 @@ const detectSignaturesInFile = async (file: File): Promise + const signatureAnnotations = annotations.filter(annotation => annotation.subtype === 'Widget' && annotation.fieldType === 'Sig' ); @@ -59,7 +75,20 @@ const detectSignaturesInFile = async (file: File): Promise boolean }).has === 'function' && + (meta as { has: (key: string) => boolean }).has('dc:signature'); + if (hasInfoSignature || hasMetadataSignature) { totalSignatures = Math.max(totalSignatures, 1); } @@ -135,6 +164,3 @@ export const useSignatureDetection = () => { reset: () => setDetectionResults([]) }; }; - -// Import React for the hook -import React from 'react'; \ No newline at end of file diff --git a/frontend/src/core/services/specialErrorToasts.ts b/frontend/src/core/services/specialErrorToasts.ts index 1468b8dc4..0d961176c 100644 --- a/frontend/src/core/services/specialErrorToasts.ts +++ b/frontend/src/core/services/specialErrorToasts.ts @@ -40,8 +40,10 @@ export function showSpecialErrorToast(rawError: string | undefined, options?: { // Best-effort translation without hard dependency on i18n config let body = mapping.defaultMessage; try { - const anyGlobal: any = (globalThis as any); - const i18next = anyGlobal?.i18next; + const globalWithI18n = globalThis as typeof globalThis & { + i18next?: { t?: (key: string, options: { defaultValue: string }) => string }; + }; + const i18next = globalWithI18n?.i18next; if (i18next && typeof i18next.t === 'function') { body = i18next.t(mapping.i18nKey, { defaultValue: mapping.defaultMessage }); } @@ -54,4 +56,3 @@ export function showSpecialErrorToast(rawError: string | undefined, options?: { return false; } - diff --git a/frontend/src/core/types/automation.ts b/frontend/src/core/types/automation.ts index 3dc2c41f5..b7efe653a 100644 --- a/frontend/src/core/types/automation.ts +++ b/frontend/src/core/types/automation.ts @@ -1,10 +1,14 @@ +import type { ComponentType } from 'react'; + /** * Types for automation functionality */ +export type AutomationParameters = Record; + export interface AutomationOperation { operation: string; - parameters: Record; + parameters: AutomationParameters; } export interface AutomationConfig { @@ -22,7 +26,7 @@ export interface AutomationTool { operation: string; name: string; configured: boolean; - parameters?: Record; + parameters?: AutomationParameters; } export type AutomationStep = typeof import('@app/constants/automation').AUTOMATION_STEPS[keyof typeof import('@app/constants/automation').AUTOMATION_STEPS]; @@ -57,12 +61,6 @@ export enum AutomationMode { SUGGESTED = 'suggested' } -export interface SuggestedAutomation { - id: string; - name: string; - description?: string; - operations: AutomationOperation[]; - createdAt: string; - updatedAt: string; - icon: any; // MUI Icon component +export interface SuggestedAutomation extends AutomationConfig { + iconComponent?: ComponentType; } diff --git a/frontend/src/core/utils/automationExecutor.ts b/frontend/src/core/utils/automationExecutor.ts index 760a86d7b..7fb3d4f86 100644 --- a/frontend/src/core/utils/automationExecutor.ts +++ b/frontend/src/core/utils/automationExecutor.ts @@ -1,31 +1,75 @@ import axios from 'axios'; +import type { ToolOperationConfig, SingleFileToolOperationConfig, MultiFileToolOperationConfig } from '@app/hooks/tools/shared/useToolOperation'; import { ToolRegistry } from '@app/data/toolsTaxonomy'; import { ToolId } from '@app/types/toolId'; import { AUTOMATION_CONSTANTS } from '@app/constants/automation'; -import { AutomationFileProcessor } from '@app/utils/automationFileProcessor'; +import { AutomationFileProcessor, type AutomationFormParameters } from '@app/utils/automationFileProcessor'; import { ToolType } from '@app/hooks/tools/shared/useToolOperation'; import { processResponse } from '@app/utils/toolResponseProcessor'; +import type { AutomationConfig, AutomationParameters } from '@app/types/automation'; + +const getHeaderValue = (headers: unknown, key: string): string | undefined => { + if (!headers || typeof headers !== 'object') return undefined; + const value = (headers as Record)[key]; + if (typeof value === 'string') return value; + if (Array.isArray(value)) { + return value.find((entry): entry is string => typeof entry === 'string' && entry.length > 0); + } + if (value && typeof value === 'object' && 'toString' in value) { + const stringValue = String(value); + return stringValue.length > 0 ? stringValue : undefined; + } + return undefined; +}; + +const isPdfResponse = (headers: unknown): boolean => { + const contentType = getHeaderValue(headers, 'content-type'); + return typeof contentType === 'string' && contentType.includes('application/pdf'); +}; + +const formatErrorMessage = (error: unknown): string => { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + if (typeof responseData === 'string') { + return responseData; + } + if (responseData && typeof responseData === 'object' && 'message' in responseData) { + const message = (responseData as { message?: unknown }).message; + if (typeof message === 'string') return message; + } + return error.message; + } + if (error instanceof Error) { + return error.message; + } + return String(error); +}; /** * Process multi-file tool response (handles ZIP or single PDF responses) */ const processMultiFileResponse = async ( responseData: Blob, - responseHeaders: any, + responseHeaders: unknown, files: File[], filePrefix: string, preserveBackendFilename?: boolean ): Promise => { // Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true) if (responseData.type === 'application/pdf' || - (responseHeaders && responseHeaders['content-type'] === 'application/pdf')) { + isPdfResponse(responseHeaders)) { // Single PDF response - use processResponse to respect preserveBackendFilename + const normalizedHeaders = + typeof responseHeaders === 'object' && responseHeaders !== null + ? (responseHeaders as Record) + : undefined; + const processedFiles = await processResponse( responseData, files, filePrefix, undefined, - preserveBackendFilename ? responseHeaders : undefined + preserveBackendFilename ? normalizedHeaders : undefined ); return processedFiles; } else { @@ -75,9 +119,9 @@ const executeApiRequest = async ( /** * Execute single-file tool operation (processes files one at a time) */ -const executeSingleFileOperation = async ( - config: any, - parameters: any, +const executeSingleFileOperation = async ( + config: SingleFileToolOperationConfig, + parameters: TParams, files: File[], filePrefix: string ): Promise => { @@ -88,7 +132,7 @@ const executeSingleFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file); + const formData = config.buildFormData(parameters, file); const processedFiles = await executeApiRequest( endpoint, @@ -106,9 +150,9 @@ const executeSingleFileOperation = async ( /** * Execute multi-file tool operation (processes all files in one request) */ -const executeMultiFileOperation = async ( - config: any, - parameters: any, +const executeMultiFileOperation = async ( + config: MultiFileToolOperationConfig, + parameters: TParams, files: File[], filePrefix: string ): Promise => { @@ -116,7 +160,7 @@ const executeMultiFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files); + const formData = config.buildFormData(parameters, files); return await executeApiRequest( endpoint, @@ -131,9 +175,12 @@ const executeMultiFileOperation = async ( /** * Execute a tool operation directly without using React hooks */ +const toFormParameters = (parameters: AutomationParameters): AutomationFormParameters => + parameters as AutomationFormParameters; + export const executeToolOperation = async ( operationName: string, - parameters: any, + parameters: AutomationParameters, files: File[], toolRegistry: ToolRegistry ): Promise => { @@ -145,12 +192,12 @@ export const executeToolOperation = async ( */ export const executeToolOperationWithPrefix = async ( operationName: string, - parameters: any, + parameters: AutomationParameters, files: File[], toolRegistry: ToolRegistry, filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX ): Promise => { - const config = toolRegistry[operationName as ToolId]?.operationConfig; + const config = toolRegistry[operationName as ToolId]?.operationConfig as ToolOperationConfig | undefined; if (!config) { throw new Error(`Tool operation not supported: ${operationName}`); } @@ -162,16 +209,18 @@ export const executeToolOperationWithPrefix = async ( return resultFiles; } + const formParameters = toFormParameters(parameters); + // Execute based on tool type if (config.toolType === ToolType.multiFile) { - return await executeMultiFileOperation(config, parameters, files, filePrefix); + return await executeMultiFileOperation(config, formParameters, files, filePrefix); } else { - return await executeSingleFileOperation(config, parameters, files, filePrefix); + return await executeSingleFileOperation(config, formParameters, files, filePrefix); } - } catch (error: any) { + } catch (error: unknown) { console.error(`❌ ${operationName} failed:`, error); - throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`); + throw new Error(`${operationName} operation failed: ${formatErrorMessage(error)}`); } }; @@ -179,7 +228,7 @@ export const executeToolOperationWithPrefix = async ( * Execute an entire automation sequence */ export const executeAutomationSequence = async ( - automation: any, + automation: AutomationConfig, initialFiles: File[], toolRegistry: ToolRegistry, onStepStart?: (stepIndex: number, operationName: string) => void, @@ -205,9 +254,11 @@ export const executeAutomationSequence = async ( try { onStepStart?.(i, operation.operation); + const operationParameters: AutomationParameters = operation.parameters ?? {}; + const resultFiles = await executeToolOperationWithPrefix( operation.operation, - operation.parameters || {}, + operationParameters, currentFiles, toolRegistry, i === automation.operations.length - 1 ? automationPrefix : '' // Only add prefix to final step @@ -217,10 +268,11 @@ export const executeAutomationSequence = async ( currentFiles = resultFiles; onStepComplete?.(i, resultFiles); - } catch (error: any) { + } catch (error: unknown) { console.error(`❌ Step ${i + 1} failed:`, error); - onStepError?.(i, error.message); - throw error; + const message = formatErrorMessage(error); + onStepError?.(i, message); + throw error instanceof Error ? error : new Error(message); } } diff --git a/frontend/src/core/utils/automationFileProcessor.ts b/frontend/src/core/utils/automationFileProcessor.ts index b07fe961b..3f35d042c 100644 --- a/frontend/src/core/utils/automationFileProcessor.ts +++ b/frontend/src/core/utils/automationFileProcessor.ts @@ -18,6 +18,8 @@ export interface AutomationProcessingResult { errors: string[]; } +export type AutomationFormParameters = Record; + export class AutomationFileProcessor { /** * Check if a blob is a ZIP file by examining its header @@ -121,11 +123,11 @@ export class AutomationFileProcessor { files: [resultFile], errors: [] }; - } catch (error: any) { + } catch (error: unknown) { return { success: false, files: [], - errors: [`Automation step failed: ${error.response?.data || error.message}`] + errors: [`Automation step failed: ${AutomationFileProcessor.formatError(error)}`] }; } } @@ -154,11 +156,11 @@ export class AutomationFileProcessor { // Multi-file responses are typically ZIP files return await this.extractAutomationZipFiles(response.data); - } catch (error: any) { + } catch (error: unknown) { return { success: false, files: [], - errors: [`Automation step failed: ${error.response?.data || error.message}`] + errors: [`Automation step failed: ${AutomationFileProcessor.formatError(error)}`] }; } } @@ -167,7 +169,7 @@ export class AutomationFileProcessor { * Build form data for automation tool operations */ static buildAutomationFormData( - parameters: Record, + parameters: AutomationFormParameters, files: File | File[], fileFieldName: string = 'fileInput' ): FormData { @@ -183,12 +185,51 @@ export class AutomationFileProcessor { // Add parameters Object.entries(parameters).forEach(([key, value]) => { if (Array.isArray(value)) { - value.forEach(item => formData.append(key, item)); + value.forEach(item => { + if (item !== undefined && item !== null) { + formData.append(key, AutomationFileProcessor.normalizeFormValue(item)); + } + }); } else if (value !== undefined && value !== null) { - formData.append(key, value); + formData.append(key, AutomationFileProcessor.normalizeFormValue(value)); } }); return formData; } + + private static normalizeFormValue(value: unknown): string | Blob { + if (value instanceof Blob || value instanceof File) { + return value; + } + if (value === null || value === undefined) { + return ''; + } + if (typeof value === 'object') { + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + return String(value); + } + + private static formatError(error: unknown): string { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + if (typeof responseData === 'string') return responseData; + if (responseData && typeof responseData === 'object' && 'message' in responseData) { + const message = (responseData as { message?: unknown }).message; + if (typeof message === 'string') { + return message; + } + } + return error.message; + } + if (error instanceof Error) { + return error.message; + } + return String(error); + } } diff --git a/frontend/src/core/utils/downloadUtils.ts b/frontend/src/core/utils/downloadUtils.ts index 4bf8223fb..64d3695e7 100644 --- a/frontend/src/core/utils/downloadUtils.ts +++ b/frontend/src/core/utils/downloadUtils.ts @@ -143,7 +143,7 @@ export function downloadTextAsFile( * @param data - Data to serialize and download * @param filename - Filename for the download */ -export function downloadJsonAsFile(data: any, filename: string): void { +export function downloadJsonAsFile(data: unknown, filename: string): void { const content = JSON.stringify(data, null, 2); downloadTextAsFile(content, filename, 'application/json'); } diff --git a/frontend/src/core/utils/settingsPendingHelper.ts b/frontend/src/core/utils/settingsPendingHelper.ts index 980930fea..2f500bb0a 100644 --- a/frontend/src/core/utils/settingsPendingHelper.ts +++ b/frontend/src/core/utils/settingsPendingHelper.ts @@ -11,9 +11,11 @@ * } */ -export interface SettingsWithPending { +type PlainObject = Record; + +export interface SettingsWithPending { _pending?: Partial; - [key: string]: any; + [key: string]: unknown; } /** @@ -23,15 +25,21 @@ export interface SettingsWithPending { * @param settings Settings object from backend (may contain _pending block) * @returns Merged settings with pending values applied */ -export function mergePendingSettings(settings: T): Omit { - if (!settings || !settings._pending) { +export function mergePendingSettings( + settings: T | null | undefined +): Omit { + if (!settings) { + return {} as Omit; + } + + if (!settings._pending) { // No pending changes, return as-is (without _pending property) - const { _pending, ...rest } = settings || {}; + const { _pending, ...rest } = settings; return rest as Omit; } // Deep merge pending changes - const merged = deepMerge(settings, settings._pending); + const merged = deepMerge(settings, settings._pending as PlainObject | undefined); // Remove _pending from result const { _pending, ...result } = merged; @@ -54,7 +62,7 @@ export function isFieldPending( } // Navigate the pending object using dot notation - const value = getNestedValue(settings._pending, fieldPath); + const value = getNestedValue(settings._pending as PlainObject | undefined, fieldPath); return value !== undefined; } @@ -80,12 +88,12 @@ export function hasPendingChanges( export function getPendingValue( settings: T | null | undefined, fieldPath: string -): any { +): unknown { if (!settings?._pending) { return undefined; } - return getNestedValue(settings._pending, fieldPath); + return getNestedValue(settings._pending as PlainObject | undefined, fieldPath); } /** @@ -98,14 +106,14 @@ export function getPendingValue( export function getCurrentValue( settings: T | null | undefined, fieldPath: string -): any { +): unknown { if (!settings) { return undefined; } // Get from settings, ignoring _pending const { _pending, ...activeSettings } = settings; - return getNestedValue(activeSettings, fieldPath); + return getNestedValue(activeSettings as PlainObject | undefined, fieldPath); } // ========== Helper Functions ========== @@ -113,11 +121,11 @@ export function getCurrentValue( /** * Deep merge two objects. Second object takes priority. */ -function deepMerge(target: any, source: any): any { - if (!source) return target; +function deepMerge(target: PlainObject | undefined, source: PlainObject | undefined): PlainObject { + if (!source) return target ?? {}; if (!target) return source; - const result = { ...target }; + const result: PlainObject = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { @@ -138,17 +146,20 @@ function deepMerge(target: any, source: any): any { /** * Get nested value using dot notation. */ -function getNestedValue(obj: any, path: string): any { +function getNestedValue(obj: PlainObject | undefined, path: string): unknown { if (!obj || !path) return undefined; const parts = path.split('.'); - let current = obj; + let current: unknown = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } - current = current[part]; + if (typeof current !== 'object' || !(part in current)) { + return undefined; + } + current = (current as PlainObject)[part]; } return current; @@ -157,7 +168,7 @@ function getNestedValue(obj: any, path: string): any { /** * Check if value is a plain object (not array, not null, not Date, etc.) */ -function isPlainObject(value: any): boolean { +function isPlainObject(value: unknown): value is PlainObject { return ( value !== null && typeof value === 'object' && diff --git a/frontend/src/core/utils/toolErrorHandler.ts b/frontend/src/core/utils/toolErrorHandler.ts index 637970adf..f992a54cd 100644 --- a/frontend/src/core/utils/toolErrorHandler.ts +++ b/frontend/src/core/utils/toolErrorHandler.ts @@ -2,32 +2,40 @@ * Standardized error handling utilities for tool operations */ +import axios from 'axios'; + /** * Default error extractor that follows the standard pattern */ -export const extractErrorMessage = (error: any): string => { - if (error.response?.data && typeof error.response.data === 'string') { - return error.response.data; +const DEFAULT_MESSAGE = 'There was an error processing your request.'; + +const resolveErrorMessage = (error: unknown, fallbackMessage: string): string => { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + if (typeof responseData === 'string') { + return responseData; + } + if (responseData && typeof responseData === 'object' && 'message' in responseData) { + const message = (responseData as { message?: unknown }).message; + if (typeof message === 'string') { + return message; + } + } + return error.message || fallbackMessage; } - if (error.message) { + if (error instanceof Error) { return error.message; } - return 'There was an error processing your request.'; + return fallbackMessage; }; +export const extractErrorMessage = (error: unknown): string => resolveErrorMessage(error, DEFAULT_MESSAGE); + /** * Creates a standardized error handler for tool operations * @param fallbackMessage - Message to show when no specific error can be extracted * @returns Error handler function that follows the standard pattern */ export const createStandardErrorHandler = (fallbackMessage: string) => { - return (error: any): string => { - if (error.response?.data && typeof error.response.data === 'string') { - return error.response.data; - } - if (error.message) { - return error.message; - } - return fallbackMessage; - }; -}; \ No newline at end of file + return (error: unknown): string => resolveErrorMessage(error, fallbackMessage); +}; diff --git a/frontend/src/core/utils/toolResponseProcessor.ts b/frontend/src/core/utils/toolResponseProcessor.ts index 1bff28b81..59f5790f7 100644 --- a/frontend/src/core/utils/toolResponseProcessor.ts +++ b/frontend/src/core/utils/toolResponseProcessor.ts @@ -15,7 +15,7 @@ export async function processResponse( originalFiles: File[], filePrefix?: string, responseHandler?: ResponseHandler, - responseHeaders?: Record + responseHeaders?: Record ): Promise { if (responseHandler) { const out = await responseHandler(blob, originalFiles); @@ -25,10 +25,14 @@ export async function processResponse( // 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 contentDisposition = responseHeaders['content-disposition'] as string | undefined; const backendFilename = getFilenameFromHeaders(contentDisposition); if (backendFilename) { - const type = blob.type || responseHeaders['content-type'] || 'application/octet-stream'; + const contentType = responseHeaders['content-type']; + const type = + blob.type || + (typeof contentType === 'string' ? contentType : undefined) || + 'application/octet-stream'; return [new File([blob], backendFilename, { type })]; } // If preserveBackendFilename was requested but no Content-Disposition header found, diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index 96d969c97..c52b71a2f 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -3,9 +3,15 @@ declare module '*.module.css'; // Auto-generated icon set JSON import declare module 'assets/material-symbols-icons.json' { + type IconDefinition = { + body: string; + width?: number; + height?: number; + [key: string]: unknown; + }; const value: { prefix: string; - icons: Record; + icons: Record; width?: number; height?: number; };