mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
853 lines
24 KiB
TypeScript
853 lines
24 KiB
TypeScript
import { chatWithAssistant, replaceCode } from '@/api/ai';
|
|
import {
|
|
VIEWS,
|
|
EDITABLE_CANVAS_VIEWS,
|
|
STORES,
|
|
AI_ASSISTANT_EXPERIMENT,
|
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
|
CREDENTIAL_EDIT_MODAL_KEY,
|
|
} from '@/constants';
|
|
import type { ChatRequest } from '@/types/assistant.types';
|
|
import type { ChatUI } from 'n8n-design-system/types/assistant';
|
|
import { defineStore } from 'pinia';
|
|
import type { PushPayload } from '@n8n/api-types';
|
|
import { computed, h, ref, watch } from 'vue';
|
|
import { useRootStore } from './root.store';
|
|
import { useUsersStore } from './users.store';
|
|
import { useRoute } from 'vue-router';
|
|
import { useSettingsStore } from './settings.store';
|
|
import { assert } from '@/utils/assert';
|
|
import { useWorkflowsStore } from './workflows.store';
|
|
import type { ICredentialType, INodeParameters, NodeError, INode } from 'n8n-workflow';
|
|
import { deepCopy } from 'n8n-workflow';
|
|
import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
|
|
import { useNDVStore } from './ndv.store';
|
|
import type { IUpdateInformation } from '@/Interface';
|
|
import { usePostHog } from './posthog.store';
|
|
import { useI18n } from '@/composables/useI18n';
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
import { useToast } from '@/composables/useToast';
|
|
import { useUIStore } from './ui.store';
|
|
import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue';
|
|
import { useCredentialsStore } from './credentials.store';
|
|
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
|
|
|
|
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,
|
|
VIEWS.WORKFLOWS,
|
|
VIEWS.CREDENTIALS,
|
|
VIEWS.PROJECTS_CREDENTIALS,
|
|
VIEWS.PROJECTS_WORKFLOWS,
|
|
VIEWS.PROJECT_SETTINGS,
|
|
VIEWS.TEMPLATE_SETUP,
|
|
];
|
|
const READABLE_TYPES = ['code-diff', 'text', 'block'];
|
|
|
|
export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|
const chatWidth = ref<number>(DEFAULT_CHAT_WIDTH);
|
|
|
|
const settings = useSettingsStore();
|
|
const rootStore = useRootStore();
|
|
const chatMessages = ref<ChatUI.AssistantMessage[]>([]);
|
|
const chatWindowOpen = ref<boolean>(false);
|
|
const usersStore = useUsersStore();
|
|
const uiStore = useUIStore();
|
|
const workflowsStore = useWorkflowsStore();
|
|
const route = useRoute();
|
|
const streaming = ref<boolean>();
|
|
const ndvStore = useNDVStore();
|
|
const { getVariant } = usePostHog();
|
|
const locale = useI18n();
|
|
const telemetry = useTelemetry();
|
|
const assistantHelpers = useAIAssistantHelpers();
|
|
|
|
const suggestions = ref<{
|
|
[suggestionId: string]: {
|
|
previous: INodeParameters;
|
|
suggested: INodeParameters;
|
|
};
|
|
}>({});
|
|
|
|
type NodeExecutionStatus = 'error' | 'not_executed' | 'success';
|
|
|
|
const chatSessionCredType = ref<ICredentialType | undefined>();
|
|
const chatSessionError = ref<ChatRequest.ErrorContext | undefined>();
|
|
const currentSessionId = ref<string | undefined>();
|
|
const currentSessionActiveExecutionId = ref<string | undefined>();
|
|
const currentSessionWorkflowId = ref<string | undefined>();
|
|
const lastUnread = ref<ChatUI.AssistantMessage | undefined>();
|
|
const nodeExecutionStatus = ref<NodeExecutionStatus>('not_executed');
|
|
// 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>();
|
|
// Indicate if last sent workflow and execution data is stale
|
|
const workflowDataStale = ref<boolean>(true);
|
|
const workflowExecutionDataStale = ref<boolean>(true);
|
|
|
|
const isExperimentEnabled = computed(
|
|
() => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant,
|
|
);
|
|
|
|
const assistantMessages = computed(() =>
|
|
chatMessages.value.filter((msg) => msg.role === 'assistant'),
|
|
);
|
|
const usersMessages = computed(() => chatMessages.value.filter((msg) => msg.role === 'user'));
|
|
|
|
const isSessionEnded = computed(() => {
|
|
const lastAssistantMessage = assistantMessages.value[assistantMessages.value.length - 1];
|
|
|
|
const sessionExplicitlyEnded =
|
|
lastAssistantMessage?.type === 'event' && lastAssistantMessage?.eventName === 'end-session';
|
|
const sessionStarted = currentSessionId.value !== undefined;
|
|
|
|
return !sessionStarted || sessionExplicitlyEnded;
|
|
});
|
|
|
|
const isAssistantOpen = computed(() => canShowAssistant.value && chatWindowOpen.value);
|
|
|
|
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(
|
|
() =>
|
|
chatMessages.value.filter(
|
|
(msg) => READABLE_TYPES.includes(msg.type) && msg.role === 'assistant' && !msg.read,
|
|
).length,
|
|
);
|
|
|
|
function resetAssistantChat() {
|
|
clearMessages();
|
|
currentSessionId.value = undefined;
|
|
chatSessionError.value = undefined;
|
|
lastUnread.value = undefined;
|
|
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
|
|
// This will prevent animation race conditions from making ndv twitchy
|
|
function openChat() {
|
|
chatWindowOpen.value = true;
|
|
chatMessages.value = chatMessages.value.map((msg) => ({ ...msg, read: true }));
|
|
uiStore.appGridWidth = window.innerWidth - chatWidth.value;
|
|
}
|
|
|
|
function closeChat() {
|
|
chatWindowOpen.value = false;
|
|
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
|
setTimeout(() => {
|
|
uiStore.appGridWidth = window.innerWidth;
|
|
// If session has ended, reset the chat
|
|
if (isSessionEnded.value) {
|
|
resetAssistantChat();
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
|
|
const read = chatWindowOpen.value;
|
|
const messages = [...chatMessages.value].filter(
|
|
(msg) => !(msg.id === id && msg.role === 'assistant'),
|
|
);
|
|
assistantThinkingMessage.value = undefined;
|
|
newMessages.forEach((msg) => {
|
|
if (msg.type === 'message') {
|
|
messages.push({
|
|
id,
|
|
type: 'text',
|
|
role: 'assistant',
|
|
content: msg.text,
|
|
quickReplies: msg.quickReplies,
|
|
codeSnippet: msg.codeSnippet,
|
|
read,
|
|
});
|
|
} else if (msg.type === 'code-diff') {
|
|
messages.push({
|
|
id,
|
|
role: 'assistant',
|
|
type: 'code-diff',
|
|
description: msg.description,
|
|
codeDiff: msg.codeDiff,
|
|
suggestionId: msg.suggestionId,
|
|
quickReplies: msg.quickReplies,
|
|
read,
|
|
});
|
|
} else if (msg.type === 'summary') {
|
|
messages.push({
|
|
id,
|
|
type: 'block',
|
|
role: 'assistant',
|
|
title: msg.title,
|
|
content: msg.content,
|
|
quickReplies: msg.quickReplies,
|
|
read,
|
|
});
|
|
} else if (msg.type === 'event') {
|
|
messages.push({
|
|
id,
|
|
type: 'event',
|
|
role: 'assistant',
|
|
eventName: msg.eventName,
|
|
read: true,
|
|
});
|
|
} else if (msg.type === 'agent-suggestion') {
|
|
messages.push({
|
|
id,
|
|
type: 'block',
|
|
role: 'assistant',
|
|
title: msg.title,
|
|
content: msg.text,
|
|
quickReplies: msg.quickReplies,
|
|
read,
|
|
});
|
|
} else if (msg.type === 'intermediate-step') {
|
|
assistantThinkingMessage.value = msg.text;
|
|
}
|
|
});
|
|
chatMessages.value = messages;
|
|
}
|
|
|
|
function updateWindowWidth(width: number) {
|
|
chatWidth.value = Math.min(Math.max(width, MIN_CHAT_WIDTH), MAX_CHAT_WIDTH);
|
|
}
|
|
|
|
function isNodeErrorActive(context: ChatRequest.ErrorContext) {
|
|
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 = [];
|
|
}
|
|
|
|
function stopStreaming() {
|
|
streaming.value = false;
|
|
}
|
|
|
|
function addAssistantError(content: string, id: string, retry?: () => Promise<void>) {
|
|
chatMessages.value.push({
|
|
id,
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content,
|
|
read: true,
|
|
retry,
|
|
});
|
|
}
|
|
|
|
function addLoadingAssistantMessage(message: string) {
|
|
assistantThinkingMessage.value = message;
|
|
}
|
|
|
|
function addUserMessage(content: string, id: string) {
|
|
chatMessages.value.push({
|
|
id,
|
|
role: 'user',
|
|
type: 'text',
|
|
content,
|
|
read: true,
|
|
});
|
|
}
|
|
|
|
function handleServiceError(e: unknown, id: string, retry?: () => Promise<void>) {
|
|
assert(e instanceof Error);
|
|
stopStreaming();
|
|
assistantThinkingMessage.value = undefined;
|
|
addAssistantError(
|
|
locale.baseText('aiAssistant.serviceError.message', { interpolate: { message: e.message } }),
|
|
id,
|
|
retry,
|
|
);
|
|
}
|
|
|
|
function onEachStreamingMessage(response: ChatRequest.ResponsePayload, id: string) {
|
|
if (response.sessionId && !currentSessionId.value) {
|
|
currentSessionId.value = response.sessionId;
|
|
telemetry.track(
|
|
'Assistant session started',
|
|
{
|
|
chat_session_id: currentSessionId.value,
|
|
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 && chatSessionTask.value === 'support') {
|
|
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
|
|
trackUserMessage(firstUserMessage.content, false);
|
|
}
|
|
} else if (currentSessionId.value !== response.sessionId) {
|
|
return;
|
|
}
|
|
addAssistantMessages(response.messages, id);
|
|
}
|
|
|
|
function getRandomId() {
|
|
return `${Math.floor(Math.random() * 100000000)}`;
|
|
}
|
|
|
|
function onDoneStreaming(id: string) {
|
|
stopStreaming();
|
|
workflowDataStale.value = false;
|
|
workflowExecutionDataStale.value = false;
|
|
lastUnread.value = chatMessages.value.find(
|
|
(msg) =>
|
|
msg.id === id && !msg.read && msg.role === 'assistant' && READABLE_TYPES.includes(msg.type),
|
|
);
|
|
setTimeout(() => {
|
|
if (lastUnread.value?.id === id) {
|
|
lastUnread.value = undefined;
|
|
}
|
|
}, 4000);
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets information about the current view and active node to provide context to the assistant
|
|
*/
|
|
function getVisualContext(
|
|
nodeInfo?: ChatRequest.NodeInfo,
|
|
): ChatRequest.AssistantContext | undefined {
|
|
if (chatSessionTask.value === 'error') {
|
|
return undefined;
|
|
}
|
|
const currentView = route.name as VIEWS;
|
|
const activeNode = workflowsStore.activeNode();
|
|
const activeNodeForLLM = activeNode
|
|
? assistantHelpers.processNodeForAssistant(activeNode, ['position', 'parameters.notice'])
|
|
: null;
|
|
const activeModals = uiStore.activeModals;
|
|
const isCredentialModalActive = activeModals.includes(CREDENTIAL_EDIT_MODAL_KEY);
|
|
const activeCredential = isCredentialModalActive
|
|
? useCredentialsStore().getCredentialTypeByName(uiStore.activeCredentialType ?? '')
|
|
: undefined;
|
|
const executionResult = workflowsStore.workflowExecutionData?.data?.resultData;
|
|
const isCurrentNodeExecuted = Boolean(
|
|
executionResult?.runData?.hasOwnProperty(activeNode?.name ?? ''),
|
|
);
|
|
const currentNodeHasError =
|
|
executionResult?.error &&
|
|
'node' in executionResult.error &&
|
|
executionResult.error.node?.name === activeNode?.name;
|
|
const nodeError = currentNodeHasError ? (executionResult.error as NodeError) : undefined;
|
|
const executionStatus = isCurrentNodeExecuted
|
|
? {
|
|
status: nodeError ? 'error' : 'success',
|
|
error: nodeError ? assistantHelpers.simplifyErrorForAssistant(nodeError) : undefined,
|
|
}
|
|
: undefined;
|
|
return {
|
|
currentView: {
|
|
name: currentView,
|
|
description: assistantHelpers.getCurrentViewDescription(currentView),
|
|
},
|
|
activeNodeInfo: {
|
|
node: activeNodeForLLM ?? undefined,
|
|
nodeIssues: !isCurrentNodeExecuted ? activeNode?.issues : undefined,
|
|
executionStatus,
|
|
nodeInputData: nodeInfo?.nodeInputData,
|
|
referencedNodes: nodeInfo?.schemas,
|
|
},
|
|
activeCredentials: activeCredential
|
|
? {
|
|
name: activeCredential?.name,
|
|
displayName: activeCredential?.displayName,
|
|
authType: nodeInfo?.authType?.name,
|
|
}
|
|
: undefined,
|
|
currentWorkflow: workflowDataStale.value
|
|
? assistantHelpers.simplifyWorkflowForAssistant(workflowsStore.workflow)
|
|
: undefined,
|
|
executionData:
|
|
workflowExecutionDataStale.value && executionResult
|
|
? assistantHelpers.simplifyResultData(executionResult)
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
async function initSupportChat(userMessage: string, credentialType?: ICredentialType) {
|
|
resetAssistantChat();
|
|
chatSessionTask.value = credentialType ? 'credentials' : 'support';
|
|
const activeNode = workflowsStore.activeNode() as INode;
|
|
const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode);
|
|
// For the initial message, only provide visual context if the task is support
|
|
const visualContext =
|
|
chatSessionTask.value === 'support' ? getVisualContext(nodeInfo) : undefined;
|
|
|
|
if (nodeInfo.authType && chatSessionTask.value === 'credentials') {
|
|
userMessage += ` I am using ${nodeInfo.authType.name}.`;
|
|
}
|
|
|
|
const id = getRandomId();
|
|
chatSessionCredType.value = credentialType;
|
|
addUserMessage(userMessage, id);
|
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
|
openChat();
|
|
streaming.value = true;
|
|
|
|
let payload: ChatRequest.InitSupportChat | ChatRequest.InitCredHelp = {
|
|
role: 'user',
|
|
type: 'init-support-chat',
|
|
user: {
|
|
firstName: usersStore.currentUser?.firstName ?? '',
|
|
},
|
|
context: visualContext,
|
|
question: userMessage,
|
|
};
|
|
if (credentialType) {
|
|
payload = {
|
|
...payload,
|
|
type: 'init-cred-help',
|
|
credentialType: {
|
|
name: credentialType.name,
|
|
displayName: credentialType.displayName,
|
|
},
|
|
};
|
|
}
|
|
|
|
chatWithAssistant(
|
|
rootStore.restApiContext,
|
|
{
|
|
payload,
|
|
},
|
|
(msg) => onEachStreamingMessage(msg, id),
|
|
() => onDoneStreaming(id),
|
|
(e) =>
|
|
handleServiceError(e, id, async () => await initSupportChat(userMessage, credentialType)),
|
|
);
|
|
}
|
|
|
|
async function initErrorHelper(context: ChatRequest.ErrorContext) {
|
|
const id = getRandomId();
|
|
if (chatSessionError.value) {
|
|
if (isNodeErrorActive(context)) {
|
|
// context has not changed
|
|
return;
|
|
}
|
|
}
|
|
|
|
resetAssistantChat();
|
|
chatSessionTask.value = 'error';
|
|
chatSessionError.value = context;
|
|
currentSessionWorkflowId.value = workflowsStore.workflowId;
|
|
|
|
if (workflowsStore.activeExecutionId) {
|
|
currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId;
|
|
}
|
|
|
|
const { authType, nodeInputData, schemas } = assistantHelpers.getNodeInfoForAssistant(
|
|
context.node,
|
|
);
|
|
|
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError'));
|
|
openChat();
|
|
|
|
streaming.value = true;
|
|
const payload: ChatRequest.RequestPayload['payload'] = {
|
|
role: 'user',
|
|
type: 'init-error-helper',
|
|
user: {
|
|
firstName: usersStore.currentUser?.firstName ?? '',
|
|
},
|
|
error: context.error,
|
|
node: assistantHelpers.processNodeForAssistant(context.node, [
|
|
'position',
|
|
'parameters.notice',
|
|
]),
|
|
nodeInputData,
|
|
executionSchema: schemas,
|
|
authType,
|
|
};
|
|
chatWithAssistant(
|
|
rootStore.restApiContext,
|
|
{
|
|
payload,
|
|
},
|
|
(msg) => onEachStreamingMessage(msg, id),
|
|
() => onDoneStreaming(id),
|
|
(e) => handleServiceError(e, id, async () => await initErrorHelper(context)),
|
|
);
|
|
}
|
|
|
|
async function sendEvent(
|
|
eventName: ChatRequest.InteractionEventName,
|
|
error?: ChatRequest.ErrorContext['error'],
|
|
) {
|
|
if (isSessionEnded.value || streaming.value) {
|
|
return;
|
|
}
|
|
assert(currentSessionId.value);
|
|
|
|
const id = getRandomId();
|
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
|
streaming.value = true;
|
|
chatWithAssistant(
|
|
rootStore.restApiContext,
|
|
{
|
|
payload: {
|
|
role: 'user',
|
|
type: 'event',
|
|
eventName,
|
|
error,
|
|
},
|
|
sessionId: currentSessionId.value,
|
|
},
|
|
(msg) => onEachStreamingMessage(msg, id),
|
|
() => onDoneStreaming(id),
|
|
(e) => handleServiceError(e, id, async () => await sendEvent(eventName, error)),
|
|
);
|
|
}
|
|
|
|
async function onNodeExecution(pushEvent: PushPayload<'nodeExecuteAfter'>) {
|
|
if (!chatSessionError.value || pushEvent.nodeName !== chatSessionError.value.node.name) {
|
|
return;
|
|
}
|
|
|
|
if (nodeExecutionStatus.value === 'success') {
|
|
return;
|
|
}
|
|
|
|
if (pushEvent.data.error && nodeExecutionStatus.value !== 'error') {
|
|
await sendEvent('node-execution-errored', pushEvent.data.error);
|
|
nodeExecutionStatus.value = 'error';
|
|
telemetry.track('User executed node after assistant suggestion', {
|
|
task: chatSessionTask.value,
|
|
chat_session_id: currentSessionId.value,
|
|
success: false,
|
|
});
|
|
} else if (
|
|
pushEvent.data.executionStatus === 'success' &&
|
|
['error', 'not_executed'].includes(nodeExecutionStatus.value)
|
|
) {
|
|
await sendEvent('node-execution-succeeded');
|
|
nodeExecutionStatus.value = 'success';
|
|
telemetry.track('User executed node after assistant suggestion', {
|
|
task: chatSessionTask.value,
|
|
chat_session_id: currentSessionId.value,
|
|
success: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function sendMessage(
|
|
chatMessage: Pick<ChatRequest.UserChatMessage, 'text' | 'quickReplyType'>,
|
|
) {
|
|
if (isSessionEnded.value || streaming.value) {
|
|
return;
|
|
}
|
|
|
|
const id = getRandomId();
|
|
|
|
const retry = async () => {
|
|
chatMessages.value = chatMessages.value.filter((msg) => msg.id !== id);
|
|
await sendMessage(chatMessage);
|
|
};
|
|
|
|
try {
|
|
addUserMessage(chatMessage.text, id);
|
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
|
|
|
streaming.value = true;
|
|
assert(currentSessionId.value);
|
|
if (
|
|
chatMessage.quickReplyType === 'new-suggestion' &&
|
|
nodeExecutionStatus.value !== 'not_executed'
|
|
) {
|
|
nodeExecutionStatus.value = 'not_executed';
|
|
}
|
|
const activeNode = workflowsStore.activeNode() as INode;
|
|
const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode);
|
|
const userContext = getVisualContext(nodeInfo);
|
|
|
|
chatWithAssistant(
|
|
rootStore.restApiContext,
|
|
{
|
|
payload: {
|
|
role: 'user',
|
|
type: 'message',
|
|
text: chatMessage.text,
|
|
quickReplyType: chatMessage.quickReplyType,
|
|
context: userContext,
|
|
},
|
|
sessionId: currentSessionId.value,
|
|
},
|
|
(msg) => onEachStreamingMessage(msg, id),
|
|
() => onDoneStreaming(id),
|
|
(e) => handleServiceError(e, id, retry),
|
|
);
|
|
trackUserMessage(chatMessage.text, !!chatMessage.quickReplyType);
|
|
} catch (e: unknown) {
|
|
// in case of assert
|
|
handleServiceError(e, id, retry);
|
|
}
|
|
}
|
|
|
|
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: 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.value,
|
|
});
|
|
}
|
|
|
|
function updateParameters(nodeName: string, parameters: INodeParameters) {
|
|
if (ndvStore.activeNodeName === nodeName) {
|
|
Object.keys(parameters).forEach((key) => {
|
|
const update: IUpdateInformation = {
|
|
node: nodeName,
|
|
name: `parameters.${key}`,
|
|
value: parameters[key],
|
|
};
|
|
|
|
ndvEventBus.emit('updateParameterValue', update);
|
|
});
|
|
} else {
|
|
workflowsStore.setNodeParameters(
|
|
{
|
|
name: nodeName,
|
|
value: parameters,
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getRelevantParameters(
|
|
parameters: INodeParameters,
|
|
keysToKeep: string[],
|
|
): INodeParameters {
|
|
return keysToKeep.reduce((accu: INodeParameters, key: string) => {
|
|
accu[key] = deepCopy(parameters[key]);
|
|
return accu;
|
|
}, {} as INodeParameters);
|
|
}
|
|
|
|
async function applyCodeDiff(index: number) {
|
|
const codeDiffMessage = chatMessages.value[index];
|
|
if (!codeDiffMessage || codeDiffMessage.type !== 'code-diff') {
|
|
throw new Error('No code diff to apply');
|
|
}
|
|
|
|
try {
|
|
assert(chatSessionError.value);
|
|
assert(currentSessionId.value);
|
|
|
|
codeDiffMessage.replacing = true;
|
|
const suggestionId = codeDiffMessage.suggestionId;
|
|
|
|
const currentWorkflow = workflowsStore.getCurrentWorkflow();
|
|
const activeNode = currentWorkflow.getNode(chatSessionError.value.node.name);
|
|
assert(activeNode);
|
|
|
|
const cached = suggestions.value[suggestionId];
|
|
if (cached) {
|
|
updateParameters(activeNode.name, cached.suggested);
|
|
} else {
|
|
const { parameters: suggested } = await replaceCode(rootStore.restApiContext, {
|
|
suggestionId: codeDiffMessage.suggestionId,
|
|
sessionId: currentSessionId.value,
|
|
});
|
|
|
|
suggestions.value[suggestionId] = {
|
|
previous: getRelevantParameters(activeNode.parameters, Object.keys(suggested)),
|
|
suggested,
|
|
};
|
|
updateParameters(activeNode.name, suggested);
|
|
}
|
|
|
|
codeDiffMessage.replaced = true;
|
|
codeNodeEditorEventBus.emit('codeDiffApplied');
|
|
showCodeUpdateToastIfNeeded(activeNode.name);
|
|
} catch (e) {
|
|
console.error(e);
|
|
codeDiffMessage.error = true;
|
|
}
|
|
codeDiffMessage.replacing = false;
|
|
}
|
|
|
|
async function undoCodeDiff(index: number) {
|
|
const codeDiffMessage = chatMessages.value[index];
|
|
if (!codeDiffMessage || codeDiffMessage.type !== 'code-diff') {
|
|
throw new Error('No code diff to apply');
|
|
}
|
|
|
|
try {
|
|
assert(chatSessionError.value);
|
|
assert(currentSessionId.value);
|
|
|
|
codeDiffMessage.replacing = true;
|
|
const suggestionId = codeDiffMessage.suggestionId;
|
|
|
|
const suggestion = suggestions.value[suggestionId];
|
|
assert(suggestion);
|
|
|
|
const currentWorkflow = workflowsStore.getCurrentWorkflow();
|
|
const activeNode = currentWorkflow.getNode(chatSessionError.value.node.name);
|
|
assert(activeNode);
|
|
|
|
const suggested = suggestion.previous;
|
|
updateParameters(activeNode.name, suggested);
|
|
|
|
codeDiffMessage.replaced = false;
|
|
codeNodeEditorEventBus.emit('codeDiffApplied');
|
|
showCodeUpdateToastIfNeeded(activeNode.name);
|
|
} catch (e) {
|
|
console.error(e);
|
|
codeDiffMessage.error = true;
|
|
}
|
|
codeDiffMessage.replacing = false;
|
|
}
|
|
|
|
function showCodeUpdateToastIfNeeded(errorNodeName: string) {
|
|
if (errorNodeName !== ndvStore.activeNodeName) {
|
|
useToast().showMessage({
|
|
type: 'success',
|
|
title: locale.baseText('aiAssistant.codeUpdated.message.title'),
|
|
message: h(AiUpdatedCodeMessage, {
|
|
nodeName: errorNodeName,
|
|
}),
|
|
duration: 4000,
|
|
});
|
|
}
|
|
}
|
|
|
|
watch(route, () => {
|
|
const activeWorkflowId = workflowsStore.workflowId;
|
|
if (
|
|
!currentSessionId.value ||
|
|
currentSessionWorkflowId.value === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
|
|
currentSessionWorkflowId.value === activeWorkflowId
|
|
) {
|
|
return;
|
|
}
|
|
resetAssistantChat();
|
|
});
|
|
|
|
watch(
|
|
() => uiStore.stateIsDirty,
|
|
() => {
|
|
workflowDataStale.value = true;
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => workflowsStore.workflowExecutionData?.data?.resultData ?? {},
|
|
() => {
|
|
workflowExecutionDataStale.value = true;
|
|
},
|
|
{ deep: true, immediate: true },
|
|
);
|
|
|
|
return {
|
|
isAssistantEnabled,
|
|
canShowAssistantButtonsOnCanvas,
|
|
chatWidth,
|
|
chatMessages,
|
|
unreadCount,
|
|
streaming,
|
|
isAssistantOpen,
|
|
canShowAssistant,
|
|
currentSessionId,
|
|
lastUnread,
|
|
isSessionEnded,
|
|
onNodeExecution,
|
|
trackUserOpenedAssistant,
|
|
closeChat,
|
|
openChat,
|
|
updateWindowWidth,
|
|
isNodeErrorActive,
|
|
initErrorHelper,
|
|
initSupportChat,
|
|
sendMessage,
|
|
applyCodeDiff,
|
|
undoCodeDiff,
|
|
resetAssistantChat,
|
|
chatWindowOpen,
|
|
addAssistantMessages,
|
|
assistantThinkingMessage,
|
|
chatSessionError,
|
|
chatSessionTask,
|
|
initCredHelp,
|
|
isCredTypeActive,
|
|
handleServiceError,
|
|
};
|
|
});
|