feat: Add credential help to Assistant (no-changelog) (#10736)

This commit is contained in:
Mutasem Aldmour 2024-09-11 16:38:39 +02:00 committed by GitHub
parent 50459bacab
commit 2bc983ba32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 284 additions and 80 deletions

View file

@ -1,10 +1,13 @@
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant';
const wf = new WorkflowPage();
const ndv = new NDV();
const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('AI Assistant::disabled', () => {
beforeEach(() => {
@ -303,3 +306,71 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.placeholderMessage().should('not.exist');
});
});
describe('AI Assistant Credential Help', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
after(() => {
aiAssistant.actions.disableAssistant();
});
it('should start credential help from node credential', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
wf.actions.openNode('Gmail');
openCredentialSelect();
clickCreateNewCredential();
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Gmail OAuth2 API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
});
it('should start credential help from credential list', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Notion API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
});
});

View file

@ -35,6 +35,8 @@ export class AIAssistant extends BasePage {
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
nodeErrorViewAssistantButton: () =>
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
credentialEditAssistantButton: () =>
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
};
actions = {

View file

@ -30,7 +30,7 @@ useHistoryHelper(route);
const loading = ref(true);
const defaultLocale = computed(() => rootStore.defaultLocale);
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtons);
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtonsOnCanvas);
const appGrid = ref<Element | null>(null);

View file

@ -33,7 +33,7 @@ async function onUserMessage(content: string, quickReplyType?: string, isFeedbac
} else {
await assistantStore.sendMessage({ text: content, quickReplyType });
}
const task = assistantStore.isSupportChatSessionInProgress ? 'support' : 'error';
const task = assistantStore.chatSessionTask;
const solutionCount = assistantStore.chatMessages.filter(
(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type),
).length;

View file

