mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
feat(editor): Add proactive starter experiment (no-changelog) (#30252)
Some checks are pending
Build: Benchmark Image / build (push) Waiting to run
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Util: Sync API Docs / sync-public-api (push) Waiting to run
Some checks are pending
Build: Benchmark Image / build (push) Waiting to run
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Util: Sync API Docs / sync-public-api (push) Waiting to run
This commit is contained in:
parent
133a5aa0ad
commit
7fdd98aa72
|
|
@ -1189,6 +1189,8 @@
|
||||||
"duplicateWorkflowDialog.errors.forbidden.message": "This action is forbidden. Do you have the correct permissions?",
|
"duplicateWorkflowDialog.errors.forbidden.message": "This action is forbidden. Do you have the correct permissions?",
|
||||||
"duplicateWorkflowDialog.errors.generic.title": "Duplicate workflow failed",
|
"duplicateWorkflowDialog.errors.generic.title": "Duplicate workflow failed",
|
||||||
"editor.mainHeader.githubButton.label": "Star n8n-io/n8n on GitHub",
|
"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.browseAllTemplates": "Browse our template library",
|
||||||
"experiments.personalizedTemplatesV3.couldntFind": "Need something different?",
|
"experiments.personalizedTemplatesV3.couldntFind": "Need something different?",
|
||||||
"experiments.personalizedTemplatesV3.exploreTemplates": "Get started with HubSpot workflows:",
|
"experiments.personalizedTemplatesV3.exploreTemplates": "Get started with HubSpot workflows:",
|
||||||
|
|
|
||||||
|
|
@ -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 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 AA_EXPERIMENT_CHECK = createExperiment('078_experiment_check_aa');
|
||||||
|
|
||||||
export const CHAT_HUB_SEMANTIC_SEARCH_EXPERIMENT = createExperiment('077_chat_hub_semantic_search');
|
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,
|
AI_BUILDER_REVIEW_CHANGES_EXPERIMENT.name,
|
||||||
MERGE_ASK_BUILD_EXPERIMENT.name,
|
MERGE_ASK_BUILD_EXPERIMENT.name,
|
||||||
AI_BUILDER_SETUP_WIZARD_EXPERIMENT.name,
|
AI_BUILDER_SETUP_WIZARD_EXPERIMENT.name,
|
||||||
|
INSTANCE_AI_PROACTIVE_AGENT_EXPERIMENT.name,
|
||||||
AA_EXPERIMENT_CHECK.name,
|
AA_EXPERIMENT_CHECK.name,
|
||||||
CHAT_HUB_SEMANTIC_SEARCH_EXPERIMENT.name,
|
CHAT_HUB_SEMANTIC_SEARCH_EXPERIMENT.name,
|
||||||
FLOATING_CHAT_HUB_PANEL_EXPERIMENT.name,
|
FLOATING_CHAT_HUB_PANEL_EXPERIMENT.name,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { N8nAssistantIcon, N8nText } from '@n8n/design-system';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import type { BaseTextKey } from '@n8n/i18n';
|
||||||
|
|
||||||
|
const STARTER_DELAY_MS = 800;
|
||||||
|
const TYPING_DURATION_MS = 600;
|
||||||
|
|
||||||
|
type StarterStage = 'waiting' | 'typing' | 'message';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const stage = ref<StarterStage>('waiting');
|
||||||
|
const timers: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timers.push(
|
||||||
|
setTimeout(() => {
|
||||||
|
stage.value = 'typing';
|
||||||
|
}, STARTER_DELAY_MS),
|
||||||
|
);
|
||||||
|
timers.push(
|
||||||
|
setTimeout(() => {
|
||||||
|
stage.value = 'message';
|
||||||
|
}, STARTER_DELAY_MS + TYPING_DURATION_MS),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
for (const timer of timers) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
v-if="stage !== 'waiting'"
|
||||||
|
:class="$style.container"
|
||||||
|
data-test-id="instance-ai-proactive-starter"
|
||||||
|
>
|
||||||
|
<div :class="$style.avatar" aria-hidden="true">
|
||||||
|
<N8nAssistantIcon size="large" theme="blank" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section :class="$style.bubble" aria-live="polite">
|
||||||
|
<div
|
||||||
|
v-if="stage === 'typing'"
|
||||||
|
:class="$style.typing"
|
||||||
|
:aria-label="
|
||||||
|
i18n.baseText('experiments.instanceAiProactiveAgent.typingLabel' as BaseTextKey)
|
||||||
|
"
|
||||||
|
role="status"
|
||||||
|
data-test-id="instance-ai-proactive-typing"
|
||||||
|
>
|
||||||
|
<span :class="$style.typingDot" />
|
||||||
|
<span :class="$style.typingDot" />
|
||||||
|
<span :class="$style.typingDot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<N8nText
|
||||||
|
v-else
|
||||||
|
tag="p"
|
||||||
|
size="large"
|
||||||
|
:class="$style.message"
|
||||||
|
data-test-id="instance-ai-proactive-message"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('experiments.instanceAiProactiveAgent.message' as BaseTextKey) }}
|
||||||
|
</N8nText>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
@use '@n8n/design-system/css/mixins/motion';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--spacing--xl) minmax(0, 1fr);
|
||||||
|
column-gap: var(--spacing--sm);
|
||||||
|
align-items: start;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
padding-top: var(--spacing--md);
|
||||||
|
|
||||||
|
@include motion.fade-in-up;
|
||||||
|
animation-duration: var(--duration--base);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--spacing--xl);
|
||||||
|
height: var(--spacing--xl);
|
||||||
|
border-radius: var(--radius--full);
|
||||||
|
background: var(--assistant--color--highlight-gradient);
|
||||||
|
box-shadow: var(--shadow--xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: var(--spacing--xl);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: min(560px, 100%);
|
||||||
|
padding: var(--spacing--sm) var(--spacing--md);
|
||||||
|
background: var(--color--background--light-3);
|
||||||
|
border-radius: var(--radius--3xs) var(--radius--xl) var(--radius--xl) var(--radius--xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
--animation--fade-in-up--duration: var(--duration--base);
|
||||||
|
--animation--fade-in-up--translate: var(--spacing--5xs);
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color--text);
|
||||||
|
line-height: var(--line-height--xl);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
@include motion.fade-in-up;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing--4xs);
|
||||||
|
padding: 0 var(--spacing--4xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typingDot {
|
||||||
|
width: var(--spacing--4xs);
|
||||||
|
height: var(--spacing--4xs);
|
||||||
|
border-radius: var(--radius--full);
|
||||||
|
background: var(--color--text--tint-1);
|
||||||
|
animation: typing-dot var(--duration--slow) ease-in-out infinite;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 240ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-dot {
|
||||||
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(calc(var(--spacing--5xs) * -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.container,
|
||||||
|
.message,
|
||||||
|
.typingDot {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typingDot {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { useInstanceAiProactiveAgentExperiment } from './useInstanceAiProactiveAgentExperiment';
|
||||||
|
export { default as InstanceAiProactiveStarterMessage } from './components/InstanceAiProactiveStarterMessage.vue';
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { InstanceAiAttachment } from '@n8n/api-types';
|
import type { InstanceAiAttachment } from '@n8n/api-types';
|
||||||
|
|
@ -10,6 +10,10 @@ import { useInstanceAiStore } from './instanceAi.store';
|
||||||
import { INSTANCE_AI_THREAD_VIEW } from './constants';
|
import { INSTANCE_AI_THREAD_VIEW } from './constants';
|
||||||
import { INSTANCE_AI_EMPTY_STATE_SUGGESTIONS } from './emptyStateSuggestions';
|
import { INSTANCE_AI_EMPTY_STATE_SUGGESTIONS } from './emptyStateSuggestions';
|
||||||
import { useCreditWarningBanner } from './composables/useCreditWarningBanner';
|
import { useCreditWarningBanner } from './composables/useCreditWarningBanner';
|
||||||
|
import {
|
||||||
|
InstanceAiProactiveStarterMessage,
|
||||||
|
useInstanceAiProactiveAgentExperiment,
|
||||||
|
} from '@/experiments/instanceAiProactiveAgent';
|
||||||
import InstanceAiInput from './components/InstanceAiInput.vue';
|
import InstanceAiInput from './components/InstanceAiInput.vue';
|
||||||
import InstanceAiEmptyState from './components/InstanceAiEmptyState.vue';
|
import InstanceAiEmptyState from './components/InstanceAiEmptyState.vue';
|
||||||
import InstanceAiViewHeader from './components/InstanceAiViewHeader.vue';
|
import InstanceAiViewHeader from './components/InstanceAiViewHeader.vue';
|
||||||
|
|
@ -22,6 +26,9 @@ const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { goToUpgrade } = usePageRedirectionHelper();
|
const { goToUpgrade } = usePageRedirectionHelper();
|
||||||
const creditBanner = useCreditWarningBanner(isLowCredits);
|
const creditBanner = useCreditWarningBanner(isLowCredits);
|
||||||
|
const { isFeatureEnabled: isProactiveAgentExperimentEnabled } =
|
||||||
|
useInstanceAiProactiveAgentExperiment();
|
||||||
|
const showProactiveStarter = computed(() => isProactiveAgentExperimentEnabled.value);
|
||||||
|
|
||||||
// Reset to a blank "no active thread" state so the sidebar doesn't keep
|
// Reset to a blank "no active thread" state so the sidebar doesn't keep
|
||||||
// highlighting the previous thread alongside the empty main view, and SSE
|
// highlighting the previous thread alongside the empty main view, and SSE
|
||||||
|
|
@ -65,7 +72,34 @@ function handleStop() {
|
||||||
<InstanceAiViewHeader />
|
<InstanceAiViewHeader />
|
||||||
|
|
||||||
<div :class="$style.contentArea">
|
<div :class="$style.contentArea">
|
||||||
<div :class="$style.emptyLayout">
|
<div v-if="showProactiveStarter" :class="$style.proactiveLayout">
|
||||||
|
<div :class="$style.proactiveMessageList">
|
||||||
|
<InstanceAiProactiveStarterMessage />
|
||||||
|
</div>
|
||||||
|
<div :class="$style.proactiveInput">
|
||||||
|
<CreditWarningBanner
|
||||||
|
v-if="creditBanner.visible.value"
|
||||||
|
:credits-remaining="store.creditsRemaining"
|
||||||
|
:credits-quota="store.creditsQuota"
|
||||||
|
@upgrade-click="goToUpgrade('instance-ai', 'upgrade-instance-ai')"
|
||||||
|
@dismiss="creditBanner.dismiss()"
|
||||||
|
/>
|
||||||
|
<InstanceAiInput
|
||||||
|
ref="chatInputRef"
|
||||||
|
:is-streaming="store.isStreaming"
|
||||||
|
:is-sending-message="store.isSendingMessage"
|
||||||
|
:is-awaiting-confirmation="store.isAwaitingConfirmation"
|
||||||
|
:current-thread-id="store.currentThreadId"
|
||||||
|
:amend-context="store.amendContext"
|
||||||
|
:contextual-suggestion="store.contextualSuggestion"
|
||||||
|
:research-mode="store.researchMode"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@stop="handleStop"
|
||||||
|
@toggle-research-mode="store.toggleResearchMode()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else :class="$style.emptyLayout">
|
||||||
<InstanceAiEmptyState />
|
<InstanceAiEmptyState />
|
||||||
<div :class="$style.centeredInput">
|
<div :class="$style.centeredInput">
|
||||||
<CreditWarningBanner
|
<CreditWarningBanner
|
||||||
|
|
@ -127,4 +161,29 @@ function handleStop() {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 680px;
|
max-width: 680px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.proactiveLayout {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proactiveMessageList {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing--lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proactiveInput {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--spacing--lg) var(--spacing--sm);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,24 @@ import { useInstanceAiStore } from '../instanceAi.store';
|
||||||
import { SidebarStateKey } from '../instanceAiLayout';
|
import { SidebarStateKey } from '../instanceAiLayout';
|
||||||
import { INSTANCE_AI_THREAD_VIEW } from '../constants';
|
import { INSTANCE_AI_THREAD_VIEW } from '../constants';
|
||||||
|
|
||||||
const { replaceMock, showErrorMock } = vi.hoisted(() => ({
|
const { experimentMocks, replaceMock, showErrorMock } = vi.hoisted(() => ({
|
||||||
|
experimentMocks: {
|
||||||
|
proactiveAgentEnabled: { value: false },
|
||||||
|
},
|
||||||
replaceMock: vi.fn(),
|
replaceMock: vi.fn(),
|
||||||
showErrorMock: vi.fn(),
|
showErrorMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/experiments/instanceAiProactiveAgent', () => ({
|
||||||
|
useInstanceAiProactiveAgentExperiment: () => ({
|
||||||
|
isFeatureEnabled: experimentMocks.proactiveAgentEnabled,
|
||||||
|
}),
|
||||||
|
InstanceAiProactiveStarterMessage: {
|
||||||
|
name: 'InstanceAiProactiveStarterMessageStub',
|
||||||
|
template: '<div data-test-id="instance-ai-proactive-starter">starter</div>',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/app/composables/usePageRedirectionHelper', () => ({
|
vi.mock('@/app/composables/usePageRedirectionHelper', () => ({
|
||||||
usePageRedirectionHelper: () => ({ goToUpgrade: vi.fn() }),
|
usePageRedirectionHelper: () => ({ goToUpgrade: vi.fn() }),
|
||||||
}));
|
}));
|
||||||
|
|
@ -80,6 +93,7 @@ describe('InstanceAiEmptyView', () => {
|
||||||
|
|
||||||
store = mockedStore(useInstanceAiStore);
|
store = mockedStore(useInstanceAiStore);
|
||||||
store.currentThreadId = 'thread-placeholder';
|
store.currentThreadId = 'thread-placeholder';
|
||||||
|
experimentMocks.proactiveAgentEnabled.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -93,6 +107,16 @@ describe('InstanceAiEmptyView', () => {
|
||||||
expect(getByTestId('instance-ai-input-stub')).toHaveTextContent('4');
|
expect(getByTestId('instance-ai-input-stub')).toHaveTextContent('4');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the proactive starter and moves suggestions out of the composer when enabled', () => {
|
||||||
|
experimentMocks.proactiveAgentEnabled.value = true;
|
||||||
|
|
||||||
|
const { getByTestId, queryByTestId } = renderView();
|
||||||
|
|
||||||
|
expect(getByTestId('instance-ai-proactive-starter')).toHaveTextContent('starter');
|
||||||
|
expect(queryByTestId('instance-ai-empty-state')).not.toBeInTheDocument();
|
||||||
|
expect(getByTestId('instance-ai-input-stub')).toHaveTextContent('unset');
|
||||||
|
});
|
||||||
|
|
||||||
it('clears the current thread on mount (AI-2408)', () => {
|
it('clears the current thread on mount (AI-2408)', () => {
|
||||||
// Without this, currentThreadId keeps pointing at the last visited thread
|
// Without this, currentThreadId keeps pointing at the last visited thread
|
||||||
// and the sidebar would highlight it alongside the empty main view.
|
// and the sidebar would highlight it alongside the empty main view.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user