mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
chore: Remove AI WF Builder planner step (no-changelog) (#19343)
This commit is contained in:
@@ -200,10 +200,6 @@
|
||||
"aiAssistant.builder.canvasPrompt.startManually.title": "Start manually",
|
||||
"aiAssistant.builder.canvasPrompt.startManually.subTitle": "Add the first node",
|
||||
"aiAssistant.builder.streamAbortedMessage": "[Task aborted]",
|
||||
"aiAssistant.builder.plan.intro": "Do you want to proceed with this plan?",
|
||||
"aiAssistant.builder.plan.approve": "Approve Plan",
|
||||
"aiAssistant.builder.plan.reject": "Request Changes",
|
||||
"aiAssistant.builder.plan.whatToChange": "What would you like to change?",
|
||||
"aiAssistant.assistant": "AI Assistant",
|
||||
"aiAssistant.newSessionModal.title.part1": "Start new",
|
||||
"aiAssistant.newSessionModal.title.part2": "session",
|
||||
|
||||
@@ -26,7 +26,6 @@ import { mockedStore } from '@/__tests__/utils';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { PLAN_APPROVAL_MESSAGE } from '@/constants';
|
||||
|
||||
vi.mock('@/event-bus', () => ({
|
||||
nodeViewEventBus: {
|
||||
@@ -978,384 +977,6 @@ describe('AskAssistantBuild', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodesPlan message handling', () => {
|
||||
it('should render plan messages with appropriate controls', async () => {
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Here is my plan:',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data from an API',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const regularMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'text' as const,
|
||||
content: 'Regular message',
|
||||
};
|
||||
|
||||
// First test with regular message - should not show plan controls
|
||||
builderStore.$patch({
|
||||
chatMessages: [regularMessage],
|
||||
});
|
||||
|
||||
const { queryByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Regular message should not have plan controls
|
||||
expect(queryByText('aiAssistant.builder.plan.approve')).not.toBeInTheDocument();
|
||||
|
||||
// Now test with plan message
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
// Plan message should have controls
|
||||
expect(queryByText('aiAssistant.builder.plan.approve')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle plan approval correctly', async () => {
|
||||
// Setup: empty workflow so initialGeneration is true
|
||||
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
|
||||
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Here is my plan:',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data from an API',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
const { getByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Find and click the approve button
|
||||
const approveButton = getByText('aiAssistant.builder.plan.approve');
|
||||
await fireEvent.click(approveButton);
|
||||
await flushPromises();
|
||||
|
||||
// Verify that sendChatMessage was called with approval message
|
||||
expect(builderStore.sendChatMessage).toHaveBeenCalledWith({
|
||||
text: PLAN_APPROVAL_MESSAGE,
|
||||
initialGeneration: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle plan rejection correctly', async () => {
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Here is my plan:',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data from an API',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
const { getByText, queryByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Find and click the reject button
|
||||
const rejectButton = getByText('aiAssistant.builder.plan.reject');
|
||||
await fireEvent.click(rejectButton);
|
||||
await flushPromises();
|
||||
|
||||
// Verify plan controls are hidden after rejection
|
||||
expect(queryByText('aiAssistant.builder.plan.approve')).not.toBeInTheDocument();
|
||||
expect(queryByText('aiAssistant.builder.plan.reject')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show plan controls only for the last plan message', async () => {
|
||||
const planMessage1 = {
|
||||
id: 'plan1',
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'First plan:',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'First plan',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const textMessage = {
|
||||
id: 'text1',
|
||||
role: 'assistant' as const,
|
||||
type: 'text' as const,
|
||||
content: 'Some text',
|
||||
};
|
||||
|
||||
const planMessage2 = {
|
||||
id: 'plan2',
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Second plan:',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.emailSend',
|
||||
nodeName: 'Send Email',
|
||||
reasoning: 'Second plan',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// First set just the first plan message
|
||||
builderStore.$patch({
|
||||
chatMessages: [planMessage1],
|
||||
});
|
||||
|
||||
const { queryByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// First plan should show controls when it's alone
|
||||
expect(queryByText('aiAssistant.builder.plan.approve')).toBeInTheDocument();
|
||||
|
||||
// Now add more messages
|
||||
builderStore.$patch({
|
||||
chatMessages: [planMessage1, textMessage, planMessage2],
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
// Now only the last plan message should have visible controls
|
||||
// We check this by seeing that approve button is still there (from plan2)
|
||||
expect(queryByText('aiAssistant.builder.plan.approve')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan status management in workflow messages watcher', () => {
|
||||
it('should disable chat when plan is pending and enable after action', async () => {
|
||||
// Add a nodes plan message as the last message
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Plan message',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
// Also need to mock workflowMessages getter
|
||||
builderStore.workflowMessages = [];
|
||||
|
||||
const { getByText, queryByRole } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Trigger the watcher by updating workflowMessages
|
||||
builderStore.workflowMessages = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'workflow-updated' as const,
|
||||
codeSnippet: '{}',
|
||||
},
|
||||
];
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// Chat should be disabled while plan is pending
|
||||
const chatInput = queryByRole('textbox');
|
||||
expect(chatInput).toHaveAttribute('disabled');
|
||||
|
||||
// Approve the plan
|
||||
const approveButton = getByText('aiAssistant.builder.plan.approve');
|
||||
await fireEvent.click(approveButton);
|
||||
await flushPromises();
|
||||
|
||||
// Chat should be enabled after approval
|
||||
const chatInputAfter = queryByRole('textbox');
|
||||
expect(chatInputAfter).not.toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('should not disable chat when last message is not a nodes plan', async () => {
|
||||
// Add a regular text message as the last message
|
||||
const textMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'text' as const,
|
||||
content: 'Regular message',
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [textMessage],
|
||||
});
|
||||
|
||||
// Mock workflowMessages getter
|
||||
builderStore.workflowMessages = [];
|
||||
|
||||
const { queryByRole } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Trigger the watcher
|
||||
builderStore.workflowMessages = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'workflow-updated' as const,
|
||||
codeSnippet: '{}',
|
||||
},
|
||||
];
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// Chat should remain enabled
|
||||
const chatInput = queryByRole('textbox');
|
||||
expect(chatInput).not.toHaveAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled state when plan is pending', () => {
|
||||
it('should disable chat input when plan status is pending', async () => {
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Plan message',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
// Mock workflowMessages getter
|
||||
builderStore.workflowMessages = [];
|
||||
|
||||
const { queryByRole } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Trigger workflow message update to set planStatus to pending
|
||||
builderStore.workflowMessages = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'workflow-updated' as const,
|
||||
codeSnippet: '{}',
|
||||
},
|
||||
];
|
||||
|
||||
await flushPromises();
|
||||
|
||||
const chatInput = queryByRole('textbox');
|
||||
expect(chatInput).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('should enable chat input after plan approval', async () => {
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Plan message',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
const { queryByRole, getByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Click approve button
|
||||
const approveButton = getByText('aiAssistant.builder.plan.approve');
|
||||
await fireEvent.click(approveButton);
|
||||
await flushPromises();
|
||||
|
||||
// Chat should be enabled after approval
|
||||
const chatInput = queryByRole('textbox');
|
||||
expect(chatInput).not.toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('should enable chat input after plan rejection', async () => {
|
||||
const nodesPlanMessage = {
|
||||
id: faker.string.uuid(),
|
||||
role: 'assistant' as const,
|
||||
type: 'custom' as const,
|
||||
customType: 'nodesPlan',
|
||||
message: 'Plan message',
|
||||
data: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeName: 'HTTP Request',
|
||||
reasoning: 'To fetch data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
builderStore.$patch({
|
||||
chatMessages: [nodesPlanMessage],
|
||||
});
|
||||
|
||||
const { queryByRole, getByText } = renderComponent();
|
||||
await flushPromises();
|
||||
|
||||
// Click reject button
|
||||
const rejectButton = getByText('aiAssistant.builder.plan.reject');
|
||||
await fireEvent.click(rejectButton);
|
||||
await flushPromises();
|
||||
|
||||
// Chat should be enabled after rejection
|
||||
const chatInput = queryByRole('textbox');
|
||||
expect(chatInput).not.toHaveAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple canvas generations correctly', async () => {
|
||||
const originalWorkflow = {
|
||||
nodes: [],
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useBuilderStore } from '@/stores/builder.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { computed, watch, ref, nextTick } from 'vue';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import AskAssistantChat from '@n8n/design-system/components/AskAssistantChat/AskAssistantChat.vue';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
import type { ChatUI, RatingFeedback } from '@n8n/design-system/types/assistant';
|
||||
import type { RatingFeedback } from '@n8n/design-system/types/assistant';
|
||||
import { isWorkflowUpdatedMessage } from '@n8n/design-system/types/assistant';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import type { NodesPlanMessageType } from './NodesPlanMessage.vue';
|
||||
import NodesPlanMessage from './NodesPlanMessage.vue';
|
||||
import { PLAN_APPROVAL_MESSAGE } from '@/constants';
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
@@ -31,7 +28,6 @@ const workflowSaver = useWorkflowSaving({ router });
|
||||
// Track processed workflow updates
|
||||
const processedWorkflowUpdates = ref(new Set<string>());
|
||||
const trackedTools = ref(new Set<string>());
|
||||
const planStatus = ref<'pending' | 'approved' | 'rejected'>();
|
||||
const assistantChatRef = ref<InstanceType<typeof AskAssistantChat> | null>(null);
|
||||
const workflowUpdated = ref<{ start: string; end: string } | undefined>();
|
||||
|
||||
@@ -81,40 +77,6 @@ function onFeedback(feedback: RatingFeedback) {
|
||||
}
|
||||
}
|
||||
|
||||
function isNodesPlanMessage(message: ChatUI.AssistantMessage): message is NodesPlanMessageType {
|
||||
return (
|
||||
message.type === 'custom' && message.customType === 'nodesPlan' && Array.isArray(message.data)
|
||||
);
|
||||
}
|
||||
|
||||
function onApprovePlan() {
|
||||
planStatus.value = 'approved';
|
||||
|
||||
telemetry.track('User clicked Approve plan', {
|
||||
session_id: builderStore.trackingSessionId,
|
||||
});
|
||||
|
||||
void onUserMessage(PLAN_APPROVAL_MESSAGE);
|
||||
}
|
||||
|
||||
function onRequestChanges() {
|
||||
planStatus.value = 'rejected';
|
||||
|
||||
telemetry.track('User clicked Request changes', {
|
||||
session_id: builderStore.trackingSessionId,
|
||||
});
|
||||
|
||||
// Focus the input after rejecting the plan
|
||||
void nextTick(() => {
|
||||
assistantChatRef.value?.focusInput();
|
||||
});
|
||||
}
|
||||
|
||||
function shouldShowPlanControls(message: NodesPlanMessageType) {
|
||||
const planMessageIndex = builderStore.chatMessages.findIndex((msg) => msg.id === message.id);
|
||||
return planMessageIndex === builderStore.chatMessages.length - 1;
|
||||
}
|
||||
|
||||
function dedupeToolNames(toolNames: string[]): string[] {
|
||||
return [...new Set(toolNames)];
|
||||
}
|
||||
@@ -175,12 +137,6 @@ watch(
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if last message is a plan message and if so, whether to show controls
|
||||
const lastMessage = builderStore.chatMessages[builderStore.chatMessages.length - 1];
|
||||
if (lastMessage && isNodesPlanMessage(lastMessage)) {
|
||||
planStatus.value = 'pending';
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
@@ -229,7 +185,6 @@ watch(currentRoute, () => {
|
||||
:user="user"
|
||||
:messages="builderStore.chatMessages"
|
||||
:streaming="builderStore.streaming"
|
||||
:disabled="planStatus === 'pending'"
|
||||
:loading-message="loadingMessage"
|
||||
:mode="i18n.baseText('aiAssistant.builder.mode')"
|
||||
:title="'n8n AI'"
|
||||
@@ -245,16 +200,6 @@ watch(currentRoute, () => {
|
||||
<template #header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template #custom-message="{ message, ...props }">
|
||||
<NodesPlanMessage
|
||||
v-if="message.customType === 'nodesPlan' && isNodesPlanMessage(message)"
|
||||
:message="message"
|
||||
:show-controls="shouldShowPlanControls(message)"
|
||||
v-bind="props"
|
||||
@approve-plan="onApprovePlan"
|
||||
@request-changes="onRequestChanges"
|
||||
/>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<n8n-text :class="$style.topText">{{
|
||||
i18n.baseText('aiAssistant.builder.placeholder')
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
<!-- eslint-disable import-x/extensions -->
|
||||
<script setup lang="ts">
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import type { ChatUI, RatingFeedback } from '@n8n/design-system';
|
||||
import BaseMessage from '@n8n/design-system/components/AskAssistantChat/messages/BaseMessage.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface NodesPlan {
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export type NodesPlanMessageType = ChatUI.CustomMessage & {
|
||||
data: NodesPlan[];
|
||||
};
|
||||
|
||||
interface Props {
|
||||
message: NodesPlanMessageType;
|
||||
showControls?: boolean;
|
||||
isFirstOfRole: boolean;
|
||||
user?: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
streaming?: boolean;
|
||||
isLastMessage?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const i18n = useI18n();
|
||||
const showControlsLocal = ref(props.showControls ?? true);
|
||||
const changesRequested = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
approvePlan: [];
|
||||
requestChanges: [];
|
||||
feedback: [RatingFeedback];
|
||||
}>();
|
||||
|
||||
function onApprovePlan() {
|
||||
showControlsLocal.value = false;
|
||||
emit('approvePlan');
|
||||
}
|
||||
|
||||
function onRequestChanges() {
|
||||
showControlsLocal.value = false;
|
||||
changesRequested.value = true;
|
||||
emit('requestChanges');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseMessage
|
||||
:class="$style.message"
|
||||
:message="message"
|
||||
:is-first-of-role="true"
|
||||
:user="user"
|
||||
@feedback="(feedback) => emit('feedback', feedback)"
|
||||
>
|
||||
{{ message.message }}
|
||||
<ol :class="$style.nodes">
|
||||
<li v-for="(node, index) in message.data" :key="index" :class="$style.node">
|
||||
<n8n-tooltip placement="left" :show-after="300">
|
||||
<template #content>
|
||||
{{ node.reasoning }}
|
||||
</template>
|
||||
<div :class="$style.node">
|
||||
<NodeIcon
|
||||
:class="$style.nodeIcon"
|
||||
:node-type="nodeTypesStore.getNodeType(node.nodeType)"
|
||||
:node-name="node.nodeName"
|
||||
:show-tooltip="false"
|
||||
:size="12"
|
||||
/>
|
||||
<span>{{ node.nodeName }}</span>
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</li>
|
||||
</ol>
|
||||
<template v-if="showControls && showControlsLocal && !streaming">
|
||||
{{ i18n.baseText('aiAssistant.builder.plan.intro') }}
|
||||
<div :class="$style.controls">
|
||||
<n8n-button type="primary" @click="onApprovePlan">{{
|
||||
i18n.baseText('aiAssistant.builder.plan.approve')
|
||||
}}</n8n-button>
|
||||
<n8n-button type="secondary" @click="onRequestChanges">{{
|
||||
i18n.baseText('aiAssistant.builder.plan.reject')
|
||||
}}</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="changesRequested">
|
||||
<span class="mb-m">{{ i18n.baseText('aiAssistant.builder.plan.whatToChange') }}</span>
|
||||
</template>
|
||||
</BaseMessage>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nodes {
|
||||
list-style: none;
|
||||
padding: var(--spacing-2xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
.nodeIcon {
|
||||
padding: var(--spacing-3xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
display: inline-flex;
|
||||
}
|
||||
.node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-2xs);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
.followUpMessage {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
||||
import type { ChatRequest } from '@/types/assistant.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import {
|
||||
isTextMessage,
|
||||
isWorkflowUpdatedMessage,
|
||||
isToolMessage,
|
||||
isPlanMessage,
|
||||
} from '@/types/assistant.types';
|
||||
import { isTextMessage, isWorkflowUpdatedMessage, isToolMessage } from '@/types/assistant.types';
|
||||
|
||||
export interface MessageProcessingResult {
|
||||
messages: ChatUI.AssistantMessage[];
|
||||
@@ -124,18 +119,6 @@ export function useBuilderMessages() {
|
||||
// Don't clear thinking for workflow updates - they're just state changes
|
||||
} else if (isToolMessage(msg)) {
|
||||
processToolMessage(messages, msg, messageId);
|
||||
} else if (isPlanMessage(msg)) {
|
||||
// Add new plan message
|
||||
messages.push({
|
||||
id: messageId,
|
||||
role: 'assistant',
|
||||
type: 'custom',
|
||||
customType: 'nodesPlan',
|
||||
message: msg.message,
|
||||
data: msg.plan,
|
||||
} satisfies ChatUI.CustomMessage);
|
||||
// Plan messages are informational, clear thinking
|
||||
shouldClearThinking = true;
|
||||
} else if ('type' in msg && msg.type === 'error' && 'content' in msg) {
|
||||
// Handle error messages from the API
|
||||
// API sends error messages with type: 'error' and content field
|
||||
@@ -390,17 +373,6 @@ export function useBuilderMessages() {
|
||||
} satisfies ChatUI.AssistantMessage;
|
||||
}
|
||||
|
||||
if (isPlanMessage(message)) {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
type: 'custom',
|
||||
customType: 'nodesPlan',
|
||||
data: message.plan,
|
||||
message: message.message,
|
||||
} satisfies ChatUI.CustomMessage;
|
||||
}
|
||||
|
||||
// Handle event messages
|
||||
if ('type' in message && message.type === 'event') {
|
||||
return {
|
||||
|
||||
@@ -166,17 +166,6 @@ export namespace ChatRequest {
|
||||
}
|
||||
|
||||
// API-only types
|
||||
export interface PlanMessage {
|
||||
role: 'assistant';
|
||||
type: 'plan';
|
||||
plan: Array<{
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
reasoning: string;
|
||||
}>;
|
||||
message?: string; // For plan-review messages
|
||||
}
|
||||
|
||||
export type MessageResponse =
|
||||
| ((
|
||||
| TextMessage
|
||||
@@ -187,7 +176,6 @@ export namespace ChatRequest {
|
||||
| ChatUI.WorkflowUpdatedMessage
|
||||
| ToolMessage
|
||||
| ChatUI.ErrorMessage
|
||||
| PlanMessage
|
||||
) & {
|
||||
quickReplies?: ChatUI.QuickReply[];
|
||||
})
|
||||
@@ -277,7 +265,3 @@ export function isEndSessionMessage(
|
||||
): msg is ChatUI.EndSessionMessage {
|
||||
return 'type' in msg && msg.type === 'event' && msg.eventName === 'end-session';
|
||||
}
|
||||
|
||||
export function isPlanMessage(msg: ChatRequest.MessageResponse): msg is ChatRequest.PlanMessage {
|
||||
return 'type' in msg && msg.type === 'plan' && 'plan' in msg && Array.isArray(msg.plan);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user