ui-nyla/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx

304 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { useApiRequest } from '../../../hooks/useApi';
import MessageItem from './DashboardChatAreaMessageItem';
import { WorkflowMessage, Document, WorkflowState } from './dashboardChatAreaTypes';
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
interface MessageListProps {
workflowState: WorkflowState;
onFilePreview?: (file: any) => void;
}
// Helper function to transform WorkflowMessage to display Message
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
let documents: Document[] = [];
// Check if we have documents directly from the API or need to fetch via fileIds
if (workflowMessage.documents && workflowMessage.documents.length > 0) {
// Use documents directly from the API
documents = workflowMessage.documents.map(doc => ({
id: doc.id || doc.fileId,
fileId: typeof doc.fileId === 'string' ? parseInt(doc.fileId) : doc.fileId,
name: doc.filename,
ext: doc.filename.split('.').pop() || 'unknown',
type: doc.mimeType,
size: doc.fileSize,
downloadUrl: `/api/workflows/files/${doc.fileId}/download`
}));
} else if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
// Fallback to legacy fileIds approach
console.log(`📎 Processing ${workflowMessage.fileIds.length} files for message ${workflowMessage.id}`);
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
try {
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
const response = await request({
url: `/api/workflows/files/${fileId}/preview`,
method: 'get'
});
const document: Document = {
id: fileId.toString(),
fileId: fileId,
name: response.name || response.fileName || `File_${fileId}`,
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
type: response.mimeType || response.type || 'application/octet-stream',
size: response.size || 0,
downloadUrl: response.downloadUrl || response.url
};
console.log(`✅ File ${fileId} metadata processed:`, document.name);
return document;
} catch (error) {
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
// Return a fallback object for failed requests
return {
id: fileId.toString(),
fileId: fileId,
name: `File_${fileId}`,
ext: 'unknown',
type: 'application/octet-stream',
size: 0
};
}
});
documents = await Promise.all(documentPromises);
}
// Try different possible field names for content (API uses 'message', some legacy might use 'content')
const possibleContent = workflowMessage.message ||
workflowMessage.content ||
(workflowMessage as any).text ||
(workflowMessage as any).body ||
'';
const transformedMessage = {
id: workflowMessage.id,
role: workflowMessage.role,
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
content: possibleContent,
timestamp: workflowMessage.publishedAt || workflowMessage.timestamp,
documents: documents
};
console.log(`🔄 Transformation result for ${workflowMessage.id}:`, {
role: workflowMessage.role,
originalMessage: workflowMessage,
originalContent: workflowMessage.content,
originalContentType: typeof workflowMessage.content,
possibleContent: possibleContent,
possibleContentType: typeof possibleContent,
transformedContent: transformedMessage.content,
transformedContentType: typeof transformedMessage.content,
documentsCount: documents.length,
hasDocuments: documents.length > 0,
// Timestamp debugging
publishedAt: workflowMessage.publishedAt,
timestamp: workflowMessage.timestamp,
finalTimestamp: transformedMessage.timestamp,
hasTimestamp: !!transformedMessage.timestamp,
allOriginalKeys: Object.keys(workflowMessage),
// Check for alternative field names
alternativeFields: {
message: (workflowMessage as any).message,
text: (workflowMessage as any).text,
body: (workflowMessage as any).body,
data: (workflowMessage as any).data
}
});
// Special logging for user messages with documents
if (workflowMessage.role === 'user' && documents.length > 0) {
console.log(`👤📎 USER message with ${documents.length} documents:`, {
messageId: workflowMessage.id,
documents: documents.map(doc => ({ name: doc.name, fileId: doc.fileId, type: doc.type }))
});
}
return transformedMessage;
};
const MessageList: React.FC<MessageListProps> = ({
workflowState,
onFilePreview
}) => {
const { request } = useApiRequest();
const [transformedMessages, setTransformedMessages] = React.useState<any[]>([]);
const [isTransforming, setIsTransforming] = React.useState(false);
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
const lastMessageCountRef = React.useRef(0);
// Transform messages when workflow messages change
React.useEffect(() => {
const transformMessages = async () => {
if (!workflowState.messages || workflowState.messages.length === 0) {
setTransformedMessages([]);
return;
}
setIsTransforming(true);
console.log(`🔄 Transforming ${workflowState.messages.length} workflow messages...`);
try {
const transformed = await Promise.all(
workflowState.messages.map(async (msg: WorkflowMessage, index: number) => {
console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`);
const content = msg.message || msg.content || '';
console.log(`📝 RAW API Message (${msg.role}):`, {
id: msg.id,
rawMessage: msg,
contentType: typeof content,
contentValue: content,
contentLength: content.length,
hasContent: !!content,
contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
fileCount: msg.fileIds?.length || 0,
allKeys: Object.keys(msg)
});
return await transformWorkflowMessage(msg, request);
})
);
console.log(`✅ Successfully transformed ${transformed.length} messages`);
setTransformedMessages(transformed);
} catch (error) {
console.error('❌ Error transforming messages:', error);
setTransformedMessages([]);
} finally {
setIsTransforming(false);
}
};
transformMessages();
}, [workflowState.messages, request]);
// Check if user is scrolled near the bottom
const checkScrollPosition = React.useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
// Consider "near bottom" if within 100px of the bottom
const isNearBottom = distanceFromBottom < 100;
setIsUserScrolledUp(!isNearBottom);
console.log('📏 Scroll position:', {
scrollTop,
scrollHeight,
clientHeight,
distanceFromBottom,
isNearBottom,
isUserScrolledUp: !isNearBottom
});
}, []);
// Scroll to bottom function
const scrollToBottom = React.useCallback(() => {
const container = scrollContainerRef.current;
if (container) {
console.log('⬇️ Auto-scrolling to bottom');
container.scrollTop = container.scrollHeight;
}
}, []);
// Auto-scroll when new messages arrive (only if user is near bottom)
React.useEffect(() => {
const currentMessageCount = transformedMessages.length;
const hadMessages = lastMessageCountRef.current > 0;
const hasNewMessages = currentMessageCount > lastMessageCountRef.current;
if (hasNewMessages && hadMessages && !isUserScrolledUp) {
console.log('🆕 New messages detected, auto-scrolling to bottom');
// Small delay to ensure DOM is updated
setTimeout(scrollToBottom, 100);
}
lastMessageCountRef.current = currentMessageCount;
}, [transformedMessages.length, isUserScrolledUp, scrollToBottom]);
// Scroll to bottom on initial load
React.useEffect(() => {
if (transformedMessages.length > 0 && lastMessageCountRef.current === 0) {
console.log('📜 Initial load, scrolling to bottom');
setTimeout(scrollToBottom, 100);
}
}, [transformedMessages.length, scrollToBottom]);
const { currentWorkflowId, isLoading, error } = workflowState;
return (
<div className={messageStyles.message_list_container}>
<div
ref={scrollContainerRef}
className={messageStyles.chat_messages}
onScroll={checkScrollPosition}
>
{error && (
<div className={messageStyles.message_error}>
Error: {error}
</div>
)}
<div className={messageStyles.messages_container}>
{transformedMessages.map((message, index) => {
console.log(`🎨 Rendering transformed message ${message.id}:`, {
role: message.role,
contentLength: message.content?.length || 0,
hasContent: !!message.content,
documentsCount: message.documents?.length || 0
});
return (
<MessageItem
key={message.id}
message={message}
index={index}
onFilePreview={onFilePreview}
/>
);
})}
</div>
{(isLoading || isTransforming) && (
<div className={messageStyles.message_loading}>
{isTransforming ? 'Processing messages...' : 'Loading messages...'}
</div>
)}
{transformedMessages.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
<div className={messageStyles.message_empty_state}>
No workflow selected. Start a conversation to create a new workflow.
</div>
)}
{transformedMessages.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
<div className={messageStyles.message_empty_state}>
No messages in this workflow yet.
</div>
)}
</div>
{/* Scroll to bottom button - positioned relative to message list container */}
<button
className={`${messageStyles.scroll_to_bottom_btn} ${
(isUserScrolledUp && transformedMessages.length > 0)
? messageStyles.visible
: messageStyles.hidden
}`}
onClick={scrollToBottom}
title="Scroll to bottom"
>
</button>
</div>
);
};
export default MessageList;