diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 445fdea0154..af23a30c62f 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1189,6 +1189,8 @@ "duplicateWorkflowDialog.errors.forbidden.message": "This action is forbidden. Do you have the correct permissions?", "duplicateWorkflowDialog.errors.generic.title": "Duplicate workflow failed", "editor.mainHeader.githubButton.label": "Star n8n-io/n8n on GitHub", + "experiments.instanceAiProactiveAgent.message": "Hey, I can build your first workflow in a few minutes. Do you know what you want to automate, or do you want help with ideas?", + "experiments.instanceAiProactiveAgent.typingLabel": "AI Assistant is typing", "experiments.personalizedTemplatesV3.browseAllTemplates": "Browse our template library", "experiments.personalizedTemplatesV3.couldntFind": "Need something different?", "experiments.personalizedTemplatesV3.exploreTemplates": "Get started with HubSpot workflows:", diff --git a/packages/frontend/editor-ui/src/app/constants/experiments.ts b/packages/frontend/editor-ui/src/app/constants/experiments.ts index 1cd18b8263d..653add26191 100644 --- a/packages/frontend/editor-ui/src/app/constants/experiments.ts +++ b/packages/frontend/editor-ui/src/app/constants/experiments.ts @@ -94,6 +94,9 @@ export const CODE_WORKFLOW_BUILDER_EXPERIMENT = createExperiment('071_coding_wor }); export const AI_BUILDER_SETUP_WIZARD_EXPERIMENT = createExperiment('079_ai_builder_setup_wizard'); +export const INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT = createExperiment( + '082_instance_ai_proactive_agent', +); export const AA_EXPERIMENT_CHECK = createExperiment('078_experiment_check_aa'); export const CHAT_HUB_SEMANTIC_SEARCH_EXPERIMENT = createExperiment('077_chat_hub_semantic_search'); @@ -122,6 +125,7 @@ export const EXPERIMENTS_TO_TRACK = [ AI_BUILDER_REVIEW_CHANGES_EXPERIMENT.name, MERGE_ASK_BUILD_EXPERIMENT.name, AI_BUILDER_SETUP_WIZARD_EXPERIMENT.name, + INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.name, AA_EXPERIMENT_CHECK.name, CHAT_HUB_SEMANTIC_SEARCH_EXPERIMENT.name, FLOATING_CHAT_HUB_PANEL_EXPERIMENT.name, diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.test.ts b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.test.ts new file mode 100644 index 00000000000..cd96ee9d3be --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.test.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest'; + +import { createComponentRenderer } from '@/__tests__/render'; +import InstanceAiProactiveStarterMessage from './InstanceAiProactiveStarterMessage.vue'; + +const renderComponent = createComponentRenderer(InstanceAiProactiveStarterMessage); + +const PROACTIVE_MESSAGE = + 'Hey, I can build your first workflow in a few minutes. Do you know what you want to automate, or do you want help with ideas?'; +const OLD_PROACTIVE_MESSAGE = + 'I can help with workflow ideas and build the workflow for you. What would you like to automate?'; + +describe('InstanceAiProactiveStarterMessage', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('delays, shows typing, then reveals the polished proactive assistant message', async () => { + const { getByTestId, queryByTestId, queryByText } = renderComponent(); + + expect(queryByTestId('instance-ai-proactive-starter')).not.toBeInTheDocument(); + + await vi.advanceTimersByTimeAsync(800); + + expect(getByTestId('instance-ai-proactive-starter')).toBeVisible(); + expect(getByTestId('instance-ai-proactive-typing')).toBeVisible(); + expect(queryByText(PROACTIVE_MESSAGE)).not.toBeInTheDocument(); + + await vi.advanceTimersByTimeAsync(600); + + expect(queryByTestId('instance-ai-proactive-typing')).not.toBeInTheDocument(); + expect(getByTestId('instance-ai-proactive-message')).toHaveTextContent(PROACTIVE_MESSAGE); + expect(queryByText(OLD_PROACTIVE_MESSAGE)).not.toBeInTheDocument(); + expect(queryByTestId('instance-ai-suggestion-build-workflow')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.vue b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.vue new file mode 100644 index 00000000000..1acc79e4518 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/components/InstanceAiProactiveStarterMessage.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/index.ts b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/index.ts new file mode 100644 index 00000000000..6357f825336 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/index.ts @@ -0,0 +1,2 @@ +export { useInstanceAiProactiveAgentExperiment } from './useInstanceAiProactiveAgentExperiment'; +export { default as InstanceAiProactiveStarterMessage } from './components/InstanceAiProactiveStarterMessage.vue'; diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.test.ts b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.test.ts new file mode 100644 index 00000000000..6e40296c113 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT } from '@/app/constants/experiments'; +import { useInstanceAiProactiveAgentExperiment } from './useInstanceAiProactiveAgentExperiment'; + +const getVariant = vi.fn(); + +vi.mock('@/app/stores/posthog.store', () => ({ + usePostHog: vi.fn(() => ({ + getVariant, + })), +})); + +describe('useInstanceAiProactiveAgentExperiment', () => { + beforeEach(() => { + getVariant.mockReset(); + }); + + it.each([ + { variant: INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.variant, enabled: true }, + { variant: INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.control, enabled: false }, + { variant: undefined, enabled: false }, + ])('returns $enabled when PostHog variant is $variant', ({ variant, enabled }) => { + getVariant.mockReturnValue(variant); + + const { isFeatureEnabled } = useInstanceAiProactiveAgentExperiment(); + + expect(isFeatureEnabled.value).toBe(enabled); + expect(getVariant).toHaveBeenCalledWith(INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.name); + }); +}); diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.ts b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.ts new file mode 100644 index 00000000000..f4efffe0edd --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiProactiveAgent/useInstanceAiProactiveAgentExperiment.ts @@ -0,0 +1,16 @@ +import { computed } from 'vue'; + +import { INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT } from '@/app/constants/experiments'; +import { usePostHog } from '@/app/stores/posthog.store'; + +export function useInstanceAiProactiveAgentExperiment() { + const posthogStore = usePostHog(); + + const isFeatureEnabled = computed( + () => + posthogStore.getVariant(INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.name) === + INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.variant, + ); + + return { isFeatureEnabled }; +} diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiEmptyView.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiEmptyView.vue index 02ddb876872..b4bd131293d 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiEmptyView.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiEmptyView.vue @@ -1,5 +1,5 @@