diff --git a/packages/frontend/editor-ui/src/app/composables/useReportWorkflowFailuresToParent.ts b/packages/frontend/editor-ui/src/app/composables/useReportWorkflowFailuresToParent.ts index c2ad37ba839..7593dc4c5c2 100644 --- a/packages/frontend/editor-ui/src/app/composables/useReportWorkflowFailuresToParent.ts +++ b/packages/frontend/editor-ui/src/app/composables/useReportWorkflowFailuresToParent.ts @@ -1,81 +1,62 @@ -import { computed, watch } from 'vue'; +import { onBeforeUnmount } from 'vue'; import { useRoute } from 'vue-router'; -import { VIEWS } from '@/app/constants'; -import { useWorkflowsStore } from '@/app/stores/workflows.store'; -import type { FixWithAiError } from '@/features/ai/instanceAi/useFixWithAiOffer'; import type { IRunData } from 'n8n-workflow'; +import { VIEWS } from '@/app/constants'; +import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store'; +import { usePushConnectionStore } from '@/app/stores/pushConnection.store'; +import type { FixWithAiError } from '@/features/ai/instanceAi/fixWithAi'; function collectErrorsFromRunData(runData: IRunData): FixWithAiError[] { const errors: FixWithAiError[] = []; - for (const [nodeName, tasks] of Object.entries(runData)) { - const lastTask = tasks?.at(-1); - const error = lastTask?.error; - + const error = tasks?.at(-1)?.error; if (!error) continue; - const description = error.description ? ` (${error.description})` : ''; - errors.push({ nodeName, errorMessage: `${error.message ?? 'Unknown error'}${description}`, }); } - return errors; } -export function useReportWorkflowFailuresToParent(workflowName?: () => string | undefined) { +/** + * Lives in the embedded canvas iframe. Listens for `executionFinished` push + * events on the iframe's own connection (parent has a separate `pushRef`, so + * iframe-triggered runs are not visible there) and forwards per-node failures + * to the parent via `postMessage` so the chat can surface a "Fix with AI" card. + * + * Reads the per-execution data store (keyed by the executionId we just got the + * finish event for) once per failed run — by then the in-iframe push handlers + * have already populated it. Avoids a deep `watch` over the whole run-data tree. + */ +export function useReportWorkflowFailuresToParent() { if (window.parent === window) return; const route = useRoute(); - const workflowsStore = useWorkflowsStore(); - const isEnabled = computed(() => route.name === VIEWS.DEMO && route.query.canExecute === 'true'); - let lastReportedKey: string | null = null; + const pushStore = usePushConnectionStore(); - watch( - [ - isEnabled, - () => workflowsStore.getWorkflowRunData, - () => workflowsStore.isWorkflowRunning, - () => workflowsStore.workflowId, - () => workflowsStore.getWorkflowExecution?.id, - ], - () => { - if (!isEnabled.value || workflowsStore.isWorkflowRunning) return; + const removeListener = pushStore.addEventListener((event) => { + if (route.name !== VIEWS.DEMO || route.query.canExecute !== 'true') return; + if (event.type !== 'executionFinished') return; + if (event.data.status === 'success') return; - const workflowId = workflowsStore.workflowId; - const runData = workflowsStore.getWorkflowRunData; + const execStore = useExecutionDataStore(createExecutionDataId(event.data.executionId)); + const runData = execStore.executionRunData; + if (!runData) return; + const errors = collectErrorsFromRunData(runData); + if (errors.length === 0) return; - if (!workflowId || !runData) return; + window.parent.postMessage( + JSON.stringify({ + command: 'reportWorkflowFailures', + workflowId: event.data.workflowId, + executionId: event.data.executionId, + errors, + }), + window.location.origin, + ); + }); - const errors = collectErrorsFromRunData(runData); - - if (errors.length === 0) return; - - const executionId = workflowsStore.getWorkflowExecution?.id; - const fingerprint = errors - .map((e) => `${e.nodeName}:${e.errorMessage}`) - .sort() - .join('|'); - - const dismissKey = executionId ?? fingerprint; - - if (!dismissKey || lastReportedKey === dismissKey) return; - - lastReportedKey = dismissKey; - - window.parent.postMessage( - JSON.stringify({ - command: 'reportWorkflowFailures', - workflowId, - workflowName: workflowName?.(), - executionId: dismissKey, - errors, - }), - window.location.origin, - ); - }, - { deep: true }, - ); + onBeforeUnmount(removeListener); } diff --git a/packages/frontend/editor-ui/src/app/layouts/DemoLayout.vue b/packages/frontend/editor-ui/src/app/layouts/DemoLayout.vue index 829b0155870..9fe16b64c02 100644 --- a/packages/frontend/editor-ui/src/app/layouts/DemoLayout.vue +++ b/packages/frontend/editor-ui/src/app/layouts/DemoLayout.vue @@ -43,7 +43,7 @@ const { setup: setupPostMessages, cleanup: cleanupPostMessages } = usePostMessag currentNDVStore, }); -useReportWorkflowFailuresToParent(() => currentWorkflowDocumentStore.value?.name ?? undefined); +useReportWorkflowFailuresToParent(); // Initialize push event handlers so relayed execution events (via postMessage // from the parent) are processed for node highlighting, execution state, etc. diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue index fe43801b82f..b31a320101d 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue @@ -50,7 +50,7 @@ import CreditWarningBanner from '@/features/ai/assistant/components/Agent/Credit import InstanceAiWorkflowPreview, { type WorkflowFailuresReport, } from './components/InstanceAiWorkflowPreview.vue'; -import { buildFixWithAiPrompt, useFixWithAiOffer } from './useFixWithAiOffer'; +import { buildFixWithAiPrompt } from './fixWithAi'; import InstanceAiDataTablePreview from './components/InstanceAiDataTablePreview.vue'; import { TabsRoot } from 'reka-ui'; @@ -87,40 +87,26 @@ const hasFloatingConfirmation = computed(() => thread.pendingConfirmations.some(isPendingItemFloating), ); -// --- Execution tracking via push events --- +// --- Execution tracking via push events (drives canvas relay) --- const executionTracking = useExecutionPushEvents(); -const fixWithAiOffer = useFixWithAiOffer(); + +// --- Fix-with-AI offer (failure data sent by the iframe via postMessage) --- +const failedRun = ref(null); +const dismissedExecutionId = ref(null); const isChatInProgress = computed( () => thread.isStreaming || thread.isSendingMessage || thread.isAwaitingConfirmation, ); -function findReadyFixWithAiOffer(workflowId?: string | null) { - const offers = fixWithAiOffer.offersByWorkflow.value; - - if (workflowId) { - const preferred = offers.get(workflowId); - if ( - preferred && - preferred.errors.length > 0 && - !fixWithAiOffer.isDismissed(preferred.executionId) - ) { - return preferred; - } - } - - for (const offer of offers.values()) { - if (offer.errors.length === 0) continue; - if (fixWithAiOffer.isDismissed(offer.executionId)) continue; - return offer; - } - - return null; -} - const activeFixWithAiOffer = computed(() => { + const run = failedRun.value; + if (!run) return null; + if (run.executionId === dismissedExecutionId.value) return null; if (isChatInProgress.value) return null; - return findReadyFixWithAiOffer(preview.activeWorkflowId.value); + return { + ...run, + workflowName: thread.producedArtifacts.get(run.workflowId)?.name, + }; }); // --- Header title --- @@ -499,7 +485,6 @@ onUnmounted(() => { contentResizeObserver?.disconnect(); resizeObserver?.disconnect(); executionTracking.cleanup(); - fixWithAiOffer.cleanup(); }); // --- Workflow preview ref for iframe relay --- @@ -529,7 +514,7 @@ function handleFixWithAiFromOffer() { const offer = activeFixWithAiOffer.value; if (!offer) return; - fixWithAiOffer.dismiss(offer.executionId); + dismissedExecutionId.value = offer.executionId; userScrolledUp.value = false; void thread.sendMessage( buildFixWithAiPrompt({ workflowName: offer.workflowName, errors: offer.errors }), @@ -541,11 +526,11 @@ function handleFixWithAiFromOffer() { function dismissFixWithAiOffer() { const offer = activeFixWithAiOffer.value; if (!offer) return; - fixWithAiOffer.dismiss(offer.executionId); + dismissedExecutionId.value = offer.executionId; } function handleWorkflowFailures(report: WorkflowFailuresReport) { - fixWithAiOffer.registerOffer(report); + failedRun.value = report; } diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts index a3bc983db2a..80747b9f9bf 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { defineComponent, h, ref } from 'vue'; +import userEvent from '@testing-library/user-event'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { createComponentRenderer } from '@/__tests__/render'; @@ -8,6 +9,7 @@ import InstanceAiThreadView from '../InstanceAiThreadView.vue'; import { useInstanceAiStore, type ThreadRuntime } from '../instanceAi.store'; import { usePushConnectionStore } from '@/app/stores/pushConnection.store'; import { SidebarStateKey } from '../instanceAiLayout'; +import type { WorkflowFailuresReport } from '../components/InstanceAiWorkflowPreview.vue'; const mockWindowSizeState = vi.hoisted(() => ({ width: { value: 1200 }, @@ -54,6 +56,20 @@ const InstanceAiInputStub = defineComponent({ }, }); +let workflowPreviewEmit: + | ((event: 'workflow-failures', payload: WorkflowFailuresReport) => void) + | null = null; + +const InstanceAiWorkflowPreviewStub = defineComponent({ + name: 'InstanceAiWorkflowPreviewStub', + emits: ['iframe-ready', 'workflow-loaded', 'workflow-failures'], + setup(_, { emit, expose }) { + workflowPreviewEmit = emit as typeof workflowPreviewEmit; + expose({ relayPushEvent: vi.fn(), requestFitView: vi.fn() }); + return () => h('div', { 'data-test-id': 'instance-ai-workflow-preview-stub' }); + }, +}); + const InstanceAiConfirmationPanelStub = defineComponent({ name: 'InstanceAiConfirmationPanelStub', props: { @@ -71,6 +87,7 @@ const renderView = createComponentRenderer(InstanceAiThreadView, { }, stubs: { InstanceAiInput: InstanceAiInputStub, + InstanceAiWorkflowPreview: InstanceAiWorkflowPreviewStub, InstanceAiConfirmationPanel: InstanceAiConfirmationPanelStub, }, }, @@ -85,6 +102,8 @@ describe('InstanceAiThreadView', () => { const pinia = createTestingPinia(); setActivePinia(pinia); + workflowPreviewEmit = null; + thread = { id: 'thread-1', messages: [], @@ -263,4 +282,75 @@ describe('InstanceAiThreadView', () => { }); expect(queryByTestId('instance-ai-artifacts-sidebar-slot')).not.toBeInTheDocument(); }); + + describe('Fix with AI card', () => { + const failureReport: WorkflowFailuresReport = { + workflowId: 'wf-1', + executionId: 'exec-1', + errors: [{ nodeName: 'Extract Emails', errorMessage: 'Intentional break' }], + }; + + function seedThreadArtifact(workflowId = 'wf-1', workflowName = 'My Workflow') { + thread.producedArtifacts = new Map([ + [workflowId, { type: 'workflow', id: workflowId, name: workflowName }], + ]) as typeof thread.producedArtifacts; + } + + async function emitFailure(report: WorkflowFailuresReport = failureReport) { + await vi.waitFor(() => { + expect(workflowPreviewEmit).not.toBeNull(); + }); + workflowPreviewEmit?.('workflow-failures', report); + } + + it('renders the card when the iframe reports a workflow failure', async () => { + seedThreadArtifact(); + const { getByTestId, findByTestId } = renderView({ props: { threadId: 'thread-1' } }); + + await emitFailure(); + + const panel = await findByTestId('instance-ai-fix-with-ai-panel'); + expect(panel).toHaveTextContent('Execution failed in ‘Extract Emails’ node'); + expect(getByTestId('instance-ai-fix-with-ai-button')).toBeInTheDocument(); + }); + + it('hides the card after dismiss', async () => { + seedThreadArtifact(); + const user = userEvent.setup(); + const { findByTestId, queryByTestId } = renderView({ props: { threadId: 'thread-1' } }); + + await emitFailure(); + await user.click(await findByTestId('instance-ai-fix-with-ai-dismiss')); + + await vi.waitFor(() => { + expect(queryByTestId('instance-ai-fix-with-ai-panel')).not.toBeInTheDocument(); + }); + }); + + it('sends a fix prompt that names the failed node, error and workflow', async () => { + seedThreadArtifact('wf-1', 'My Workflow'); + const user = userEvent.setup(); + const { findByTestId } = renderView({ props: { threadId: 'thread-1' } }); + + await emitFailure(); + await user.click(await findByTestId('instance-ai-fix-with-ai-button')); + + expect(thread.sendMessage).toHaveBeenCalledOnce(); + const [prompt] = vi.mocked(thread.sendMessage).mock.calls[0]; + expect(prompt).toContain('Extract Emails'); + expect(prompt).toContain('Intentional break'); + expect(prompt).toContain('My Workflow'); + }); + + it('hides the card while the chat is busy', async () => { + seedThreadArtifact(); + thread.isStreaming = true; + + const { queryByTestId } = renderView({ props: { threadId: 'thread-1' } }); + + await emitFailure(); + + expect(queryByTestId('instance-ai-fix-with-ai-panel')).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiWorkflowPreview.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiWorkflowPreview.test.ts index 6deb7b7865b..83823cde644 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiWorkflowPreview.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiWorkflowPreview.test.ts @@ -176,7 +176,6 @@ describe('InstanceAiWorkflowPreview', () => { dispatchIframeMessage({ command: 'reportWorkflowFailures', workflowId: 'wf-1', - workflowName: 'My Workflow', executionId: 'exec-1', errors: [{ nodeName: 'Extract Emails', errorMessage: 'Intentional break' }], }); @@ -188,7 +187,6 @@ describe('InstanceAiWorkflowPreview', () => { [ { workflowId: 'wf-1', - workflowName: 'My Workflow', executionId: 'exec-1', errors: [{ nodeName: 'Extract Emails', errorMessage: 'Intentional break' }], }, @@ -200,7 +198,6 @@ describe('InstanceAiWorkflowPreview', () => { const { emitted } = renderComponent({ props: { workflowId: null } }); dispatchIframeMessage({ command: 'reportWorkflowFailures', errors: [] }); - dispatchIframeMessage({ command: 'reportWorkflowFailures', workflowId: 'wf-1', diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/fixWithAi.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/fixWithAi.test.ts new file mode 100644 index 00000000000..9141366dac1 --- /dev/null +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/fixWithAi.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import { buildFixWithAiPrompt, isFixWithAiError } from '../fixWithAi'; + +vi.mock('@n8n/i18n', () => ({ + useI18n: () => ({ + baseText: (key: string, options?: { interpolate?: Record }) => { + if (!options?.interpolate) return key; + return `${key}:${JSON.stringify(options.interpolate)}`; + }, + }), +})); + +describe('isFixWithAiError', () => { + it('accepts valid error objects', () => { + expect(isFixWithAiError({ nodeName: 'HTTP Request', errorMessage: 'failed' })).toBe(true); + }); + + it('rejects invalid values', () => { + expect(isFixWithAiError(null)).toBe(false); + expect(isFixWithAiError({ nodeName: 'HTTP Request' })).toBe(false); + expect(isFixWithAiError({ nodeName: 1, errorMessage: 'x' })).toBe(false); + }); +}); + +describe('buildFixWithAiPrompt', () => { + it('builds a single-node prompt with workflow name', () => { + expect( + buildFixWithAiPrompt({ + workflowName: 'My Workflow', + errors: [{ nodeName: 'Extract Emails', errorMessage: 'boom' }], + }), + ).toContain('instanceAi.fixWithAi.prompt.singleInWorkflow'); + }); + + it('builds a single-node prompt without workflow name', () => { + expect( + buildFixWithAiPrompt({ + errors: [{ nodeName: 'Extract Emails', errorMessage: 'boom' }], + }), + ).toContain('instanceAi.fixWithAi.prompt.single'); + }); + + it('builds a multi-node prompt with workflow name', () => { + expect( + buildFixWithAiPrompt({ + workflowName: 'My Workflow', + errors: [ + { nodeName: 'Node A', errorMessage: 'first' }, + { nodeName: 'Node B', errorMessage: 'second' }, + ], + }), + ).toContain('instanceAi.fixWithAi.prompt.multipleInWorkflow'); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useFixWithAiOffer.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useFixWithAiOffer.test.ts deleted file mode 100644 index 51d72975456..00000000000 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useFixWithAiOffer.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { buildFixWithAiPrompt, isFixWithAiError, useFixWithAiOffer } from '../useFixWithAiOffer'; - -vi.mock('@n8n/i18n', () => ({ - useI18n: () => ({ - baseText: (key: string, options?: { interpolate?: Record }) => { - if (!options?.interpolate) return key; - return `${key}:${JSON.stringify(options.interpolate)}`; - }, - }), -})); - -describe('isFixWithAiError', () => { - it('accepts valid error objects', () => { - expect(isFixWithAiError({ nodeName: 'HTTP Request', errorMessage: 'failed' })).toBe(true); - }); - - it('rejects invalid values', () => { - expect(isFixWithAiError(null)).toBe(false); - expect(isFixWithAiError({ nodeName: 'HTTP Request' })).toBe(false); - }); -}); - -describe('buildFixWithAiPrompt', () => { - it('builds a single-node prompt with workflow name', () => { - expect( - buildFixWithAiPrompt({ - workflowName: 'My Workflow', - errors: [{ nodeName: 'Extract Emails', errorMessage: 'boom' }], - }), - ).toContain('instanceAi.fixWithAi.prompt.singleInWorkflow'); - }); - - it('builds a multi-node prompt', () => { - const prompt = buildFixWithAiPrompt({ - workflowName: 'My Workflow', - errors: [ - { nodeName: 'Node A', errorMessage: 'first' }, - { nodeName: 'Node B', errorMessage: 'second' }, - ], - }); - - expect(prompt).toContain('instanceAi.fixWithAi.prompt.multipleInWorkflow'); - }); -}); - -describe('useFixWithAiOffer', () => { - it('registerOffer stores failures reported from the preview iframe', () => { - const { offersByWorkflow, registerOffer } = useFixWithAiOffer(); - - registerOffer({ - workflowId: 'wf-1', - workflowName: 'My Workflow', - executionId: 'exec-99', - errors: [{ nodeName: 'Extract Emails', errorMessage: 'boom' }], - }); - - expect(offersByWorkflow.value.get('wf-1')).toEqual({ - workflowId: 'wf-1', - workflowName: 'My Workflow', - executionId: 'exec-99', - errors: [{ nodeName: 'Extract Emails', errorMessage: 'boom' }], - }); - }); - - it('ignores registerOffer when errors array is empty', () => { - const { offersByWorkflow, registerOffer } = useFixWithAiOffer(); - - registerOffer({ - workflowId: 'wf-1', - executionId: 'exec-99', - errors: [], - }); - - expect(offersByWorkflow.value.has('wf-1')).toBe(false); - }); - - it('tracks dismiss state by execution id', () => { - const { dismiss, isDismissed } = useFixWithAiOffer(); - - dismiss('exec-1'); - expect(isDismissed('exec-1')).toBe(true); - expect(isDismissed('exec-2')).toBe(false); - }); - - it('cleanup clears offers and dismiss state', () => { - const { offersByWorkflow, registerOffer, dismiss, cleanup, isDismissed } = useFixWithAiOffer(); - - registerOffer({ - workflowId: 'wf-1', - executionId: 'exec-1', - errors: [{ nodeName: 'Node', errorMessage: 'failed' }], - }); - dismiss('exec-1'); - cleanup(); - - expect(offersByWorkflow.value.size).toBe(0); - expect(isDismissed('exec-1')).toBe(false); - }); -}); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowPreview.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowPreview.vue index 1e997b6c74f..89b2eeda925 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowPreview.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiWorkflowPreview.vue @@ -6,11 +6,7 @@ import type { PushMessage } from '@n8n/api-types'; import WorkflowPreview from '@/app/components/WorkflowPreview.vue'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; import type { IWorkflowDb } from '@/Interface'; -import { - isFixWithAiError, - type FixWithAiError, - type FixWithAiOfferState, -} from '../useFixWithAiOffer'; +import { isFixWithAiError, type FixWithAiError } from '../fixWithAi'; const props = withDefaults( defineProps<{ @@ -21,7 +17,11 @@ const props = withDefaults( { refreshKey: 0 }, ); -export type WorkflowFailuresReport = FixWithAiOfferState; +export interface WorkflowFailuresReport { + workflowId: string; + executionId: string; + errors: FixWithAiError[]; +} const emit = defineEmits<{ 'iframe-ready': []; @@ -49,11 +49,6 @@ function getPreviewIframe(): HTMLIFrameElement | null { ); } -function parseWorkflowFailureErrors(errors: unknown): FixWithAiError[] { - if (!Array.isArray(errors)) return []; - return errors.filter(isFixWithAiError); -} - function isMessageFromPreviewIframe(event: MessageEvent): boolean { if (event.origin !== window.location.origin) return false; @@ -63,6 +58,11 @@ function isMessageFromPreviewIframe(event: MessageEvent): boolean { return event.source === iframeWindow; } +function parseWorkflowFailureErrors(errors: unknown): FixWithAiError[] { + if (!Array.isArray(errors)) return []; + return errors.filter(isFixWithAiError); +} + function handleIframeMessage(event: MessageEvent) { if (!isMessageFromPreviewIframe(event)) return; if (typeof event.data !== 'string' || !event.data.includes('"command"')) return; @@ -81,7 +81,6 @@ function handleIframeMessage(event: MessageEvent) { } emit('workflow-failures', { workflowId: json.workflowId, - workflowName: typeof json.workflowName === 'string' ? json.workflowName : undefined, executionId: json.executionId, errors, }); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/useFixWithAiOffer.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/fixWithAi.ts similarity index 56% rename from packages/frontend/editor-ui/src/features/ai/instanceAi/useFixWithAiOffer.ts rename to packages/frontend/editor-ui/src/features/ai/instanceAi/fixWithAi.ts index a0923f2c191..04e723ca391 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/useFixWithAiOffer.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/fixWithAi.ts @@ -1,4 +1,3 @@ -import { ref } from 'vue'; import { useI18n } from '@n8n/i18n'; export interface FixWithAiError { @@ -6,13 +5,6 @@ export interface FixWithAiError { errorMessage: string; } -export interface FixWithAiOfferState { - workflowId: string; - workflowName?: string; - executionId: string; - errors: FixWithAiError[]; -} - export function isFixWithAiError(value: unknown): value is FixWithAiError { return ( typeof value === 'object' && @@ -22,9 +14,10 @@ export function isFixWithAiError(value: unknown): value is FixWithAiError { ); } -export function buildFixWithAiPrompt( - context: Pick, -): string { +export function buildFixWithAiPrompt(context: { + workflowName?: string; + errors: FixWithAiError[]; +}): string { const i18n = useI18n(); if (context.errors.length === 1) { @@ -59,37 +52,3 @@ export function buildFixWithAiPrompt( interpolate: { errorList }, }); } - -export function useFixWithAiOffer() { - const offersByWorkflow = ref(new Map()); - const dismissedExecutionIds = ref(new Set()); - - function registerOffer(input: FixWithAiOfferState) { - if (input.errors.length === 0) return; - - const next = new Map(offersByWorkflow.value); - next.set(input.workflowId, input); - offersByWorkflow.value = next; - } - - function dismiss(executionId: string) { - dismissedExecutionIds.value = new Set([...dismissedExecutionIds.value, executionId]); - } - - function isDismissed(executionId: string): boolean { - return dismissedExecutionIds.value.has(executionId); - } - - function cleanup() { - offersByWorkflow.value = new Map(); - dismissedExecutionIds.value = new Set(); - } - - return { - offersByWorkflow, - registerOffer, - dismiss, - isDismissed, - cleanup, - }; -}