From 3386aa81ccbfa2ed21ff7ac2b00369fb5a1376c7 Mon Sep 17 00:00:00 2001 From: Romeo Balta <7095569+romeobalta@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:53:22 +0100 Subject: [PATCH] fix(editor): Insert instance ai prompt suggestions instead of submit (#32008) --- .../WorkflowPreviewSuggestions.test.ts | 51 ++++++++++ .../components/WorkflowPreviewSuggestions.vue | 6 +- .../__tests__/InstanceAiInput.test.ts | 96 ++++++------------- .../InstanceAiPromptSuggestions.test.ts | 14 +-- .../instanceAi/components/InstanceAiInput.vue | 11 --- .../InstanceAiPromptSuggestions.vue | 12 +-- 6 files changed, 94 insertions(+), 96 deletions(-) create mode 100644 packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.test.ts diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.test.ts b/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.test.ts new file mode 100644 index 00000000000..50a031aef06 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.test.ts @@ -0,0 +1,51 @@ +import { fireEvent } from '@testing-library/vue'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createComponentRenderer } from '@/__tests__/render'; +import { INSTANCE_AI_WORKFLOW_PREVIEW_SUGGESTIONS as suggestions } from '../suggestions'; +import WorkflowPreviewSuggestions from './WorkflowPreviewSuggestions.vue'; + +const telemetryTrack = vi.fn(); + +vi.mock('@/app/composables/useTelemetry', () => ({ + useTelemetry: () => ({ track: telemetryTrack }), +})); + +const renderComponent = createComponentRenderer(WorkflowPreviewSuggestions, { + props: { + suggestions, + disabled: false, + }, +}); + +describe('WorkflowPreviewSuggestions', () => { + beforeEach(() => { + telemetryTrack.mockReset(); + }); + + it('inserts the clicked suggestion without submitting it', async () => { + const { emitted, getByTestId } = renderComponent(); + const suggestion = suggestions[1]; + + expect(suggestion).toBeDefined(); + if (!suggestion) throw new Error('Missing workflow preview suggestion fixture'); + + await fireEvent.click(getByTestId(`instance-ai-suggestion-${suggestion.id}`)); + + expect(emitted()['insert-suggestion']).toEqual([ + [ + { + promptKey: suggestion.promptKey, + suggestionId: suggestion.id, + suggestionKind: 'prompt', + position: 2, + }, + ], + ]); + expect(emitted()['submit-suggestion']).toBeUndefined(); + expect(emitted()['preview-change']).toEqual([[null]]); + expect(emitted()['workflow-preview']).toEqual([[null]]); + expect(telemetryTrack).toHaveBeenCalledWith('AI Assistant suggestion button clicked', { + suggestion_id: suggestion.id, + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.vue b/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.vue index d70498b0365..5ed0d83bb97 100644 --- a/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.vue +++ b/packages/frontend/editor-ui/src/experiments/instanceAiWorkflowPreviewSuggestions/components/WorkflowPreviewSuggestions.vue @@ -12,7 +12,7 @@ const props = defineProps<{ disabled: boolean; }>(); -interface SubmitSuggestionPayload { +interface InsertSuggestionPayload { promptKey: BaseTextKey; suggestionId: string; suggestionKind: 'prompt'; @@ -21,7 +21,7 @@ interface SubmitSuggestionPayload { const emit = defineEmits<{ 'preview-change': [promptKey: BaseTextKey | null]; - 'submit-suggestion': [payload: SubmitSuggestionPayload]; + 'insert-suggestion': [payload: InsertSuggestionPayload]; 'workflow-preview': [workflowFile: string | null]; }>(); @@ -88,7 +88,7 @@ function handleSuggestionClick(suggestion: WorkflowPreviewSuggestion) { }); clearPreview(); - emit('submit-suggestion', { + emit('insert-suggestion', { promptKey: suggestion.promptKey, suggestionId: suggestion.id, suggestionKind: 'prompt', diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts index bb00b96198e..c6e17dfe175 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiInput.test.ts @@ -50,46 +50,6 @@ vi.mock('@/app/composables/useTelemetry', () => ({ useTelemetry: vi.fn(() => ({ track: telemetryTrack })), })); -const CustomSuggestionsComponent = defineComponent({ - name: 'CustomSuggestionsComponent', - props: { - suggestions: { - type: Array as PropType, - required: true, - }, - disabled: { - type: Boolean, - required: true, - }, - }, - emits: ['submit-suggestion'], - setup(props, { emit }) { - return () => - h( - 'button', - { - type: 'button', - 'data-test-id': 'custom-suggestion-submit', - disabled: props.disabled, - onClick: () => { - const [suggestion] = props.suggestions; - if (!suggestion || !isPromptSuggestion(suggestion)) { - return; - } - - emit('submit-suggestion', { - promptKey: suggestion.promptKey, - suggestionId: 'custom-build-workflow', - suggestionKind: 'prompt', - position: 1, - }); - }, - }, - 'Custom suggestion', - ); - }, -}); - const CustomInsertSuggestionsComponent = defineComponent({ name: 'CustomInsertSuggestionsComponent', props: { @@ -364,7 +324,7 @@ describe('InstanceAiInput', () => { expect(textbox).toHaveAttribute('placeholder', initialPlaceholder); }); - it('submits immediately when a prompt suggestion is clicked', async () => { + it('inserts a prompt suggestion and submits it only when send is clicked', async () => { const { emitted, getByRole, getByTestId } = renderComponent({ props: { isStreaming: false, @@ -375,6 +335,13 @@ describe('InstanceAiInput', () => { const textbox = getByRole('textbox'); await userEvent.click(getByTestId('instance-ai-suggestion-build-agent')); + expect(emitted().submit).toBeUndefined(); + expect(textbox).toHaveValue( + 'I want to build a new agent. Help me figure out what to build. Ask me what the main purpose of the agent is, what should trigger it into action, what apps, tools, or knowledge it should have access to, and whether I have a preference for the AI model used.', + ); + + await userEvent.click(getByTestId('instance-ai-send-button')); + expect(emitted().submit?.[0]).toEqual([ 'I want to build a new agent. Help me figure out what to build. Ask me what the main purpose of the agent is, what should trigger it into action, what apps, tools, or knowledge it should have access to, and whether I have a preference for the AI model used.', undefined, @@ -382,25 +349,6 @@ describe('InstanceAiInput', () => { expect(textbox).toHaveValue(''); }); - it('submits from a caller-provided suggestions component through the existing flow', async () => { - const { emitted, getByRole, getByTestId } = renderComponent({ - props: { - isStreaming: false, - suggestions, - suggestionsComponent: CustomSuggestionsComponent, - }, - }); - - const textbox = getByRole('textbox'); - await userEvent.click(getByTestId('custom-suggestion-submit')); - - expect(emitted().submit?.[0]).toEqual([ - "I want to build a new workflow. Help me figure out what to build. Ask me what's the end goal, what should trigger it, and what apps or services are involved.", - undefined, - ]); - expect(textbox).toHaveValue(''); - }); - it('inserts from a caller-provided suggestions component without submitting', async () => { const { emitted, getByRole, getByTestId, queryByTestId } = renderComponent({ props: { @@ -552,7 +500,7 @@ describe('InstanceAiInput', () => { expect(textbox).toHaveValue(''); }); - it('opens quick examples and submits immediately when an example is clicked', async () => { + it('opens quick examples and inserts an example without submitting', async () => { const { emitted, getByRole, getByTestId, queryByTestId } = renderComponent({ props: { isStreaming: false, @@ -570,11 +518,10 @@ describe('InstanceAiInput', () => { await userEvent.click(getByTestId('instance-ai-quick-example-answer-support-requests')); const textbox = getByRole('textbox'); - expect(emitted().submit?.[0]).toEqual([ + expect(emitted().submit).toBeUndefined(); + expect(textbox).toHaveValue( 'When a new email arrives in our Outlook inbox, use Claude to summarize what the prospect is looking for, rate its urgency and potential value, then notify the right person in Slack based on the product and region of the prospect.', - undefined, - ]); - expect(textbox).toHaveValue(''); + ); expect(queryByTestId('instance-ai-quick-examples-panel')).not.toBeInTheDocument(); }); @@ -872,9 +819,9 @@ describe('InstanceAiInput', () => { }); }); - it('tracks top-level suggestion selection before submit', async () => { + it('tracks top-level suggestion selection when inserting into the composer', async () => { const onSubmit = vi.fn(); - const { getByTestId } = renderComponent({ + const { getByRole, getByTestId, queryByTestId } = renderComponent({ props: { isStreaming: false, suggestions, @@ -891,6 +838,12 @@ describe('InstanceAiInput', () => { await userEvent.click(getByTestId('instance-ai-suggestion-build-workflow')); + await waitFor(() => { + expect(getByRole('textbox')).toHaveValue( + "I want to build a new workflow. Help me figure out what to build. Ask me what's the end goal, what should trigger it, and what apps or services are involved.", + ); + expect(queryByTestId('instance-ai-suggestion-build-workflow')).not.toBeInTheDocument(); + }); expect(telemetryTrack).toHaveBeenCalledWith('Instance AI prompt suggestion selected', { thread_id: 'thread-1', suggestion_catalog_version: 'v1', @@ -898,7 +851,7 @@ describe('InstanceAiInput', () => { suggestion_kind: 'prompt', position: 1, }); - expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).not.toHaveBeenCalled(); }); it('tracks quick-example suggestion selection with semantic payload', async () => { @@ -925,7 +878,7 @@ describe('InstanceAiInput', () => { }); it('never includes prompt text in telemetry payloads', async () => { - const { getByTestId } = renderComponent({ + const { getByRole, getByTestId } = renderComponent({ props: { isStreaming: false, suggestions, @@ -934,6 +887,11 @@ describe('InstanceAiInput', () => { telemetryTrack.mockClear(); await userEvent.click(getByTestId('instance-ai-suggestion-build-workflow')); + const textbox = getByRole('textbox'); + await userEvent.clear(textbox); + await waitFor(() => { + expect(getByTestId('instance-ai-suggestion-quick-examples')).toBeVisible(); + }); await userEvent.click(getByTestId('instance-ai-suggestion-quick-examples')); await userEvent.click(getByTestId('instance-ai-quick-example-answer-support-requests')); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiPromptSuggestions.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiPromptSuggestions.test.ts index a8e690d8153..820a0d480cd 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiPromptSuggestions.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiPromptSuggestions.test.ts @@ -37,7 +37,7 @@ describe('InstanceAiPromptSuggestions', () => { ]); }); - it('emits semantic suggestion events for quick examples and prompts', async () => { + it('emits semantic insert events for quick examples and prompts', async () => { const { emitted, getByTestId } = renderComponent({ props: { suggestions, @@ -58,7 +58,7 @@ describe('InstanceAiPromptSuggestions', () => { await userEvent.click(getByTestId('instance-ai-suggestion-quick-examples')); await userEvent.click(getByTestId('instance-ai-quick-example-answer-support-requests')); - expect(emitted()['submit-suggestion']).toEqual([ + expect(emitted()['insert-suggestion']).toEqual([ [ { promptKey: 'instanceAi.emptyState.suggestions.buildAgent.prompt', @@ -92,7 +92,7 @@ describe('InstanceAiPromptSuggestions', () => { await new Promise((resolve) => setTimeout(resolve, 350)); - expect(emitted()['submit-suggestion']).toEqual([ + expect(emitted()['insert-suggestion']).toEqual([ [ { promptKey: 'instanceAi.emptyState.suggestions.buildWorkflow.prompt', @@ -139,7 +139,7 @@ describe('InstanceAiPromptSuggestions', () => { expect(emitted()['preview-change']?.at(-1)).toEqual([null]); }); - it('does not emit preview or submit events while disabled', async () => { + it('does not emit preview or insert events while disabled', async () => { const { emitted, getByTestId, queryByTestId } = renderComponent({ props: { suggestions, @@ -154,7 +154,7 @@ describe('InstanceAiPromptSuggestions', () => { expect(queryByTestId('instance-ai-quick-examples-panel')).not.toBeInTheDocument(); expect(emitted()['preview-change']).toBeUndefined(); - expect(emitted()['submit-suggestion']).toBeUndefined(); + expect(emitted()['insert-suggestion']).toBeUndefined(); expect(emitted()['quick-examples-opened']).toBeUndefined(); }); @@ -171,7 +171,7 @@ describe('InstanceAiPromptSuggestions', () => { const beforePreviewChanges = emitted()['preview-change']?.length ?? 0; const beforeQuickExamplesOpened = emitted()['quick-examples-opened']?.length ?? 0; - const beforeSubmitSuggestion = emitted()['submit-suggestion']?.length ?? 0; + const beforeInsertSuggestion = emitted()['insert-suggestion']?.length ?? 0; await userEvent.hover(getByTestId('instance-ai-quick-example-monitor-competitors')); await userEvent.unhover(getByTestId('instance-ai-quick-example-monitor-competitors')); @@ -180,7 +180,7 @@ describe('InstanceAiPromptSuggestions', () => { expect(getByTestId('instance-ai-quick-examples-panel')).toBeVisible(); expect(emitted()['preview-change']?.length ?? 0).toBe(beforePreviewChanges); expect(emitted()['quick-examples-opened']?.length ?? 0).toBe(beforeQuickExamplesOpened); - expect(emitted()['submit-suggestion']?.length ?? 0).toBe(beforeSubmitSuggestion); + expect(emitted()['insert-suggestion']?.length ?? 0).toBe(beforeInsertSuggestion); }); it('closes quick examples and clears preview when clicking outside', async () => { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue index 0456516e8d8..19f3f7a5dbc 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiInput.vue @@ -20,7 +20,6 @@ type SuggestionSelectionPayload = { suggestionKind: 'prompt' | 'quick_example'; position: number; }; -// Experiment cleanup: remove with instanceAiPromptSuggestionsV2. type SelectedSuggestionDraft = SuggestionSelectionPayload & { originalPrompt: string; }; @@ -72,7 +71,6 @@ const inputText = ref(''); const attachedFiles = ref([]); const chatInputRef = ref | null>(null); const previewPromptKey = ref(null); -// Experiment cleanup: remove with instanceAiPromptSuggestionsV2. const selectedSuggestionDraft = ref(null); function focus() { @@ -157,7 +155,6 @@ watch( { immediate: true }, ); -// Experiment cleanup: remove with instanceAiPromptSuggestionsV2. watch(inputText, (text) => { if (text.length === 0) { selectedSuggestionDraft.value = null; @@ -245,7 +242,6 @@ function getTelemetryContext() { }; } -// Experiment cleanup: remove with instanceAiPromptSuggestionsV2. function trackSelectedSuggestionSubmitted(message: string) { const selectedSuggestion = selectedSuggestionDraft.value; if (!selectedSuggestion) { @@ -291,12 +287,6 @@ function handleSuggestionsCycled(payload: SuggestionsCyclePayload) { }); } -function handleSuggestionSubmit(payload: SuggestionSelectionPayload) { - trackSuggestionSelected(payload); - submitComposerMessage(i18n.baseText(payload.promptKey)); -} - -// Experiment cleanup: remove with instanceAiPromptSuggestionsV2. async function handleSuggestionInsert(payload: SuggestionSelectionPayload) { trackSuggestionSelected(payload); previewPromptKey.value = null; @@ -391,7 +381,6 @@ const resizable = computed(() => { @quick-examples-opened="handleQuickExamplesOpened" @cycle-suggestions="handleSuggestionsCycled" @insert-suggestion="handleSuggestionInsert" - @submit-suggestion="handleSuggestionSubmit" @workflow-preview="emit('workflow-preview', $event)" /> diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiPromptSuggestions.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiPromptSuggestions.vue index 17d01888fe1..fc60e43fada 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiPromptSuggestions.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiPromptSuggestions.vue @@ -14,7 +14,7 @@ const props = defineProps<{ disabled: boolean; }>(); -interface SubmitSuggestionPayload { +interface InsertSuggestionPayload { promptKey: BaseTextKey; suggestionId: string; suggestionKind: 'prompt' | 'quick_example'; @@ -24,7 +24,7 @@ interface SubmitSuggestionPayload { const emit = defineEmits<{ 'preview-change': [promptKey: BaseTextKey | null]; 'quick-examples-opened': [payload: { suggestionId: string; position: number }]; - 'submit-suggestion': [payload: SubmitSuggestionPayload]; + 'insert-suggestion': [payload: InsertSuggestionPayload]; }>(); const i18n = useI18n(); @@ -70,13 +70,13 @@ function getQuickExamplePosition(exampleId: string) { return index >= 0 ? index + 1 : 0; } -function submitSuggestion(payload: SubmitSuggestionPayload) { +function insertSuggestion(payload: InsertSuggestionPayload) { if (props.disabled) { return; } closeQuickExamples(); - emit('submit-suggestion', payload); + emit('insert-suggestion', payload); } function handleDocumentKeydown(event: KeyboardEvent) { @@ -142,7 +142,7 @@ function handleSuggestionClick(suggestion: InstanceAiEmptyStateSuggestion) { clearHoverTimer(); if (isPromptSuggestion(suggestion)) { - submitSuggestion({ + insertSuggestion({ promptKey: suggestion.promptKey, suggestionId: suggestion.id, suggestionKind: 'prompt', @@ -251,7 +251,7 @@ function handleQuickExampleLeave() { :data-test-id="`instance-ai-quick-example-${example.id}`" :disabled="props.disabled" @click=" - submitSuggestion({ + insertSuggestion({ promptKey: example.promptKey, suggestionId: example.id, suggestionKind: 'quick_example',