frontend_nyla/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx
2025-08-20 16:57:41 +02:00

273 lines
No EOL
12 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 { 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;