From f3fa3f9c30ea3493f013f353c53fbbf49a77d512 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Thu, 4 Dec 2025 15:17:59 +0200 Subject: [PATCH] fix(core): Address chat feedback items (no-changelog) (#22725) --- packages/@n8n/api-types/src/chat-hub.ts | 26 ++++++++++++++++- .../trigger/ChatTrigger/ChatTrigger.node.ts | 16 +++++++++-- .../chat-hub/chat-hub-workflow.service.ts | 28 +++++++++++++++++-- .../src/modules/chat-hub/chat-hub.service.ts | 25 ++++++++++++++--- .../src/modules/chat-hub/chat-hub.types.ts | 3 +- .../src/features/ai/chatHub/ChatView.vue | 1 + .../src/features/ai/chatHub/chat.store.ts | 3 ++ .../components/ChatConversationHeader.vue | 4 ++- .../ai/chatHub/components/ModelSelector.vue | 10 +++++-- 9 files changed, 101 insertions(+), 15 deletions(-) diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 1bd6bd557b8..dcfbc15dd85 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -267,6 +267,27 @@ export const chatAttachmentSchema = z.object({ fileName: z.string(), }); +export const isValidTimeZone = (tz: string): boolean => { + try { + // Throws if invalid timezone + new Intl.DateTimeFormat('en-US', { timeZone: tz }); + return true; + } catch { + return false; + } +}; + +export const StrictTimeZoneSchema = z + .string() + .min(1) + .max(50) + .regex(/^[A-Za-z0-9_/+-]+$/) + .refine(isValidTimeZone, { + message: 'Unknown or invalid time zone', + }); + +export const TimeZoneSchema = StrictTimeZoneSchema.optional().catch(undefined); + export type ChatAttachment = z.infer; export class ChatHubSendMessageRequest extends Z.class({ @@ -283,7 +304,8 @@ export class ChatHubSendMessageRequest extends Z.class({ ), tools: z.array(INodeSchema), attachments: z.array(chatAttachmentSchema), - agentName: z.string(), + agentName: z.string().optional(), + timeZone: TimeZoneSchema, }) {} export class ChatHubRegenerateMessageRequest extends Z.class({ @@ -294,6 +316,7 @@ export class ChatHubRegenerateMessageRequest extends Z.class({ name: z.string(), }), ), + timeZone: TimeZoneSchema, }) {} export class ChatHubEditMessageRequest extends Z.class({ @@ -306,6 +329,7 @@ export class ChatHubEditMessageRequest extends Z.class({ name: z.string(), }), ), + timeZone: TimeZoneSchema, }) {} export class ChatHubUpdateConversationRequest extends Z.class({ diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts index 65ae8ed16b5..de087b51a4e 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -323,7 +323,7 @@ export class ChatTrigger extends Node { }, { displayName: - 'Chat will be live at the URL above once you activate this workflow. Live executions will show up in the ‘executions’ tab', + 'Chat will be live at the URL above once this workflow is published. Live executions will show up in the ‘executions’ tab', name: 'hostedChatNotice', type: 'notice', displayOptions: { @@ -336,7 +336,7 @@ export class ChatTrigger extends Node { }, { displayName: - 'Follow the instructions here to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you activate this workflow', + 'Follow the instructions here to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you publish this workflow', name: 'embeddedChatNotice', type: 'notice', displayOptions: { @@ -406,6 +406,18 @@ export class ChatTrigger extends Node { }, }, }, + { + displayName: + 'Your n8n users will be able to use this agent in Chat once this workflow is published. Make sure to share this workflow with at least viewer access to all users who should use it. Currently, only streaming response mode is supported.', + name: 'availableInChatNotice', + type: 'notice', + displayOptions: { + show: { + availableInChat: [true], + }, + }, + default: '', + }, { displayName: 'Agent Name', name: 'agentName', diff --git a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts index ec64ffe6c33..dc05fae2804 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts @@ -9,6 +9,7 @@ import { } from '@n8n/db'; import { Service } from '@n8n/di'; import { EntityManager } from '@n8n/typeorm'; +import { DateTime } from 'luxon'; import { AGENT_LANGCHAIN_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, @@ -52,6 +53,7 @@ export class ChatHubWorkflowService { model: ChatHubConversationModel, systemMessage: string | undefined, tools: INode[], + timeZone: string, trx?: EntityManager, ): Promise<{ workflowData: IWorkflowBase; executionData: IRunExecutionData }> { return await withTransaction(this.workflowRepository.manager, trx, async (em) => { @@ -69,6 +71,7 @@ export class ChatHubWorkflowService { model, systemMessage, tools, + timeZone, }); const newWorkflow = new WorkflowEntity(); @@ -258,6 +261,7 @@ export class ChatHubWorkflowService { model, systemMessage, tools, + timeZone, }: { userId: string; sessionId: ChatSessionId; @@ -268,9 +272,10 @@ export class ChatHubWorkflowService { model: ChatHubConversationModel; systemMessage?: string; tools: INode[]; + timeZone: string; }) { const chatTriggerNode = this.buildChatTriggerNode(); - const toolsAgentNode = this.buildToolsAgentNode(model, systemMessage); + const toolsAgentNode = this.buildToolsAgentNode(model, timeZone, systemMessage); const modelNode = this.buildModelNode(credentials, model); const memoryNode = this.buildMemoryNode(20); const restoreMemoryNode = this.buildRestoreMemoryNode(history); @@ -463,7 +468,24 @@ export class ChatHubWorkflowService { }; } - private buildToolsAgentNode(model: ChatHubConversationModel, systemMessage?: string): INode { + getSystemMessageMetadata(timeZone: string) { + const now = DateTime.now().setZone(timeZone).toISO({ + includeOffset: true, + }); + + return `The user's current local date and time is: ${now} (timezone: ${timeZone}). +When you need to reference “now”, use this date and time.`; + } + + private getBaseSystemMessage(timeZone: string) { + return 'You are a helpful assistant.\n' + this.getSystemMessageMetadata(timeZone); + } + + private buildToolsAgentNode( + model: ChatHubConversationModel, + timeZone: string, + systemMessage?: string, + ): INode { return { parameters: { promptType: 'define', @@ -474,7 +496,7 @@ export class ChatHubWorkflowService { model.provider !== 'n8n' && model.provider !== 'custom-agent' ? getMaxContextWindowTokens(model.provider, model.model) : undefined, - systemMessage, + systemMessage: systemMessage ?? this.getBaseSystemMessage(timeZone), }, }, type: AGENT_LANGCHAIN_NODE_TYPE, diff --git a/packages/cli/src/modules/chat-hub/chat-hub.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.service.ts index 7f89db12bf8..03383da4a99 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -23,6 +23,7 @@ import { Logger } from '@n8n/backend-common'; import { ExecutionRepository, IExecutionResponse, User, WorkflowRepository, In } from '@n8n/db'; import { Service } from '@n8n/di'; import type { EntityManager } from '@n8n/typeorm'; +import { GlobalConfig } from '@n8n/config'; import type { Response } from 'express'; import { ErrorReporter, InstanceSettings } from 'n8n-core'; import { @@ -104,6 +105,7 @@ export class ChatHubService { private readonly chatHubSettingsService: ChatHubSettingsService, private readonly chatHubAttachmentService: ChatHubAttachmentService, private readonly instanceSettings: InstanceSettings, + private readonly globalConfig: GlobalConfig, ) {} async getModels( @@ -1029,7 +1031,9 @@ export class ChatHubService { previousMessageId, tools, attachments, + timeZone, } = payload; + const tz = timeZone ?? this.globalConfig.generic.timezone; const credentialId = this.getModelCredential(model, credentials); @@ -1079,6 +1083,7 @@ export class ChatHubService { message, tools, processedAttachments, + tz, trx, ); }); @@ -1125,7 +1130,8 @@ export class ChatHubService { } async editMessage(res: Response, user: User, payload: EditMessagePayload) { - const { sessionId, editId, messageId, message, model, credentials } = payload; + const { sessionId, editId, messageId, message, model, credentials, timeZone } = payload; + const tz = timeZone ?? this.globalConfig.generic.timezone; const workflow = await this.messageRepository.manager.transaction(async (trx) => { const session = await this.getChatSession(user, sessionId, trx); @@ -1170,6 +1176,7 @@ export class ChatHubService { message, session.tools, attachments, + tz, trx, ); } @@ -1195,7 +1202,8 @@ export class ChatHubService { } async regenerateAIMessage(res: Response, user: User, payload: RegenerateMessagePayload) { - const { sessionId, retryId, model, credentials } = payload; + const { sessionId, retryId, model, credentials, timeZone } = payload; + const tz = timeZone ?? this.globalConfig.generic.timezone; const { workflow: { workflowData, executionData }, @@ -1242,6 +1250,7 @@ export class ChatHubService { message, session.tools, attachments, + tz, trx, ); @@ -1273,6 +1282,7 @@ export class ChatHubService { message: string, tools: INode[], attachments: IBinaryData[], + timeZone: string, trx: EntityManager, ) { if (model.provider === 'n8n') { @@ -1293,6 +1303,7 @@ export class ChatHubService { history, message, attachments, + timeZone, trx, ); } @@ -1307,6 +1318,7 @@ export class ChatHubService { undefined, tools, attachments, + timeZone, trx, ); } @@ -1321,6 +1333,7 @@ export class ChatHubService { systemMessage: string | undefined, tools: INode[], attachments: IBinaryData[], + timeZone: string, trx: EntityManager, ) { await this.chatHubSettingsService.ensureModelIsAllowed(model); @@ -1342,6 +1355,7 @@ export class ChatHubService { model, systemMessage, tools, + timeZone, trx, ); } @@ -1353,6 +1367,7 @@ export class ChatHubService { history: ChatHubMessage[], message: string, attachments: IBinaryData[], + timeZone: string, trx: EntityManager, ) { const agent = await this.chatHubAgentService.getAgentById(agentId, user.id); @@ -1370,7 +1385,8 @@ export class ChatHubService { throw new BadRequestError('Credentials not set for agent'); } - const systemMessage = agent.systemPrompt; + const systemMessage = + agent.systemPrompt + '\n' + this.chatHubWorkflowService.getSystemMessageMetadata(timeZone); const model: ChatHubBaseLLMModel = { provider: agent.provider, @@ -1396,6 +1412,7 @@ export class ChatHubService { systemMessage, tools, attachments, + timeZone, trx, ); } @@ -2074,7 +2091,7 @@ export class ChatHubService { model: ChatHubConversationModel, credentialId: string | null, tools: INode[], - agentName: string, + agentName?: string, trx?: EntityManager, ) { await this.ensureValidModel(user, model); diff --git a/packages/cli/src/modules/chat-hub/chat-hub.types.ts b/packages/cli/src/modules/chat-hub/chat-hub.types.ts index ad7248e7c11..afaee379b29 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.types.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.types.ts @@ -22,6 +22,7 @@ export interface BaseMessagePayload { sessionId: ChatSessionId; model: ChatHubConversationModel; credentials: INodeCredentials; + timeZone?: string; } export interface HumanMessagePayload extends BaseMessagePayload { @@ -30,7 +31,7 @@ export interface HumanMessagePayload extends BaseMessagePayload { previousMessageId: ChatMessageId | null; attachments: ChatAttachment[]; tools: INode[]; - agentName: string; + agentName?: string; } export interface RegenerateMessagePayload extends BaseMessagePayload { retryId: ChatMessageId; diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue index 2718f2cd9d2..99a38d27d64 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -566,6 +566,7 @@ function onFilesDropped(files: File[]) { :selected-model="selectedModel" :credentials="credentialsByProvider" :ready-to-show-model-selector="isNewSession || !!currentConversation" + :is-new-session="isNewSession" @select-model="handleSelectModel" @edit-custom-agent="handleEditAgent" @create-custom-agent="openNewAgentCreator" diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts index cafdd824848..ebcf3e98900 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts @@ -548,6 +548,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { tools, attachments, agentName, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, onStreamMessage, onStreamDone, @@ -618,6 +619,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { messageId: promptId, message: content, credentials, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, onStreamMessage, onStreamDone, @@ -654,6 +656,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { { model, credentials, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, onStreamMessage, onStreamDone, diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatConversationHeader.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatConversationHeader.vue index 44e303b7620..f176cce9103 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatConversationHeader.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatConversationHeader.vue @@ -17,7 +17,8 @@ import { useI18n } from '@n8n/i18n'; import { computed, useTemplateRef } from 'vue'; import { useRouter } from 'vue-router'; -const { selectedModel, credentials, readyToShowModelSelector } = defineProps<{ +const { isNewSession, selectedModel, credentials, readyToShowModelSelector } = defineProps<{ + isNewSession: boolean; selectedModel: ChatModelDto | null; credentials: CredentialsMap | null; readyToShowModelSelector: boolean; @@ -79,6 +80,7 @@ defineExpose({ text icon-size="large" :aria-label="i18n.baseText('chatHub.chat.header.button.newChat')" + :disabled="isNewSession" @click="onNewChat" /> { id: provider, title: providerDisplayNames[provider], submenu: [ + configureMenu, + { isDivider: true as const, id: 'divider' }, { id: `${provider}::loading`, title: i18n.baseText('generic.loadingEllipsis'), disabled: true, }, - { isDivider: true as const, id: 'divider' }, - configureMenu, ], }); continue; @@ -234,9 +234,13 @@ const menu = computed(() => { } as const, ] : []), - configureMenu, ]); + submenu.unshift( + configureMenu, + ...(submenu.length > 1 ? [{ isDivider: true as const, id: 'divider' }] : []), + ); + menuItems.push({ id: provider, title: providerDisplayNames[provider],