feat(editor): Add AI Assistant support chat (#10656)

This commit is contained in:
Milorad FIlipović 2024-09-05 10:54:35 +02:00 committed by GitHub
parent 899b0a19ef
commit 3a8078068e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 223 additions and 79 deletions

View file

@ -31,7 +31,8 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.askAssistantFloatingButton().click(); aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible'); aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible'); aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInputWrapper().should('not.exist'); aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.closeChatButton().should('be.visible'); aiAssistant.getters.closeChatButton().should('be.visible');
aiAssistant.getters.closeChatButton().click(); aiAssistant.getters.closeChatButton().click();
aiAssistant.getters.askAssistantChat().should('not.be.visible'); aiAssistant.getters.askAssistantChat().should('not.be.visible');
@ -137,29 +138,6 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
}); });
it('should send message to assistant when node is executed only once', () => {
const TOTAL_REQUEST_COUNT = 1;
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
cy.get('@chatRequest.all').then((interceptions) => {
expect(interceptions).to.have.length(TOTAL_REQUEST_COUNT);
});
// Executing the same node should not send a new message if users haven't responded to quick replies
ndv.getters.nodeExecuteButton().click();
cy.get('@chatRequest.all').then((interceptions) => {
expect(interceptions).to.have.length(TOTAL_REQUEST_COUNT);
});
aiAssistant.getters.chatMessagesAssistant().should('have.length', 2);
});
it('should show quick replies when node is executed after new suggestion', () => { it('should show quick replies when node is executed after new suggestion', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => { cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
req.reply((res) => { req.reply((res) => {
@ -281,4 +259,32 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.chatMessagesSystem().should('have.length', 1); aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
}); });
it('should reset session after it ended and sidebar is closed', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
req.reply((res) => {
if (['init-support-chat'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
} else {
res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' });
}
});
}).as('chatRequest');
aiAssistant.actions.openChat();
aiAssistant.actions.sendMessage('Hello');
cy.wait('@chatRequest');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// After closing and reopening the chat, all messages should be still there
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
// End the session
aiAssistant.actions.sendMessage('Thanks, bye');
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// Now, session should be reset
aiAssistant.getters.placeholderMessage().should('be.visible');
});
}); });

View file

@ -1,9 +1,9 @@
{ {
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT", "sessionId": "1",
"messages": [ "messages": [
{ {
"role": "assistant", "role": "assistant",
"type": "agent-suggestion", "type": "message",
"title": "Glad to Help", "title": "Glad to Help",
"text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!" "text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!"
}, },

View file

@ -38,13 +38,24 @@ export class AIAssistant extends BasePage {
}; };
actions = { actions = {
enableAssistant(): void { enableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor); overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
cy.enableFeature(AI_ASSISTANT_FEATURE.name); cy.enableFeature(AI_ASSISTANT_FEATURE.name);
}, },
disableAssistant(): void { disableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor); overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
cy.disableFeature(AI_ASSISTANT_FEATURE.name); cy.disableFeature(AI_ASSISTANT_FEATURE.name);
}, },
sendMessage: (message: string) => {
this.getters.chatInput().type(message).type('{enter}');
},
closeChat: () => {
this.getters.closeChatButton().click();
this.getters.askAssistantChat().should('not.be.visible');
},
openChat: () => {
this.getters.askAssistantFloatingButton().click();
this.getters.askAssistantChat().should('be.visible');
},
}; };
} }

View file

@ -247,3 +247,23 @@ AssistantThinkingChat.args = {
}, },
loadingMessage: 'Thinking...', loadingMessage: 'Thinking...',
}; };
export const WithCodeSnippet = Template.bind({});
WithCodeSnippet.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
messages: getMessages([
{
id: '58575953',
type: 'text',
role: 'assistant',
content:
'To filter every other item in the Code node, you can use the following JavaScript code snippet. This code will iterate through the incoming items and only pass through every other item.',
codeSnippet:
"node.on('input', function(msg) {\n if (msg.seed) { dummyjson.seed = msg.seed; }\n try {\n var value = dummyjson.parse(node.template, {mockdata: msg});\n if (node.syntax === 'json') {\n try { value = JSON.parse(value); }\n catch(e) { node.error(RED._('datagen.errors.json-error')); }\n }\n if (node.fieldType === 'msg') {\n RED.util.setMessageProperty(msg,node.field,value);\n }\n else if (node.fieldType === 'flow') {\n node.context().flow.set(node.field,value);\n }\n else if (node.fieldType === 'global') {\n node.context().global.set(node.field,value);\n }\n node.send(msg);\n }\n catch(e) {",
read: true,
},
]),
};

View file

