diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 4b9879aaa6e..518d78461ab 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -274,6 +274,7 @@ export class ChatHubSendMessageRequest extends Z.class({ ), tools: z.array(INodeSchema), attachments: z.array(chatAttachmentSchema), + agentName: z.string(), }) {} export class ChatHubRegenerateMessageRequest extends Z.class({ @@ -301,10 +302,12 @@ export class ChatHubEditMessageRequest extends Z.class({ export class ChatHubUpdateConversationRequest extends Z.class({ title: z.string().optional(), credentialId: z.string().max(36).optional(), - provider: chatHubProviderSchema.optional(), - model: z.string().max(64).optional(), - workflowId: z.string().max(36).optional(), - agentId: z.string().uuid().optional(), + agent: z + .object({ + model: chatHubConversationModelSchema, + name: z.string(), + }) + .optional(), tools: z.array(INodeSchema).optional(), }) {} @@ -324,7 +327,7 @@ export interface ChatHubSessionDto { model: string | null; workflowId: string | null; agentId: string | null; - agentName: string | null; + agentName: string; createdAt: string; updatedAt: string; tools: INode[]; diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index 1fcf791ed31..62dc013087f 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -97,10 +97,9 @@ export class ChatHubSession extends WithTimestamps { @Column({ type: 'varchar', length: 36, nullable: true }) agentId: string | null; - /* - * Cached name of the custom agent to use (if applicable). - * In case agent gets deleted - * Only set when provider is 'custom-agent'. + /** + * Cached display name of the agent/model. + * Used for all providers (LLM providers, custom agents, and n8n workflows). */ @Column({ type: 'varchar', length: 128, nullable: true }) agentName: string | null; diff --git a/packages/cli/src/modules/chat-hub/chat-hub.constants.ts b/packages/cli/src/modules/chat-hub/chat-hub.constants.ts index 5568247e020..e45df575a87 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.constants.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.constants.ts @@ -4,7 +4,7 @@ import type { INodeTypeNameVersion } from 'n8n-workflow'; export const CONVERSATION_TITLE_GENERATION_PROMPT = `Generate a concise, descriptive title for this conversation based on the user's message. Requirements: -- 3 to 5 words +- 2 to 5 words - Use normal sentence case (not title case) - No quotation marks - Only output the title, nothing else 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 6494481cd34..71c5407ee96 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -16,6 +16,7 @@ import { ChatHubN8nModel, ChatHubCustomAgentModel, emptyChatModelsResponse, + type ChatHubUpdateConversationRequest, } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { ExecutionRepository, IExecutionResponse, User, WorkflowRepository } from '@n8n/db'; @@ -68,6 +69,7 @@ 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 type { ChatHubSession } from './chat-hub-session.entity'; @Service() export class ChatHubService { @@ -1002,7 +1004,15 @@ export class ChatHubService { try { const result = await this.messageRepository.manager.transaction(async (trx) => { let session = await this.getChatSession(user, sessionId, trx); - session ??= await this.createChatSession(user, sessionId, model, credentialId, tools, trx); + session ??= await this.createChatSession( + user, + sessionId, + model, + credentialId, + tools, + payload.agentName, + trx, + ); await this.ensurePreviousMessage(previousMessageId, sessionId, trx); const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m])); @@ -1974,44 +1984,10 @@ export class ChatHubService { model: ChatHubConversationModel, credentialId: string | null, tools: INode[], + agentName: string, trx?: EntityManager, ) { - let agentName: string | undefined = undefined; - - if (model.provider === 'custom-agent') { - // Find the agent to get its name - const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id); - if (!agent) { - throw new BadRequestError('Agent not found for chat session initialization'); - } - agentName = agent.name; - } - - if (model.provider === 'n8n') { - // Find the workflow to get its name - const workflow = await this.workflowFinderService.findWorkflowForUser( - model.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; - } + await this.ensureValidModel(user, model); return await this.sessionRepository.createChatSession( { @@ -2065,7 +2041,7 @@ export class ChatHubService { model: session.model, workflowId: session.workflowId, agentId: session.agentId, - agentName: session.agentName, + agentName: session.agentName ?? '', createdAt: session.createdAt.toISOString(), updatedAt: session.updatedAt.toISOString(), tools: session.tools, @@ -2097,7 +2073,7 @@ export class ChatHubService { model: session.model, workflowId: session.workflowId, agentId: session.agentId, - agentName: session.agentName, + agentName: session.agentName ?? '', createdAt: session.createdAt.toISOString(), updatedAt: session.updatedAt.toISOString(), tools: session.tools, @@ -2184,15 +2160,7 @@ export class ChatHubService { async updateSession( user: User, sessionId: ChatSessionId, - updates: { - title?: string; - credentialId?: string | null; - provider?: ChatHubProvider; - model?: string | null; - workflowId?: string | null; - agentId?: string | null; - agentName?: string | null; - }, + updates: ChatHubUpdateConversationRequest, ) { const session = await this.sessionRepository.getOneById(sessionId, user.id); @@ -2200,62 +2168,34 @@ export class ChatHubService { 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 }, - ); + // Prepare the actual updates to be sent to the repository + const sessionUpdates: Partial = {}; - if (!workflow) { - throw new BadRequestError('Workflow not found'); + if (updates.agent) { + const model = updates.agent.model; + + await this.ensureValidModel(user, model); + + sessionUpdates.agentName = updates.agent.name; + sessionUpdates.provider = model.provider; + sessionUpdates.model = null; + sessionUpdates.credentialId = null; + sessionUpdates.agentId = null; + sessionUpdates.workflowId = null; + + if (updates.agent.model.provider === 'n8n') { + sessionUpdates.workflowId = updates.agent.model.workflowId; + } else if (updates.agent.model.provider === 'custom-agent') { + sessionUpdates.agentId = updates.agent.model.agentId; + } else { + sessionUpdates.model = updates.agent.model.model; } - - 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 (updates.title !== undefined) sessionUpdates.title = updates.title; + if (updates.credentialId !== undefined) sessionUpdates.credentialId = updates.credentialId; - 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 === '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); + return await this.sessionRepository.updateChatSession(sessionId, sessionUpdates); } /** @@ -2271,4 +2211,35 @@ export class ChatHubService { await this.chatHubAttachmentService.deleteAllBySessionId(sessionId); await this.sessionRepository.deleteChatHubSession(sessionId); } + + private async ensureValidModel(user: User, model: ChatHubConversationModel) { + if (model.provider === 'custom-agent') { + // Find the agent to get its name + const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id); + if (!agent) { + throw new BadRequestError('Agent not found for chat session initialization'); + } + } + + if (model.provider === 'n8n') { + // Find the workflow to get its name + const workflow = await this.workflowFinderService.findWorkflowForUser( + model.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', + ); + } + } + } } 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 b6b6c5d7152..4e13bf93a3c 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.types.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.types.ts @@ -30,6 +30,7 @@ export interface HumanMessagePayload extends BaseMessagePayload { previousMessageId: ChatMessageId | null; attachments: ChatAttachment[]; tools: INode[]; + 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 487265665fe..a8c1b4c1331 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -22,7 +22,6 @@ import { } from '@/features/ai/chatHub/constants'; import { useUsersStore } from '@/features/settings/users/users.store'; import { - chatHubConversationModelSchema, type ChatHubLLMProvider, PROVIDER_CREDENTIAL_TYPE_MAP, type ChatHubConversationModel, @@ -43,6 +42,10 @@ import { useChatCredentials } from '@/features/ai/chatHub/composables/useChatCre import ChatLayout from '@/features/ai/chatHub/components/ChatLayout.vue'; import { INodesSchema, type INode } from 'n8n-workflow'; import { useFileDrop } from '@/features/ai/chatHub/composables/useFileDrop'; +import { + type ChatHubConversationModelWithCachedDisplayName, + chatHubConversationModelWithCachedDisplayNameSchema, +} from '@/features/ai/chatHub/chat.types'; import { useI18n } from '@n8n/i18n'; const router = useRouter(); @@ -65,12 +68,9 @@ const isNewSession = computed(() => sessionId.value !== route.params.id); const scrollableRef = useTemplateRef('scrollable'); const scrollContainerRef = computed(() => scrollableRef.value?.parentElement ?? null); const currentConversation = computed(() => - sessionId.value - ? chatStore.sessions.find((session) => session.id === sessionId.value) - : undefined, + sessionId.value ? chatStore.sessions.byId[sessionId.value] : undefined, ); const currentConversationTitle = computed(() => currentConversation.value?.title); -const readyToShowMessages = computed(() => chatStore.agentsReady); // TODO: This also depends on the model, not all base LLM models support tools. const canSelectTools = computed(() => isLlmProvider(selectedModel.value?.model.provider)); @@ -80,7 +80,7 @@ const { arrivedState, measure } = useScroll(scrollContainerRef, { offset: { bottom: 100 }, }); -const defaultModel = useLocalStorage( +const defaultModel = useLocalStorage( LOCAL_STORAGE_CHAT_HUB_SELECTED_MODEL(usersStore.currentUserId ?? 'anonymous'), null, { @@ -89,7 +89,7 @@ const defaultModel = useLocalStorage( serializer: { read: (value) => { try { - return chatHubConversationModelSchema.parse(JSON.parse(value)); + return chatHubConversationModelWithCachedDisplayNameSchema.parse(JSON.parse(value)); } catch (error) { return null; } @@ -99,6 +99,10 @@ const defaultModel = useLocalStorage( }, ); +const defaultModelName = computed(() => + defaultModel.value ? chatStore.getAgent(defaultModel.value).name : undefined, +); + const defaultTools = useLocalStorage( LOCAL_STORAGE_CHAT_HUB_SELECTED_TOOLS(usersStore.currentUserId ?? 'anonymous'), null, @@ -154,25 +158,32 @@ const modelFromQuery = computed(() => { }); const selectedModel = computed(() => { - if (!chatStore.agentsReady) { - return null; + if (!isNewSession.value) { + const model = currentConversation.value ? unflattenModel(currentConversation.value) : null; + + if (!model) { + return null; + } + + return chatStore.getAgent( + model, + (currentConversation.value?.agentName || currentConversation.value?.model) ?? undefined, + ); } if (modelFromQuery.value) { return modelFromQuery.value; } - if (currentConversation.value?.provider) { - const model = unflattenModel(currentConversation.value); - - return model ? chatStore.getAgent(model) : null; - } - if (chatStore.streaming?.sessionId === sessionId.value) { - return chatStore.getAgent(chatStore.streaming.model); + return chatStore.getAgent(chatStore.streaming.model, chatStore.streaming.agentName); } - return defaultModel.value ? chatStore.getAgent(defaultModel.value) : null; + if (!defaultModel.value) { + return null; + } + + return chatStore.getAgent(defaultModel.value, defaultModel.value.cachedDisplayName); }); const { credentialsByProvider, selectCredential } = useChatCredentials( @@ -207,6 +218,21 @@ const credentialsForSelectedProvider = computed !credentialsForSelectedProvider.value); +const issue = computed(() => { + if (!chatStore.agentsReady) { + return null; + } + + if (!selectedModel.value) { + return 'missingAgent'; + } + + if (isMissingSelectedCredential.value) { + return 'missingCredentials'; + } + + return null; +}); const editingMessageId = ref(); const didSubmitInCurrentSession = ref(false); @@ -235,9 +261,9 @@ function scrollToMessage(messageId: ChatMessageId) { // Scroll to the bottom when a new message is added watch( - [readyToShowMessages, () => chatMessages.value[chatMessages.value.length - 1]?.id], - ([ready, lastMessageId]) => { - if (!ready || !lastMessageId) { + () => chatMessages.value[chatMessages.value.length - 1]?.id, + (lastMessageId) => { + if (!lastMessageId) { return; } @@ -271,7 +297,7 @@ watch( const model = findOneFromModelsResponse(models) ?? null; if (model) { - void handleSelectModel(model); + void handleSelectAgent(model); } }, { immediate: true }, @@ -322,6 +348,17 @@ watch( { immediate: true }, ); +// Keep cached display name up-to-date +watch( + defaultModelName, + (name) => { + if (defaultModel.value && name) { + defaultModel.value = { ...defaultModel.value, cachedDisplayName: name }; + } + }, + { immediate: true }, +); + function onSubmit(message: string, attachments: File[]) { if ( !message.trim() || @@ -342,6 +379,7 @@ function onSubmit(message: string, attachments: File[]) { credentialsForSelectedProvider.value, canSelectTools.value ? selectedTools.value : [], attachments, + selectedModel.value.name, ); inputRef.value?.setText(''); @@ -406,18 +444,24 @@ function handleRegenerateMessage(message: ChatHubMessageDto) { ); } -async function handleSelectModel(selection: ChatModelDto) { +async function handleSelectModel(selection: ChatHubConversationModel, displayName?: string) { + const agentName = displayName ?? chatStore.getAgent(selection)?.name ?? ''; + if (currentConversation.value) { try { - await chatStore.updateSessionModel(sessionId.value, selection.model); + await chatStore.updateSessionModel(sessionId.value, selection, agentName); } catch (error) { toast.showError(error, 'Could not update selected model'); } } else { - defaultModel.value = selection.model; + defaultModel.value = { ...selection, cachedDisplayName: agentName }; } } +async function handleSelectAgent(selection: ChatModelDto) { + await handleSelectModel(selection.model, selection.name); +} + function handleSwitchAlternative(messageId: string) { shouldSkipNextScrollTrigger.value = true; chatStore.switchAlternative(sessionId.value, messageId); @@ -501,8 +545,8 @@ function onFilesDropped(files: File[]) { ref="headerRef" :selected-model="selectedModel" :credentials="credentialsByProvider" - :ready-to-show-model-selector="chatStore.agentsReady" - @select-model="handleSelectModel" + :ready-to-show-model-selector="isNewSession || !!currentConversation" + @select-model="handleSelectAgent" @edit-custom-agent="handleEditAgent" @create-custom-agent="openNewAgentCreator" @select-credential="selectCredential" @@ -510,7 +554,6 @@ function onFilesDropped(files: File[]) { /> { const telemetry = useTelemetry(); const agents = ref(); - const sessions = ref(); + const sessions = ref<{ + byId: Partial>; + ids: string[] | null; + hasMore: boolean; + nextCursor: string | null; + }>({ byId: {}, ids: null, hasMore: false, nextCursor: null }); const sessionsLoadingMore = ref(false); const streaming = ref(); @@ -291,7 +295,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => { return; } - if (!reset && sessions.value && !sessions.value.hasMore && sessions.value.data.length > 0) { + if ( + !reset && + sessions.value && + !sessions.value.hasMore && + (sessions.value.ids ?? []).length > 0 + ) { return; } @@ -306,10 +315,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => { new Promise((resolve) => setTimeout(resolve, 500)), ]); - sessions.value = { - ...response, - data: [...(reset ? [] : (sessions.value?.data ?? [])), ...response.data], - }; + if (reset || sessions.value.ids === null) { + sessions.value.ids = []; + } + + sessions.value.hasMore = response.hasMore; + sessions.value.nextCursor = response.nextCursor; + + for (const session of response.data) { + sessions.value.ids.push(session.id); + sessions.value.byId[session.id] = session; + } } finally { sessionsLoadingMore.value = false; } @@ -322,7 +338,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } async function fetchMessages(sessionId: string) { - const { conversation } = await fetchMessagesApi(rootStore.restApiContext, sessionId); + const { conversation, session } = await fetchMessagesApi(rootStore.restApiContext, sessionId); const messages = linkMessages(Object.values(conversation.messages)); @@ -334,6 +350,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { messages, activeMessageChain: computeActiveChain(messages, latestMessage?.id ?? null), }); + sessions.value.byId[sessionId] = session; } function onBeginMessage() { @@ -348,17 +365,6 @@ export const useChatStore = defineStore(CHAT_STORE, () => { ); addMessage(streaming.value.sessionId, message); - - if (sessions.value?.data.some((session) => session.id === streaming.value?.sessionId)) { - return; - } - - sessions.value = { - hasMore: false, - nextCursor: null, - ...sessions.value, - data: [...(sessions.value?.data ?? []), createSessionFromStreamingState(streaming.value)], - }; } function ensureMessage(sessionId: ChatSessionId, messageId: ChatMessageId): ChatMessage { @@ -475,6 +481,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { credentials: ChatHubSendMessageRequest['credentials'], tools: INode[], files: File[] = [], + agentName: string, ) { const messageId = uuidv4(); const conversation = ensureConversation(sessionId); @@ -517,8 +524,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => { model, retryOfMessageId: null, tools, + agentName, }; + if (!sessions.value.byId[sessionId]) { + sessions.value.byId[sessionId] = createSessionFromStreamingState(streaming.value); + if (!sessions.value.ids) { + sessions.value.ids = []; + } + sessions.value.ids.unshift(sessionId); + } + sendMessageApi( rootStore.restApiContext, { @@ -530,6 +546,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { previousMessageId, tools, attachments, + agentName, }, onStreamMessage, onStreamDone, @@ -588,6 +605,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { model, retryOfMessageId: null, tools: [], + agentName: sessions.value.byId[sessionId]?.agentName ?? '', }; editMessageApi( @@ -625,6 +643,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { model, retryOfMessageId: retryId, tools: [], + agentName: sessions.value.byId[sessionId]?.agentName ?? '', }; regenerateMessageApi( @@ -652,22 +671,18 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } function updateSession(sessionId: ChatSessionId, toUpdate: Partial) { - if (!sessions.value) { - return; - } + const session = sessions.value.byId[sessionId]; - sessions.value.data = sessions.value.data?.map((session) => - session.id === sessionId - ? { - ...session, - ...toUpdate, - } - : session, - ); + if (session) { + sessions.value.byId[sessionId] = { + ...session, + ...toUpdate, + }; + } } async function updateToolsInSession(sessionId: ChatSessionId, tools: INode[]) { - const session = sessions.value?.data?.find((s) => s.id === sessionId); + const session = sessions.value?.byId[sessionId]; if (!session) { throw new Error(`Session with ID ${sessionId} not found`); } @@ -685,20 +700,22 @@ export const useChatStore = defineStore(CHAT_STORE, () => { updateSession(sessionId, updated.session); } - async function updateSessionModel(sessionId: ChatSessionId, model: ChatHubConversationModel) { - await updateConversationApi(rootStore.restApiContext, sessionId, model); - updateSession(sessionId, model); + async function updateSessionModel( + sessionId: ChatSessionId, + model: ChatHubConversationModel, + agentName: string, + ) { + await updateConversationApi(rootStore.restApiContext, sessionId, { + agent: { model, name: agentName }, + }); + updateSession(sessionId, { ...model, agentName }); } async function deleteSession(sessionId: ChatSessionId) { await deleteConversationApi(rootStore.restApiContext, sessionId); - if (sessions.value) { - sessions.value = { - ...sessions.value, - data: sessions.value.data?.filter((session) => session.id !== sessionId), - }; - } + delete sessions.value.byId[sessionId]; + sessions.value.ids = sessions.value.ids?.filter((id) => id !== sessionId) ?? null; } function switchAlternative(sessionId: ChatSessionId, messageId: ChatMessageId) { @@ -781,33 +798,23 @@ export const useChatStore = defineStore(CHAT_STORE, () => { await fetchAgents(credentials); } - function getAgent(model: ChatHubConversationModel) { - if (!agents.value) return null; - - const agent = agents.value[model.provider]?.models.find((agent) => + function getAgent(model: ChatHubConversationModel, fallbackName: string = ''): ChatModelDto { + const agent = agents.value?.[model.provider]?.models.find((agent) => isMatchedAgent(agent, model), ); - if (!agent) { - if (!isLlmProviderModel(model)) { - return null; - } - - // Allow custom models chosen by ID even if they are not in the fetched list - return { - model: { - provider: model.provider, - model: model.model, - }, - name: model.model, - description: null, - createdAt: null, - updatedAt: null, - allowFileUploads: true, - }; + if (agent) { + return agent; } - return agent; + return { + model, + name: fallbackName, + description: null, + createdAt: null, + updatedAt: null, + allowFileUploads: true, + }; } async function fetchAllChatSettings() { @@ -862,8 +869,8 @@ export const useChatStore = defineStore(CHAT_STORE, () => { /** * conversations */ - sessions: computed(() => sessions.value?.data ?? []), - sessionsReady: computed(() => sessions.value !== undefined), + sessions, + sessionsReady: computed(() => sessions.value.ids !== null), sessionsLoading: computed(() => sessionsLoadingMore.value), fetchSessions, fetchMoreSessions, diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.types.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.types.ts index 3912f4547ec..93b87dc62c4 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.types.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.types.ts @@ -8,9 +8,11 @@ import { type ChatHubConversationModel, type EnrichedStructuredChunk, type ChatHubProvider, + chatHubConversationModelSchema, } from '@n8n/api-types'; import type { INode } from 'n8n-workflow'; import { z } from 'zod'; +import { isLlmProviderModel } from './chat.utils'; export interface UserMessage { id: string; @@ -80,6 +82,7 @@ export interface ChatStreamingState extends Partial ({ + ...value, + cachedDisplayName: value.cachedDisplayName || (isLlmProviderModel(value) ? value.model : ''), + })); + +export type ChatHubConversationModelWithCachedDisplayName = z.infer< + typeof chatHubConversationModelWithCachedDisplayNameSchema +>; 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 b6cf34fd07b..a1839cf745c 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 @@ -340,7 +340,7 @@ export function createSessionFromStreamingState(streaming: ChatStreamingState): ownerId: '', lastMessageAt: new Date().toISOString(), credentialId: null, - agentName: null, + agentName: streaming.agentName, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tools: streaming.tools, diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue index 49ad58efa7b..3b625b44ccd 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue @@ -46,7 +46,7 @@ const credentialIdForSelectedModelProvider = computed( () => selectedModel.value && agentMergedCredentials.value[selectedModel.value.provider], ); const selectedAgent = computed( - () => selectedModel.value && chatStore.getAgent(selectedModel.value), + () => selectedModel.value && chatStore.getAgent(selectedModel.value, selectedModel.value.model), ); const isEditMode = computed(() => !!props.data.agentId); @@ -270,6 +270,7 @@ function onSelectTools(newTools: INode[]) { :include-custom-agents="false" :credentials="agentMergedCredentials" :disabled="isLoadingAgent" + warn-missing-credentials @change="onModelChange" @select-credential="onCredentialSelected" /> 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 2448f6d6c4c..8c1d1cf0c27 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 @@ -1,13 +1,18 @@