diff --git a/invokeai/frontend/web/src/common/util/zodErrorLogger.ts b/invokeai/frontend/web/src/common/util/zodErrorLogger.ts new file mode 100644 index 00000000000..7fe5ab6e2e0 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/zodErrorLogger.ts @@ -0,0 +1,45 @@ +import type { z } from 'zod'; + +interface ZodErrorLogOptions { + context?: string; +} + +/** + * Logs Zod validation errors with a simple description and complete error data. + */ +export const logZodError = (error: z.ZodError, options: ZodErrorLogOptions = {}): void => { + const { context = 'Validation' } = options; + + // eslint-disable-next-line no-console + console.error(`${context} failed with ${error.issues.length} errors:`, { + summary: error.issues.map((issue) => `${issue.path.join('.') || 'root'}: ${issue.message}`), + issues: error.issues, + }); +}; + +/** + * Creates a human-readable summary of Zod errors suitable for user-facing messages. + * Limits the number of errors shown and provides a count of remaining errors. + */ +export const createZodErrorSummary = ( + error: z.ZodError, + maxErrors: number = 10, + pathFormatter?: (path: string, message: string) => string +): string => { + const defaultFormatter = (path: string, message: string) => `${path || 'Root'}: ${message}`; + + const formatter = pathFormatter || defaultFormatter; + + const errors = error.issues.map((issue) => formatter(issue.path.join('.'), issue.message)); + + const visibleErrors = errors.slice(0, maxErrors); + const remainingCount = errors.length - maxErrors; + + let summary = visibleErrors.join('; '); + + if (remainingCount > 0) { + summary += ` (+${remainingCount} more error${remainingCount === 1 ? '' : 's'})`; + } + + return summary; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index c3c0849ed10..26a3c3f7153 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -39,8 +39,21 @@ export const useLoadWorkflowFromFile = () => { dispatch(workflowLoadedFromFile()); onSuccess?.(validatedWorkflow); resolve(validatedWorkflow); - } catch { - // This is catching the error from the parsing the JSON file + } catch (e) { + // Log file parsing/loading errors + if (e instanceof SyntaxError) { + // eslint-disable-next-line no-console + console.error('Workflow file JSON parsing failed:', { + fileName: file.name, + error: e.message, + }); + } else { + // eslint-disable-next-line no-console + console.error('Workflow file loading failed:', { + fileName: file.name, + error: e, + }); + } onError?.(); reject(); } finally { diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts index 39d5a9cd917..39202350da7 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import { useAppDispatch } from 'app/store/storeHooks'; +import { createZodErrorSummary, logZodError } from 'common/util/zodErrorLogger'; import { getIsFormEmpty } from 'features/nodes/components/sidePanel/builder/form-manipulation'; import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState'; import { $templates, workflowLoaded } from 'features/nodes/store/nodesSlice'; @@ -16,7 +17,6 @@ import { useCallback } from 'react'; import { serializeError } from 'serialize-error'; import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks'; import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error/v4'; const log = logger('workflows'); @@ -120,10 +120,46 @@ export const useValidateAndLoadWorkflow = () => { }); } else if (e instanceof z.ZodError) { // There was a problem validating the workflow itself - const { message } = fromZodError(e, { - prefix: t('nodes.workflowValidation'), - }); - log.error({ error: serializeError(e) }, message); + + // Log comprehensive error information for developers + logZodError(e, { context: 'Workflow validation' }); + + // Create human-readable message for toast with node-specific formatting + const pathFormatter = (path: string, message: string) => { + const nodeMatch = path.match(/nodes\.(\d+)\.data\.inputs\.([^.]+)(?:\.(.+))?/); + const nodeDataMatch = path.match(/nodes\.(\d+)\.data\.([^.]+)/); + const nodeMatch2 = path.match(/nodes\.(\d+)\.([^.]+)/); + + if (nodeMatch) { + const [, nodeIndex, fieldName, subField] = nodeMatch; + const fieldPath = subField ? `${fieldName}.${subField}` : fieldName; + return `Node ${parseInt(nodeIndex, 10) + 1} input "${fieldPath}": ${message}`; + } else if (nodeDataMatch) { + const [, nodeIndex, fieldName] = nodeDataMatch; + return `Node ${parseInt(nodeIndex, 10) + 1} data "${fieldName}": ${message}`; + } else if (nodeMatch2) { + const [, nodeIndex, fieldName] = nodeMatch2; + return `Node ${parseInt(nodeIndex, 10) + 1} "${fieldName}": ${message}`; + } else if (path.startsWith('nodes.')) { + const pathParts = path.split('.'); + const nodeIndex = pathParts[1]; + const remainingPath = pathParts.slice(2).join('.'); + return `Node ${parseInt(nodeIndex, 10) + 1} (${remainingPath || 'general'}): ${message}`; + } else { + return `${path || 'Workflow'}: ${message}`; + } + }; + + const errorSummary = createZodErrorSummary(e, 10, pathFormatter); + const message = `${t('nodes.workflowValidation')}: ${errorSummary}`; + + log.error( + { + error: serializeError(e), + totalIssues: e.issues.length, + }, + `Workflow validation failed with ${e.issues.length} issues: ${message}` + ); toast({ id: 'UNABLE_TO_VALIDATE_WORKFLOW', title: t('nodes.unableToValidateWorkflow'), diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index a697148322d..fa11de36353 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import path from 'path'; import { visualizer } from 'rollup-plugin-visualizer'; import type { PluginOption } from 'vite'; -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; import dts from 'vite-plugin-dts'; import eslint from 'vite-plugin-eslint'; @@ -11,6 +11,15 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { loggerContextPlugin } from './vite-plugin-logger-context'; export default defineConfig(({ mode }) => { + // Load env file based on mode in the current working directory. + // Set the third parameter to '' to load all env regardless of the VITE_ prefix. + const env = loadEnv(mode, process.cwd(), ''); + + // Get InvokeAI API base URL from environment variable, default to localhost + const apiBaseUrl = env.INVOKEAI_API_BASE_URL || 'http://127.0.0.1:9090'; + const apiHost = new URL(apiBaseUrl).host; + const wsProtocol = apiBaseUrl.startsWith('https') ? 'wss' : 'ws'; + if (mode === 'package') { return { base: './', @@ -81,16 +90,16 @@ export default defineConfig(({ mode }) => { server: { proxy: { '/ws/socket.io': { - target: 'ws://127.0.0.1:9090', + target: `${wsProtocol}://${apiHost}`, ws: true, }, '/openapi.json': { - target: 'http://127.0.0.1:9090/openapi.json', + target: `${apiBaseUrl}/openapi.json`, rewrite: (path) => path.replace(/^\/openapi.json/, ''), changeOrigin: true, }, '/api/': { - target: 'http://127.0.0.1:9090/api/', + target: `${apiBaseUrl}/api/`, rewrite: (path) => path.replace(/^\/api/, ''), changeOrigin: true, },