@ -163,11 +163,16 @@ function growInput() {
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span> <span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span <div
v-else v-else
:class="$style.assistantText" :class="$style.assistantText"
v-html="renderMarkdown(message.content)" v-html="renderMarkdown(message.content)"
></span> ></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
v-html="renderMarkdown(message.codeSnippet).trim()"
></div>
<BlinkingCursor <BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'" v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
/> />
@ -243,20 +248,16 @@ function growInput() {
</p> </p>
<p> <p>
{{ t('assistantChat.placeholder.2') }} {{ t('assistantChat.placeholder.2') }}
</p>
<p>
{{ t('assistantChat.placeholder.3') }}
<InlineAskAssistantButton size="small" :static="true" /> <InlineAskAssistantButton size="small" :static="true" />
{{ t('assistantChat.placeholder.4') }} {{ t('assistantChat.placeholder.3') }}
</p> </p>
<p> <p>
{{ t('assistantChat.placeholder.5') }} {{ t('assistantChat.placeholder.4') }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="messages?.length"
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }" :class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
data-test-id="chat-input-wrapper" data-test-id="chat-input-wrapper"
> >
@ -407,8 +408,29 @@ p {
.textMessage { .textMessage {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
word-break: break-word;
}
.code-snippet {
border: var(--border-base);
background-color: var(--color-foreground-xlight);
border-radius: var(--border-radius-base);
padding: var(--spacing-2xs);
font-family: var(--font-family-monospace);
max-height: 218px; // 12 lines
overflow: auto;
pre {
white-space-collapse: collapse;
}
code {
background-color: transparent;
font-size: var(--font-size-3xs);
}
} }
.block { .block {

View file

@ -132,7 +132,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span <div
class="assistantText" class="assistantText"
> >
<p> <p>
@ -144,9 +144,10 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
</p> </p>
</span> </div>
<!--v-if--> <!--v-if-->
<!--v-if-->
</div> </div>
<!--v-if--> <!--v-if-->
</div> </div>
@ -449,6 +450,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
</span> </span>
<!--v-if--> <!--v-if-->
<!--v-if-->
</div> </div>
<!--v-if--> <!--v-if-->
</div> </div>
@ -842,13 +844,10 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
class="info" class="info"
> >
<p> <p>
I'm your Assistant, here to guide you through your journey with n8n. I can answer most questions about building workflows in n8n.
</p> </p>
<p> <p>
While I'm still learning, I'm already equipped to help you debug any errors you might encounter. For specific tasks, youll see the
</p>
<p>
If you run into an issue with a node, you'll see the
<button <button
class="button" class="button"
style="height: 18px;" style="height: 18px;"
@ -901,15 +900,33 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
</div> </div>
</div> </div>
</button> </button>
button button in the UI.
</p> </p>
<p> <p>
Clicking it will start a chat with me, and I'll do my best to assist you! How can I help?
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!--v-if--> <div
class="inputWrapper"
data-test-id="chat-input-wrapper"
>
<textarea
data-test-id="chat-input"
placeholder="Enter your response..."
rows="1"
wrap="hard"
/>
<n8n-icon-button
class="sendButton"
data-test-id="send-message-button"
disabled="true"
icon="paper-plane"
size="large"
type="text"
/>
</div>
</div> </div>
</div> </div>
`; `;
@ -1046,7 +1063,7 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span <div
class="assistantText" class="assistantText"
> >
<p> <p>
@ -1058,9 +1075,10 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
</p> </p>
</span> </div>
<!--v-if--> <!--v-if-->
<!--v-if-->
</div> </div>
<!--v-if--> <!--v-if-->
</div> </div>
@ -1295,7 +1313,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span <div
class="assistantText" class="assistantText"
> >
<p> <p>
@ -1307,9 +1325,10 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
</p> </p>
</span> </div>
<!--v-if--> <!--v-if-->
<!--v-if-->
</div> </div>
<!--v-if--> <!--v-if-->
</div> </div>

View file

@ -39,13 +39,10 @@ export default {
'assistantChat.you': 'You', 'assistantChat.you': 'You',
'assistantChat.quickRepliesTitle': 'Quick reply 👇', 'assistantChat.quickRepliesTitle': 'Quick reply 👇',
'assistantChat.placeholder.1': () => 'assistantChat.placeholder.1': () =>
"I'm your Assistant, here to guide you through your journey with n8n.", 'I can answer most questions about building workflows in n8n.',
'assistantChat.placeholder.2': 'assistantChat.placeholder.2': 'For specific tasks, youll see the',
"While I'm still learning, I'm already equipped to help you debug any errors you might encounter.", 'assistantChat.placeholder.3': 'button in the UI.',
'assistantChat.placeholder.3': "If you run into an issue with a node, you'll see the", 'assistantChat.placeholder.4': 'How can I help?',
'assistantChat.placeholder.4': 'button',
'assistantChat.placeholder.5':
"Clicking it will start a chat with me, and I'll do my best to assist you!",
'assistantChat.inputPlaceholder': 'Enter your response...', 'assistantChat.inputPlaceholder': 'Enter your response...',
'inlineAskAssistantButton.asked': 'Asked', 'inlineAskAssistantButton.asked': 'Asked',
} as N8nLocale; } as N8nLocale;

View file

@ -3,6 +3,7 @@ export namespace ChatUI {
role: 'assistant' | 'user'; role: 'assistant' | 'user';
type: 'text'; type: 'text';
content: string; content: string;
codeSnippet?: string;
} }
export interface SummaryBlock { export interface SummaryBlock {

View file

@ -27,14 +27,16 @@ function onResizeDebounced(data: { direction: string; x: number; width: number }
} }
async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) { async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) {
await assistantStore.sendMessage({ text: content, quickReplyType }); // If there is no current session running, initialize the support chat session
const task = 'error'; if (!assistantStore.currentSessionId) {
const solutionCount = await assistantStore.initSupportChat(content);
task === 'error' } else {
? assistantStore.chatMessages.filter( await assistantStore.sendMessage({ text: content, quickReplyType });
(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type), }
).length const task = assistantStore.isSupportChatSessionInProgress ? 'support' : 'error';
: null; const solutionCount = assistantStore.chatMessages.filter(
(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type),
).length;
if (isFeedback) { if (isFeedback) {
telemetry.track('User gave feedback', { telemetry.track('User gave feedback', {
task, task,

View file

@ -143,7 +143,7 @@ defineExpose({
<style lang="scss" module> <style lang="scss" module>
.floatingNodes { .floatingNodes {
position: fixed; position: absolute;
bottom: 0; bottom: 0;
top: 0; top: 0;
right: 0; right: 0;

View file

@ -79,9 +79,13 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
ENABLED_VIEWS.includes(route.name as VIEWS), ENABLED_VIEWS.includes(route.name as VIEWS),
); );
const assistantMessages = computed(() =>
chatMessages.value.filter((msg) => msg.role === 'assistant'),
);
const usersMessages = computed(() => chatMessages.value.filter((msg) => msg.role === 'user'));
const isSessionEnded = computed(() => { const isSessionEnded = computed(() => {
const assistantMessages = chatMessages.value.filter((msg) => msg.role === 'assistant'); const lastAssistantMessage = assistantMessages.value[assistantMessages.value.length - 1];
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
const sessionExplicitlyEnded = const sessionExplicitlyEnded =
lastAssistantMessage?.type === 'event' && lastAssistantMessage?.eventName === 'end-session'; lastAssistantMessage?.type === 'event' && lastAssistantMessage?.eventName === 'end-session';
@ -106,6 +110,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
).length, ).length,
); );
const isSupportChatSessionInProgress = computed(() => {
return currentSessionId.value !== undefined && chatSessionError.value === undefined;
});
watch(route, () => { watch(route, () => {
const activeWorkflowId = workflowsStore.workflowId; const activeWorkflowId = workflowsStore.workflowId;
if (!currentSessionId.value || currentSessionWorkflowId.value === activeWorkflowId) { if (!currentSessionId.value || currentSessionWorkflowId.value === activeWorkflowId) {
@ -137,17 +145,21 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
// Looks smoother if we wait for slide animation to finish before updating the grid width // Looks smoother if we wait for slide animation to finish before updating the grid width
setTimeout(() => { setTimeout(() => {
uiStore.appGridWidth = window.innerWidth; uiStore.appGridWidth = window.innerWidth;
// If session has ended, reset the chat
if (isSessionEnded.value) {
resetAssistantChat();
}
}, 200); }, 200);
} }
function addAssistantMessages(assistantMessages: ChatRequest.MessageResponse[], id: string) { function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
const read = chatWindowOpen.value; const read = chatWindowOpen.value;
const messages = [...chatMessages.value].filter( const messages = [...chatMessages.value].filter(
(msg) => !(msg.id === id && msg.role === 'assistant'), (msg) => !(msg.id === id && msg.role === 'assistant'),
); );
assistantThinkingMessage.value = undefined; assistantThinkingMessage.value = undefined;
// TODO: simplify // TODO: simplify
assistantMessages.forEach((msg) => { newMessages.forEach((msg) => {
if (msg.type === 'message') { if (msg.type === 'message') {
messages.push({ messages.push({
id, id,
@ -155,6 +167,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
role: 'assistant', role: 'assistant',
content: msg.text, content: msg.text,
quickReplies: msg.quickReplies, quickReplies: msg.quickReplies,
codeSnippet: msg.codeSnippet,
read, read,
}); });
} else if (msg.type === 'code-diff') { } else if (msg.type === 'code-diff') {
@ -262,10 +275,15 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
'Assistant session started', 'Assistant session started',
{ {
chat_session_id: currentSessionId.value, chat_session_id: currentSessionId.value,
task: 'error', task: isSupportChatSessionInProgress.value ? 'support' : 'error',
}, },
{ withPostHog: true }, { withPostHog: true },
); );
// Track first user message in support chat now that we have a session id
if (usersMessages.value.length === 1 && isSupportChatSessionInProgress.value) {
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
trackUserMessage(firstUserMessage.content, false);
}
} else if (currentSessionId.value !== response.sessionId) { } else if (currentSessionId.value !== response.sessionId) {
return; return;
} }
@ -289,6 +307,33 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}, 4000); }, 4000);
} }
async function initSupportChat(userMessage: string) {
const id = getRandomId();
resetAssistantChat();
chatSessionError.value = undefined;
currentSessionActiveExecutionId.value = undefined;
currentSessionWorkflowId.value = workflowsStore.workflowId;
addUserMessage(userMessage, id);
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
streaming.value = true;
chatWithAssistant(
rootStore.restApiContext,
{
payload: {
role: 'user',
type: 'init-support-chat',
user: {
firstName: usersStore.currentUser?.firstName ?? '',
},
question: userMessage,
},
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
(e) => handleServiceError(e, id),
);
}
async function initErrorHelper(context: ChatRequest.ErrorContext) { async function initErrorHelper(context: ChatRequest.ErrorContext) {
const id = getRandomId(); const id = getRandomId();
if (chatSessionError.value) { if (chatSessionError.value) {
@ -433,18 +478,26 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
() => onDoneStreaming(id), () => onDoneStreaming(id),
(e) => handleServiceError(e, id), (e) => handleServiceError(e, id),
); );
telemetry.track('User sent message in Assistant', { trackUserMessage(chatMessage.text, !!chatMessage.quickReplyType);
message: chatMessage.text,
is_quick_reply: !!chatMessage.quickReplyType,
chat_session_id: currentSessionId.value,
message_number: chatMessages.value.filter((msg) => msg.role === 'user').length,
});
} catch (e: unknown) { } catch (e: unknown) {
// in case of assert // in case of assert
handleServiceError(e, id); handleServiceError(e, id);
} }
} }
function trackUserMessage(message: string, isQuickReply: boolean) {
if (!currentSessionId.value) {
return;
}
telemetry.track('User sent message in Assistant', {
message,
is_quick_reply: isQuickReply,
chat_session_id: currentSessionId.value,
message_number: usersMessages.value.length,
task: isSupportChatSessionInProgress.value ? 'support' : 'error',
});
}
function updateParameters(nodeName: string, parameters: INodeParameters) { function updateParameters(nodeName: string, parameters: INodeParameters) {
if (ndvStore.activeNodeName === nodeName) { if (ndvStore.activeNodeName === nodeName) {
Object.keys(parameters).forEach((key) => { Object.keys(parameters).forEach((key) => {
@ -584,6 +637,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
updateWindowWidth, updateWindowWidth,
isNodeErrorActive, isNodeErrorActive,
initErrorHelper, initErrorHelper,
initSupportChat,
sendMessage, sendMessage,
applyCodeDiff, applyCodeDiff,
undoCodeDiff, undoCodeDiff,
@ -591,5 +645,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
chatWindowOpen, chatWindowOpen,
addAssistantMessages, addAssistantMessages,
assistantThinkingMessage, assistantThinkingMessage,
chatSessionError,
isSupportChatSessionInProgress,
}; };
}); });

View file

@ -32,6 +32,15 @@ export namespace ChatRequest {
authType?: { name: string; value: string }; authType?: { name: string; value: string };
} }
export interface InitSupportChat {
role: 'user';
type: 'init-support-chat';
user: {
firstName: string;
};
question: string;
}
export type InteractionEventName = 'node-execution-succeeded' | 'node-execution-errored'; export type InteractionEventName = 'node-execution-succeeded' | 'node-execution-errored';
interface EventRequestPayload { interface EventRequestPayload {
@ -50,7 +59,7 @@ export namespace ChatRequest {
export type RequestPayload = export type RequestPayload =
| { | {
payload: InitErrorHelper; payload: InitErrorHelper | InitSupportChat;
} }
| { | {
payload: EventRequestPayload | UserChatMessage; payload: EventRequestPayload | UserChatMessage;
@ -77,6 +86,7 @@ export namespace ChatRequest {
type: 'message'; type: 'message';
text: string; text: string;
step?: 'n8n_documentation' | 'n8n_forum'; step?: 'n8n_documentation' | 'n8n_forum';
codeSnippet?: string;
} }
interface AssistantSummaryMessage { interface AssistantSummaryMessage {