mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-29 19:10:43 +08:00
feat(core): Add Chat Trigger toggle that controls whether a WF shows up on chat hub (no-changelog) (#21267)
This commit is contained in:
@@ -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<typeof openAIModelSchema>;
|
||||
export type ChatHubAnthropicModel = z.infer<typeof anthropicModelSchema>;
|
||||
export type ChatHubGoogleModel = z.infer<typeof googleModelSchema>;
|
||||
export type ChatHubN8nModel = z.infer<typeof n8nModelSchema>;
|
||||
export type ChatHubCustomAgentModel = z.infer<typeof chatAgentSchema>;
|
||||
export type ChatHubConversationModel = z.infer<typeof chatHubConversationModelSchema>;
|
||||
|
||||
/**
|
||||
@@ -81,13 +81,19 @@ export const chatModelsRequestSchema = z.object({
|
||||
|
||||
export type ChatModelsRequest = z.infer<typeof chatModelsRequestSchema>;
|
||||
|
||||
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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -756,7 +756,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
);
|
||||
}
|
||||
|
||||
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<WorkflowEntity> {
|
||||
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<WorkflowEntity, 'id' | 'name' | 'active'> & Partial<Pick<WorkflowEntity, 'nodes'>>
|
||||
> = await qb
|
||||
.select([
|
||||
'workflow.id',
|
||||
'workflow.name',
|
||||
'workflow.active',
|
||||
...(includeNodes ? ['workflow.nodes'] : []),
|
||||
])
|
||||
.where(whereClause, parameters)
|
||||
.getMany();
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ export class ChatHubController {
|
||||
@Body payload: ChatHubUpdateConversationRequest,
|
||||
): Promise<ChatHubConversationResponse> {
|
||||
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);
|
||||
|
||||
@@ -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<ChatHubLLMProvider, INodeTypeNameVersion> = {
|
||||
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<ChatModelsResponse[ChatHubProvider]> {
|
||||
): Promise<ChatModelsResponse['openai']> {
|
||||
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<ChatModelsResponse[ChatHubProvider]> {
|
||||
): Promise<ChatModelsResponse['anthropic']> {
|
||||
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<ChatModelsResponse[ChatHubProvider]> {
|
||||
): Promise<ChatModelsResponse['google']> {
|
||||
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<ChatModelsResponse[ChatHubProvider]> {
|
||||
private async fetchAgentWorkflowsAsModels(user: User): Promise<ChatModelsResponse['n8n']> {
|
||||
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);
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface ModelWithCredentials {
|
||||
workflowId?: string;
|
||||
credentialId: string | null;
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } : {}) };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChatHubConversationModel>((agent) => ({
|
||||
provider: 'custom-agent',
|
||||
agentId: agent.id,
|
||||
.map<ChatModelDto>((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 () => {
|
||||
<ChatAgentCard
|
||||
v-for="model in models"
|
||||
:key="`${model.provider}::${model.provider === 'custom-agent' ? model.agentId : model.provider === 'n8n' ? model.workflowId : model.model}`"
|
||||
:model="model"
|
||||
:model="chatStore.getModel(model)!"
|
||||
:agents="chatStore.agents"
|
||||
:workflows-by-id="workflowsStore.workflowsById"
|
||||
@edit="handleEditAgent(model)"
|
||||
|
||||
@@ -85,43 +85,43 @@ const selectedModel = computed<ChatHubConversationModel | null>(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -615,14 +615,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
): Promise<ChatHubConversationModel> {
|
||||
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,
|
||||
|
||||
@@ -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<Record<string, IWorkflowDb>>,
|
||||
): 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<Record<string, IWorkflowDb>>,
|
||||
): 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<Record<string, IWorkflowDb>>,
|
||||
): 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) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nTooltip :show-after="100" placement="left" :disabled="!tooltip">
|
||||
<template #content>{{ describeConversationModel(model) }}</template>
|
||||
<template v-if="modelDto" #content>{{ describeConversationModel(modelDto) }}</template>
|
||||
<N8nAvatar
|
||||
v-if="model.provider === 'custom-agent'"
|
||||
:first-name="model.name"
|
||||
:first-name="modelDto?.name"
|
||||
:size="size === 'lg' ? 'medium' : size === 'sm' ? 'xxsmall' : 'xsmall'"
|
||||
/>
|
||||
<N8nIcon
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { getAgentRoute, getTimestamp } from '@/features/ai/chatHub/chat.utils';
|
||||
import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vue';
|
||||
import type { ChatHubAgentDto, ChatHubConversationModel } from '@n8n/api-types';
|
||||
import type { ChatHubAgentDto, ChatModelDto } from '@n8n/api-types';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { N8nIconButton, N8nText } from '@n8n/design-system';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
@@ -9,7 +9,7 @@ import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
const { model, agents, workflowsById } = defineProps<{
|
||||
model: ChatHubConversationModel;
|
||||
model: ChatModelDto;
|
||||
agents: ChatHubAgentDto[];
|
||||
workflowsById: Partial<Record<string, IWorkflowDb>>;
|
||||
}>();
|
||||
@@ -20,9 +20,12 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const description = computed(() => {
|
||||
if (model.provider === 'custom-agent') {
|
||||
const agent = agents.find((a) => a.id === model.agentId);
|
||||
if (model.model.provider === 'custom-agent') {
|
||||
const agent = agents.find(
|
||||
(a) => model.model.provider === 'custom-agent' && a.id === model.model.agentId,
|
||||
);
|
||||
|
||||
// TODO: there's model.description that we could just use?
|
||||
if (agent?.description) {
|
||||
return agent.description;
|
||||
}
|
||||
@@ -31,13 +34,13 @@ const description = computed(() => {
|
||||
return 'No description';
|
||||
});
|
||||
|
||||
const updatedAt = computed(() => getTimestamp(model, 'updatedAt', agents, workflowsById));
|
||||
const createdAt = computed(() => getTimestamp(model, 'createdAt', agents, workflowsById));
|
||||
const updatedAt = computed(() => getTimestamp(model.model, 'updatedAt', agents, workflowsById));
|
||||
const createdAt = computed(() => getTimestamp(model.model, 'createdAt', agents, workflowsById));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink :to="getAgentRoute(model)" :class="$style.card">
|
||||
<ChatAgentAvatar :model="model" size="lg" />
|
||||
<RouterLink :to="getAgentRoute(model.model)" :class="$style.card">
|
||||
<ChatAgentAvatar :model="model.model" size="lg" />
|
||||
|
||||
<div :class="$style.content">
|
||||
<N8nText tag="h3" size="medium" bold :class="$style.title">
|
||||
@@ -48,7 +51,7 @@ const createdAt = computed(() => getTimestamp(model, 'createdAt', agents, workfl
|
||||
</N8nText>
|
||||
<div :class="$style.metadata">
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ model.provider === 'n8n' ? 'n8n workflow' : 'Custom agent' }}
|
||||
{{ model.model.provider === 'n8n' ? 'n8n workflow' : 'Custom agent' }}
|
||||
</N8nText>
|
||||
<N8nText v-if="updatedAt" size="small" color="text-light">
|
||||
Last updated <TimeAgo :date="String(updatedAt)" />
|
||||
@@ -59,7 +62,7 @@ const createdAt = computed(() => getTimestamp(model, 'createdAt', agents, workfl
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="model.provider === 'custom-agent'" :class="$style.actions">
|
||||
<div v-if="model.model.provider === 'custom-agent'" :class="$style.actions">
|
||||
<N8nIconButton
|
||||
icon="pen"
|
||||
type="tertiary"
|
||||
|
||||
@@ -12,8 +12,6 @@ import { computed, nextTick, onBeforeMount, ref, useTemplateRef, watch } from 'v
|
||||
import VueMarkdown from 'vue-markdown-render';
|
||||
import type { ChatMessage } from '../chat.types';
|
||||
import ChatMessageActions from './ChatMessageActions.vue';
|
||||
import { useChatStore } from '@/features/ai/chatHub/chat.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { restoreConversationModelFromMessageOrSession } from '@/features/ai/chatHub/chat.utils';
|
||||
|
||||
const { message, compact, isEditing, isStreaming, minHeight } = defineProps<{
|
||||
@@ -36,8 +34,6 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const clipboard = useClipboard();
|
||||
const chatStore = useChatStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const editedText = ref('');
|
||||
const textareaRef = useTemplateRef('textarea');
|
||||
@@ -51,13 +47,7 @@ const speech = useSpeechSynthesis(messageContent, {
|
||||
volume: 1,
|
||||
});
|
||||
|
||||
const model = computed(() =>
|
||||
restoreConversationModelFromMessageOrSession(
|
||||
message,
|
||||
chatStore.agents,
|
||||
workflowsStore.workflowsById,
|
||||
),
|
||||
);
|
||||
const model = computed(() => restoreConversationModelFromMessageOrSession(message));
|
||||
|
||||
async function handleCopy() {
|
||||
const text = message.content;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ChatHubConversationModel, ChatHubLLMProvider } from '@n8n/api-type
|
||||
import { N8nIconButton, N8nInput, N8nText } from '@n8n/design-system';
|
||||
import { useSpeechRecognition } from '@vueuse/core';
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useChatStore } from '../chat.store';
|
||||
|
||||
const { selectedModel, isMissingCredentials } = defineProps<{
|
||||
isResponding: boolean;
|
||||
@@ -21,6 +22,7 @@ const emit = defineEmits<{
|
||||
|
||||
const inputRef = useTemplateRef<HTMLElement>('inputRef');
|
||||
const message = ref('');
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -30,12 +32,17 @@ const speechInput = useSpeechRecognition({
|
||||
lang: navigator.language,
|
||||
});
|
||||
|
||||
const selected = computed(() => {
|
||||
if (!selectedModel || !chatStore.models) return null;
|
||||
return chatStore.getModel(selectedModel) ?? null;
|
||||
});
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (!selectedModel) {
|
||||
return 'Select a model';
|
||||
}
|
||||
|
||||
return `Message ${selectedModel.name}`;
|
||||
return `Message ${selected.value?.name ?? 'a model'}...`;
|
||||
});
|
||||
|
||||
function onMic() {
|
||||
|
||||
+1
-11
@@ -6,8 +6,6 @@ import { type ChatHubSessionDto } from '@n8n/api-types';
|
||||
import { N8nInput } from '@n8n/design-system';
|
||||
import type { ActionDropdownItem } from '@n8n/design-system/types';
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useChatStore } from '../chat.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { restoreConversationModelFromMessageOrSession } from '@/features/ai/chatHub/chat.utils';
|
||||
|
||||
const { session, isRenaming, active } = defineProps<{
|
||||
@@ -23,20 +21,12 @@ const emit = defineEmits<{
|
||||
delete: [sessionId: string];
|
||||
}>();
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const input = useTemplateRef('input');
|
||||
const editedLabel = ref('');
|
||||
|
||||
type SessionAction = 'rename' | 'delete';
|
||||
|
||||
const model = computed(() =>
|
||||
restoreConversationModelFromMessageOrSession(
|
||||
session,
|
||||
chatStore.agents,
|
||||
workflowsStore.workflowsById,
|
||||
),
|
||||
);
|
||||
const model = computed(() => restoreConversationModelFromMessageOrSession(session));
|
||||
|
||||
const dropdownItems = computed<Array<ActionDropdownItem<SessionAction>>>(() => [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,12 @@ import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { N8nNavigationDropdown, N8nIcon, N8nButton, N8nText, N8nAvatar } from '@n8n/design-system';
|
||||
import { type ComponentProps } from 'vue-component-type-helpers';
|
||||
import { PROVIDER_CREDENTIAL_TYPE_MAP, chatHubProviderSchema } from '@n8n/api-types';
|
||||
import type { ChatHubProvider, ChatHubConversationModel, ChatHubLLMProvider } from '@n8n/api-types';
|
||||
import type {
|
||||
ChatHubProvider,
|
||||
ChatHubConversationModel,
|
||||
ChatHubLLMProvider,
|
||||
ChatHubCustomAgentModel,
|
||||
} from '@n8n/api-types';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
@@ -37,8 +42,8 @@ function handleSelectCredentials(provider: ChatHubProvider, id: string) {
|
||||
emit('selectCredential', provider, id);
|
||||
}
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const i18n = useI18n();
|
||||
const chatStore = useChatStore();
|
||||
const dropdownRef = useTemplateRef('dropdownRef');
|
||||
const credentialSelectorProvider = ref<Exclude<ChatHubProvider, 'n8n' | 'custom-agent'> | null>(
|
||||
null,
|
||||
@@ -46,6 +51,11 @@ const credentialSelectorProvider = ref<Exclude<ChatHubProvider, 'n8n' | 'custom-
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
const selectedModelDto = computed(() => {
|
||||
if (!props.selectedModel || !chatStore.models) return null;
|
||||
return chatStore.getModel(props.selectedModel) ?? null;
|
||||
});
|
||||
|
||||
const credentialsName = computed(() =>
|
||||
props.selectedModel
|
||||
? credentialsStore.getCredentialById(props.credentials[props.selectedModel.provider] ?? '')
|
||||
@@ -55,13 +65,13 @@ const credentialsName = computed(() =>
|
||||
|
||||
const menu = computed(() => {
|
||||
const agents = chatStore.models?.['custom-agent'].models;
|
||||
const agentOptions = (agents ?? [])
|
||||
.filter((model) => 'agentId' in model)
|
||||
.map<ComponentProps<typeof N8nNavigationDropdown>['menu'][number]>((agent) => ({
|
||||
id: `agent::${agent.agentId}`,
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}));
|
||||
const agentOptions = (agents ?? []).map<
|
||||
ComponentProps<typeof N8nNavigationDropdown>['menu'][number]
|
||||
>((agent) => ({
|
||||
id: `agent::${(agent.model as ChatHubCustomAgentModel).agentId}`,
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}));
|
||||
|
||||
const agentMenu: ComponentProps<typeof N8nNavigationDropdown>['menu'][number] = {
|
||||
id: 'custom-agents',
|
||||
@@ -93,9 +103,14 @@ const menu = computed(() => {
|
||||
const modelOptions =
|
||||
models.length > 0
|
||||
? models
|
||||
.filter((model) => model.provider !== 'custom-agent')
|
||||
.filter((model) => model.model.provider !== 'custom-agent')
|
||||
.map<ComponentProps<typeof N8nNavigationDropdown>['menu'][number]>((model) => {
|
||||
const identifier = model.provider === 'n8n' ? model.workflowId : model.model;
|
||||
const identifier =
|
||||
model.model.provider === 'n8n'
|
||||
? model.model.workflowId
|
||||
: model.model.provider !== 'custom-agent'
|
||||
? model.model.model
|
||||
: model.model.agentId;
|
||||
|
||||
return {
|
||||
id: `${provider}::${identifier}`,
|
||||
@@ -133,8 +148,8 @@ const menu = computed(() => {
|
||||
});
|
||||
|
||||
const selectedLabel = computed(() => {
|
||||
if (!props.selectedModel) return 'Select model';
|
||||
return props.selectedModel.name;
|
||||
if (!selectedModelDto.value) return 'Select model';
|
||||
return selectedModelDto.value.name;
|
||||
});
|
||||
|
||||
function openCredentialsSelectorOrCreate(provider: ChatHubLLMProvider) {
|
||||
@@ -159,10 +174,12 @@ function onSelect(id: string) {
|
||||
emit('createAgent');
|
||||
} else {
|
||||
const agents = chatStore.models?.['custom-agent'].models;
|
||||
const selected = agents?.find((agent) => 'agentId' in agent && agent.agentId === value);
|
||||
const selected = agents?.find(
|
||||
(agent) => agent.model.provider === 'custom-agent' && agent.model.agentId === value,
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
emit('change', selected);
|
||||
emit('change', selected.model);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -184,14 +201,18 @@ function onSelect(id: string) {
|
||||
const model = parsedProvider === 'n8n' ? null : identifier;
|
||||
const workflowId = parsedProvider === 'n8n' ? identifier : null;
|
||||
const selected = chatStore.models?.[parsedProvider].models
|
||||
.filter((m) => m.provider !== 'custom-agent')
|
||||
.find((m) => (m.provider === 'n8n' ? m.workflowId === workflowId : m.model === model));
|
||||
.filter((m) => m.model.provider !== 'custom-agent')
|
||||
.find((m) =>
|
||||
m.model.provider === 'n8n'
|
||||
? m.model.workflowId === workflowId
|
||||
: m.model.provider !== 'custom-agent' && m.model.model === model,
|
||||
);
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('change', selected);
|
||||
emit('change', selected.model);
|
||||
}
|
||||
|
||||
function handleCreateNewCredential(provider: ChatHubLLMProvider) {
|
||||
@@ -246,8 +267,8 @@ defineExpose({
|
||||
/>
|
||||
|
||||
<ChatAgentAvatar
|
||||
v-if="selectedModel"
|
||||
:model="selectedModel"
|
||||
v-if="selectedModelDto"
|
||||
:model="selectedModelDto.model"
|
||||
:size="credentialsName ? 'md' : 'sm'"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
|
||||
+11
-1
@@ -25,18 +25,20 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { CHAT_TRIGGER_NODE_TYPE, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import {
|
||||
getMainAuthField,
|
||||
getNodeAuthFields,
|
||||
isAuthRelatedParameter,
|
||||
} from '@/utils/nodeTypesUtils';
|
||||
import { injectWorkflowState } from '@/composables/useWorkflowState';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
export function useNodeSettingsParameters() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const telemetry = useTelemetry();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
@@ -202,6 +204,14 @@ export function useNodeSettingsParameters() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide chat hub toggle on chat trigger when module isn't enabled.
|
||||
// Remove this check when feature is generally available.
|
||||
if (nodeType?.name === CHAT_TRIGGER_NODE_TYPE && parameter.name === 'availableInChat') {
|
||||
if (!settingsStore.isChatFeatureEnabled) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (parameter[displayKey] === undefined) {
|
||||
// If it is not defined no need to do a proper check
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user