From 516fd846563a1031698a9d0fc105ba085365d6c1 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Wed, 29 Oct 2025 13:58:22 +0200 Subject: [PATCH] feat(core): Add Chat Trigger toggle that controls whether a WF shows up on chat hub (no-changelog) (#21267) --- packages/@n8n/api-types/src/chat-hub.ts | 19 +- packages/@n8n/api-types/src/index.ts | 6 + .../src/repositories/workflow.repository.ts | 13 +- .../trigger/ChatTrigger/ChatTrigger.node.ts | 47 +++- .../chat-hub/chat-hub-agent.service.ts | 11 +- .../modules/chat-hub/chat-hub.controller.ts | 2 +- .../src/modules/chat-hub/chat-hub.service.ts | 220 +++++++++++++----- .../src/modules/chat-hub/chat-hub.types.ts | 1 - .../cli/src/workflows/workflow.service.ts | 22 +- .../features/ai/chatHub/ChatAgentsView.vue | 21 +- .../src/features/ai/chatHub/ChatView.vue | 23 +- .../src/features/ai/chatHub/chat.store.ts | 28 ++- .../src/features/ai/chatHub/chat.utils.ts | 28 +-- .../ai/chatHub/components/ChatAgentAvatar.vue | 13 +- .../ai/chatHub/components/ChatAgentCard.vue | 23 +- .../ai/chatHub/components/ChatMessage.vue | 12 +- .../ai/chatHub/components/ChatPrompt.vue | 9 +- .../components/ChatSessionMenuItem.vue | 12 +- .../ai/chatHub/components/ModelSelector.vue | 61 +++-- .../composables/useNodeSettingsParameters.ts | 12 +- 20 files changed, 394 insertions(+), 189 deletions(-) diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 743885b9243..1f4c389d6ec 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -33,31 +33,26 @@ export const PROVIDER_CREDENTIAL_TYPE_MAP: Record< */ const openAIModelSchema = z.object({ provider: z.literal('openai'), - name: z.string(), model: z.string(), }); const anthropicModelSchema = z.object({ provider: z.literal('anthropic'), - name: z.string(), model: z.string(), }); const googleModelSchema = z.object({ provider: z.literal('google'), - name: z.string(), model: z.string(), }); const n8nModelSchema = z.object({ provider: z.literal('n8n'), - name: z.string(), workflowId: z.string(), }); const chatAgentSchema = z.object({ provider: z.literal('custom-agent'), - name: z.string(), agentId: z.string(), }); @@ -69,6 +64,11 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [ chatAgentSchema, ]); +export type ChatHubOpenAIModel = z.infer; +export type ChatHubAnthropicModel = z.infer; +export type ChatHubGoogleModel = z.infer; +export type ChatHubN8nModel = z.infer; +export type ChatHubCustomAgentModel = z.infer; export type ChatHubConversationModel = z.infer; /** @@ -81,13 +81,19 @@ export const chatModelsRequestSchema = z.object({ export type ChatModelsRequest = z.infer; +export interface ChatModelDto { + model: ChatHubConversationModel; + name: string; + description: string | null; +} + /** * Response type for fetching available chat models */ export type ChatModelsResponse = Record< ChatHubProvider, { - models: ChatHubConversationModel[]; + models: ChatModelDto[]; error?: string; } >; @@ -135,7 +141,6 @@ export class ChatHubUpdateConversationRequest extends Z.class({ model: z.string().max(64).optional(), workflowId: z.string().max(36).optional(), agentId: z.string().uuid().optional(), - agentName: z.string().max(128).optional(), }) {} export type ChatHubMessageType = 'human' | 'ai' | 'system' | 'tool' | 'generic'; diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index 43698ce8b42..26fa7cdc865 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -8,6 +8,12 @@ export type * from './api-keys'; export type * from './community-node-types'; export { chatHubConversationModelSchema, + type ChatModelDto, + type ChatHubOpenAIModel, + type ChatHubAnthropicModel, + type ChatHubGoogleModel, + type ChatHubN8nModel, + type ChatHubCustomAgentModel, type ChatHubConversationModel, chatHubProviderSchema, type ChatHubProvider, diff --git a/packages/@n8n/db/src/repositories/workflow.repository.ts b/packages/@n8n/db/src/repositories/workflow.repository.ts index a4a07e30816..4ef5dcbedd3 100644 --- a/packages/@n8n/db/src/repositories/workflow.repository.ts +++ b/packages/@n8n/db/src/repositories/workflow.repository.ts @@ -756,7 +756,7 @@ export class WorkflowRepository extends Repository { ); } - async findWorkflowsWithNodeType(nodeTypes: string[]) { + async findWorkflowsWithNodeType(nodeTypes: string[], includeNodes: boolean = false) { if (!nodeTypes?.length) return []; const qb = this.createQueryBuilder('workflow'); @@ -766,8 +766,15 @@ export class WorkflowRepository extends Repository { this.globalConfig.database.type, ); - const workflows: Array<{ id: string; name: string; active: boolean }> = await qb - .select(['workflow.id', 'workflow.name', 'workflow.active']) + const workflows: Array< + Pick & Partial> + > = await qb + .select([ + 'workflow.id', + 'workflow.name', + 'workflow.active', + ...(includeNodes ? ['workflow.nodes'] : []), + ]) .where(whereClause, parameters) .getMany(); 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 9ac2b173575..3f6a075a829 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -219,8 +219,8 @@ export class ChatTrigger extends Node { icon: 'fa:comments', iconColor: 'black', group: ['trigger'], - version: [1, 1.1, 1.2, 1.3], - defaultVersion: 1.3, + version: [1, 1.1, 1.2, 1.3, 1.4], + defaultVersion: 1.4, description: 'Runs the workflow when an n8n generated webchat is submitted', defaults: { name: 'When chat message received', @@ -392,6 +392,49 @@ export class ChatTrigger extends Node { default: 'Hi there! 👋\nMy name is Nathan. How can I assist you today?', description: 'Default messages shown at the start of the chat, one per line', }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Make Available in n8n Chat', + name: 'availableInChat', + type: 'boolean', + default: false, + noDataExpression: true, + description: 'Whether to make the agent available in n8n Chat', + displayOptions: { + show: { + '@version': [{ _cnd: { gte: 1.4 } }], + }, + }, + }, + { + displayName: 'Agent Name', + name: 'agentName', + type: 'string', + default: '', + noDataExpression: true, + description: 'The name of the agent on n8n Chat', + displayOptions: { + show: { + availableInChat: [true], + }, + }, + }, + { + displayName: 'Agent Description', + name: 'agentDescription', + type: 'string', + typeOptions: { + rows: 2, + }, + default: '', + noDataExpression: true, + description: 'The description of the agent on n8n Chat', + displayOptions: { + show: { + availableInChat: [true], + }, + }, + }, { displayName: 'Options', name: 'options', diff --git a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts index 641b88ca59d..96a5fdeb984 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts @@ -1,4 +1,6 @@ +import { ChatModelsResponse } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; +import type { User } from '@n8n/db'; import { Service } from '@n8n/di'; import { v4 as uuidv4 } from 'uuid'; @@ -6,9 +8,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { ChatHubAgent } from './chat-hub-agent.entity'; import { ChatHubAgentRepository } from './chat-hub-agent.repository'; -import { ChatModelsResponse } from '@n8n/api-types'; import { ChatHubCredentialsService } from './chat-hub-credentials.service'; -import type { User } from '@n8n/db'; @Service() export class ChatHubAgentService { @@ -23,9 +23,12 @@ export class ChatHubAgentService { return { models: agents.map((agent) => ({ - provider: 'custom-agent', name: agent.name, - agentId: agent.id, + description: agent.description ?? null, + model: { + provider: 'custom-agent', + agentId: agent.id, + }, })), }; } diff --git a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts index cb74b678fe5..7c2eb330a6a 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts @@ -226,7 +226,7 @@ export class ChatHubController { @Body payload: ChatHubUpdateConversationRequest, ): Promise { if (Object.keys(payload).length > 0) { - await this.chatService.updateSession(req.user.id, sessionId, payload); + await this.chatService.updateSession(req.user, sessionId, payload); } return await this.chatService.getConversation(req.user.id, sessionId); 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 6e3c4b8f780..0399eaab2d5 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -47,6 +47,17 @@ import { } from 'n8n-workflow'; import { v4 as uuidv4 } from 'uuid'; +import { ActiveExecutions } from '@/active-executions'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { ExecutionService } from '@/executions/execution.service'; +import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; +import { getBase } from '@/workflow-execute-additional-data'; +import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; +import { WorkflowService } from '@/workflows/workflow.service'; + import { ChatHubAgentService } from './chat-hub-agent.service'; import { ChatHubCredentialsService } from './chat-hub-credentials.service'; import type { ChatHubMessage } from './chat-hub-message.entity'; @@ -63,17 +74,6 @@ import { ChatHubSessionRepository } from './chat-session.repository'; import { getMaxContextWindowTokens } from './context-limits'; import { interceptResponseWrites, createStructuredChunkAggregator } from './stream-capturer'; -import { ActiveExecutions } from '@/active-executions'; -import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; -import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { ExecutionService } from '@/executions/execution.service'; -import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; -import { getBase } from '@/workflow-execute-additional-data'; -import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; -import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; -import { WorkflowService } from '@/workflows/workflow.service'; - const providerNodeTypeMapping: Record = { openai: { name: '@n8n/n8n-nodes-langchain.lmChatOpenAi', @@ -205,7 +205,7 @@ export class ChatHubService { case 'google': return await this.fetchGoogleModels(credentials, additionalData); case 'n8n': - return await this.fetchCustomAgentWorkflows(user); + return await this.fetchAgentWorkflowsAsModels(user); case 'custom-agent': return await this.chatHubAgentService.getAgentsByUserIdAsModels(user.id); } @@ -214,7 +214,7 @@ export class ChatHubService { private async fetchOpenAiModels( credentials: INodeCredentials, additionalData: IWorkflowExecuteAdditionalData, - ): Promise { + ): Promise { const resourceLocatorResults = await this.nodeParametersService.getResourceLocatorResults( 'searchModels', 'parameters.model', @@ -226,9 +226,12 @@ export class ChatHubService { return { models: resourceLocatorResults.results.map((result) => ({ - provider: 'openai', name: String(result.value), - model: String(result.value), + description: null, + model: { + provider: 'openai', + model: String(result.value), + }, })), }; } @@ -236,7 +239,7 @@ export class ChatHubService { private async fetchAnthropicModels( credentials: INodeCredentials, additionalData: IWorkflowExecuteAdditionalData, - ): Promise { + ): Promise { const resourceLocatorResults = await this.nodeParametersService.getResourceLocatorResults( 'searchModels', 'parameters.model', @@ -248,9 +251,12 @@ export class ChatHubService { return { models: resourceLocatorResults.results.map((result) => ({ - provider: 'anthropic', name: String(result.value), - model: String(result.value), + description: null, + model: { + provider: 'anthropic', + model: String(result.value), + }, })), }; } @@ -258,7 +264,7 @@ export class ChatHubService { private async fetchGoogleModels( credentials: INodeCredentials, additionalData: IWorkflowExecuteAdditionalData, - ): Promise { + ): Promise { const results = await this.nodeParametersService.getOptionsViaLoadOptions( { // From Gemini node @@ -308,27 +314,60 @@ export class ChatHubService { return { models: results.map((result) => ({ - provider: 'google', name: String(result.value), - model: String(result.value), + description: null, + model: { + provider: 'google', + model: String(result.value), + }, })), }; } - private async fetchCustomAgentWorkflows( - user: User, - ): Promise { + private async fetchAgentWorkflowsAsModels(user: User): Promise { const nodeTypes = [CHAT_TRIGGER_NODE_TYPE]; - const workflows = await this.workflowService.getWorkflowsWithNodesIncluded(user, nodeTypes); + const workflows = await this.workflowService.getWorkflowsWithNodesIncluded( + user, + nodeTypes, + true, + ); return { models: workflows + // Ensure the user has at least read access to the workflow + .filter((workflow) => workflow.scopes.includes('workflow:read')) .filter((workflow) => workflow.active) - .map((workflow) => ({ - provider: 'n8n', - name: workflow.name ?? 'Unnamed workflow', - workflowId: workflow.id, - })), + .flatMap((workflow) => { + const chatTrigger = workflow.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE); + if (!chatTrigger) { + return []; + } + + if (chatTrigger.parameters.availableInChat !== true) { + return []; + } + + const name = + typeof chatTrigger.parameters.agentName === 'string' && + chatTrigger.parameters.agentName.length > 0 + ? chatTrigger.parameters.agentName + : workflow.name; + + return [ + { + name: name ?? 'Unknown Agent', + description: + typeof chatTrigger.parameters.agentDescription === 'string' && + chatTrigger.parameters.agentDescription.length > 0 + ? chatTrigger.parameters.agentDescription + : null, + model: { + provider: 'n8n', + workflowId: workflow.id, + }, + }, + ]; + }), }; } @@ -443,10 +482,7 @@ export class ChatHubService { const { sessionId, messageId, message, model, credentials, previousMessageId } = payload; const provider = payload.model.provider; - const selectedModel: ModelWithCredentials = { - ...payload.model, - credentialId: provider !== 'n8n' ? this.pickCredentialId(provider, credentials) : null, - }; + const selectedModel = this.getModelWithCredentials(model, credentials); const workflow = await this.messageRepository.manager.transaction(async (trx) => { const session = await this.getChatSession(user, sessionId, selectedModel, true, trx); @@ -507,7 +543,7 @@ export class ChatHubService { const { sessionId, editId, messageId, message, model, credentials } = payload; const provider = payload.model.provider; - const selectedModel: ModelWithCredentials = this.getModelWithCredentials(model, credentials); + const selectedModel = this.getModelWithCredentials(model, credentials); const workflow = await this.messageRepository.manager.transaction(async (trx) => { const session = await this.getChatSession(user, sessionId, selectedModel, true, trx); @@ -593,13 +629,7 @@ export class ChatHubService { const { sessionId, retryId, model, credentials } = payload; const provider = payload.model.provider; - const selectedModel: ModelWithCredentials = { - ...payload.model, - credentialId: - payload.model.provider !== 'n8n' - ? this.pickCredentialId(payload.model.provider, payload.credentials) - : null, - }; + const selectedModel = this.getModelWithCredentials(model, credentials); const { workflow, retryOfMessageId, previousMessageId } = await this.messageRepository.manager.transaction(async (trx) => { @@ -739,7 +769,6 @@ export class ChatHubService { const model: ChatHubConversationModel = { provider: agent.provider, model: agent.model, - name: agent.model, }; const credentials: INodeCredentials = { @@ -797,11 +826,6 @@ export class ChatHubService { ); } - // const agents = workflowEntity.nodes.filter((node) => node.type === AGENT_LANGCHAIN_NODE_TYPE); - // if (agents.length !== 1) { - // throw new BadRequestError('Workflow must have exactly one AI Agent node'); - // } - return { workflowData: { ...workflowEntity, @@ -1344,17 +1368,10 @@ export class ChatHubService { credentials: INodeCredentials, ) { const provider = selectedModel.provider; - const agentId = selectedModel?.provider === 'custom-agent' ? selectedModel.agentId : undefined; - const agentName = - selectedModel?.provider === 'custom-agent' && 'name' in selectedModel - ? selectedModel.name - : undefined; const modelWithCredentials: ModelWithCredentials = { ...selectedModel, credentialId: provider !== 'n8n' ? this.pickCredentialId(provider, credentials) : null, - agentId, - agentName, // used for sessions }; return modelWithCredentials; @@ -1374,11 +1391,49 @@ export class ChatHubService { throw new NotFoundError('Chat session not found'); } + let agentName: string | undefined = undefined; + + if (selectedModel?.provider === 'custom-agent') { + // Find the agent to get its name + const agent = await this.chatHubAgentService.getAgentById(selectedModel.agentId!, user.id); + if (!agent) { + throw new BadRequestError('Agent not found for chat session initialization'); + } + agentName = agent.name; + } + + if (selectedModel?.provider === 'n8n') { + // Find the workflow to get its name + const workflow = await this.workflowFinderService.findWorkflowForUser( + selectedModel.workflowId!, + user, + ['workflow:read'], + { includeTags: false, includeParentFolder: false }, + ); + + if (!workflow) { + throw new BadRequestError('Workflow not found for chat session initialization'); + } + + const chatTrigger = workflow.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE); + if (!chatTrigger) { + throw new BadRequestError( + 'Chat trigger not found in workflow for chat session initialization', + ); + } + agentName = + typeof chatTrigger.parameters.agentName === 'string' && + chatTrigger.parameters.agentName.length > 0 + ? chatTrigger.parameters.agentName + : workflow.name; + } + return await this.sessionRepository.createChatSession( { id: sessionId, ownerId: user.id, title: 'New Chat', + agentName, ...selectedModel, }, trx, @@ -1571,7 +1626,7 @@ export class ChatHubService { * Updates a session with the provided fields */ async updateSession( - userId: string, + user: User, sessionId: ChatSessionId, updates: { title?: string; @@ -1580,23 +1635,68 @@ export class ChatHubService { model?: string | null; workflowId?: string | null; agentId?: string | null; - agentName?: string; + agentName?: string | null; }, ) { - const session = await this.sessionRepository.getOneById(sessionId, userId); + const session = await this.sessionRepository.getOneById(sessionId, user.id); if (!session) { throw new NotFoundError('Session not found'); } + if (updates.workflowId) { + // Validate the workflow exists and is accessible + const workflow = await this.workflowFinderService.findWorkflowForUser( + updates.workflowId, + user, + ['workflow:read'], + { includeTags: false, includeParentFolder: false }, + ); + + if (!workflow) { + throw new BadRequestError('Workflow not found'); + } + + const chatTriggers = workflow.nodes.filter((node) => node.type === CHAT_TRIGGER_NODE_TYPE); + + if (chatTriggers.length !== 1) { + throw new BadRequestError('Workflow must have exactly one chat trigger'); + } + + const chatTrigger = chatTriggers[0]; + + updates.agentName = + typeof chatTrigger.parameters.agentName === 'string' && + chatTrigger.parameters.agentName.length > 0 + ? chatTrigger.parameters.agentName + : workflow.name; + } + + if (updates.agentId) { + // Validate the agent exists and is accessible + const agent = await this.chatHubAgentService.getAgentById(updates.agentId, user.id); + + if (!agent) { + throw new BadRequestError('Agent not found'); + } + + updates.agentName = agent.name; + } + if (updates.provider === 'n8n') { // n8n provider only stores workflowId updates.model = null; updates.credentialId = null; updates.agentId = null; - } else if (updates.provider) { - // Other providers can't have workflowId + } else if (updates.provider === 'custom-agent') { + // custom-agent provider only stores agentId & Agent name + updates.model = null; + updates.credentialId = null; updates.workflowId = null; + } else if (updates.provider) { + updates.workflowId = null; + updates.agentId = null; + updates.agentName = null; } return await this.sessionRepository.updateChatSession(sessionId, updates); 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 14896855326..44f413fb21c 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.types.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.types.ts @@ -12,7 +12,6 @@ export interface ModelWithCredentials { workflowId?: string; credentialId: string | null; agentId?: string; - agentName?: string; name?: string; } diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 27fc84fba41..15215a4bbfe 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -587,8 +587,11 @@ export class WorkflowService { ); } - async getWorkflowsWithNodesIncluded(user: User, nodeTypes: string[]) { - const foundWorkflows = await this.workflowRepository.findWorkflowsWithNodeType(nodeTypes); + async getWorkflowsWithNodesIncluded(user: User, nodeTypes: string[], includeNodes = false) { + const foundWorkflows = await this.workflowRepository.findWorkflowsWithNodeType( + nodeTypes, + includeNodes, + ); let { workflows } = await this.workflowRepository.getManyAndCount( foundWorkflows.map((w) => w.id), @@ -598,13 +601,16 @@ export class WorkflowService { workflows = await this.processSharedWorkflows(workflows); } - workflows = await this.addUserScopes(workflows, user); + const withScopes = await this.addUserScopes(workflows, user); - this.cleanupSharedField(workflows); + this.cleanupSharedField(withScopes); - return workflows.map((workflow) => ({ - resourceType: 'workflow', - ...workflow, - })); + return withScopes.map((workflow) => { + const nodes = includeNodes + ? (foundWorkflows.find((w) => w.id === workflow.id)?.nodes ?? []) + : undefined; + + return { resourceType: 'workflow', ...workflow, ...(includeNodes ? { nodes } : {}) }; + }); } } diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue index 13be5952048..6ecc2b0cbb8 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue @@ -19,7 +19,7 @@ import AgentEditorModal from '@/features/ai/chatHub/components/AgentEditorModal. import ChatAgentCard from '@/features/ai/chatHub/components/ChatAgentCard.vue'; import { useChatCredentials } from '@/features/ai/chatHub/composables/useChatCredentials'; import { useUsersStore } from '@/features/settings/users/users.store'; -import { type ChatHubConversationModel } from '@n8n/api-types'; +import type { ChatModelDto, ChatHubConversationModel } from '@n8n/api-types'; import { filterAndSortAgents } from '@/features/ai/chatHub/chat.utils'; import type { ChatAgentFilter } from '@/features/ai/chatHub/chat.types'; import { useChatHubSidebarState } from '@/features/ai/chatHub/composables/useChatHubSidebarState'; @@ -47,16 +47,15 @@ const { credentialsByProvider } = useChatCredentials(usersStore.currentUserId ?? const allModels = computed(() => chatStore.agents - .map((agent) => ({ - provider: 'custom-agent', - agentId: agent.id, + .map((agent) => ({ + model: { + provider: 'custom-agent', + agentId: agent.id, + }, name: agent.name, + description: agent.description, })) - .concat( - (chatStore.models?.n8n?.models ?? []).flatMap((model) => - model.provider === 'n8n' ? [{ ...model, type: 'n8n-workflow' }] : [], - ), - ), + .concat(chatStore.models?.n8n.models ?? []), ); const models = computed(() => { @@ -65,7 +64,7 @@ const models = computed(() => { agentFilter.value, chatStore.agents, workflowsStore.workflowsById, // TODO: ensure workflows are fetched - ); + ).map((modelDto) => modelDto.model); }); const providerOptions = [ @@ -193,7 +192,7 @@ onMounted(async () => { (() => { const n8nModel = chatStore.models?.n8n.models.find( (m) => currentConversation.value && - m.provider === 'n8n' && - m.workflowId === currentConversation.value.workflowId, + m.model.provider === 'n8n' && + m.model.workflowId === currentConversation.value.workflowId, ); if (!n8nModel) { return null; } - model = n8nModel; + model = n8nModel.model; } else if (currentConversation.value.provider === 'custom-agent') { const agentModel = chatStore.models?.['custom-agent'].models.find( (m) => currentConversation.value && - m.provider === 'custom-agent' && - m.agentId === currentConversation.value.agentId, + m.model.provider === 'custom-agent' && + m.model.agentId === currentConversation.value.agentId, ); if (!agentModel) { return null; } - model = agentModel; + model = agentModel.model; } else { const chatModel = chatStore.models?.[currentConversation.value.provider].models.find( (m) => currentConversation.value && currentConversation.value?.provider !== 'n8n' && currentConversation.value?.provider !== 'custom-agent' && - m.provider === currentConversation.value?.provider && - m.model === currentConversation.value.model, + m.model.provider === currentConversation.value?.provider && + m.model.model === currentConversation.value.model, ); if (!chatModel) { return null; } - model = chatModel; + model = chatModel.model; } } else { model = defaultModel.value; @@ -274,7 +274,6 @@ watch( await handleSelectModel({ provider: 'custom-agent', agentId: agent.id, - name: agent.name, }); } return; @@ -283,11 +282,11 @@ watch( // Handle n8n workflow selection if (typeof workflowId === 'string') { const n8nModel = chatStore.models?.n8n?.models.find( - (m) => m.provider === 'n8n' && m.workflowId === workflowId, + (m) => m.model.provider === 'n8n' && m.model.workflowId === workflowId, ); if (n8nModel) { - await handleSelectModel(n8nModel); + await handleSelectModel(n8nModel.model); } } }, 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 28f61bb1ae4..6c27603e2ba 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 @@ -615,14 +615,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => { ): Promise { const agent = await createAgentApi(rootStore.restApiContext, payload); agents.value.push(agent); - const model = { - provider: 'custom-agent' as const, - agentId: agent.id, + const agentModel = { + model: { + provider: 'custom-agent' as const, + agentId: agent.id, + }, name: agent.name, + description: agent.description ?? null, }; - models.value?.['custom-agent'].models.push(model); + models.value?.['custom-agent'].models.push(agentModel); - return model; + return agentModel.model; } async function updateAgent( @@ -654,8 +657,23 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } } + function getModel(identifier: ChatHubConversationModel) { + if (!models.value) return; + + return models.value[identifier.provider].models.find((m) => { + if (identifier.provider === 'n8n') { + return m.model.provider === 'n8n' && m.model.workflowId === identifier.workflowId; + } else if (identifier.provider === 'custom-agent') { + return m.model.provider === 'custom-agent' && m.model.agentId === identifier.agentId; + } else { + return m.model.provider === identifier.provider && m.model.model === identifier.model; + } + }); + } + return { models, + getModel, sessions, agents, currentEditingAgent, diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.utils.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.utils.ts index 186646fe5e3..dcd0ff6602c 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.utils.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.utils.ts @@ -4,6 +4,7 @@ import { type ChatModelsResponse, type ChatHubSessionDto, type ChatHubAgentDto, + type ChatModelDto, } from '@n8n/api-types'; import type { ChatMessage, GroupedConversations, ChatAgentFilter } from './chat.types'; import { CHAT_VIEW } from './constants'; @@ -14,7 +15,7 @@ export function findOneFromModelsResponse( ): ChatHubConversationModel | undefined { for (const provider of chatHubProviderSchema.options) { if (response[provider].models.length > 0) { - return response[provider].models[0]; + return response[provider].models[0].model; } } @@ -104,8 +105,6 @@ export function getAgentRoute(model: ChatHubConversationModel) { export function restoreConversationModelFromMessageOrSession( messageOrSession: ChatHubSessionDto | ChatMessage, - agents: ChatHubAgentDto[], - workflowsById: Partial>, ): ChatHubConversationModel | null { if (messageOrSession.provider === null) { return null; @@ -120,9 +119,6 @@ export function restoreConversationModelFromMessageOrSession( return { provider: 'custom-agent', agentId: messageOrSession.agentId, - name: - agents.find((agent) => agent.id === messageOrSession.agentId)?.name ?? - `Custom agent ${messageOrSession.agentId}`, }; case 'n8n': if (!messageOrSession.workflowId) { @@ -132,9 +128,6 @@ export function restoreConversationModelFromMessageOrSession( return { provider: 'n8n', workflowId: messageOrSession.workflowId, - name: - workflowsById[messageOrSession.workflowId]?.name ?? - `n8n workflow ${messageOrSession.workflowId}`, }; default: if (messageOrSession.model === null) { @@ -144,13 +137,12 @@ export function restoreConversationModelFromMessageOrSession( return { provider: messageOrSession.provider, model: messageOrSession.model, - name: messageOrSession.model, }; } } -export function describeConversationModel(model: ChatHubConversationModel) { - switch (model.provider) { +export function describeConversationModel(model: ChatModelDto) { + switch (model.model.provider) { case 'n8n': return `n8n workflow ${model.name}`; case 'custom-agent': @@ -167,7 +159,7 @@ export function getTimestamp( workflowsById: Partial>, ): number | null { if (model.provider === 'custom-agent') { - const agent = agents.find((a) => a.id === model.agentId); + const agent = agents.find((a) => model.provider === 'custom-agent' && a.id === model.agentId); return agent?.[type] ? Date.parse(agent[type]) : null; } @@ -184,11 +176,11 @@ export function getTimestamp( } export function filterAndSortAgents( - models: ChatHubConversationModel[], + models: ChatModelDto[], filter: ChatAgentFilter, agents: ChatHubAgentDto[], workflowsById: Partial>, -): ChatHubConversationModel[] { +): ChatModelDto[] { let filtered = models; // Apply search filter @@ -199,13 +191,13 @@ export function filterAndSortAgents( // Apply provider filter if (filter.provider !== '') { - filtered = filtered.filter((model) => model.provider === filter.provider); + filtered = filtered.filter((model) => model.model.provider === filter.provider); } // Apply sorting filtered = [...filtered].sort((a, b) => { - const dateA = getTimestamp(a, filter.sortBy, agents, workflowsById); - const dateB = getTimestamp(b, filter.sortBy, agents, workflowsById); + const dateA = getTimestamp(a.model, filter.sortBy, agents, workflowsById); + const dateB = getTimestamp(b.model, filter.sortBy, agents, workflowsById); // Sort by dates (newest first) if (dateA && dateB) { diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatAgentAvatar.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatAgentAvatar.vue index ca0fcbb9c02..aa396b2ad9f 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatAgentAvatar.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatAgentAvatar.vue @@ -3,20 +3,27 @@ import { describeConversationModel } from '@/features/ai/chatHub/chat.utils'; import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue'; import { type ChatHubConversationModel, PROVIDER_CREDENTIAL_TYPE_MAP } from '@n8n/api-types'; import { N8nAvatar, N8nIcon, N8nTooltip } from '@n8n/design-system'; +import { useChatStore } from '../chat.store'; +import { computed } from 'vue'; -defineProps<{ +const props = defineProps<{ model: ChatHubConversationModel; size: 'sm' | 'md' | 'lg'; tooltip?: boolean; }>(); + +const chatStore = useChatStore(); +const modelDto = computed(() => { + return chatStore.getModel(props.model) ?? null; +});