mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
387 lines
10 KiB
TypeScript
387 lines
10 KiB
TypeScript
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
|
import type { ChatRequest } from '@/types/assistant.types';
|
|
import { useI18n } from '@n8n/i18n';
|
|
import { isTextMessage, isWorkflowUpdatedMessage, isToolMessage } from '@/types/assistant.types';
|
|
|
|
export interface MessageProcessingResult {
|
|
messages: ChatUI.AssistantMessage[];
|
|
thinkingMessage?: string;
|
|
shouldClearThinking: boolean;
|
|
}
|
|
|
|
export function useBuilderMessages() {
|
|
const locale = useI18n();
|
|
|
|
/**
|
|
* Clear rating from all messages
|
|
*/
|
|
function clearRatingLogic(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
|
return messages.map((message) => {
|
|
if (message.type === 'text' && 'showRating' in message) {
|
|
// Pick all properties except showRating and ratingStyle
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { showRating, ratingStyle, ...cleanMessage } = message;
|
|
return cleanMessage;
|
|
}
|
|
return message;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply rating logic to messages - only show rating on the last AI text message after workflow-updated
|
|
* when no tools are running
|
|
*/
|
|
function applyRatingLogic(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
|
const { hasAnyRunningTools, isStillThinking } = getThinkingState(messages);
|
|
|
|
// Don't apply rating if tools are still running
|
|
if (hasAnyRunningTools || isStillThinking) {
|
|
// Remove any existing ratings
|
|
return clearRatingLogic(messages);
|
|
}
|
|
|
|
// Find the index of the last workflow-updated message
|
|
let lastWorkflowUpdateIndex = -1;
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
if (messages[i].type === 'workflow-updated') {
|
|
lastWorkflowUpdateIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no workflow-updated, return messages as-is
|
|
if (lastWorkflowUpdateIndex === -1) {
|
|
return messages;
|
|
}
|
|
|
|
// Find the last assistant text message after workflow-updated
|
|
let lastAssistantTextIndex = -1;
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
if (
|
|
messages[i].type === 'text' &&
|
|
messages[i].role === 'assistant' &&
|
|
i > lastWorkflowUpdateIndex
|
|
) {
|
|
lastAssistantTextIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Apply rating only to the last assistant text message after workflow-updated
|
|
return messages.map((message, index) => {
|
|
if (
|
|
message.type === 'text' &&
|
|
message.role === 'assistant' &&
|
|
index === lastAssistantTextIndex
|
|
) {
|
|
return {
|
|
...message,
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
};
|
|
}
|
|
// Remove any existing rating from other messages
|
|
if (message.type === 'text' && 'showRating' in message) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { showRating, ratingStyle, ...cleanMessage } = message;
|
|
return cleanMessage;
|
|
}
|
|
return message;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process a single message and add it to the messages array
|
|
*/
|
|
function processSingleMessage(
|
|
messages: ChatUI.AssistantMessage[],
|
|
msg: ChatRequest.MessageResponse,
|
|
messageId: string,
|
|
): boolean {
|
|
let shouldClearThinking = false;
|
|
|
|
if (isTextMessage(msg)) {
|
|
messages.push({
|
|
id: messageId,
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: msg.text,
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage);
|
|
shouldClearThinking = true;
|
|
} else if (isWorkflowUpdatedMessage(msg)) {
|
|
messages.push({
|
|
...msg,
|
|
id: messageId,
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage);
|
|
// Don't clear thinking for workflow updates - they're just state changes
|
|
} else if (isToolMessage(msg)) {
|
|
processToolMessage(messages, msg, messageId);
|
|
} else if ('type' in msg && msg.type === 'error' && 'content' in msg) {
|
|
// Handle error messages from the API
|
|
// API sends error messages with type: 'error' and content field
|
|
messages.push({
|
|
id: messageId,
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: msg.content,
|
|
read: false,
|
|
});
|
|
shouldClearThinking = true;
|
|
}
|
|
|
|
return shouldClearThinking;
|
|
}
|
|
|
|
/**
|
|
* Process a tool message - either update existing or add new
|
|
*/
|
|
function processToolMessage(
|
|
messages: ChatUI.AssistantMessage[],
|
|
msg: ChatRequest.ToolMessage,
|
|
messageId: string,
|
|
): void {
|
|
// Use toolCallId as the message ID for consistency across updates
|
|
const toolMessageId = msg.toolCallId || messageId;
|
|
|
|
// Check if we already have this tool message
|
|
const existingIndex = msg.toolCallId
|
|
? messages.findIndex((m) => m.type === 'tool' && m.toolCallId === msg.toolCallId)
|
|
: -1;
|
|
|
|
if (existingIndex !== -1) {
|
|
// Update existing tool message - merge updates array
|
|
const existing = messages[existingIndex] as ChatUI.ToolMessage;
|
|
const toolMessage: ChatUI.ToolMessage = {
|
|
...existing,
|
|
status: msg.status,
|
|
updates: [...(existing.updates || []), ...(msg.updates || [])],
|
|
};
|
|
messages[existingIndex] = toolMessage as ChatUI.AssistantMessage;
|
|
} else {
|
|
// Add new tool message
|
|
const toolMessage: ChatUI.AssistantMessage = {
|
|
id: toolMessageId,
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: msg.toolName,
|
|
toolCallId: msg.toolCallId,
|
|
status: msg.status,
|
|
updates: msg.updates || [],
|
|
read: false,
|
|
};
|
|
messages.push(toolMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If any tools are running, then it's still running tools and not done thinking
|
|
* If all tools are done and no text response yet, then it's still thinking
|
|
* Otherwise, it's done
|
|
*
|
|
* @param messages
|
|
* @returns
|
|
*/
|
|
function getThinkingState(messages: ChatUI.AssistantMessage[]): {
|
|
hasAnyRunningTools: boolean;
|
|
isStillThinking: boolean;
|
|
} {
|
|
const allToolMessages = messages.filter(
|
|
(msg): msg is ChatUI.ToolMessage => msg.type === 'tool',
|
|
);
|
|
const hasAnyRunningTools = allToolMessages.some((msg) => msg.status === 'running');
|
|
if (hasAnyRunningTools) {
|
|
return {
|
|
hasAnyRunningTools: true,
|
|
isStillThinking: false,
|
|
};
|
|
}
|
|
|
|
const hasCompletedTools = allToolMessages.some((msg) => msg.status === 'completed');
|
|
|
|
// Find the last completed tool message
|
|
let lastCompletedToolIndex = -1;
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
const msg = messages[i];
|
|
if (msg.type === 'tool' && msg.status === 'completed') {
|
|
lastCompletedToolIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if there's any text message after the last completed tool
|
|
// Note: workflow-updated messages shouldn't count as they're just canvas state updates
|
|
let hasTextAfterTools = false;
|
|
if (lastCompletedToolIndex !== -1) {
|
|
for (let i = lastCompletedToolIndex + 1; i < messages.length; i++) {
|
|
const msg = messages[i];
|
|
if (msg.type === 'text') {
|
|
hasTextAfterTools = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasAnyRunningTools: false,
|
|
isStillThinking: hasCompletedTools && !hasTextAfterTools,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine the thinking message based on tool states
|
|
*/
|
|
function determineThinkingMessage(messages: ChatUI.AssistantMessage[]): string | undefined {
|
|
const { hasAnyRunningTools, isStillThinking } = getThinkingState(messages);
|
|
|
|
if (hasAnyRunningTools) {
|
|
return locale.baseText('aiAssistant.thinkingSteps.runningTools');
|
|
} else if (isStillThinking) {
|
|
return locale.baseText('aiAssistant.thinkingSteps.processingResults');
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function processAssistantMessages(
|
|
currentMessages: ChatUI.AssistantMessage[],
|
|
newMessages: ChatRequest.MessageResponse[],
|
|
baseId: string,
|
|
): MessageProcessingResult {
|
|
const mutableMessages = [...currentMessages];
|
|
let shouldClearThinking = false;
|
|
|
|
newMessages.forEach((msg, index) => {
|
|
// Generate unique ID for each message in the batch
|
|
const messageId = `${baseId}-${index}`;
|
|
const clearThinking = processSingleMessage(mutableMessages, msg, messageId);
|
|
shouldClearThinking = shouldClearThinking || clearThinking;
|
|
});
|
|
|
|
const thinkingMessage = determineThinkingMessage(mutableMessages);
|
|
|
|
// Apply rating logic only to messages after workflow-updated
|
|
const finalMessages = applyRatingLogic(mutableMessages);
|
|
|
|
return {
|
|
messages: finalMessages,
|
|
thinkingMessage,
|
|
shouldClearThinking: shouldClearThinking && mutableMessages.length > currentMessages.length,
|
|
};
|
|
}
|
|
|
|
function createUserMessage(content: string, id: string): ChatUI.AssistantMessage {
|
|
return {
|
|
id,
|
|
role: 'user',
|
|
type: 'text',
|
|
content,
|
|
read: true,
|
|
};
|
|
}
|
|
|
|
function createAssistantMessage(content: string, id: string): ChatUI.AssistantMessage {
|
|
return {
|
|
id,
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content,
|
|
read: true,
|
|
};
|
|
}
|
|
|
|
function createErrorMessage(
|
|
content: string,
|
|
id: string,
|
|
retry?: () => Promise<void>,
|
|
): ChatUI.AssistantMessage {
|
|
return {
|
|
id,
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content,
|
|
retry,
|
|
read: false,
|
|
};
|
|
}
|
|
|
|
function clearMessages(): ChatUI.AssistantMessage[] {
|
|
return [];
|
|
}
|
|
|
|
function addMessages(
|
|
currentMessages: ChatUI.AssistantMessage[],
|
|
newMessages: ChatUI.AssistantMessage[],
|
|
): ChatUI.AssistantMessage[] {
|
|
return [...currentMessages, ...newMessages];
|
|
}
|
|
|
|
function mapAssistantMessageToUI(
|
|
message: ChatRequest.MessageResponse,
|
|
id: string,
|
|
): ChatUI.AssistantMessage {
|
|
// Handle specific message types using type guards
|
|
if (isTextMessage(message)) {
|
|
return {
|
|
id,
|
|
role: message.role ?? 'assistant',
|
|
type: 'text',
|
|
content: message.text,
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage;
|
|
}
|
|
|
|
if (isWorkflowUpdatedMessage(message)) {
|
|
return {
|
|
...message,
|
|
id,
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage;
|
|
}
|
|
|
|
if (isToolMessage(message)) {
|
|
return {
|
|
id,
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: message.toolName,
|
|
toolCallId: message.toolCallId,
|
|
status: message.status,
|
|
updates: message.updates || [],
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage;
|
|
}
|
|
|
|
// Handle event messages
|
|
if ('type' in message && message.type === 'event') {
|
|
return {
|
|
...message,
|
|
id,
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage;
|
|
}
|
|
|
|
// Default fallback
|
|
return {
|
|
id,
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: locale.baseText('aiAssistant.thinkingSteps.thinking'),
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage;
|
|
}
|
|
|
|
return {
|
|
processAssistantMessages,
|
|
createUserMessage,
|
|
createAssistantMessage,
|
|
createErrorMessage,
|
|
clearMessages,
|
|
addMessages,
|
|
mapAssistantMessageToUI,
|
|
applyRatingLogic,
|
|
clearRatingLogic,
|
|
};
|
|
}
|