feat(editor): Add personalized Instance AI prompt suggestions (no-changelog) (backport to release-candidate/2.29.x) (#33292)

Co-authored-by: Romeo Balta <7095569+romeobalta@users.noreply.github.com>
This commit is contained in:
n8n-assistant[bot] 2026-06-30 14:32:36 +01:00 committed by GitHub
parent 20c11c09d0
commit bce40f26fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 148 additions and 47 deletions

View File

@ -1255,8 +1255,10 @@
"experiments.instanceAiPersonalizedPromptSuggestions.fallbackSuggestions.qualifyInboundLeads.description": "Research, score, and route new leads.",
"experiments.instanceAiPersonalizedPromptSuggestions.fallbackSuggestions.scheduleSocialPosts.description": "Turn weekly ideas into scheduled posts.",
"experiments.instanceAiPersonalizedPromptSuggestions.fallbackSuggestions.whatsappSupportAgent.description": "Reply to common support questions instantly.",
"experiments.instanceAiPersonalizedPromptSuggestions.heading": "Suggested for you",
"experiments.instanceAiPersonalizedPromptSuggestions.heading": "Start from an example",
"experiments.instanceAiPersonalizedPromptSuggestions.helper": "Picked for you. Change anything.",
"experiments.instanceAiPersonalizedPromptSuggestions.seeMore": "See more",
"experiments.instanceAiPersonalizedPromptSuggestions.input.placeholder": "Tip: Ask me for suggestions if you're not sure what to build",
"experiments.instanceAiSplitEmptyState.canvas.ready": "Ready when you are",
"experiments.instanceAiSplitEmptyState.canvas.tip": "You can build new automations, edit existing ones, or ask questions",
"experiments.instanceAiSplitEmptyState.canvas.tipLabel": "Tip:",

View File

@ -115,7 +115,7 @@ export const INSTANCE_AI_WORKFLOW_PREVIEW_SUGGESTIONS_EXPERIMENT = createExperim
'087_instance_ai_workflow_preview_suggestions',
);
export const INSTANCE_AI_PERSONALIZED_PROMPT_SUGGESTIONS_EXPERIMENT = createExperiment(
'090_instance_ai_personalized_prompt_suggestions',
'093_instance_ai_personalized_prompt_suggestions',
{
control: 'control',
variantCards: 'variant-cards',

View File

@ -81,6 +81,8 @@ describe('InstanceAiPersonalizedPromptSuggestions', () => {
it('renders cards with titles and descriptions', () => {
const { getByText } = renderComponent();
expect(getByText('Start from an example')).toBeVisible();
expect(getByText('Picked for you. Change anything.')).toBeVisible();
expect(getByText('Qualify hot leads')).toBeVisible();
expect(getByText('Score new form submissions and assign the best ones')).toBeVisible();
});

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useI18n } from '@n8n/i18n';
import { N8nIcon } from '@n8n/design-system';
import type {
PersonalizedPromptDisplaySuggestion,
@ -121,20 +122,29 @@ function toggleSuggestions() {
<template>
<div :class="$style.suggestions" data-test-id="instance-ai-personalized-prompt-suggestions">
<div :class="$style.header">
<span :class="$style.heading">
{{ i18n.baseText('experiments.instanceAiPersonalizedPromptSuggestions.heading') }}
</span>
<button
v-if="showSeeMore"
type="button"
:class="$style.seeMoreButton"
data-test-id="instance-ai-personalized-see-more"
:disabled="props.disabled"
@click="toggleSuggestions"
>
{{ i18n.baseText('experiments.instanceAiPersonalizedPromptSuggestions.seeMore') }}
</button>
<div
:class="[$style.header, props.format === 'cards' ? $style.cardHeader : $style.compactHeader]"
>
<div :class="$style.headingRow">
<div :class="$style.headingCopy">
<span :class="$style.heading">
{{ i18n.baseText('experiments.instanceAiPersonalizedPromptSuggestions.heading') }}
</span>
<span :class="$style.helper">
{{ i18n.baseText('experiments.instanceAiPersonalizedPromptSuggestions.helper') }}
</span>
</div>
<button
v-if="showSeeMore"
type="button"
:class="$style.seeMoreButton"
data-test-id="instance-ai-personalized-see-more"
:disabled="props.disabled"
@click="toggleSuggestions"
>
{{ i18n.baseText('experiments.instanceAiPersonalizedPromptSuggestions.seeMore') }}
</button>
</div>
</div>
<div
@ -160,6 +170,12 @@ function toggleSuggestions() {
@focus="previewSuggestion(suggestion)"
@blur="clearPreview"
>
<N8nIcon
v-if="props.format === 'list'"
icon="sparkles"
size="xlarge"
:class="$style.listIcon"
/>
<span :class="$style.textGroup">
<span :class="$style.suggestionTitle">{{ suggestion.shortTitle }}</span>
<span :class="$style.suggestionDescription">
@ -180,22 +196,49 @@ function toggleSuggestions() {
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing--2xs);
min-height: var(--spacing--md);
/* Inset to align with the card/row text, which is padded by --spacing--sm. */
padding: 0 var(--spacing--sm);
flex-direction: column;
align-items: stretch;
gap: var(--spacing--4xs);
margin-bottom: var(--spacing--xs);
}
.compactHeader {
padding: 0;
}
.cardHeader {
gap: var(--spacing--2xs);
margin-bottom: var(--spacing--lg);
}
.headingRow {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing--xs);
min-width: 0;
}
.headingCopy {
display: flex;
flex: 1;
flex-direction: column;
gap: var(--spacing--5xs);
min-width: 0;
}
.heading {
color: var(--text-color--subtler);
font-size: var(--font-size--2xs);
color: var(--text-color);
font-size: var(--font-size--sm);
font-weight: var(--font-weight--bold);
line-height: var(--line-height--sm);
letter-spacing: 0.04em;
text-transform: uppercase;
line-height: var(--line-height--lg);
}
.helper {
color: var(--text-color--subtle);
font-size: var(--font-size--xs);
font-weight: var(--font-weight--regular);
line-height: var(--line-height--md);
}
.suggestionList {
@ -206,7 +249,7 @@ function toggleSuggestions() {
/* Cards: a 2-column grid of soft surface cards that lift further on hover/focus. */
.cardList {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacing--2xs);
gap: var(--spacing--md);
}
/* List: a single column of softly filled rows, each led by a brand sparkle. */
@ -230,6 +273,7 @@ function toggleSuggestions() {
transition:
color var(--duration--snappy) var(--easing--ease-out),
background-color var(--duration--snappy) var(--easing--ease-out),
border-color var(--duration--snappy) var(--easing--ease-out),
box-shadow var(--duration--snappy) var(--easing--ease-out),
transform var(--duration--snappy) var(--easing--ease-out);
@ -274,16 +318,22 @@ function toggleSuggestions() {
-webkit-line-clamp: 2;
}
/* --- Cards: plain text at rest; the surface card materializes on hover/focus. --- */
/* --- Cards: visible surface at rest, with stronger affordance on hover/focus. --- */
.cardButton {
padding: var(--spacing--xs) var(--spacing--sm);
align-items: flex-start;
min-height: var(--height--5xl);
padding: var(--spacing--sm);
background: light-dark(var(--background--surface), var(--background--subtle));
border: 1px solid var(--border-color--subtle);
border-radius: var(--radius--md);
box-shadow: var(--shadow--2xs);
}
.cardButton:not(:disabled):hover,
.cardButton:not(:disabled):focus-visible {
background: light-dark(var(--background--surface), var(--background--subtle));
box-shadow: var(--shadow--sm);
background: var(--background--hover);
border-color: var(--border-color--strong);
box-shadow: var(--shadow--xs);
transform: translateY(-2px);
outline: none;
z-index: 1;
@ -295,16 +345,27 @@ function toggleSuggestions() {
0 0 0 2px var(--focus--border-color);
}
/* --- List: plain rows (title + description); the surface fills in on hover/focus. --- */
/* --- List: compact card rows with the brand sparkle cue. --- */
.listIcon {
flex: 0 0 auto;
margin-top: 0;
color: var(--color--primary);
}
.listButton {
align-items: center;
padding: var(--spacing--xs) var(--spacing--sm);
background: light-dark(var(--background--surface), var(--background--subtle));
border: 1px solid var(--border-color--subtle);
border-radius: var(--radius--md);
box-shadow: var(--shadow--2xs);
}
.listButton:not(:disabled):hover,
.listButton:not(:disabled):focus-visible {
background: light-dark(var(--background--surface), var(--background--subtle));
box-shadow: var(--shadow--sm);
background: var(--background--hover);
border-color: var(--border-color--strong);
box-shadow: var(--shadow--xs);
transform: translateY(-1px);
outline: none;
z-index: 1;
@ -320,6 +381,7 @@ function toggleSuggestions() {
.seeMoreButton {
flex-shrink: 0;
padding: 0;
margin-top: var(--spacing--3xs);
color: var(--text-color--subtle);
font-size: var(--font-size--2xs);
font-weight: var(--font-weight--medium);
@ -353,6 +415,17 @@ function toggleSuggestions() {
}
}
@media (max-width: 360px) {
.headingRow {
flex-direction: column;
gap: var(--spacing--2xs);
}
.seeMoreButton {
margin-top: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.suggestionButton {
transition: none;

View File

@ -19,7 +19,7 @@ Use a new PostHog multivariant experiment flag:
```ts
export const INSTANCE_AI_PERSONALIZED_PROMPT_SUGGESTIONS_EXPERIMENT = createExperiment(
'090_instance_ai_personalized_prompt_suggestions',
'093_instance_ai_personalized_prompt_suggestions',
{
control: 'control',
variantCards: 'variant-cards',
@ -218,6 +218,7 @@ Treatment behavior:
- Clicking a suggestion does not auto-send.
- Focus the composer after insertion.
- Hover/focus preview uses the full `builderPrompt` as the ghost placeholder, matching current v2 behavior.
- When no suggestion hover/focus preview is active, use the experiment placeholder `Tip: Ask me for suggestions if you're not sure what to build`.
- Suggestions are hidden once the composer is dirty, busy, gated by setup, or unavailable, matching the current input behavior.
- Show a `See more` CTA above the suggestions, aligned to the right, only when the initial suggestion source is `matrix`, `role_default`, or `global_top_performers`.
- The `See more` label stays `See more` in both states.
@ -279,6 +280,22 @@ Secondary/diagnostic metrics:
- Source performance split by `suggestion_source`.
- Cards vs list performance split by `suggestion_format`.
## Feedback Follow-up
Source: Notion feedback from 2026-06-22.
Must change:
- [x] Align card content to the top instead of vertically centering it.
- [x] Make the card variant feel more clickable.
Nice to have:
- [x] Replace `Suggested for you` with `Start from an example`.
- [x] Add helper copy under the header: `Picked for you. Change anything.`
- [x] Use the experiment placeholder: `Tip: Ask me for suggestions if you're not sure what to build`.
- [x] Add an `AI Assistant` pill near the top so users recognize the feature/icon faster.
## Implementation TODO
- [x] Create `packages/frontend/editor-ui/src/experiments/instanceAiPersonalizedPromptSuggestions/`.

View File

@ -72,6 +72,8 @@ const INSTANCE_AI_WORKFLOW_PREVIEW_SUGGESTIONS_PLACEHOLDER_KEY =
'experiments.instanceAiWorkflowPreviewSuggestions.input.placeholder' as BaseTextKey;
const INSTANCE_AI_SPLIT_EMPTY_STATE_PLACEHOLDER_KEY: BaseTextKey =
'experiments.instanceAiSplitEmptyState.input.placeholder';
const INSTANCE_AI_PERSONALIZED_PROMPT_SUGGESTIONS_PLACEHOLDER_KEY: BaseTextKey =
'experiments.instanceAiPersonalizedPromptSuggestions.input.placeholder';
// Experiment cleanup: remove with instanceAiSplitEmptyState. The split layout
// locks the composer to a constant height so hovering an example only swaps
// the placeholder text — the examples list below it never shifts.
@ -223,7 +225,7 @@ const emptyStatePromptSuggestionProps = computed(() => {
if (!resolution) {
return {
suggestions: [],
placeholderKey: INSTANCE_AI_PROMPT_SUGGESTIONS_V2_PLACEHOLDER_KEY,
placeholderKey: INSTANCE_AI_PERSONALIZED_PROMPT_SUGGESTIONS_PLACEHOLDER_KEY,
};
}
@ -241,7 +243,7 @@ const emptyStatePromptSuggestionProps = computed(() => {
personalizedPromptSuggestionsVariant.value,
resolution.telemetryPayload,
),
placeholderKey: INSTANCE_AI_PROMPT_SUGGESTIONS_V2_PLACEHOLDER_KEY,
placeholderKey: INSTANCE_AI_PERSONALIZED_PROMPT_SUGGESTIONS_PLACEHOLDER_KEY,
};
}

View File

@ -454,7 +454,7 @@ describe('InstanceAiEmptyView', () => {
'v4-personalized',
);
expect(getByTestId('instance-ai-input-placeholder-key')).toHaveTextContent(
'experiments.instanceAiPromptSuggestionsV2.input.placeholder',
'experiments.instanceAiPersonalizedPromptSuggestions.input.placeholder',
);
expect(getByTestId('instance-ai-input-suggestions-component-props')).toHaveTextContent(
'"format":"cards"',
@ -466,7 +466,7 @@ describe('InstanceAiEmptyView', () => {
'"suggestion_catalog_version":"v4-personalized"',
);
expect(getByTestId('instance-ai-input-suggestion-telemetry-payload')).toHaveTextContent(
'"$feature/090_instance_ai_personalized_prompt_suggestions":"variant-cards"',
'"$feature/093_instance_ai_personalized_prompt_suggestions":"variant-cards"',
);
});
@ -511,7 +511,7 @@ describe('InstanceAiEmptyView', () => {
expect(getByTestId('instance-ai-input-suggestions')).toHaveTextContent('0');
expect(getByTestId('instance-ai-input-suggestions-component')).toHaveTextContent('unset');
expect(getByTestId('instance-ai-input-placeholder-key')).toHaveTextContent(
'experiments.instanceAiPromptSuggestionsV2.input.placeholder',
'experiments.instanceAiPersonalizedPromptSuggestions.input.placeholder',
);
});
@ -536,6 +536,9 @@ describe('InstanceAiEmptyView', () => {
expect(getByTestId('instance-ai-input-suggestions-component-props')).toHaveTextContent(
'"showSeeMore":false',
);
expect(getByTestId('instance-ai-input-placeholder-key')).toHaveTextContent(
'experiments.instanceAiPersonalizedPromptSuggestions.input.placeholder',
);
expect(getByTestId('instance-ai-input-suggestion-telemetry-payload')).toHaveTextContent(
'"metadata_load_state":"timed_out"',
);

View File

@ -891,7 +891,7 @@ describe('InstanceAiInput', () => {
profile_role: 'sales',
metadata_load_state: 'loaded',
variant: 'variant-cards',
'$feature/090_instance_ai_personalized_prompt_suggestions': 'variant-cards',
'$feature/093_instance_ai_personalized_prompt_suggestions': 'variant-cards',
},
},
});
@ -912,7 +912,7 @@ describe('InstanceAiInput', () => {
profile_role: 'sales',
metadata_load_state: 'loaded',
variant: 'variant-cards',
'$feature/090_instance_ai_personalized_prompt_suggestions': 'variant-cards',
'$feature/093_instance_ai_personalized_prompt_suggestions': 'variant-cards',
suggestion_id: 'custom-raw-prompt',
suggestion_kind: 'prompt',
position: 1,

View File

@ -63,7 +63,7 @@ describe('instance ai prompt suggestions telemetry', () => {
suggestion_source: 'matrix',
metadata_load_state: 'loaded',
variant: 'variant-cards',
'$feature/090_instance_ai_personalized_prompt_suggestions': 'variant-cards',
'$feature/093_instance_ai_personalized_prompt_suggestions': 'variant-cards',
},
});
@ -74,7 +74,7 @@ describe('instance ai prompt suggestions telemetry', () => {
suggestion_source: 'matrix',
metadata_load_state: 'loaded',
variant: 'variant-cards',
'$feature/090_instance_ai_personalized_prompt_suggestions': 'variant-cards',
'$feature/093_instance_ai_personalized_prompt_suggestions': 'variant-cards',
});
});

View File

@ -38,6 +38,8 @@ type SuggestionsCyclePayload = {
};
type SuggestionPreviewPayload = BaseTextKey | { prompt: string } | null;
const SUGGESTIONS_TRANSITION_DURATION = { enter: 450, leave: 320 };
const DEFAULT_AUTOSIZE_ROWS = 3;
const DEFAULT_MAX_AUTOSIZE_ROWS = 6;
const props = withDefaults(
defineProps<{
@ -413,9 +415,9 @@ const resizable = computed(() => {
return { minRows: props.fixedRows, maxRows: props.fixedRows };
}
if (previewPrompt.value) {
return { minRows: 2, maxRows: 2 };
return { minRows: DEFAULT_AUTOSIZE_ROWS, maxRows: DEFAULT_AUTOSIZE_ROWS };
}
return undefined;
return { minRows: DEFAULT_AUTOSIZE_ROWS, maxRows: DEFAULT_MAX_AUTOSIZE_ROWS };
});
</script>