diff --git a/api/core/workflow/generator/prompts/vibe_prompts.py b/api/core/workflow/generator/prompts/vibe_prompts.py index 11845efbbd..ce1d449c9d 100644 --- a/api/core/workflow/generator/prompts/vibe_prompts.py +++ b/api/core/workflow/generator/prompts/vibe_prompts.py @@ -709,6 +709,17 @@ def parse_vibe_response(content: str) -> dict[str, Any]: "raw_content": content[:500], # First 500 chars for debugging } + # Handle double-encoded JSON (when json.loads returns a string) + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + return { + "intent": "error", + "error": "Failed to parse double-encoded JSON", + "raw_content": data[:500], + } + # Validate and normalize if "intent" not in data: data["intent"] = "generate" # Default assumption diff --git a/api/core/workflow/generator/runner.py b/api/core/workflow/generator/runner.py index 24092c2276..9dec455505 100644 --- a/api/core/workflow/generator/runner.py +++ b/api/core/workflow/generator/runner.py @@ -6,7 +6,11 @@ from collections.abc import Sequence import json_repair from core.model_manager import ModelManager -from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage +from core.model_runtime.entities.message_entities import ( + SystemPromptMessage, + TextPromptMessageContent, + UserPromptMessage, +) from core.model_runtime.entities.model_entities import ModelType from core.workflow.generator.prompts.builder_prompts import ( BUILDER_SYSTEM_PROMPT, @@ -105,7 +109,31 @@ class WorkflowGenerator: model_parameters=model_parameters, stream=False, ) + # Extract text content from response plan_content = response.message.content + if isinstance(plan_content, list): + # Extract text from content list + text_parts = [] + for content in plan_content: + if isinstance(content, TextPromptMessageContent): + text_parts.append(content.data) + plan_content = "".join(text_parts) + elif plan_content is None: + plan_content = "" + + # Check if LLM returned empty content + if not plan_content or not plan_content.strip(): + usage = response.usage if hasattr(response, "usage") else "N/A" + logger.error("LLM returned empty content. Usage: %s", usage) + return { + "intent": "error", + "error": ( + "LLM model returned empty response. This may indicate: " + "(1) Model refusal/content policy, (2) Model configuration issue, " + "(3) Plugin communication error. Try a different model or check model settings." + ), + } + # Reuse parse_vibe_response logic or simple load plan_data = parse_vibe_response(plan_content) except Exception as e: @@ -212,13 +240,52 @@ class WorkflowGenerator: stream=False, ) # Builder output is raw JSON nodes/edges + # Extract text content from response build_content = build_res.message.content + if isinstance(build_content, list): + # Extract text from content list + text_parts = [] + for content in build_content: + if isinstance(content, TextPromptMessageContent): + text_parts.append(content.data) + build_content = "".join(text_parts) + elif build_content is None: + build_content = "" + match = re.search(r"```(?:json)?\s*([\s\S]+?)```", build_content) if match: build_content = match.group(1) + # Check if LLM returned empty content + if not build_content or not build_content.strip(): + usage = build_res.usage if hasattr(build_res, "usage") else "N/A" + logger.error("Builder LLM returned empty content. Usage: %s", usage) + raise ValueError( + "LLM model returned empty response. This may indicate: " + "(1) Model refusal/content policy, (2) Model configuration issue, " + "(3) Plugin communication error. Try a different model or check model settings." + ) + workflow_data = json_repair.loads(build_content) + # Handle double-encoded JSON (when json_repair.loads returns a string) + # Keep decoding until we get a dict + max_decode_attempts = 3 + decode_attempts = 0 + while isinstance(workflow_data, str) and decode_attempts < max_decode_attempts: + workflow_data = json_repair.loads(workflow_data) + decode_attempts += 1 + + # If still a string, it's not valid JSON structure + if not isinstance(workflow_data, dict): + logger.error( + "workflow_data is not a dict after %s decode attempts. Type: %s, Value preview: %s", + decode_attempts, + type(workflow_data), + str(workflow_data)[:200], + ) + raise ValueError(f"Expected dict, got {type(workflow_data).__name__}") + if "nodes" not in workflow_data: workflow_data["nodes"] = [] diff --git a/web/__tests__/goto-anything/command-selector.test.tsx b/web/__tests__/goto-anything/command-selector.test.tsx index f0168ab3be..fe5fa37d75 100644 --- a/web/__tests__/goto-anything/command-selector.test.tsx +++ b/web/__tests__/goto-anything/command-selector.test.tsx @@ -1,4 +1,4 @@ -import type { ActionItem } from '../../app/components/goto-anything/actions/types' +import type { ScopeDescriptor } from '../../app/components/goto-anything/actions/types' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import CommandSelector from '../../app/components/goto-anything/command-selector' @@ -20,36 +20,37 @@ vi.mock('cmdk', () => ({ })) describe('CommandSelector', () => { - const mockActions: Record = { - app: { - key: '@app', + const mockScopes: ScopeDescriptor[] = [ + { + id: 'app', shortcut: '@app', title: 'Search Applications', description: 'Search apps', search: vi.fn(), }, - knowledge: { - key: '@knowledge', + { + id: 'knowledge', shortcut: '@kb', + aliases: ['@knowledge'], title: 'Search Knowledge', description: 'Search knowledge bases', search: vi.fn(), }, - plugin: { - key: '@plugin', + { + id: 'plugin', shortcut: '@plugin', title: 'Search Plugins', description: 'Search plugins', search: vi.fn(), }, - node: { - key: '@node', + { + id: 'node', shortcut: '@node', title: 'Search Nodes', description: 'Search workflow nodes', search: vi.fn(), }, - } + ] const mockOnCommandSelect = vi.fn() const mockOnCommandValueChange = vi.fn() @@ -62,7 +63,7 @@ describe('CommandSelector', () => { it('should render all actions when no filter is provided', () => { render( , ) @@ -76,7 +77,7 @@ describe('CommandSelector', () => { it('should render empty filter as showing all actions', () => { render( , @@ -93,7 +94,7 @@ describe('CommandSelector', () => { it('should filter actions based on searchFilter - single match', () => { render( , @@ -108,7 +109,7 @@ describe('CommandSelector', () => { it('should filter actions with multiple matches', () => { render( , @@ -123,7 +124,7 @@ describe('CommandSelector', () => { it('should be case-insensitive when filtering', () => { render( , @@ -136,7 +137,7 @@ describe('CommandSelector', () => { it('should match partial strings', () => { render( , @@ -153,7 +154,7 @@ describe('CommandSelector', () => { it('should show empty state when no matches found', () => { render( , @@ -171,7 +172,7 @@ describe('CommandSelector', () => { it('should not show empty state when filter is empty', () => { render( , @@ -185,7 +186,7 @@ describe('CommandSelector', () => { it('should call onCommandValueChange when filter changes and first item differs', () => { const { rerender } = render( { rerender( { it('should not call onCommandValueChange if current value still exists', () => { const { rerender } = render( { rerender( { it('should handle onCommandSelect callback correctly', () => { render( , @@ -250,7 +251,7 @@ describe('CommandSelector', () => { it('should handle empty actions object', () => { render( , @@ -262,7 +263,7 @@ describe('CommandSelector', () => { it('should handle special characters in filter', () => { render( , @@ -277,7 +278,7 @@ describe('CommandSelector', () => { it('should handle undefined onCommandValueChange gracefully', () => { const { rerender } = render( , @@ -286,7 +287,7 @@ describe('CommandSelector', () => { expect(() => { rerender( , @@ -299,7 +300,7 @@ describe('CommandSelector', () => { it('should work without searchFilter prop (backward compatible)', () => { render( , ) @@ -313,7 +314,7 @@ describe('CommandSelector', () => { it('should work without commandValue and onCommandValueChange props', () => { render( , diff --git a/web/__tests__/goto-anything/match-action.test.ts b/web/__tests__/goto-anything/match-action.test.ts index 66b170d45e..1983a98a88 100644 --- a/web/__tests__/goto-anything/match-action.test.ts +++ b/web/__tests__/goto-anything/match-action.test.ts @@ -1,5 +1,5 @@ import type { Mock } from 'vitest' -import type { ActionItem } from '../../app/components/goto-anything/actions/types' +import type { ScopeDescriptor } from '../../app/components/goto-anything/actions/types' // Import after mocking to get mocked version import { matchAction } from '../../app/components/goto-anything/actions' @@ -13,10 +13,11 @@ vi.mock('../../app/components/goto-anything/actions', () => ({ vi.mock('../../app/components/goto-anything/actions/commands/registry') // Implement the actual matchAction logic for testing -const actualMatchAction = (query: string, actions: Record) => { - const result = Object.values(actions).find((action) => { +const actualMatchAction = (query: string, scopes: ScopeDescriptor[]) => { + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + return scopes.find((scope) => { // Special handling for slash commands - if (action.key === '/') { + if (scope.id === 'slash' || scope.shortcut === '/') { // Get all registered commands from the registry const allCommands = slashCommandRegistry.getAllCommands() @@ -33,39 +34,41 @@ const actualMatchAction = (query: string, actions: Record) = }) } - const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`) + const shortcuts = [scope.shortcut, ...(scope.aliases || [])].map(escapeRegExp) + const reg = new RegExp(`^(${shortcuts.join('|')})(?:\\s|$)`) return reg.test(query) }) - return result } // Replace mock with actual implementation ;(matchAction as Mock).mockImplementation(actualMatchAction) describe('matchAction Logic', () => { - const mockActions: Record = { - app: { - key: '@app', - shortcut: '@a', + const mockScopes: ScopeDescriptor[] = [ + { + id: 'app', + shortcut: '@app', + aliases: ['@a'], title: 'Search Applications', description: 'Search apps', search: vi.fn(), }, - knowledge: { - key: '@knowledge', + { + id: 'knowledge', shortcut: '@kb', + aliases: ['@knowledge'], title: 'Search Knowledge', description: 'Search knowledge bases', search: vi.fn(), }, - slash: { - key: '/', + { + id: 'slash', shortcut: '/', title: 'Commands', description: 'Execute commands', search: vi.fn(), }, - } + ] beforeEach(() => { vi.clearAllMocks() @@ -81,32 +84,32 @@ describe('matchAction Logic', () => { describe('@ Actions Matching', () => { it('should match @app with key', () => { - const result = matchAction('@app', mockActions) - expect(result).toBe(mockActions.app) + const result = matchAction('@app', mockScopes) + expect(result).toBe(mockScopes[0]) }) it('should match @app with shortcut', () => { - const result = matchAction('@a', mockActions) - expect(result).toBe(mockActions.app) + const result = matchAction('@a', mockScopes) + expect(result).toBe(mockScopes[0]) }) it('should match @knowledge with key', () => { - const result = matchAction('@knowledge', mockActions) - expect(result).toBe(mockActions.knowledge) + const result = matchAction('@knowledge', mockScopes) + expect(result).toBe(mockScopes[1]) }) it('should match @knowledge with shortcut @kb', () => { - const result = matchAction('@kb', mockActions) - expect(result).toBe(mockActions.knowledge) + const result = matchAction('@kb', mockScopes) + expect(result).toBe(mockScopes[1]) }) it('should match with text after action', () => { - const result = matchAction('@app search term', mockActions) - expect(result).toBe(mockActions.app) + const result = matchAction('@app search term', mockScopes) + expect(result).toBe(mockScopes[0]) }) it('should not match partial @ actions', () => { - const result = matchAction('@ap', mockActions) + const result = matchAction('@ap', mockScopes) expect(result).toBeUndefined() }) }) @@ -114,47 +117,47 @@ describe('matchAction Logic', () => { describe('Slash Commands Matching', () => { describe('Direct Mode Commands', () => { it('should not match direct mode commands', () => { - const result = matchAction('/docs', mockActions) + const result = matchAction('/docs', mockScopes) expect(result).toBeUndefined() }) it('should not match direct mode with arguments', () => { - const result = matchAction('/docs something', mockActions) + const result = matchAction('/docs something', mockScopes) expect(result).toBeUndefined() }) it('should not match any direct mode command', () => { - expect(matchAction('/community', mockActions)).toBeUndefined() - expect(matchAction('/feedback', mockActions)).toBeUndefined() - expect(matchAction('/account', mockActions)).toBeUndefined() + expect(matchAction('/community', mockScopes)).toBeUndefined() + expect(matchAction('/feedback', mockScopes)).toBeUndefined() + expect(matchAction('/account', mockScopes)).toBeUndefined() }) }) describe('Submenu Mode Commands', () => { it('should match submenu mode commands exactly', () => { - const result = matchAction('/theme', mockActions) - expect(result).toBe(mockActions.slash) + const result = matchAction('/theme', mockScopes) + expect(result).toBe(mockScopes[2]) }) it('should match submenu mode with arguments', () => { - const result = matchAction('/theme dark', mockActions) - expect(result).toBe(mockActions.slash) + const result = matchAction('/theme dark', mockScopes) + expect(result).toBe(mockScopes[2]) }) it('should match all submenu commands', () => { - expect(matchAction('/language', mockActions)).toBe(mockActions.slash) - expect(matchAction('/language en', mockActions)).toBe(mockActions.slash) + expect(matchAction('/language', mockScopes)).toBe(mockScopes[2]) + expect(matchAction('/language en', mockScopes)).toBe(mockScopes[2]) }) }) describe('Slash Without Command', () => { it('should not match single slash', () => { - const result = matchAction('/', mockActions) + const result = matchAction('/', mockScopes) expect(result).toBeUndefined() }) it('should not match unregistered commands', () => { - const result = matchAction('/unknown', mockActions) + const result = matchAction('/unknown', mockScopes) expect(result).toBeUndefined() }) }) @@ -162,28 +165,28 @@ describe('matchAction Logic', () => { describe('Edge Cases', () => { it('should handle empty query', () => { - const result = matchAction('', mockActions) + const result = matchAction('', mockScopes) expect(result).toBeUndefined() }) it('should handle whitespace only', () => { - const result = matchAction(' ', mockActions) + const result = matchAction(' ', mockScopes) expect(result).toBeUndefined() }) it('should handle regular text without actions', () => { - const result = matchAction('search something', mockActions) + const result = matchAction('search something', mockScopes) expect(result).toBeUndefined() }) it('should handle special characters', () => { - const result = matchAction('#tag', mockActions) + const result = matchAction('#tag', mockScopes) expect(result).toBeUndefined() }) it('should handle multiple @ or /', () => { - expect(matchAction('@@app', mockActions)).toBeUndefined() - expect(matchAction('//theme', mockActions)).toBeUndefined() + expect(matchAction('@@app', mockScopes)).toBeUndefined() + expect(matchAction('//theme', mockScopes)).toBeUndefined() }) }) @@ -193,7 +196,7 @@ describe('matchAction Logic', () => { { name: 'test', mode: 'direct' }, ]) - const result = matchAction('/test', mockActions) + const result = matchAction('/test', mockScopes) expect(result).toBeUndefined() }) @@ -202,8 +205,8 @@ describe('matchAction Logic', () => { { name: 'test', mode: 'submenu' }, ]) - const result = matchAction('/test', mockActions) - expect(result).toBe(mockActions.slash) + const result = matchAction('/test', mockScopes) + expect(result).toBe(mockScopes[2]) }) it('should treat undefined mode as submenu', () => { @@ -211,25 +214,25 @@ describe('matchAction Logic', () => { { name: 'test' }, // No mode specified ]) - const result = matchAction('/test', mockActions) - expect(result).toBe(mockActions.slash) + const result = matchAction('/test', mockScopes) + expect(result).toBe(mockScopes[2]) }) }) describe('Registry Integration', () => { it('should call getAllCommands when matching slash', () => { - matchAction('/theme', mockActions) + matchAction('/theme', mockScopes) expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled() }) it('should not call getAllCommands for @ actions', () => { - matchAction('@app', mockActions) + matchAction('@app', mockScopes) expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled() }) it('should handle empty command list', () => { ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([]) - const result = matchAction('/anything', mockActions) + const result = matchAction('/anything', mockScopes) expect(result).toBeUndefined() }) }) diff --git a/web/__tests__/goto-anything/search-error-handling.test.ts b/web/__tests__/goto-anything/search-error-handling.test.ts index 42eb829583..2651d98f36 100644 --- a/web/__tests__/goto-anything/search-error-handling.test.ts +++ b/web/__tests__/goto-anything/search-error-handling.test.ts @@ -9,7 +9,7 @@ import type { MockedFunction } from 'vitest' * 4. Ensure errors don't propagate to UI layer causing "search failed" */ -import { Actions, searchAnything } from '@/app/components/goto-anything/actions' +import { appScope, knowledgeScope, pluginScope, searchAnything } from '@/app/components/goto-anything/actions' import { fetchAppList } from '@/service/apps' import { postMarketplace } from '@/service/base' import { fetchDatasets } from '@/service/datasets' @@ -57,10 +57,8 @@ describe('GotoAnything Search Error Handling', () => { // Mock marketplace API failure (403 permission denied) mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden')) - const pluginAction = Actions.plugin - // Directly call plugin action's search method - const result = await pluginAction.search('@plugin', 'test', 'en') + const result = await pluginScope.search('@plugin', 'test', 'en') // Should return empty array instead of throwing error expect(result).toEqual([]) @@ -80,8 +78,7 @@ describe('GotoAnything Search Error Handling', () => { data: { plugins: [] }, }) - const pluginAction = Actions.plugin - const result = await pluginAction.search('@plugin', '', 'en') + const result = await pluginScope.search('@plugin', '', 'en') expect(result).toEqual([]) }) @@ -92,8 +89,7 @@ describe('GotoAnything Search Error Handling', () => { data: null, }) - const pluginAction = Actions.plugin - const result = await pluginAction.search('@plugin', 'test', 'en') + const result = await pluginScope.search('@plugin', 'test', 'en') expect(result).toEqual([]) }) @@ -104,8 +100,7 @@ describe('GotoAnything Search Error Handling', () => { // Mock app API failure mockFetchAppList.mockRejectedValue(new Error('API Error')) - const appAction = Actions.app - const result = await appAction.search('@app', 'test', 'en') + const result = await appScope.search('@app', 'test', 'en') expect(result).toEqual([]) }) @@ -114,8 +109,7 @@ describe('GotoAnything Search Error Handling', () => { // Mock knowledge API failure mockFetchDatasets.mockRejectedValue(new Error('API Error')) - const knowledgeAction = Actions.knowledge - const result = await knowledgeAction.search('@knowledge', 'test', 'en') + const result = await knowledgeScope.search('@knowledge', 'test', 'en') expect(result).toEqual([]) }) @@ -128,19 +122,20 @@ describe('GotoAnything Search Error Handling', () => { mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 }) mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed')) - const result = await searchAnything('en', 'test') + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', 'test', undefined, allScopes) // Should return successful results even if plugin search fails expect(result).toEqual([]) - expect(console.warn).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error)) + expect(console.warn).toHaveBeenCalled() }) it('@plugin dedicated search should return empty array when API fails', async () => { // Mock plugin API failure mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable')) - const pluginAction = Actions.plugin - const result = await searchAnything('en', '@plugin test', pluginAction) + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', '@plugin test', pluginScope, allScopes) // Should return empty array instead of throwing error expect(result).toEqual([]) @@ -150,8 +145,8 @@ describe('GotoAnything Search Error Handling', () => { // Mock app API failure mockFetchAppList.mockRejectedValue(new Error('App service unavailable')) - const appAction = Actions.app - const result = await searchAnything('en', '@app test', appAction) + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', '@app test', appScope, allScopes) expect(result).toEqual([]) }) @@ -165,13 +160,13 @@ describe('GotoAnything Search Error Handling', () => { mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed')) const actions = [ - { name: '@plugin', action: Actions.plugin }, - { name: '@app', action: Actions.app }, - { name: '@knowledge', action: Actions.knowledge }, + { name: '@plugin', scope: pluginScope }, + { name: '@app', scope: appScope }, + { name: '@knowledge', scope: knowledgeScope }, ] - for (const { name, action } of actions) { - const result = await action.search(name, 'test', 'en') + for (const { name, scope } of actions) { + const result = await scope.search(name, 'test', 'en') expect(result).toEqual([]) } }) @@ -181,7 +176,8 @@ describe('GotoAnything Search Error Handling', () => { it('empty search term should be handled properly', async () => { mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } }) - const result = await searchAnything('en', '@plugin ', Actions.plugin) + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', '@plugin ', pluginScope, allScopes) expect(result).toEqual([]) }) @@ -191,7 +187,8 @@ describe('GotoAnything Search Error Handling', () => { mockPostMarketplace.mockRejectedValue(timeoutError) - const result = await searchAnything('en', '@plugin test', Actions.plugin) + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', '@plugin test', pluginScope, allScopes) expect(result).toEqual([]) }) @@ -199,7 +196,8 @@ describe('GotoAnything Search Error Handling', () => { const parseError = new SyntaxError('Unexpected token in JSON') mockPostMarketplace.mockRejectedValue(parseError) - const result = await searchAnything('en', '@plugin test', Actions.plugin) + const allScopes = [appScope, knowledgeScope, pluginScope] + const result = await searchAnything('en', '@plugin test', pluginScope, allScopes) expect(result).toEqual([]) }) }) diff --git a/web/app/components/goto-anything/actions/commands/banana.spec.tsx b/web/app/components/goto-anything/actions/commands/banana.spec.tsx index 47a1418aba..420cf40af6 100644 --- a/web/app/components/goto-anything/actions/commands/banana.spec.tsx +++ b/web/app/components/goto-anything/actions/commands/banana.spec.tsx @@ -1,16 +1,18 @@ import { isInWorkflowPage, VIBE_COMMAND_EVENT } from '@/app/components/workflow/constants' -import i18n from '@/i18n-config/i18next-config' import { bananaCommand } from './banana' import { registerCommands, unregisterCommands } from './command-bus' -vi.mock('@/i18n-config/i18next-config', () => ({ - default: { - t: vi.fn((key: string, options?: Record) => { - if (!options) - return key - return `${key}:${JSON.stringify(options)}` - }), - }, +// Mock i18n for testing +const mockI18n = { + t: vi.fn((key: string, options?: Record) => { + if (!options) + return key + return `${key}:${JSON.stringify(options)}` + }), +} + +vi.mock('react-i18next', () => ({ + getI18n: () => mockI18n, })) vi.mock('@/app/components/workflow/constants', async () => { @@ -31,7 +33,7 @@ vi.mock('./command-bus', () => ({ const mockedIsInWorkflowPage = vi.mocked(isInWorkflowPage) const mockedRegisterCommands = vi.mocked(registerCommands) const mockedUnregisterCommands = vi.mocked(unregisterCommands) -const mockedT = vi.mocked(i18n.t) +const mockedT = mockI18n.t type CommandArgs = { dsl?: string } type CommandMap = Record void | Promise> diff --git a/web/app/components/workflow/nodes/code/default.ts b/web/app/components/workflow/nodes/code/default.ts index afc0965347..3ed6226a6e 100644 --- a/web/app/components/workflow/nodes/code/default.ts +++ b/web/app/components/workflow/nodes/code/default.ts @@ -25,7 +25,7 @@ const nodeDefault: NodeDefault = { const { code, variables } = payload if (!errorMessages && variables.filter(v => !v.variable).length > 0) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) - if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0) + if (!errorMessages && variables.filter(v => !v.value_selector || !v.value_selector.length).length > 0) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) }) if (!errorMessages && !code) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.code`, { ns: 'workflow' }) }) diff --git a/web/app/components/workflow/nodes/llm/default.ts b/web/app/components/workflow/nodes/llm/default.ts index cdd5bfbe6a..9ea406cf91 100644 --- a/web/app/components/workflow/nodes/llm/default.ts +++ b/web/app/components/workflow/nodes/llm/default.ts @@ -95,7 +95,7 @@ const nodeDefault: NodeDefault = { payload.prompt_config?.jinja2_variables.forEach((i) => { if (!errorMessages && !i.variable) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) - if (!errorMessages && !i.value_selector.length) + if (!errorMessages && (!i.value_selector || !i.value_selector.length)) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) }) }) } diff --git a/web/app/components/workflow/nodes/template-transform/default.ts b/web/app/components/workflow/nodes/template-transform/default.ts index 5ca4388747..5018d4a493 100644 --- a/web/app/components/workflow/nodes/template-transform/default.ts +++ b/web/app/components/workflow/nodes/template-transform/default.ts @@ -24,7 +24,7 @@ const nodeDefault: NodeDefault = { if (!errorMessages && variables.filter(v => !v.variable).length > 0) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) - if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0) + if (!errorMessages && variables.filter(v => !v.value_selector || !v.value_selector.length).length > 0) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) }) if (!errorMessages && !template) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t('nodes.templateTransform.code', { ns: 'workflow' }) })