mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat: Add credential help to Assistant (no-changelog) (#10736)
This commit is contained in:
parent
50459bacab
commit
2bc983ba32
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue