n8n-editor-ui
Version:
Workflow Editor UI for n8n
521 lines (520 loc) • 20.8 kB
JavaScript
import { C as computed, Cn as toDisplayString, D as createElementBlock, Gt as unref, It as ref, L as h, P as defineComponent, _t as watch, et as openBlock, j as createTextVNode, w as createBaseVNode } from "./vue.runtime.esm-bundler-tP5dCd7J.js";
import { _t as useI18n } from "./_MapCache-ficiegRb.js";
import { T as createEventBus, v as useRoute } from "./truncate-B1HVeveJ.js";
import { $o as CREDENTIAL_EDIT_MODAL_KEY, Dr as useUsersStore, Ec as STORES, M as useNDVStore, N as useWorkflowsStore, O as useTelemetry, _n as chatWithAssistant, bn as useAIAssistantHelpers, fs as v4_default, gn as useCredentialsStore, gs as useSettingsStore, ia as codeNodeEditorEventBus, oa as useUIStore, v as useToast, yn as replaceCode } from "./builder.store-sBTWwxRU.js";
import { Io as EDITABLE_CANVAS_VIEWS, wa as deepCopy } from "./constants-BbpucWL4.js";
import { g as assert, k as defineStore, r as useRootStore } from "./_baseOrderBy-B_6CV8x6.js";
const useChatPanelStateStore = defineStore(STORES.CHAT_PANEL_STATE, () => {
return {
isOpen: ref(false),
width: ref(400),
activeMode: ref("builder")
};
});
const ndvEventBus = createEventBus();
var _hoisted_1 = ["data-action-parameter-node"];
var AiUpdatedCodeMessage_default = /* @__PURE__ */ defineComponent({
__name: "AiUpdatedCodeMessage",
props: { nodeName: {} },
setup(__props) {
const i18n = useI18n();
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", null, [
createTextVNode(toDisplayString(unref(i18n).baseText("aiAssistant.codeUpdated.message.body1")) + " ", 1),
createBaseVNode("a", {
"data-action": "openNodeDetail",
"data-action-parameter-node": _ctx.nodeName
}, toDisplayString(_ctx.nodeName), 9, _hoisted_1),
createTextVNode(" " + toDisplayString(unref(i18n).baseText("aiAssistant.codeUpdated.message.body2")), 1)
]);
};
}
});
var READABLE_TYPES = [
"code-diff",
"text",
"block"
];
const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const settings = useSettingsStore();
const rootStore = useRootStore();
const chatPanelStateStore = useChatPanelStateStore();
const chatMessages = ref([]);
const usersStore = useUsersStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const route = useRoute();
const streaming = ref();
const ndvStore = useNDVStore();
const locale = useI18n();
const telemetry = useTelemetry();
const assistantHelpers = useAIAssistantHelpers();
const suggestions = ref({});
const chatSessionCredType = ref();
const chatSessionError = ref();
const currentSessionId = ref();
const currentSessionActiveExecutionId = ref(void 0);
const currentSessionWorkflowId = ref();
const lastUnread = ref();
const nodeExecutionStatus = ref("not_executed");
const assistantThinkingMessage = ref();
const chatSessionTask = ref();
const workflowDataStale = ref(true);
const workflowExecutionDataStale = ref(true);
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";
return !(currentSessionId.value !== void 0) || sessionExplicitlyEnded;
});
const isAssistantEnabled = computed(() => settings.isAiAssistantEnabled);
const hideAssistantFloatingButton = computed(() => EDITABLE_CANVAS_VIEWS.includes(route.name) && !workflowsStore.activeNode());
const unreadCount = computed(() => chatMessages.value.filter((msg) => READABLE_TYPES.includes(msg.type) && msg.role === "assistant" && !msg.read).length);
const isFloatingButtonShown = computed(() => !chatPanelStateStore.isOpen && !hideAssistantFloatingButton.value && isAssistantEnabled.value && EDITABLE_CANVAS_VIEWS.includes(route.name));
function resetAssistantChat() {
clearMessages();
currentSessionId.value = void 0;
chatSessionError.value = void 0;
lastUnread.value = void 0;
currentSessionActiveExecutionId.value = void 0;
suggestions.value = {};
nodeExecutionStatus.value = "not_executed";
chatSessionCredType.value = void 0;
chatSessionTask.value = void 0;
currentSessionWorkflowId.value = workflowsStore.workflowId;
}
function addAssistantMessages(newMessages, id) {
const read = chatPanelStateStore.isOpen && chatPanelStateStore.activeMode === "assistant";
const messages = [...chatMessages.value].filter((msg) => !(msg.id === id && msg.role === "assistant"));
assistantThinkingMessage.value = void 0;
(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 isNodeErrorActive(context) {
const targetNode = context.node.name;
return chatSessionTask.value === "error" && workflowsStore.activeExecutionId === currentSessionActiveExecutionId.value && targetNode === chatSessionError.value?.node.name;
}
function isCredTypeActive(credType) {
return chatSessionTask.value === "credentials" && credType.name === chatSessionCredType.value?.name;
}
function clearMessages() {
chatMessages.value = [];
}
function stopStreaming() {
streaming.value = false;
}
function addAssistantError(content, id, retry) {
chatMessages.value.push({
id,
role: "assistant",
type: "error",
content,
read: true,
retry
});
}
function addLoadingAssistantMessage(message) {
assistantThinkingMessage.value = message;
}
function addUserMessage(content, id) {
chatMessages.value.push({
id,
role: "user",
type: "text",
content,
read: true
});
}
function handleServiceError(e, id, retry) {
assert(e instanceof Error);
stopStreaming();
assistantThinkingMessage.value = void 0;
addAssistantError(locale.baseText("aiAssistant.serviceError.message", { interpolate: { message: e.message } }), id, retry);
}
function onEachStreamingMessage(response, id) {
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
});
if (usersMessages.value.length === 1 && chatSessionTask.value === "support") {
const firstUserMessage = usersMessages.value[0];
trackUserMessage(firstUserMessage.content, false);
}
} else if (currentSessionId.value !== response.sessionId) return;
addAssistantMessages(response.messages, id);
}
function getRandomId() {
return `${Math.floor(Math.random() * 1e8)}`;
}
function onDoneStreaming(id) {
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 = void 0;
}, 4e3);
}
async function initCredHelp(credType) {
const hasExistingSession = !!currentSessionId.value;
await initSupportChat(`How do I set up the credentials for ${credType.displayName}?`, credType);
trackUserOpenedAssistant({
source: "credential",
task: "credentials",
has_existing_session: hasExistingSession
});
}
function getVisualContext(nodeInfo) {
if (chatSessionTask.value === "error") return;
const currentView = route.name;
const activeNode = workflowsStore.activeNode();
const activeNodeForLLM = activeNode ? assistantHelpers.processNodeForAssistant(activeNode, ["position", "parameters.notice"]) : null;
const activeCredential = uiStore.activeModals.includes("editCredential") ? useCredentialsStore().getCredentialTypeByName(uiStore.activeCredentialType ?? "") : void 0;
const executionResult = workflowsStore.workflowExecutionData?.data?.resultData;
const isCurrentNodeExecuted = Boolean(executionResult?.runData?.hasOwnProperty(activeNode?.name ?? ""));
const nodeError = executionResult?.error && "node" in executionResult.error && executionResult.error.node?.name === activeNode?.name ? executionResult.error : void 0;
const executionStatus = isCurrentNodeExecuted ? {
status: nodeError ? "error" : "success",
error: nodeError ? assistantHelpers.simplifyErrorForAssistant(nodeError) : void 0
} : void 0;
return {
currentView: {
name: currentView,
description: assistantHelpers.getCurrentViewDescription(currentView)
},
activeNodeInfo: {
node: activeNodeForLLM ?? void 0,
nodeIssues: !isCurrentNodeExecuted ? activeNode?.issues : void 0,
executionStatus,
nodeInputData: nodeInfo?.nodeInputData,
referencedNodes: nodeInfo?.schemas
},
activeCredentials: activeCredential ? {
name: activeCredential?.name,
displayName: activeCredential?.displayName,
authType: nodeInfo?.authType?.name
} : void 0,
currentWorkflow: workflowDataStale.value ? assistantHelpers.simplifyWorkflowForAssistant(workflowsStore.workflow) : void 0,
executionData: workflowExecutionDataStale.value && executionResult ? assistantHelpers.simplifyResultData(executionResult) : void 0
};
}
async function initSupportChat(userMessage, credentialType) {
resetAssistantChat();
chatSessionTask.value = credentialType ? "credentials" : "support";
const activeNode = workflowsStore.activeNode();
const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode);
const visualContext = chatSessionTask.value === "support" ? getVisualContext(nodeInfo) : void 0;
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"));
streaming.value = true;
let payload = {
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) {
const id = getRandomId();
if (chatSessionError.value) {
if (isNodeErrorActive(context)) 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"));
streaming.value = true;
const 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, 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) {
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) {
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();
const userContext = getVisualContext(assistantHelpers.getNodeInfoForAssistant(activeNode));
chatWithAssistant(rootStore.restApiContext, {
payload: {
id: v4_default(),
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) {
handleServiceError(e, id, retry);
}
}
function trackUserMessage(message, isQuickReply) {
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 }) {
const canvasStatus = workflowsStore.allNodes.length === 0 ? "empty" : "existing_workflow";
telemetry.track("User opened assistant", {
source,
task,
has_existing_session,
instance_id: rootStore.instanceId,
workflow_id: workflowsStore.workflowId,
canvas_status: canvasStatus,
node_type: chatSessionError.value?.node?.type,
error: chatSessionError.value?.error,
chat_session_id: currentSessionId.value
});
}
function updateParameters(workflowState, nodeName, parameters) {
if (ndvStore.activeNodeName === nodeName) Object.keys(parameters).forEach((key) => {
const update = {
node: nodeName,
name: `parameters.${key}`,
value: parameters[key]
};
ndvEventBus.emit("updateParameterValue", update);
});
else workflowState.setNodeParameters({
name: nodeName,
value: parameters
}, true);
}
function getRelevantParameters(parameters, keysToKeep) {
return keysToKeep.reduce((accu, key) => {
accu[key] = deepCopy(parameters[key]);
return accu;
}, {});
}
async function applyCodeDiff(workflowState, index) {
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 activeNode = workflowsStore.workflowObject.getNode(chatSessionError.value.node.name);
assert(activeNode);
const cached = suggestions.value[suggestionId];
if (cached) updateParameters(workflowState, 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(workflowState, 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(workflowState, index) {
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 activeNode = workflowsStore.workflowObject.getNode(chatSessionError.value.node.name);
assert(activeNode);
const suggested = suggestion.previous;
updateParameters(workflowState, 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) {
if (errorNodeName !== ndvStore.activeNodeName) useToast().showMessage({
type: "success",
title: locale.baseText("aiAssistant.codeUpdated.message.title"),
message: h(AiUpdatedCodeMessage_default, { nodeName: errorNodeName }),
duration: 4e3
});
}
watch(route, () => {
const activeWorkflowId = workflowsStore.workflowId;
if (!currentSessionId.value || !currentSessionWorkflowId.value || currentSessionWorkflowId.value === activeWorkflowId) return;
resetAssistantChat();
});
watch(() => uiStore.stateIsDirty, () => {
workflowDataStale.value = true;
});
watch(() => workflowsStore.workflowExecutionResultDataLastUpdate, () => {
workflowExecutionDataStale.value = true;
}, { immediate: true });
return {
isAssistantEnabled,
hideAssistantFloatingButton,
chatMessages,
unreadCount,
streaming,
currentSessionId,
lastUnread,
isSessionEnded,
isFloatingButtonShown,
onNodeExecution,
trackUserOpenedAssistant,
isNodeErrorActive,
initErrorHelper,
initSupportChat,
sendMessage,
applyCodeDiff,
undoCodeDiff,
resetAssistantChat,
addAssistantMessages,
assistantThinkingMessage,
chatSessionError,
chatSessionTask,
initCredHelp,
isCredTypeActive,
handleServiceError
};
});
export { ndvEventBus as n, useChatPanelStateStore as r, useAssistantStore as t };