feat(editor): Match app builder idea pills to the assistant home suggestions (no-changelog)
Some checks failed
CI: Python / Checks (push) Has been cancelled

This commit is contained in:
Elias Meire 2026-09-28 10:52:15 +02:00
parent 9536d15c94
commit 994af0f487
No known key found for this signature in database
2 changed files with 52 additions and 59 deletions

View File

@ -7717,9 +7717,14 @@
"instanceAi.backgroundTask.completed": "Background task finished",
"instanceAi.backgroundTask.failed": "Background task failed",
"instanceAi.input.newAppPlaceholder": "Describe the app you want to build...",
"instanceAi.newApp.example.todo": "A todo app that saves tasks in a data table",
"instanceAi.newApp.example.form": "A form that submits to one of my workflows and shows the reply",
"instanceAi.newApp.example.agentChat": "A chat page for one of my published agents",
"instanceAi.newApp.suggestions.eventRsvp.label": "Event RSVP page",
"instanceAi.newApp.suggestions.eventRsvp.prompt": "An RSVP page for our next community meetup. Guests pick the sessions they want to join, tell us about dietary needs, and get a confirmation email with a calendar invite.",
"instanceAi.newApp.suggestions.feedbackBoard.label": "Feedback board",
"instanceAi.newApp.suggestions.feedbackBoard.prompt": "A feedback board where customers suggest features and upvote ideas from others. My team marks ideas as planned or shipped, and the people who voted get notified.",
"instanceAi.newApp.suggestions.equipmentBooking.label": "Equipment booking",
"instanceAi.newApp.suggestions.equipmentBooking.prompt": "A booking app for our shared office gear. People reserve cameras, mics, and meeting kits by the hour, see what's free right now, and get a reminder to bring items back.",
"instanceAi.newApp.suggestions.onboardingHub.label": "Onboarding hub",
"instanceAi.newApp.suggestions.onboardingHub.prompt": "An onboarding hub for new hires. Each person gets a checklist for their first two weeks, their manager sees their progress, and HR hears about it when someone gets stuck.",
"instanceAi.appBlueprint.title": "Review app blueprint",
"instanceAi.appBlueprint.titleResolved": "App blueprint",
"instanceAi.appBlueprint.titleExpired": "App blueprint (expired)",

View File

