diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 289af2a4caa..83d77e1abfa 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -127,20 +127,17 @@ export interface ChatHubMessageDto { retryOfMessageId: ChatMessageId | null; revisionOfMessageId: ChatMessageId | null; runIndex: number; - - responseIds: ChatMessageId[]; - retryIds: ChatMessageId[]; - revisionIds: ChatMessageId[]; } export type ChatHubConversationsResponse = ChatHubSessionDto[]; +export interface ChatHubConversationDto { + messages: Record; + rootIds: ChatMessageId[]; + activeMessageChain: ChatMessageId[]; +} + export interface ChatHubConversationResponse { session: ChatHubSessionDto; - - conversation: { - messages: Record; - rootIds: string[]; - activeMessageChain: string[]; - }; + conversation: ChatHubConversationDto; } diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index c4153f5f9e2..23fe5ce10af 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -24,6 +24,7 @@ export { type ChatSessionId, type ChatHubMessageDto, type ChatHubSessionDto, + type ChatHubConversationDto, type ChatHubConversationResponse, type ChatHubConversationsResponse, } from './chat-hub'; diff --git a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts index bf2b9e8cb29..cbb8ac377ab 100644 --- a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts +++ b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts @@ -299,9 +299,6 @@ describe('chatHub', () => { expect(messages[msg4.id].content).toBe('message 4a'); expect(messages[msg5.id].content).toBe('message 3b'); expect(messages[msg6.id].content).toBe('message 4b'); - expect(messages[msg3.id].revisionIds).toEqual([msg5.id]); - expect(messages[msg3.id].responseIds).toEqual([msg4.id]); - expect(messages[msg3.id].retryIds).toEqual([]); expect(messages[msg5.id].previousMessageId).toBe(msg2.id); }); @@ -458,9 +455,6 @@ describe('chatHub', () => { expect(activeMessageChain[1]).toBe(msg2.id); expect(activeMessageChain[2]).toBe(msg3.id); expect(activeMessageChain[3]).toBe(msg5.id); - expect(messages[msg4.id].revisionIds).toEqual([]); - expect(messages[msg4.id].responseIds).toEqual([]); - expect(messages[msg4.id].retryIds).toEqual([msg5.id]); expect(messages[msg5.id].previousMessageId).toBe(msg3.id); expect(messages[msg5.id].retryOfMessageId).toBe(msg4.id); }); @@ -523,7 +517,7 @@ describe('chatHub', () => { turnId: ids[2], createdAt: new Date('2025-01-03T00:10:00Z'), }); - const msg4a = await messagesRepository.createChatMessage({ + await messagesRepository.createChatMessage({ id: ids[3], sessionId: session.id, name: 'ChatGPT', @@ -544,7 +538,7 @@ describe('chatHub', () => { turnId: ids[4], createdAt: new Date('2025-01-03T00:20:00Z'), }); - const msg4b = await messagesRepository.createChatMessage({ + await messagesRepository.createChatMessage({ id: ids[5], sessionId: session.id, name: 'ChatGPT', @@ -613,22 +607,6 @@ describe('chatHub', () => { expect(activeMessageChain[2]).toBe(msg3d.id); expect(activeMessageChain[3]).toBe(msg4c.id); - expect(messages[msg1.id].revisionIds).toEqual([msg1b.id]); - expect(messages[msg1b.id].responseIds).toEqual([]); - expect(messages[msg1b.id].retryIds).toEqual([]); - - expect(messages[msg2.id].revisionIds).toEqual([]); - expect(messages[msg2.id].responseIds).toEqual([msg3a.id, msg3b.id]); - expect(messages[msg2.id].retryIds).toEqual([msg2r.id]); - - expect(messages[msg3b.id].revisionIds).toEqual([]); - expect(messages[msg3b.id].responseIds).toEqual([msg4b.id]); - expect(messages[msg3b.id].retryIds).toEqual([]); - - expect(messages[msg4a.id].revisionIds).toEqual([]); - expect(messages[msg4a.id].responseIds).toEqual([]); - expect(messages[msg4a.id].retryIds).toEqual([]); - expect(messages[msg2r.id].previousMessageId).toBe(msg1.id); expect(messages[msg2r.id].retryOfMessageId).toBe(msg2.id); }); 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 65dda1cf00a..b553f758b04 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -867,8 +867,9 @@ export class ChatHubService { } const messages = await this.messageRepository.getManyBySessionId(sessionId); - const messagesGraph: Record = - this.buildMessagesGraph(messages); + const messagesGraph: Record = Object.fromEntries( + messages.map((m) => [m.id, this.convertMessageToDto(m)]), + ); const rootIds = messages.filter((r) => r.previousMessageId === null).map((r) => r.id); const activeMessages = messages.filter((m) => m.state === 'active'); @@ -897,69 +898,27 @@ export class ChatHubService { }; } - private buildMessagesGraph(messages: ChatHubMessage[]) { - const messagesGraph: Record = {}; + private convertMessageToDto(message: ChatHubMessage): ChatHubMessageDto { + return { + id: message.id, + sessionId: message.sessionId, + type: message.type, + name: message.name, + content: message.content, + provider: message.provider, + model: message.model, + workflowId: message.workflowId, + executionId: message.executionId, + state: message.state, + createdAt: message.createdAt.toISOString(), + updatedAt: message.updatedAt.toISOString(), - for (const message of messages) { - messagesGraph[message.id] = { - id: message.id, - sessionId: message.sessionId, - type: message.type, - name: message.name, - content: message.content, - provider: message.provider, - model: message.model, - workflowId: message.workflowId, - executionId: message.executionId, - state: message.state, - createdAt: message.createdAt.toISOString(), - updatedAt: message.updatedAt.toISOString(), - - previousMessageId: message.previousMessageId, - turnId: message.turnId, - retryOfMessageId: message.retryOfMessageId, - revisionOfMessageId: message.revisionOfMessageId, - runIndex: message.runIndex, - - responseIds: [], - retryIds: [], - revisionIds: [], - }; - } - - for (const node of Object.values(messagesGraph)) { - if (node.previousMessageId && messagesGraph[node.previousMessageId]) { - messagesGraph[node.previousMessageId].responseIds.push(node.id); - } - if (node.retryOfMessageId && messagesGraph[node.retryOfMessageId]) { - messagesGraph[node.retryOfMessageId].retryIds.push(node.id); - } - if (node.revisionOfMessageId && messagesGraph[node.revisionOfMessageId]) { - messagesGraph[node.revisionOfMessageId].revisionIds.push(node.id); - } - } - - const sortByRunThenTime = (first: ChatMessageId, second: ChatMessageId) => { - const a = messagesGraph[first]; - const b = messagesGraph[second]; - - if (a.runIndex !== b.runIndex) { - return a.runIndex - b.runIndex; - } - - if (a.createdAt !== b.createdAt) { - return a.createdAt < b.createdAt ? -1 : 1; - } - - return a.id < b.id ? -1 : 1; + previousMessageId: message.previousMessageId, + turnId: message.turnId, + retryOfMessageId: message.retryOfMessageId, + revisionOfMessageId: message.revisionOfMessageId, + runIndex: message.runIndex, }; - - for (const node of Object.values(messagesGraph)) { - node.responseIds.sort(sortByRunThenTime); - node.retryIds.sort(sortByRunThenTime); - node.revisionIds.sort(sortByRunThenTime); - } - return messagesGraph; } /** diff --git a/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue index ccc8fdfff4e..b3e6dfe7535 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue @@ -10,18 +10,14 @@ import CredentialSelectorModal from './components/CredentialSelectorModal.vue'; import { useChatStore } from './chat.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { useUIStore } from '@/stores/ui.store'; -import { - type ChatMessage as ChatMessageType, - credentialsMapSchema, - type CredentialsMap, - type Suggestion, -} from './chat.types'; +import { credentialsMapSchema, type CredentialsMap, type Suggestion } from './chat.types'; import { chatHubConversationModelSchema, type ChatHubProvider, PROVIDER_CREDENTIAL_TYPE_MAP, type ChatHubConversationModel, chatHubProviderSchema, + type ChatHubMessageDto, } from '@n8n/api-types'; import { useLocalStorage, useMediaQuery } from '@vueuse/core'; import { @@ -113,7 +109,7 @@ const mergedCredentials = computed(() => ({ ...selectedCredentials.value, })); -const chatMessages = computed(() => chatStore.messagesBySession[sessionId.value] ?? []); +const chatMessages = computed(() => chatStore.getActiveMessages(sessionId.value)); const isNewChat = computed(() => route.name === CHAT_VIEW); const inputPlaceholder = computed(() => { if (!selectedModel.value) { @@ -192,7 +188,7 @@ watch( async ([id, isNew]) => { didSubmitInCurrentSession.value = false; - if (!isNew && !chatStore.messagesBySession[id]) { + if (!isNew && !chatStore.getConversation(id)) { try { await chatStore.fetchMessages(id); } catch (error) { @@ -277,8 +273,8 @@ function handleCancelEditMessage() { editingMessageId.value = undefined; } -function handleEditMessage(message: ChatMessageType) { - if (chatStore.isResponding || message.type === 'error' || !selectedModel.value) { +function handleEditMessage(message: ChatHubMessageDto) { + if (chatStore.isResponding || message.type !== 'human' || !selectedModel.value) { return; } @@ -288,7 +284,7 @@ function handleEditMessage(message: ChatMessageType) { return; } - chatStore.editMessage(sessionId.value, message.id, message.text, selectedModel.value, { + chatStore.editMessage(sessionId.value, message.id, message.content, selectedModel.value, { [PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: { id: credentialsId, name: '', @@ -297,8 +293,8 @@ function handleEditMessage(message: ChatMessageType) { editingMessageId.value = undefined; } -function handleRegenerateMessage(message: ChatMessageType) { - if (chatStore.isResponding || message.type === 'error' || !selectedModel.value) { +function handleRegenerateMessage(message: ChatHubMessageDto) { + if (chatStore.isResponding || message.type !== 'ai' || !selectedModel.value) { return; } diff --git a/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts b/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts index c21ba4133c2..cab1a582aba 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts +++ b/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts @@ -18,25 +18,157 @@ import type { ChatHubSessionDto, ChatMessageId, ChatSessionId, + ChatHubMessageDto, } from '@n8n/api-types'; -import type { StructuredChunk, ChatMessage, CredentialsMap } from './chat.types'; +import type { StructuredChunk, CredentialsMap, ChatMessage, ChatConversation } from './chat.types'; export const useChatStore = defineStore(CHAT_STORE, () => { const rootStore = useRootStore(); const models = ref(); const loadingModels = ref(false); - const ongoingStreaming = ref<{ messageId: string; replyToMessageId: string }>(); - const messagesBySession = ref>>({}); - const sessions = ref([]); - + const ongoingStreaming = ref<{ messageId: ChatMessageId; replyToMessageId: ChatMessageId }>(); const isResponding = computed(() => ongoingStreaming.value !== undefined); - const getLastMessage = (sessionId: string) => { - const msgs = messagesBySession.value[sessionId]; - if (!msgs || msgs.length === 0) return null; - return msgs[msgs.length - 1]; + const conversationsBySession = ref>(new Map()); + const sessions = ref([]); + + const getConversation = (sessionId: ChatSessionId): ChatConversation | undefined => + conversationsBySession.value.get(sessionId); + + const getActiveMessages = (sessionId: ChatSessionId): ChatMessage[] => { + const conversation = getConversation(sessionId); + if (!conversation) return []; + + return conversation.activeMessageChain.map((id) => conversation.messages[id]).filter(Boolean); }; + function ensureConversation(sessionId: ChatSessionId): ChatConversation { + if (!conversationsBySession.value.has(sessionId)) { + conversationsBySession.value.set(sessionId, { + messages: {}, + rootIds: [], + activeMessageChain: [], + }); + } + + const conversation = conversationsBySession.value.get(sessionId); + if (!conversation) { + throw new Error(`Conversation for session ID ${sessionId} not found`); + } + + return conversation; + } + + function computeActiveChain(conversation: ChatConversation, lastMessageId: ChatMessageId | null) { + const messages = conversation.messages; + const chain: ChatMessageId[] = []; + + if (!lastMessageId) { + return chain; + } + const visited = new Set(); + + let current: ChatMessageId | null = lastMessageId; + + while (current && !visited.has(current)) { + chain.unshift(current); + visited.add(current); + current = messages[current]?.previousMessageId ?? null; + } + + return chain; + } + + function computeAlternativesAndResponses(messages: ChatHubMessageDto[]) { + const messagesGraph: Record = {}; + + for (const message of messages) { + messagesGraph[message.id] = { + ...message, + responses: [], + alternatives: [], + }; + } + + for (const node of Object.values(messagesGraph)) { + if (node.previousMessageId && messagesGraph[node.previousMessageId]) { + messagesGraph[node.previousMessageId].responses.push(node.id); + } + if (node.retryOfMessageId && messagesGraph[node.retryOfMessageId]) { + messagesGraph[node.retryOfMessageId].alternatives.push(node.id); + } + if (node.revisionOfMessageId && messagesGraph[node.revisionOfMessageId]) { + messagesGraph[node.revisionOfMessageId].alternatives.push(node.id); + } + } + + const sortByRunThenTime = (first: ChatMessageId, second: ChatMessageId) => { + const a = messagesGraph[first]; + const b = messagesGraph[second]; + + if (a.runIndex !== b.runIndex) { + return a.runIndex - b.runIndex; + } + + if (a.createdAt !== b.createdAt) { + return a.createdAt < b.createdAt ? -1 : 1; + } + + return a.id < b.id ? -1 : 1; + }; + + // Second pass: Add cross-links for alternatives + for (const node of Object.values(messagesGraph)) { + if (!node.alternatives.includes(node.id)) { + node.alternatives.push(node.id); + } + + if (node.retryOfMessageId && messagesGraph[node.retryOfMessageId]) { + node.alternatives.push(node.retryOfMessageId); + for (const other of messagesGraph[node.retryOfMessageId].alternatives) { + if (other !== node.id && !node.alternatives.includes(other)) { + node.alternatives.push(other); + } + } + } + + if (node.revisionOfMessageId && messagesGraph[node.revisionOfMessageId]) { + node.alternatives.push(node.revisionOfMessageId); + for (const other of messagesGraph[node.revisionOfMessageId].alternatives) { + if (other !== node.id && !node.alternatives.includes(other)) { + node.alternatives.push(other); + } + } + } + + node.responses.sort(sortByRunThenTime); + node.alternatives.sort(sortByRunThenTime); + } + + return messagesGraph; + } + + function addMessage(sessionId: ChatSessionId, message: ChatMessage) { + const conversation = ensureConversation(sessionId); + + conversation.messages[message.id] = message; + + // TODO: Recomputing the entire graph shouldn't be needed here, we could just + conversation.messages = computeAlternativesAndResponses(Object.values(conversation.messages)); + + conversation.activeMessageChain = computeActiveChain(conversation, message.id); + } + + function appendMessage(sessionId: ChatSessionId, messageId: ChatMessageId, chunk: string) { + const conversation = ensureConversation(sessionId); + const message = conversation.messages[messageId]; + if (!message) { + throw new Error(`Message with ID ${messageId} not found in session ${sessionId}`); + } + + message.content += chunk; + } + async function fetchChatModels(credentialMap: CredentialsMap) { loadingModels.value = true; models.value = await fetchChatModelsApi(rootStore.restApiContext, { @@ -52,86 +184,55 @@ export const useChatStore = defineStore(CHAT_STORE, () => { async function fetchMessages(sessionId: string) { const { conversation } = await fetchMessagesApi(rootStore.restApiContext, sessionId); - const { messages, activeMessageChain } = conversation; - messagesBySession.value = { - ...messagesBySession.value, - [sessionId]: activeMessageChain.map((id) => ({ - id: messages[id].id, - role: messages[id].type === 'ai' ? 'assistant' : 'user', - type: 'message' as const, - text: messages[id].content, - key: messages[id].id, - })), - }; - } + const messages = computeAlternativesAndResponses(Object.values(conversation.messages)); - function addUserMessage(sessionId: string, content: string, id: string) { - messagesBySession.value = { - ...messagesBySession.value, - [sessionId]: [ - ...(messagesBySession.value[sessionId] ?? []), - { - id, - key: id, - role: 'user', - type: 'message', - text: content, - }, - ], - }; - } - - function addAiMessage(sessionId: string, content: string, id: string, key: string) { - messagesBySession.value = { - ...messagesBySession.value, - [sessionId]: [ - ...(messagesBySession.value[sessionId] ?? []), - { - id, - key, - role: 'assistant', - type: 'message', - text: content, - }, - ], - }; - } - - function appendMessage(sessionId: string, content: string, key: string) { - messagesBySession.value = { - ...messagesBySession.value, - [sessionId]: (messagesBySession.value[sessionId] ?? []).map((msg) => { - if (msg.key === key && msg.type === 'message') { - return { - ...msg, - text: msg.text + content, - }; - } - return msg; - }), - }; + conversationsBySession.value.set(sessionId, { + ...conversation, + messages, + }); } function onBeginMessage( sessionId: string, messageId: string, replyToMessageId: string, - nodeId: string, - runIndex?: number, + retryOfMessageId: string | null, + _nodeId: string, + _runIndex?: number, ) { ongoingStreaming.value = { messageId, replyToMessageId }; - addAiMessage(sessionId, '', messageId, `${messageId}-${nodeId}-${runIndex ?? 0}`); + addMessage(sessionId, { + id: messageId, + sessionId, + type: 'ai', + name: 'AI', + content: '', + provider: null, + model: null, + workflowId: null, + executionId: null, + state: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + previousMessageId: replyToMessageId, + turnId: null, + retryOfMessageId, + revisionOfMessageId: null, + runIndex: 0, + responses: [], + alternatives: [], + }); } function onChunk( sessionId: string, messageId: string, chunk: string, - nodeId?: string, - runIndex?: number, + _nodeId?: string, + _runIndex?: number, ) { - appendMessage(sessionId, chunk, `${messageId}-${nodeId}-${runIndex ?? 0}`); + appendMessage(sessionId, messageId, chunk); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -144,13 +245,14 @@ export const useChatStore = defineStore(CHAT_STORE, () => { message: StructuredChunk, messageId: string, replyToMessageId: string, + retryOfMessageId: string | null, ) { const nodeId = message.metadata?.nodeId || 'unknown'; const runIndex = message.metadata?.runIndex; switch (message.type) { case 'begin': - onBeginMessage(sessionId, messageId, replyToMessageId, nodeId, runIndex); + onBeginMessage(sessionId, messageId, replyToMessageId, retryOfMessageId, nodeId, runIndex); break; case 'item': onChunk(sessionId, messageId, message.content ?? '', nodeId, runIndex); @@ -169,8 +271,6 @@ export const useChatStore = defineStore(CHAT_STORE, () => { onEndMessage(messageId, nodeId, runIndex); break; } - - // addAssistantMessages(response.messages); } async function onStreamDone() { @@ -191,9 +291,32 @@ export const useChatStore = defineStore(CHAT_STORE, () => { ) { const messageId = uuidv4(); const replyId = uuidv4(); - const previousMessageId = getLastMessage(sessionId)?.id ?? null; + const conversation = ensureConversation(sessionId); + const previousMessageId = conversation.activeMessageChain.length + ? conversation.activeMessageChain[conversation.activeMessageChain.length - 1] + : null; - addUserMessage(sessionId, message, messageId); + addMessage(sessionId, { + id: messageId, + sessionId, + type: 'human', + name: 'User', + content: message, + provider: null, + model: null, + workflowId: null, + executionId: null, + state: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + previousMessageId, + turnId: null, + retryOfMessageId: null, + revisionOfMessageId: null, + runIndex: 0, + responses: [], + alternatives: [], + }); sendMessageApi( rootStore.restApiContext, @@ -206,7 +329,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { credentials, previousMessageId, }, - (chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, messageId), + (chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, messageId, null), onStreamDone, onStreamError, ); @@ -222,11 +345,30 @@ export const useChatStore = defineStore(CHAT_STORE, () => { const messageId = uuidv4(); const replyId = uuidv4(); - addUserMessage(sessionId, message, messageId); + const conversation = ensureConversation(sessionId); + const previousMessageId = conversation.messages[editId]?.previousMessageId ?? null; - // TODO: remove descendants of the message being edited - // or better yet, turn the frontend chat into a graph and - // maintain the visible active chain, and this would just switch to that branch. + addMessage(sessionId, { + id: messageId, + sessionId, + type: 'human', + name: 'User', + content: message, + provider: null, + model: null, + workflowId: null, + executionId: null, + state: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + previousMessageId, + turnId: null, + retryOfMessageId: null, + revisionOfMessageId: editId, + runIndex: 0, + responses: [], + alternatives: [], + }); editMessageApi( rootStore.restApiContext, @@ -239,7 +381,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { message, credentials, }, - (chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, messageId), + (chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, messageId, null), onStreamDone, onStreamError, ); @@ -252,10 +394,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => { credentials: ChatHubSendMessageRequest['credentials'], ) { const replyId = uuidv4(); + const conversation = ensureConversation(sessionId); + const previousMessageId = conversation.messages[retryId]?.previousMessageId ?? null; - // TODO: remove descendants of the message being retried - // or better yet, turn the frontend chat into a graph and - // maintain the visible active chain, and this would just switch to that branch. + if (!previousMessageId) { + throw new Error('No previous message to base regeneration on'); + } regenerateMessageApi( rootStore.restApiContext, @@ -266,7 +410,8 @@ export const useChatStore = defineStore(CHAT_STORE, () => { replyId, credentials, }, - (chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, retryId), + (chunk: StructuredChunk) => + onStreamMessage(sessionId, chunk, replyId, previousMessageId, retryId), onStreamDone, onStreamError, ); @@ -290,19 +435,20 @@ export const useChatStore = defineStore(CHAT_STORE, () => { return { models, + sessions, + conversationsBySession, loadingModels, - messagesBySession, isResponding, ongoingStreaming, - sessions, fetchChatModels, sendMessage, editMessage, regenerateMessage, - addUserMessage, fetchSessions, fetchMessages, renameSession, deleteSession, + getConversation, + getActiveMessages, }; }); diff --git a/packages/frontend/editor-ui/src/features/chatHub/chat.types.ts b/packages/frontend/editor-ui/src/features/chatHub/chat.types.ts index 7d4512d8a8e..de75a4596c3 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/chat.types.ts +++ b/packages/frontend/editor-ui/src/features/chatHub/chat.types.ts @@ -1,4 +1,10 @@ -import { chatHubProviderSchema, type ChatHubSessionDto } from '@n8n/api-types'; +import { + chatHubProviderSchema, + type ChatHubMessageDto, + type ChatMessageId, + type ChatHubSessionDto, + type ChatHubConversationDto, +} from '@n8n/api-types'; import { z } from 'zod'; export interface UserMessage { @@ -26,7 +32,15 @@ export interface ErrorMessage { } export type StreamChunk = AssistantMessage | ErrorMessage; -export type ChatMessage = UserMessage | AssistantMessage | ErrorMessage; + +export interface ChatMessage extends ChatHubMessageDto { + responses: ChatMessageId[]; + alternatives: ChatMessageId[]; +} + +export interface ChatConversation extends ChatHubConversationDto { + messages: Record; +} export interface StreamOutput { messages: StreamChunk[]; diff --git a/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessage.vue b/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessage.vue index 7005cf80bd3..968ae786323 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessage.vue +++ b/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessage.vue @@ -1,5 +1,4 @@