304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
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;
|