@ -11,7 +11,7 @@ import {
watch,
} from 'vue';
import { storeToRefs } from 'pinia';
import { N8nChatMessage, N8nIcon, N8nIconButton, N8nScrollArea, N8nText } from '@n8n/design-system';
import { N8nChatMessage, N8nIconButton, N8nScrollArea, N8nText } from '@n8n/design-system';
import { useScroll } from '@vueuse/core';
import { useI18n } from '@n8n/i18n';
import type {
@ -62,7 +62,8 @@ import {
stashPendingHandoffContext,
type PendingComposerDraft,
} from '../composables/useInstanceAiHandoff';
import { INSTANCE_AI_PREFILL_TYPE_FALLBACK, type InstanceAiMessageAuthorship } from '../prefills';
import type { InstanceAiMessageAuthorship } from '../prefills';
import type { InstanceAiEmptyStateSuggestion } from '../emptyStateSuggestions';
import type { AppPreviewDiagnostics } from '../composables/useAppPreviewDiagnostics';
import { useAppThreadScope } from '@/features/apps/composables/useAppThreadScope';
import type {
@ -251,21 +252,44 @@ const hasAssistantResponse = computed(() => displayedMessages.some((m) => m.role
// The new-app page: a bound app does not exist yet, so the composer asks for the idea first.
const appScope = useAppThreadScope();
const isNewAppPage = computed(() => appScope !== null && !appScope.value.appId);
const NEW_APP_EXAMPLES = [
{ key: 'instanceAi.newApp.example.todo', icon: 'square-check' },
{ key: 'instanceAi.newApp.example.form', icon: 'file-text' },
{ key: 'instanceAi.newApp.example.agentChat', icon: 'message-circle' },
] as const;
const showNewAppExamples = computed(
() => isNewAppPage.value && displayedMessages.length === 0 && !thread.isStreaming,
// Idea starters, rendered by the composer like the assistant home page's pills. Its own
// catalog version keeps their suggestion telemetry apart from the home page's.
const NEW_APP_SUGGESTIONS_VERSION = 'app-builder-v1';
const NEW_APP_SUGGESTIONS: readonly InstanceAiEmptyStateSuggestion[] = [
{
type: 'prompt',
id: 'event-rsvp',
icon: 'calendar',
labelKey: 'instanceAi.newApp.suggestions.eventRsvp.label',
promptKey: 'instanceAi.newApp.suggestions.eventRsvp.prompt',
},
{
type: 'prompt',
id: 'feedback-board',
icon: 'messages-square',
labelKey: 'instanceAi.newApp.suggestions.feedbackBoard.label',
promptKey: 'instanceAi.newApp.suggestions.feedbackBoard.prompt',
},
{
type: 'prompt',
id: 'equipment-booking',
icon: 'laptop',
labelKey: 'instanceAi.newApp.suggestions.equipmentBooking.label',
promptKey: 'instanceAi.newApp.suggestions.equipmentBooking.prompt',
},
{
type: 'prompt',
id: 'onboarding-hub',
icon: 'clipboard-list',
labelKey: 'instanceAi.newApp.suggestions.onboardingHub.label',
promptKey: 'instanceAi.newApp.suggestions.onboardingHub.prompt',
},
];
const newAppSuggestions = computed(() =>
isNewAppPage.value && displayedMessages.length === 0 && !thread.isStreaming
? NEW_APP_SUGGESTIONS
: undefined,
);
function useNewAppExample(key: (typeof NEW_APP_EXAMPLES)[number]['key']) {
chatInputRef.value?.setPrefill({
text: i18n.baseText(key),
prefillType: INSTANCE_AI_PREFILL_TYPE_FALLBACK,
});
chatInputRef.value?.focus();
}
// True when at least one pending confirmation should occupy the chat-input
// slot (questions, generic approvals, or domain/web-search access). Drives
@ -1040,23 +1064,6 @@ defineExpose({
@upgrade-click="goToUpgrade('instance-ai', 'upgrade-instance-ai')"
@dismiss="creditBanner.dismiss()"
/>
<div
v-if="showNewAppExamples"
:class="$style.newAppExamples"
data-test-id="instance-ai-new-app-examples"
>
<button
v-for="(example, index) in NEW_APP_EXAMPLES"
:key="example.key"
type="button"
:class="$style.newAppExample"
:style="{ animationDelay: `${index * 50}ms` }"
@click="useNewAppExample(example.key)"
>
<N8nIcon :icon="example.icon" :size="12" :class="$style.newAppExampleIcon" />
<span>{{ i18n.baseText(example.key) }}</span>
</button>
</div>
<slot name="above-input" />
<div :class="$style.inputSwap">
<Transition name="input-swap">
@ -1091,6 +1098,10 @@ defineExpose({
:placeholder-key="
isNewAppPage ? 'instanceAi.input.newAppPlaceholder' : undefined
"
:suggestions="newAppSuggestions"
:suggestion-catalog-version="
newAppSuggestions ? NEW_APP_SUGGESTIONS_VERSION : undefined
"
@submit="handleSubmit"
@stop="handleStop"
@dismiss-context-chip="dismissComposerContextChip"
@ -1112,8 +1123,6 @@ defineExpose({
</template>
<style lang="scss" module>
@use '../../shared/styles/prompt-suggestion-buttons' as promptSuggestions;
@property --instance-ai-artifacts-layout-width {
syntax: '<length>';
inherits: true;
@ -1255,27 +1264,6 @@ defineExpose({
line-height: var(--line-height--md);
}
.newAppExamples {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--spacing--2xs);
margin-bottom: var(--spacing--xs);
}
.newAppExample {
@include promptSuggestions.prompt-suggestion-button;
}
.newAppExampleIcon {
@include promptSuggestions.prompt-suggestion-icon;
.newAppExample:hover &,
.newAppExample:focus-visible & {
opacity: 1;
}
}
// The leaving child is detached from layout (see `.input-swap-leave-active`
// below) so the slot follows the entering child's intrinsic height during
// the cross-fade.