@ -1,16 +1,12 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useAssistantStore } from '@/stores/assistant.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import AssistantAvatar from 'n8n-design-system/components/AskAssistantAvatar/AssistantAvatar.vue';
import AskAssistantButton from 'n8n-design-system/components/AskAssistantButton/AskAssistantButton.vue';
import { computed } from 'vue';
const assistantStore = useAssistantStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const workflowStore = useWorkflowsStore();
const lastUnread = computed(() => {
const msg = assistantStore.lastUnread;
@ -28,18 +24,17 @@ const lastUnread = computed(() => {
const onClick = () => {
assistantStore.openChat();
telemetry.track('User opened assistant', {
assistantStore.trackUserOpenedAssistant({
source: 'canvas',
task: 'placeholder',
has_existing_session: !assistantStore.isSessionEnded,
workflow_id: workflowStore.workflowId,
});
};
</script>
<template>
<div
v-if="assistantStore.canShowAssistantButtons && !assistantStore.isAssistantOpen"
v-if="assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.isAssistantOpen"
:class="$style.container"
data-test-id="ask-assistant-floating-button"
>

View file

@ -7,19 +7,16 @@ import { useI18n } from '@/composables/useI18n';
import { useUIStore } from '@/stores/ui.store';
import type { ChatRequest } from '@/types/assistant.types';
import { useAssistantStore } from '@/stores/assistant.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { ICredentialType } from 'n8n-workflow';
const i18n = useI18n();
const uiStore = useUIStore();
const assistantStore = useAssistantStore();
const workflowsStore = useWorkflowsStore();
const telemetry = useTelemetry();
const props = defineProps<{
name: string;
data: {
context: ChatRequest.ErrorContext;
context: { errorHelp: ChatRequest.ErrorContext } | { credHelp: { credType: ICredentialType } };
};
}>();
@ -28,16 +25,16 @@ const close = () => {
};
const startNewSession = async () => {
await assistantStore.initErrorHelper(props.data.context);
telemetry.track('User opened assistant', {
source: 'error',
task: 'error',
has_existing_session: true,
workflow_id: workflowsStore.workflowId,
node_type: props.data.context.node.type,
error: props.data.context.error,
chat_session_id: assistantStore.currentSessionId,
});
if ('errorHelp' in props.data.context) {
await assistantStore.initErrorHelper(props.data.context.errorHelp);
assistantStore.trackUserOpenedAssistant({
source: 'error',
task: 'error',
has_existing_session: true,
});
} else if ('credHelp' in props.data.context) {
await assistantStore.initCredHelp(props.data.context.credHelp.credType);
}
close();
};
</script>

View file

@ -18,6 +18,7 @@ import {
CREDENTIAL_DOCS_EXPERIMENT,
DOCS_DOMAIN,
EnterpriseEditionFeature,
NEW_ASSISTANT_SESSION_MODAL,
} from '@/constants';
import type { PermissionsRecord } from '@/permissions';
import { addCredentialTranslation } from '@/plugins/i18n';
@ -34,6 +35,8 @@ import OauthButton from './OauthButton.vue';
import CredentialDocs from './CredentialDocs.vue';
import { CREDENTIAL_MARKDOWN_DOCS } from './docs';
import { usePostHog } from '@/stores/posthog.store';
import { useAssistantStore } from '@/stores/assistant.store';
import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue';
type Props = {
mode: string;
@ -74,6 +77,7 @@ const ndvStore = useNDVStore();
const rootStore = useRootStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const assistantStore = useAssistantStore();
const i18n = useI18n();
const telemetry = useTelemetry();
@ -167,6 +171,19 @@ const isMissingCredentials = computed(() => props.credentialType === null);
const isNewCredential = computed(() => props.mode === 'new' && !props.credentialId);
const isAskAssistantAvailable = computed(
() =>
documentationUrl.value &&
documentationUrl.value.includes(DOCS_DOMAIN) &&
props.credentialProperties.length &&
props.credentialPermissions.update &&
assistantStore.isAssistantEnabled,
);
const assistantAlreadyAsked = computed<boolean>(() => {
return assistantStore.isCredTypeActive(props.credentialType);
});
const docs = computed(() => CREDENTIAL_MARKDOWN_DOCS[props.credentialType.name]);
const showCredentialDocs = computed(
() =>
@ -191,6 +208,24 @@ function onAuthTypeChange(newType: string): void {
emit('authTypeChanged', newType);
}
async function onAskAssistantClick() {
const sessionInProgress = !assistantStore.isSessionEnded;
if (sessionInProgress) {
uiStore.openModalWithData({
name: NEW_ASSISTANT_SESSION_MODAL,
data: {
context: {
credHelp: {
credType: props.credentialType,
},
},
},
});
return;
}
await assistantStore.initCredHelp(props.credentialType);
}
watch(showOAuthSuccessBanner, (newValue, oldValue) => {
if (newValue && !oldValue) {
emit('scrollToTop');
@ -284,6 +319,15 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
@auth-type-changed="onAuthTypeChange"
/>
<div
v-if="isAskAssistantAvailable"
:class="$style.askAssistantButton"
data-test-id="credentail-edit-ask-assistant-button"
>
<InlineAskAssistantButton :asked="assistantAlreadyAsked" @click="onAskAssistantClick" />
<span>for setup instructions</span>
</div>
<CopyInput
v-if="isOAuthType && !allOAuth2BasePropertiesOverridden"
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
@ -384,4 +428,13 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
.googleReconnectLabel {
margin-right: var(--spacing-3xs);
}
.askAssistantButton {
display: flex;
align-items: center;
> span {
margin-left: var(--spacing-3xs);
font-size: var(--font-size-s);
}
}
</style>

View file

@ -23,8 +23,6 @@ import type { ChatRequest } from '@/types/assistant.types';
import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue';
import { useUIStore } from '@/stores/ui.store';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store';
type Props = {
// TODO: .node can be undefined
@ -41,9 +39,7 @@ const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore();
const rootStore = useRootStore();
const assistantStore = useAssistantStore();
const workflowsStore = useWorkflowsStore();
const uiStore = useUIStore();
const telemetry = useTelemetry();
const displayCause = computed(() => {
return JSON.stringify(props.error.cause ?? '').length < MAX_DISPLAY_DATA_SIZE;
@ -123,7 +119,7 @@ const isAskAssistantAvailable = computed(() => {
return false;
}
const isCustomNode = node.value.type === undefined || isCommunityPackageName(node.value.type);
return assistantStore.canShowAssistantButtons && !isCustomNode;
return assistantStore.canShowAssistantButtonsOnCanvas && !isCustomNode;
});
const assistantAlreadyAsked = computed(() => {
@ -411,7 +407,7 @@ function copySuccess() {
async function onAskAssistantClick() {
const { message, lineNumber, description } = props.error;
const sessionInProgress = !assistantStore.isSessionEnded;
const errorPayload: ChatRequest.ErrorContext = {
const errorHelp: ChatRequest.ErrorContext = {
error: {
name: props.error.name,
message,
@ -424,18 +420,15 @@ async function onAskAssistantClick() {
if (sessionInProgress) {
uiStore.openModalWithData({
name: NEW_ASSISTANT_SESSION_MODAL,
data: { context: errorPayload },
data: { context: { errorHelp } },
});
return;
}
await assistantStore.initErrorHelper(errorPayload);
telemetry.track('User opened assistant', {
await assistantStore.initErrorHelper(errorHelp);
assistantStore.trackUserOpenedAssistant({
source: 'error',
task: 'error',
has_existing_session: false,
workflow_id: workflowsStore.workflowId,
node_type: node.value.type,
error: props.error,
});
}
</script>

View file

@ -271,8 +271,9 @@ describe('AI Assistant store', () => {
mockPostHogVariant('control');
setAssistantEnabled(true);
expect(assistantStore.isAssistantEnabled).toBe(false);
expect(assistantStore.canShowAssistant).toBe(false);
expect(assistantStore.canShowAssistantButtons).toBe(false);
expect(assistantStore.canShowAssistantButtonsOnCanvas).toBe(false);
});
it('should not show assistant if disabled in settings', () => {
@ -280,8 +281,9 @@ describe('AI Assistant store', () => {
mockPostHogVariant('variant');
setAssistantEnabled(false);
expect(assistantStore.isAssistantEnabled).toBe(false);
expect(assistantStore.canShowAssistant).toBe(false);
expect(assistantStore.canShowAssistantButtons).toBe(false);
expect(assistantStore.canShowAssistantButtonsOnCanvas).toBe(false);
});
it('should show assistant if all conditions are met', () => {
@ -289,8 +291,9 @@ describe('AI Assistant store', () => {
setAssistantEnabled(true);
mockPostHogVariant('variant');
expect(assistantStore.isAssistantEnabled).toBe(true);
expect(assistantStore.canShowAssistant).toBe(true);
expect(assistantStore.canShowAssistantButtons).toBe(true);
expect(assistantStore.canShowAssistantButtonsOnCanvas).toBe(true);
});
it('should initialize assistant chat session on node error', async () => {

View file

@ -16,7 +16,7 @@ import { useRoute } from 'vue-router';
import { useSettingsStore } from './settings.store';
import { assert } from '@/utils/assert';
import { useWorkflowsStore } from './workflows.store';
import type { INodeParameters } from 'n8n-workflow';
import type { ICredentialType, INodeParameters } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
import { useNDVStore } from './ndv.store';
@ -39,7 +39,12 @@ import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue';
export const MAX_CHAT_WIDTH = 425;
export const MIN_CHAT_WIDTH = 250;
export const DEFAULT_CHAT_WIDTH = 330;
export const ENABLED_VIEWS = [...EDITABLE_CANVAS_VIEWS, VIEWS.EXECUTION_PREVIEW];
export const ENABLED_VIEWS = [
...EDITABLE_CANVAS_VIEWS,
VIEWS.EXECUTION_PREVIEW,
VIEWS.WORKFLOWS,
VIEWS.CREDENTIALS,
];
const READABLE_TYPES = ['code-diff', 'text', 'block'];
export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
@ -65,6 +70,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
suggested: INodeParameters;
};
}>({});
const chatSessionCredType = ref<ICredentialType | undefined>();
const chatSessionError = ref<ChatRequest.ErrorContext | undefined>();
const currentSessionId = ref<string | undefined>();
const currentSessionActiveExecutionId = ref<string | undefined>();
@ -74,18 +81,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
// This is used to show a message when the assistant is performing intermediate steps
// We use streaming for assistants that support it, and this for agents
const assistantThinkingMessage = ref<string | undefined>();
const chatSessionTask = ref<'error' | 'support' | 'credentials' | undefined>();
const isExperimentEnabled = computed(
() => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant,
);
const canShowAssistant = computed(
() =>
isExperimentEnabled.value &&
settings.isAiAssistantEnabled &&
ENABLED_VIEWS.includes(route.name as VIEWS),
);
const assistantMessages = computed(() =>
chatMessages.value.filter((msg) => msg.role === 'assistant'),
);
@ -103,11 +104,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const isAssistantOpen = computed(() => canShowAssistant.value && chatWindowOpen.value);
const canShowAssistantButtons = computed(
() =>
isExperimentEnabled.value &&
settings.isAiAssistantEnabled &&
EDITABLE_CANVAS_VIEWS.includes(route.name as VIEWS),
const isAssistantEnabled = computed(
() => isExperimentEnabled.value && settings.isAiAssistantEnabled,
);
const canShowAssistant = computed(
() => isAssistantEnabled.value && ENABLED_VIEWS.includes(route.name as VIEWS),
);
const canShowAssistantButtonsOnCanvas = computed(
() => isAssistantEnabled.value && EDITABLE_CANVAS_VIEWS.includes(route.name as VIEWS),
);
const unreadCount = computed(
@ -117,10 +123,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
).length,
);
const isSupportChatSessionInProgress = computed(() => {
return currentSessionId.value !== undefined && chatSessionError.value === undefined;
});
watch(route, () => {
const activeWorkflowId = workflowsStore.workflowId;
if (
@ -141,6 +143,9 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
currentSessionActiveExecutionId.value = undefined;
suggestions.value = {};
nodeExecutionStatus.value = 'not_executed';
chatSessionCredType.value = undefined;
chatSessionTask.value = undefined;
currentSessionWorkflowId.value = workflowsStore.workflowId;
}
// As assistant sidebar opens and closes, use window width to calculate the container width
@ -169,7 +174,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
(msg) => !(msg.id === id && msg.role === 'assistant'),
);
assistantThinkingMessage.value = undefined;
// TODO: simplify
newMessages.forEach((msg) => {
if (msg.type === 'message') {
messages.push({
@ -235,11 +239,18 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const targetNode = context.node.name;
return (
chatSessionTask.value === 'error' &&
workflowsStore.activeExecutionId === currentSessionActiveExecutionId.value &&
targetNode === chatSessionError.value?.node.name
);
}
function isCredTypeActive(credType: ICredentialType) {
return (
chatSessionTask.value === 'credentials' && credType.name === chatSessionCredType.value?.name
);
}
function clearMessages() {
chatMessages.value = [];
}
@ -286,13 +297,18 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
'Assistant session started',
{
chat_session_id: currentSessionId.value,
task: isSupportChatSessionInProgress.value ? 'support' : 'error',
task: chatSessionTask.value,
node_type: chatSessionError.value?.node.type,
credential_type: chatSessionCredType.value?.name,
},
{ withPostHog: true },
);
// Track first user message in support chat now that we have a session id
if (usersMessages.value.length === 1 && isSupportChatSessionInProgress.value) {
if (
usersMessages.value.length === 1 &&
!currentSessionId.value &&
chatSessionTask.value === 'support'
) {
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
trackUserMessage(firstUserMessage.content, false);
}
@ -319,26 +335,53 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}, 4000);
}
async function initSupportChat(userMessage: string) {
async function initCredHelp(credType: ICredentialType) {
const hasExistingSession = !!currentSessionId.value;
const credentialName = credType.displayName;
const question = `How do I set up the credentials for ${credentialName}?`;
await initSupportChat(question, credType);
trackUserOpenedAssistant({
source: 'credential',
task: 'credentials',
has_existing_session: hasExistingSession,
});
}
async function initSupportChat(userMessage: string, credentialType?: ICredentialType) {
const id = getRandomId();
resetAssistantChat();
chatSessionError.value = undefined;
currentSessionActiveExecutionId.value = undefined;
currentSessionWorkflowId.value = workflowsStore.workflowId;
chatSessionTask.value = credentialType ? 'credentials' : 'support';
chatSessionCredType.value = credentialType;
chatWindowOpen.value = true;
addUserMessage(userMessage, id);
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
streaming.value = true;
let payload: ChatRequest.InitSupportChat | ChatRequest.InitCredHelp = {
role: 'user',
type: 'init-support-chat',
user: {
firstName: usersStore.currentUser?.firstName ?? '',
},
question: userMessage,
};
if (credentialType) {
payload = {
...payload,
type: 'init-cred-help',
credentialType: {
name: credentialType.name,
displayName: credentialType.displayName,
},
};
}
chatWithAssistant(
rootStore.restApiContext,
{
payload: {
role: 'user',
type: 'init-support-chat',
user: {
firstName: usersStore.currentUser?.firstName ?? '',
},
question: userMessage,
},
payload,
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
@ -356,6 +399,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}
resetAssistantChat();
chatSessionTask.value = 'error';
chatSessionError.value = context;
currentSessionWorkflowId.value = workflowsStore.workflowId;
@ -437,7 +481,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
await sendEvent('node-execution-errored', pushEvent.data.error);
nodeExecutionStatus.value = 'error';
telemetry.track('User executed node after assistant suggestion', {
task: 'error',
task: chatSessionTask.value,
chat_session_id: currentSessionId.value,
success: false,
});
@ -448,7 +492,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
await sendEvent('node-execution-succeeded');
nodeExecutionStatus.value = 'success';
telemetry.track('User executed node after assistant suggestion', {
task: 'error',
task: chatSessionTask.value,
chat_session_id: currentSessionId.value,
success: true,
});
@ -506,7 +550,36 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
is_quick_reply: isQuickReply,
chat_session_id: currentSessionId.value,
message_number: usersMessages.value.length,
task: isSupportChatSessionInProgress.value ? 'support' : 'error',
task: chatSessionTask.value,
});
}
function trackUserOpenedAssistant({
source,
task,
has_existing_session,
}: { has_existing_session: boolean } & (
| {
source: 'error';
task: 'error';
}
| {
source: 'canvas';
task: 'placeholder';
}
| {
source: 'credential';
task: 'credentials';
}
)) {
telemetry.track('User opened assistant', {
source,
task,
has_existing_session,
workflow_id: workflowsStore.workflowId,
node_type: chatSessionError.value?.node?.type,
error: chatSessionError.value?.error,
chat_session_id: currentSessionId,
});
}
@ -632,17 +705,19 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}
return {
isAssistantEnabled,
canShowAssistantButtonsOnCanvas,
chatWidth,
chatMessages,
unreadCount,
streaming,
isAssistantOpen,
canShowAssistant,
canShowAssistantButtons,
currentSessionId,
lastUnread,
isSessionEnded,
onNodeExecution,
trackUserOpenedAssistant,
closeChat,
openChat,
updateWindowWidth,
@ -657,6 +732,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
addAssistantMessages,
assistantThinkingMessage,
chatSessionError,
isSupportChatSessionInProgress,
chatSessionTask,
initCredHelp,
isCredTypeActive,
};
});

View file

@ -41,6 +41,19 @@ export namespace ChatRequest {
question: string;
}
export interface InitCredHelp {
role: 'user';
type: 'init-cred-help';
user: {
firstName: string;
};
question: string;
credentialType: {
name: string;
displayName: string;
};
}
export type InteractionEventName = 'node-execution-succeeded' | 'node-execution-errored';
interface EventRequestPayload {
@ -59,7 +72,7 @@ export namespace ChatRequest {
export type RequestPayload =
| {
payload: InitErrorHelper | InitSupportChat;
payload: InitErrorHelper | InitSupportChat | InitCredHelp;
}
| {
payload: EventRequestPayload | UserChatMessage;