273 lines
No EOL
12 KiB
TypeScript
273 lines
No EOL
12 KiB
TypeScript
import React from "react";
|
||
import { useFileDownload } from "../../../hooks/useWorkflows";
|
||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
|
||
|
||
interface MessageItemProps {
|
||
message: Message;
|
||
index: number;
|
||
onFilePreview?: (file: any) => void;
|
||
}
|
||
|
||
// Helper function to format file size
|
||
const formatFileSize = (bytes?: number): string => {
|
||
if (!bytes) return '';
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||
};
|
||
|
||
|
||
// Helper function to get file icon based on type or extension
|
||
const getFileIcon = (type?: string, ext?: string): string => {
|
||
// Use extension first if available, then fall back to MIME type
|
||
const extension = ext?.toLowerCase();
|
||
const mimeType = type?.toLowerCase();
|
||
|
||
// Check extension first
|
||
if (extension) {
|
||
// Images
|
||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
|
||
// Videos
|
||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥';
|
||
// Audio
|
||
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵';
|
||
// Documents
|
||
if (extension === 'pdf') return '📕';
|
||
if (['doc', 'docx'].includes(extension)) return '📘';
|
||
if (['xls', 'xlsx'].includes(extension)) return '📊';
|
||
if (['ppt', 'pptx'].includes(extension)) return '📋';
|
||
if (['txt', 'md', 'rtf'].includes(extension)) return '📝';
|
||
// Archives
|
||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
|
||
// Code files
|
||
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻';
|
||
}
|
||
|
||
// Fall back to MIME type if extension didn't match
|
||
if (mimeType) {
|
||
if (mimeType.includes('image')) return '🖼️';
|
||
if (mimeType.includes('video')) return '🎥';
|
||
if (mimeType.includes('audio')) return '🎵';
|
||
if (mimeType.includes('pdf')) return '📕';
|
||
if (mimeType.includes('text')) return '📝';
|
||
if (mimeType.includes('word') || mimeType.includes('document')) return '📘';
|
||
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
|
||
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋';
|
||
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
|
||
}
|
||
|
||
return '📄';
|
||
};
|
||
|
||
const MessageItem: React.FC<MessageItemProps> = ({ message, onFilePreview }) => {
|
||
const { downloadFile, isDownloading } = useFileDownload();
|
||
|
||
// Debug: Log what the MessageItem is receiving
|
||
console.log(`🎭 MessageItem rendering:`, {
|
||
messageId: message.id,
|
||
messageRole: message.role,
|
||
content: message.content?.substring(0, 50) + (message.content?.length > 50 ? '...' : ''),
|
||
contentLength: message.content?.length || 0,
|
||
hasContent: !!message.content,
|
||
hasDocuments: !!(message.documents),
|
||
documentsArray: message.documents,
|
||
documentsLength: message.documents?.length || 0,
|
||
documentsCheck: message.documents && message.documents.length > 0,
|
||
// Timestamp debugging
|
||
timestamp: message.timestamp,
|
||
hasTimestamp: !!message.timestamp,
|
||
timestampType: typeof message.timestamp,
|
||
|
||
});
|
||
|
||
const handleDocumentClick = (document: Document) => {
|
||
console.log(`🖱️ Document clicked:`, document);
|
||
// If there's a downloadUrl, use it; otherwise try the url
|
||
const downloadLink = document.downloadUrl || document.url;
|
||
|
||
if (downloadLink) {
|
||
// Open the document in a new tab
|
||
window.open(downloadLink, '_blank');
|
||
}
|
||
};
|
||
|
||
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
|
||
console.log(`👁️ Preview requested for:`, document);
|
||
|
||
// Use fileId if available, otherwise try to use id as fallback
|
||
const fileId = document.fileId || parseInt(document.id || '0');
|
||
|
||
if (!fileId || isNaN(fileId)) {
|
||
console.error('❌ Invalid file ID for preview:', document);
|
||
return;
|
||
}
|
||
|
||
console.log('✅ MessageItem - Previewing file:', { fileId, document });
|
||
|
||
// Call the parent callback to show preview in the file preview quadrant
|
||
if (onFilePreview) {
|
||
onFilePreview({
|
||
id: fileId.toString(),
|
||
name: document.name,
|
||
mimeType: document.type || 'application/octet-stream',
|
||
size: document.size,
|
||
fileId: fileId
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
|
||
console.log(`⬇️ Download requested for:`, document);
|
||
|
||
// Use fileId if available, otherwise try to use id as fallback
|
||
const fileId = document.fileId || parseInt(document.id || '0');
|
||
|
||
if (!fileId) {
|
||
console.error('❌ No file ID for download:', document);
|
||
return;
|
||
}
|
||
|
||
// Construct filename with extension if available
|
||
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
||
|
||
console.log(`💾 Downloading file ${fileId} as "${fileName}"`);
|
||
await downloadFile(fileId, fileName);
|
||
};
|
||
|
||
// Debug: Log document check before rendering
|
||
const hasDocuments = message.documents && message.documents.length > 0;
|
||
console.log(`🔍 About to check documents:`, {
|
||
hasDocuments: !!(message.documents),
|
||
documentsLength: message.documents?.length || 0,
|
||
willRenderFiles: hasDocuments
|
||
});
|
||
|
||
// Log if no documents
|
||
if (!hasDocuments) {
|
||
console.log(`📭 No documents to render for message ${message.id}`);
|
||
}
|
||
|
||
// Format timestamp
|
||
const formatTimestamp = (timestamp?: string) => {
|
||
console.log(`⏰ formatTimestamp called with:`, { timestamp, type: typeof timestamp, hasValue: !!timestamp });
|
||
|
||
if (!timestamp) {
|
||
console.log(`⏰ No timestamp provided, returning empty string`);
|
||
return '';
|
||
}
|
||
|
||
const date = new Date(timestamp);
|
||
console.log(`⏰ Parsed date:`, { date, isValid: !isNaN(date.getTime()) });
|
||
|
||
if (isNaN(date.getTime())) {
|
||
console.log(`⏰ Invalid date, returning empty string`);
|
||
return '';
|
||
}
|
||
|
||
const now = new Date();
|
||
const isToday = date.toDateString() === now.toDateString();
|
||
|
||
let formatted = '';
|
||
if (isToday) {
|
||
formatted = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
} else {
|
||
formatted = date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' +
|
||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
console.log(`⏰ Formatted timestamp:`, { formatted });
|
||
return formatted;
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`${messageStyles.message_item} ${
|
||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||
}`}
|
||
>
|
||
<div className={messageStyles.message_header}>
|
||
{message.role === 'user' ? 'You' : message.agentName}
|
||
{message.timestamp && (
|
||
<span className={messageStyles.message_timestamp_inline}>
|
||
• {formatTimestamp(message.timestamp)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className={`${messageStyles.message_bubble} ${
|
||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||
}`}>
|
||
<div className={messageStyles.message_content}>
|
||
{message.content || (
|
||
<span className={messageStyles.message_no_content}>
|
||
[No message content]
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{hasDocuments && (
|
||
<div className={`${messageStyles.message_documents} ${
|
||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||
}`}>
|
||
<div className={`${messageStyles.message_documents_header} ${
|
||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||
}`}>
|
||
📎 {message.role === 'user' ? 'Uploaded' : 'Attached'} Files ({message.documents!.length})
|
||
</div>
|
||
<div>
|
||
{message.documents!.map((document, docIndex) => {
|
||
console.log(`📄 Rendering document ${docIndex + 1}:`, document);
|
||
return (
|
||
<div
|
||
key={document.id || docIndex}
|
||
className={messageStyles.message_document_item}
|
||
onClick={() => handleDocumentClick(document)}
|
||
title={`Click to open ${document.name}`}
|
||
>
|
||
<span className={messageStyles.message_document_icon}>
|
||
{getFileIcon(document.type, document.ext)}
|
||
</span>
|
||
<div className={messageStyles.message_document_info}>
|
||
<div className={messageStyles.message_document_name}>
|
||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||
</div>
|
||
{document.size && (
|
||
<div className={messageStyles.message_document_size}>
|
||
{formatFileSize(document.size)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className={messageStyles.message_document_actions}>
|
||
<button
|
||
onClick={(e) => handlePreview(document, e)}
|
||
className={messageStyles.message_document_action}
|
||
title="Preview file"
|
||
>
|
||
👁️
|
||
</button>
|
||
<button
|
||
onClick={(e) => handleDownload(document, e)}
|
||
disabled={isDownloading}
|
||
className={messageStyles.message_document_action}
|
||
title="Download file"
|
||
>
|
||
⬇️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MessageItem;
|