mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
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:
parent
20c11c09d0
commit
bce40f26fb
|
|
@ -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:",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/`.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user