feat: Add Chat Trigger node (#7409)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
Co-authored-by: Jesper Bylund <mail@jesperbylund.com>
Co-authored-by: OlegIvaniv <me@olegivaniv.com>
Co-authored-by: Deborah <deborah@starfallprojects.co.uk>
Co-authored-by: Jan Oberhauser <janober@users.noreply.github.com>
Co-authored-by: Jon <jonathan.bennetts@gmail.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: Giulio Andreini <andreini@netseven.it>
Co-authored-by: Mason Geloso <Mason.geloso@gmail.com>
Co-authored-by: Mason Geloso <hone@Masons-Mac-mini.local>
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
Alex Grozav
2024-01-09 13:11:39 +02:00
committed by GitHub
parent 1387541e33
commit af49e95cc7
90 changed files with 2671 additions and 668 deletions

View File

@@ -4,7 +4,7 @@ import { computed, ref } from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '../constants';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
@@ -43,11 +43,30 @@ const tabs = ref([
const currentTab = ref('cdn');
const webhookNode = computed(() => {
return workflowsStore.workflow.nodes.find((node) => node.type === WEBHOOK_NODE_TYPE);
for (const type of [CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE]) {
const node = workflowsStore.workflow.nodes.find((node) => node.type === type);
if (node) {
// This has to be kept up-to-date with the mode in the Chat-Trigger node
if (type === CHAT_TRIGGER_NODE_TYPE && !node.parameters.public) {
continue;
}
return {
type,
node,
};
}
}
return null;
});
const webhookUrl = computed(() => {
return `${rootStore.getWebhookUrl}${webhookNode.value ? `/${webhookNode.value.webhookId}` : ''}`;
const url = `${rootStore.getWebhookUrl}${
webhookNode.value ? `/${webhookNode.value.node.webhookId}` : ''
}`;
return webhookNode.value?.type === CHAT_TRIGGER_NODE_TYPE ? `${url}/chat` : url;
});
function indentLines(code: string, indent: string = ' ') {
@@ -57,7 +76,7 @@ function indentLines(code: string, indent: string = ' ') {
.join('\n');
}
const importCode = 'import';
const importCode = 'import'; // To avoid vite from parsing the import statement
const commonCode = computed(() => ({
import: `${importCode} '@n8n/chat/style.css';
${importCode} { createChat } from '@n8n/chat';`,
@@ -126,29 +145,38 @@ function closeDialog() {
<n8n-tabs v-model="currentTab" :options="tabs" />
<div v-if="currentTab !== 'cdn'">
<n8n-text>
{{ i18n.baseText('chatEmbed.install') }}
</n8n-text>
<div class="mb-s">
<n8n-text>
{{ i18n.baseText('chatEmbed.install') }}
</n8n-text>
</div>
<CodeNodeEditor :model-value="commonCode.install" is-read-only />
</div>
<n8n-text>
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
<template #code>
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
</template>
</i18n-t>
</n8n-text>
<div class="mb-s">
<n8n-text>
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
<template #code>
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
</template>
</i18n-t>
</n8n-text>
</div>
<HtmlEditor v-if="currentTab === 'cdn'" :model-value="cdnCode" is-read-only />
<HtmlEditor v-if="currentTab === 'vue'" :model-value="vueCode" is-read-only />
<CodeNodeEditor v-if="currentTab === 'react'" :model-value="reactCode" is-read-only />
<CodeNodeEditor v-if="currentTab === 'other'" :model-value="otherCode" is-read-only />
<n8n-info-tip>
<n8n-text>
{{ i18n.baseText('chatEmbed.packageInfo.description') }}
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window size="small" bold>
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window bold>
{{ i18n.baseText('chatEmbed.packageInfo.link') }}
</n8n-link>
</n8n-text>
<n8n-info-tip class="mt-s">
{{ i18n.baseText('chatEmbed.chatTriggerNode') }}
</n8n-info-tip>
</div>
</template>

View File

@@ -6,6 +6,7 @@
:style="nodeWrapperStyles"
data-test-id="canvas-node"
:data-name="data.name"
:data-node-type="nodeType?.name"
@contextmenu="(e: MouseEvent) => openContextMenu(e, 'node-right-click')"
>
<div v-show="isSelected" class="select-background"></div>
@@ -1026,6 +1027,11 @@ export default defineComponent({
left: -67px;
}
}
&[data-node-type='@n8n/n8n-nodes-langchain.chatTrigger'] {
--configurable-node-min-input-count: 1;
--configurable-node-input-width: 176px;
}
}
&--trigger .node-default .node-box {

View File

@@ -12,6 +12,7 @@ import type {
import {
AGENT_NODE_TYPE,
BASIC_CHAIN_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
MANUAL_CHAT_TRIGGER_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
NODE_CREATOR_OPEN_SOURCES,
@@ -190,7 +191,9 @@ export const useActions = () => {
];
const isChatTriggerMissing =
allNodes.find((node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE) === undefined;
allNodes.find((node) =>
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type),
) === undefined;
const isCompatibleNode = addedNodes.some((node) => COMPATIBLE_CHAT_NODES.includes(node.type));
return isCompatibleNode && isChatTriggerMissing;
@@ -211,7 +214,7 @@ export const useActions = () => {
}
if (shouldPrependChatTrigger(addedNodes)) {
addedNodes.unshift({ type: MANUAL_CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
addedNodes.unshift({ type: CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
connections.push({
from: { nodeIndex: 0 },
to: { nodeIndex: 1 },

View File

@@ -276,9 +276,14 @@ export default defineComponent({
return null;
},
showTriggerPanel(): boolean {
const override = !!this.activeNodeType?.triggerPanel;
if (typeof this.activeNodeType?.triggerPanel === 'boolean') {
return override;
}
const isWebhookBasedNode = !!this.activeNodeType?.webhooks?.length;
const isPollingNode = this.activeNodeType?.polling;
const override = !!this.activeNodeType?.triggerPanel;
return (
!this.readOnly && this.isTriggerNode && (isWebhookBasedNode || isPollingNode || override)
);

View File

@@ -30,6 +30,7 @@ import {
MANUAL_TRIGGER_NODE_TYPE,
MODAL_CONFIRM,
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
} from '@/constants';
import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow';
@@ -40,6 +41,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { nodeViewEventBus } from '@/event-bus';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({
@@ -116,6 +118,9 @@ export default defineComponent({
isManualTriggerNode(): boolean {
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
},
isChatNode(): boolean {
return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE);
},
isFormTriggerNode(): boolean {
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
},
@@ -186,6 +191,10 @@ export default defineComponent({
return this.label;
}
if (this.isChatNode) {
return this.$locale.baseText('ndv.execute.testChat');
}
if (this.isWebhookNode) {
return this.$locale.baseText('ndv.execute.listenForTestEvent');
}
@@ -212,7 +221,10 @@ export default defineComponent({
},
async onClick() {
if (this.isListeningForEvents) {
if (this.isChatNode) {
this.ndvStore.setActiveNodeName(null);
nodeViewEventBus.emit('openChat');
} else if (this.isListeningForEvents) {
await this.stopWaitingForWebhook();
} else if (this.isListeningForWorkflowEvents) {
this.$emit('stopExecution');

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="webhooksNode.length" class="webhooks">
<div v-if="webhooksNode.length && visibleWebhookUrls.length > 0" class="webhooks">
<div
class="clickable headline"
:class="{ expanded: !isMinimized }"
@@ -11,31 +11,22 @@
</div>
<el-collapse-transition>
<div v-if="!isMinimized" class="node-webhooks">
<div class="url-selection">
<div v-if="!isProductionOnly" class="url-selection">
<el-row>
<el-col :span="24">
<n8n-radio-buttons
v-model="showUrlFor"
:options="[
{ label: baseText.testUrl, value: 'test' },
{
label: baseText.productionUrl,
value: 'production',
},
]"
/>
<n8n-radio-buttons v-model="showUrlFor" :options="urlOptions" />
</el-col>
</el-row>
</div>
<n8n-tooltip
v-for="(webhook, index) in webhooksNode.filter((webhook) => !webhook.ndvHideUrl)"
v-for="(webhook, index) in visibleWebhookUrls"
:key="index"
class="item"
:content="baseText.clickToCopy"
placement="left"
>
<div v-if="!webhook.ndvHideMethod" class="webhook-wrapper">
<div v-if="isWebhookMethodVisible(webhook)" class="webhook-wrapper">
<div class="http-field">
<div class="http-method">
{{ getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
@@ -65,7 +56,12 @@ import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
import { defineComponent } from 'vue';
import { useToast } from '@/composables/useToast';
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
import {
CHAT_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
} from '@/constants';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useClipboard } from '@/composables/useClipboard';
@@ -91,6 +87,27 @@ export default defineComponent({
};
},
computed: {
isProductionOnly(): boolean {
return this.nodeType && PRODUCTION_ONLY_TRIGGER_NODE_TYPES.includes(this.nodeType.name);
},
urlOptions(): Array<{ label: string; value: string }> {
return [
...(this.isProductionOnly ? [] : [{ label: this.baseText.testUrl, value: 'test' }]),
{
label: this.baseText.productionUrl,
value: 'production',
},
];
},
visibleWebhookUrls(): IWebhookDescription[] {
return this.webhooksNode.filter((webhook) => {
if (typeof webhook.ndvHideUrl === 'string') {
return !this.getWebhookExpressionValue(webhook, 'ndvHideUrl');
}
return !webhook.ndvHideUrl;
});
},
webhooksNode(): IWebhookDescription[] {
if (this.nodeType === null || this.nodeType.webhooks === undefined) {
return [];
@@ -103,6 +120,20 @@ export default defineComponent({
baseText() {
const nodeType = this.nodeType.name;
switch (nodeType) {
case CHAT_TRIGGER_NODE_TYPE:
return {
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.chatTrigger'),
clickToDisplay: this.$locale.baseText(
'nodeWebhooks.clickToDisplayWebhookUrls.formTrigger',
),
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls.chatTrigger'),
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls.chatTrigger'),
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title.chatTrigger'),
copyMessage: this.$locale.baseText('nodeWebhooks.showMessage.message.chatTrigger'),
};
case FORM_TRIGGER_NODE_TYPE:
return {
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.formTrigger'),
@@ -153,10 +184,21 @@ export default defineComponent({
},
getWebhookUrlDisplay(webhookData: IWebhookDescription): string {
if (this.node) {
return this.getWebhookUrl(webhookData, this.node, this.showUrlFor);
return this.getWebhookUrl(
webhookData,
this.node,
this.isProductionOnly ? 'production' : this.showUrlFor,
);
}
return '';
},
isWebhookMethodVisible(webhook: IWebhookDescription): boolean {
if (typeof webhook.ndvHideMethod === 'string') {
return !this.getWebhookExpressionValue(webhook, 'ndvHideMethod');
}
return !webhook.ndvHideMethod;
},
},
});
</script>

View File

@@ -168,7 +168,6 @@ import {
isAuthRelatedParameter,
} from '@/utils/nodeTypesUtils';
import { get, set } from 'lodash-es';
import { nodeViewEventBus } from '@/event-bus';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
const FixedCollectionParameter = defineAsyncComponent(
@@ -476,14 +475,16 @@ export default defineComponent({
this.$emit('activate');
}
},
/**
* Handles default node button parameter type actions
* @param parameter
*/
onButtonAction(parameter: INodeProperties) {
const action: string | undefined = parameter.typeOptions?.action;
switch (action) {
case 'openChat':
this.ndvStore.setActiveNodeName(null);
nodeViewEventBus.emit('openChat');
break;
default:
return;
}
},
isNodeAuthField(name: string): boolean {

View File

@@ -1,7 +1,7 @@
<template>
<div :class="$style.container">
<transition name="fade" mode="out-in">
<div v-if="hasIssues" key="empty"></div>
<div v-if="hasIssues || hideContent" key="empty"></div>
<div v-else-if="isListeningForEvents" key="listening">
<n8n-pulse>
<NodeIcon :node-type="nodeType" :size="40"></NodeIcon>
@@ -45,6 +45,12 @@
{{ listeningHint }}
</n8n-text>
</div>
<div v-if="displayChatButton">
<n8n-button @click="openWebhookUrl()" class="mb-xl">
{{ $locale.baseText('ndv.trigger.chatTrigger.openChat') }}
</n8n-button>
</div>
<NodeExecuteButton
data-test-id="trigger-execute-button"
:node-name="nodeName"
@@ -105,6 +111,7 @@
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import {
CHAT_TRIGGER_NODE_TYPE,
VIEWS,
WEBHOOK_NODE_TYPE,
WORKFLOW_SETTINGS_MODAL_KEY,
@@ -156,6 +163,36 @@ export default defineComponent({
return null;
},
hideContent(): boolean {
if (!this.nodeType?.triggerPanel) {
return false;
}
if (
this.nodeType?.triggerPanel &&
this.nodeType?.triggerPanel.hasOwnProperty('hideContent')
) {
const hideContent = this.nodeType?.triggerPanel.hideContent;
if (typeof hideContent === 'boolean') {
return hideContent;
}
if (this.node) {
const hideContentValue = this.getCurrentWorkflow().expression.getSimpleParameterValue(
this.node,
hideContent,
'internal',
{},
);
if (typeof hideContentValue === 'boolean') {
return hideContentValue;
}
}
}
return false;
},
hasIssues(): boolean {
return Boolean(
this.node?.issues && (this.node.issues.parameters || this.node.issues.credentials),
@@ -168,6 +205,13 @@ export default defineComponent({
return '';
},
displayChatButton(): boolean {
return Boolean(
this.node &&
this.node.type === CHAT_TRIGGER_NODE_TYPE &&
this.node.parameters.mode !== 'webhook',
);
},
isWebhookNode(): boolean {
return Boolean(this.node && this.node.type === WEBHOOK_NODE_TYPE);
},
@@ -219,11 +263,16 @@ export default defineComponent({
: this.$locale.baseText('ndv.trigger.webhookNode.listening');
},
listeningHint(): string {
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
? this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint')
: this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
switch (this.nodeType?.name) {
case CHAT_TRIGGER_NODE_TYPE:
return this.$locale.baseText('ndv.trigger.webhookBasedNode.chatTrigger.serviceHint');
case FORM_TRIGGER_NODE_TYPE:
return this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint');
default:
return this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
interpolate: { service: this.serviceName },
});
});
}
},
header(): string {
const serviceName = this.nodeType ? getTriggerNodeServiceName(this.nodeType) : '';
@@ -349,6 +398,15 @@ export default defineComponent({
this.executionsHelpEventBus.emit('expand');
}
},
openWebhookUrl() {
this.$telemetry.track('User clicked ndv link', {
workflow_id: this.workflowsStore.workflowId,
session_id: this.sessionId,
pane: 'input',
type: 'open-chat',
});
window.open(this.webhookTestUrl, '_blank', 'noreferrer');
},
onLinkClick(e: MouseEvent) {
if (!e.target) {
return;

View File

@@ -15,7 +15,7 @@
@keydown.stop
>
<template #content>
<div v-loading="isLoading" class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
<div class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
<div class="messages ignore-key-press">
<div
v-for="message in messages"
@@ -64,6 +64,7 @@
</div>
</div>
</div>
<MessageTyping ref="messageContainer" v-if="isLoading" />
</div>
<div v-if="node" class="logs-wrapper" data-test-id="lm-chat-logs">
<n8n-text class="logs-title" tag="p" size="large">{{
@@ -128,6 +129,7 @@ import {
AI_CODE_NODE_TYPE,
AI_SUBCATEGORY,
CHAT_EMBED_MODAL_KEY,
CHAT_TRIGGER_NODE_TYPE,
MANUAL_CHAT_TRIGGER_NODE_TYPE,
VIEWS,
WORKFLOW_LM_CHAT_MODAL_KEY,
@@ -137,13 +139,17 @@ import { workflowRun } from '@/mixins/workflowRun';
import { get, last } from 'lodash-es';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from 'n8n-design-system/utils';
import type { IDataObject, INodeType, INode, ITaskData } from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import type { INodeUi, IUser } from '@/Interface';
import { useExternalHooks } from '@/composables/useExternalHooks';
// eslint-disable-next-line import/no-unresolved
import MessageTyping from '@n8n/chat/components/MessageTyping.vue';
const RunDataAi = defineAsyncComponent(async () => import('@/components/RunDataAi/RunDataAi.vue'));
interface ChatMessage {
@@ -167,6 +173,7 @@ export default defineComponent({
name: 'WorkflowLMChat',
components: {
Modal,
MessageTyping,
RunDataAi,
},
mixins: [workflowRun],
@@ -246,18 +253,17 @@ export default defineComponent({
},
setConnectedNode() {
const workflow = this.getCurrentWorkflow();
const triggerNode = workflow.queryNodes(
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
);
const triggerNode = this.getTriggerNode();
if (!triggerNode.length) {
if (!triggerNode) {
this.showError(
new Error('Chat Trigger Node could not be found!'),
'Trigger Node not found',
);
return;
}
const workflow = this.getCurrentWorkflow();
const chatNode = this.workflowsStore.getNodes().find((node: INodeUi): boolean => {
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) return false;
@@ -288,7 +294,7 @@ export default defineComponent({
const parentNodes = workflow.getParentNodes(node.name);
const isChatChild = parentNodes.some(
(parentNodeName) => parentNodeName === triggerNode[0].name,
(parentNodeName) => parentNodeName === triggerNode.name,
);
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
@@ -311,7 +317,7 @@ export default defineComponent({
const workflow = this.getCurrentWorkflow();
const connectedMemoryInputs =
workflow.connectionsByDestinationNode[this.connectedNode.name]?.memory;
workflow.connectionsByDestinationNode[this.connectedNode.name][NodeConnectionType.AiMemory];
if (!connectedMemoryInputs) return [];
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
@@ -330,9 +336,9 @@ export default defineComponent({
action: string;
chatHistory?: unknown[];
response?: {
chat_history?: unknown[];
sessionId?: unknown[];
};
} => get(data, 'data.memory.0.0.json')!,
} => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json'])!,
)
?.find((data) =>
['chatHistory', 'loadMemoryVariables'].includes(data?.action) ? data : undefined,
@@ -342,12 +348,12 @@ export default defineComponent({
if (memoryOutputData?.chatHistory) {
chatHistory = memoryOutputData?.chatHistory as LangChainMessage[];
} else if (memoryOutputData?.response) {
chatHistory = memoryOutputData?.response.chat_history as LangChainMessage[];
chatHistory = memoryOutputData?.response.sessionId as LangChainMessage[];
} else {
return [];
}
return chatHistory.map((message) => {
return (chatHistory || []).map((message) => {
return {
text: message.kwargs.content,
sender: last(message.id) === 'HumanMessage' ? 'user' : 'bot',
@@ -382,8 +388,8 @@ export default defineComponent({
getTriggerNode(): INode | null {
const workflow = this.getCurrentWorkflow();
const triggerNode = workflow.queryNodes(
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
const triggerNode = workflow.queryNodes((nodeType: INodeType) =>
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
);
if (!triggerNode.length) {
@@ -403,7 +409,16 @@ export default defineComponent({
return;
}
const inputKey = triggerNode.typeVersion < 1.1 ? 'input' : 'chat_input';
let inputKey = 'chatInput';
if (triggerNode.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && triggerNode.typeVersion < 1.1) {
inputKey = 'input';
}
if (triggerNode.type === CHAT_TRIGGER_NODE_TYPE) {
inputKey = 'chatInput';
}
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser ?? ({} as IUser);
const nodeData: ITaskData = {
startTime: new Date().getTime(),
@@ -414,6 +429,8 @@ export default defineComponent({
[
{
json: {
sessionId: `test-${currentUser.id || 'unknown'}`,
action: 'sendMessage',
[inputKey]: message,
},
},
@@ -549,13 +566,18 @@ export default defineComponent({
padding-top: 1.5em;
margin-right: 1em;
.chat-message {
float: left;
margin: var(--spacing-2xs) var(--spacing-s);
}
.message {
float: left;
position: relative;
width: 100%;
.content {
border-radius: var(--border-radius-large);
border-radius: var(--border-radius-base);
line-height: 1.5;
margin: var(--spacing-2xs) var(--spacing-s);
max-width: 75%;
@@ -565,8 +587,9 @@ export default defineComponent({
&.bot {
background-color: var(--color-lm-chat-bot-background);
border: 1px solid var(--color-lm-chat-bot-border);
color: var(--color-lm-chat-bot-color);
float: left;
border-bottom-left-radius: 0;
.message-options {
left: 1.5em;
@@ -575,9 +598,10 @@ export default defineComponent({
&.user {
background-color: var(--color-lm-chat-user-background);
border: 1px solid var(--color-lm-chat-user-border);
color: var(--color-lm-chat-user-color);
float: right;
text-align: right;
border-bottom-right-radius: 0;
.message-options {
right: 1.5em;

View File

@@ -1,9 +1,5 @@
import WorkflowLMChatModal from '@/components/WorkflowLMChat.vue';
import {
AGENT_NODE_TYPE,
MANUAL_CHAT_TRIGGER_NODE_TYPE,
WORKFLOW_LM_CHAT_MODAL_KEY,
} from '@/constants';
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
import { createComponentRenderer } from '@/__tests__/render';
import { fireEvent, waitFor } from '@testing-library/vue';
import { uuid } from '@jsplumb/util';
@@ -32,7 +28,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
name: 'Test Workflow',
connections: withConnections
? {
'On new manual Chat Message': {
'Chat Trigger': {
main: [
[
{
@@ -48,8 +44,8 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
active: true,
nodes: [
createTestNode({
name: 'On new manual Chat Message',
type: MANUAL_CHAT_TRIGGER_NODE_TYPE,
name: 'Chat Trigger',
type: CHAT_TRIGGER_NODE_TYPE,
}),
...(withAgentNode
? [
@@ -71,7 +67,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
nodeTypesStore.setNodeTypes(
mockNodeTypesToArray({
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_CHAT_TRIGGER_NODE_TYPE],
[CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[CHAT_TRIGGER_NODE_TYPE],
[AGENT_NODE_TYPE]: testingNodeTypes[AGENT_NODE_TYPE],
}),
);