From 28f0293adae06245d239208a356ee87a94c797ed Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Wed, 20 Aug 2025 16:57:41 +0200 Subject: [PATCH] fetching logs --- .../DashboardChatAreaLogItem.tsx | 170 +++++++++ .../DashboardChatAreaMessageItem.tsx | 4 +- .../DashboardChatAreaMessageList.tsx | 328 ++++++++++++++++-- .../DashboardChatMessages.module.css | 181 +++++++++- .../DashboardChat/dashboardChatAreaTypes.ts | 4 + .../DashboardChat/useWorkflowManager.ts | 26 +- src/hooks/useWorkflows.ts | 51 ++- 7 files changed, 710 insertions(+), 54 deletions(-) create mode 100644 src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx new file mode 100644 index 0000000..3901bcc --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx @@ -0,0 +1,170 @@ +import React, { useState } from "react"; +import { WorkflowLog } from "./dashboardChatAreaTypes"; +import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css'; + +interface LogItemProps { + log: WorkflowLog; + index: number; +} + +const LogItem: React.FC = ({ log }) => { + const [showDetails, setShowDetails] = useState(false); + + // Format timestamp with robust parsing (same logic as MessageList) + const formatTimestamp = (timestamp: any) => { + console.log(`⏰ LogItem formatTimestamp called with:`, { + timestamp, + type: typeof timestamp, + hasValue: !!timestamp + }); + + if (!timestamp) { + console.log(`⏰ LogItem: No timestamp provided`); + return 'No timestamp'; + } + + // Handle different timestamp formats (same as safeParseDate) + let dateToTry = timestamp; + + // If it's a number, check if it's in seconds or milliseconds + if (typeof timestamp === 'number') { + // If it's a 10-digit number, it's likely seconds since epoch + if (timestamp < 10000000000) { + dateToTry = timestamp * 1000; // Convert seconds to milliseconds + } else { + dateToTry = timestamp; // Already in milliseconds + } + } + // If it's a string that looks like a number, parse it and handle seconds/milliseconds + else if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) { + const numericTimestamp = parseInt(timestamp); + // If it's a 10-digit number, it's likely seconds since epoch + if (numericTimestamp < 10000000000) { + dateToTry = numericTimestamp * 1000; // Convert seconds to milliseconds + } else { + dateToTry = numericTimestamp; // Already in milliseconds + } + } + // If it's already a Date object + else if (timestamp instanceof Date) { + dateToTry = timestamp; + } + + const date = new Date(dateToTry); + + // Check if the date is valid + if (isNaN(date.getTime())) { + console.warn(`⚠️ LogItem: Invalid timestamp detected:`, { + originalTimestamp: timestamp, + type: typeof timestamp, + processedTimestamp: dateToTry + }); + return `Invalid: ${timestamp}`; + } + + console.log(`✅ LogItem: Successfully parsed timestamp:`, { + original: timestamp, + originalType: typeof timestamp, + processed: dateToTry, + wasConverted: dateToTry !== timestamp, + parsed: date.toISOString() + }); + + 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(`⏰ LogItem: Formatted timestamp:`, { formatted }); + return formatted; + }; + + // Determine log level for styling + const logLevel = log.level || (log.type?.toLowerCase() as 'info' | 'warning' | 'error' | 'debug') || 'info'; + + // Debug: Log what the LogItem is receiving + console.log(`📋 LogItem rendering:`, { + logId: log.id, + logType: log.type, + logLevel: logLevel, + message: log.message?.substring(0, 50) + (log.message?.length > 50 ? '...' : ''), + timestamp: log.timestamp, + timestampType: typeof log.timestamp, + fullLogObject: log, + allLogKeys: Object.keys(log), + hasDetails: !!(log.details || log.performance) + }); + + return ( +
+
+ + {logLevel.toUpperCase()} + + + {formatTimestamp(log.timestamp)} + +
+ +
+ {log.message} +
+ + {log.source && ( +
+ Source: {log.source} +
+ )} + + {log.progress !== undefined && log.progress >= 0 && ( +
+ Progress: {log.progress}% +
+ )} + + {(log.details || log.performance) && ( + <> + + + {showDetails && ( +
+ {log.details && ( +
+ Details: +
{JSON.stringify(log.details, null, 2)}
+
+ )} + {log.performance && ( +
+ Performance: +
{JSON.stringify(log.performance, null, 2)}
+
+ )} +
+ )} + + )} +
+ ); +}; + +export default LogItem; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx index 67dd908..a5f8fd1 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx @@ -17,6 +17,7 @@ const formatFileSize = (bytes?: number): string => { 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 @@ -76,7 +77,8 @@ const MessageItem: React.FC = ({ message, onFilePreview }) => // Timestamp debugging timestamp: message.timestamp, hasTimestamp: !!message.timestamp, - timestampType: typeof message.timestamp + timestampType: typeof message.timestamp, + }); const handleDocumentClick = (document: Document) => { diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx index 61760d8..196cf6c 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx @@ -1,9 +1,223 @@ import React from 'react'; import { useApiRequest } from '../../../hooks/useApi'; import MessageItem from './DashboardChatAreaMessageItem'; -import { WorkflowMessage, Document, WorkflowState } from './dashboardChatAreaTypes'; +import LogItem from './DashboardChatAreaLogItem'; +import { WorkflowMessage, Document, WorkflowState, WorkflowLog } from './dashboardChatAreaTypes'; import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css'; +// Helper function to parse task progress from message content first line +const parseTaskProgress = (content: string): { current: number; total: number; percentage: number } | null => { + // Get the first line of the message + const firstLine = content.split('\n')[0].trim(); + + // Look for patterns like "Starting Task 1/5", "Task 2/10 Completed Successfully!", etc. + const taskPattern = /(?:Starting\s+)?Task\s+(\d+)\/(\d+)/i; + const match = firstLine.match(taskPattern); + + if (match) { + const current = parseInt(match[1]); + const total = parseInt(match[2]); + const percentage = Math.round((current / total) * 100); + + console.log(`📊 Parsed task progress from first line:`, { + firstLine, + current, + total, + percentage, + originalText: match[0] + }); + return { current, total, percentage }; + } + + return null; +}; + +// Helper function to analyze workflow progress from all messages +const analyzeWorkflowProgress = (messages: any[]): { + current: number; + total: number; + percentage: number; + isLoading: boolean; +} | null => { + if (messages.length === 0) return null; + + // Check if the last message is from user (indicating we're waiting for assistant response) + const lastMessage = messages[messages.length - 1]; + const isWaitingForAssistant = lastMessage?.role === 'user'; + + let latestProgress: { current: number; total: number; percentage: number } | null = null; + + // Go through messages in reverse order (latest first) to find the most recent progress + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (message.role === 'assistant' && message.content) { + const progress = parseTaskProgress(message.content); + if (progress) { + latestProgress = progress; + break; // Use the most recent progress found + } + } + } + + console.log(`🔄 Analyzed workflow progress:`, { + totalMessages: messages.length, + lastMessageRole: lastMessage?.role, + isWaitingForAssistant, + latestProgress + }); + + // If we're waiting for assistant response after a user message, show loading state + if (isWaitingForAssistant) { + return { + current: 0, + total: 0, + percentage: 0, + isLoading: true + }; + } + + // If we have progress and we're not waiting, show the progress + if (latestProgress) { + return { + ...latestProgress, + isLoading: false + }; + } + + return null; +}; + +// Helper function to safely parse timestamps +const safeParseDate = (timestamp: any, fallback: number = Date.now()): Date => { + if (!timestamp) { + console.warn(`⚠️ No timestamp provided, using fallback:`, { fallback }); + return new Date(fallback); + } + + // Handle different timestamp formats + let dateToTry = timestamp; + + // If it's a number, check if it's in seconds or milliseconds + if (typeof timestamp === 'number') { + // If it's a 10-digit number, it's likely seconds since epoch + if (timestamp < 10000000000) { + dateToTry = timestamp * 1000; // Convert seconds to milliseconds + } else { + dateToTry = timestamp; // Already in milliseconds + } + } + // If it's a string that looks like a number, parse it and handle seconds/milliseconds + else if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) { + const numericTimestamp = parseInt(timestamp); + // If it's a 10-digit number, it's likely seconds since epoch + if (numericTimestamp < 10000000000) { + dateToTry = numericTimestamp * 1000; // Convert seconds to milliseconds + } else { + dateToTry = numericTimestamp; // Already in milliseconds + } + } + // If it's already a Date object + else if (timestamp instanceof Date) { + dateToTry = timestamp; + } + + // Try to parse the timestamp + const date = new Date(dateToTry); + + // Check if the date is valid + if (isNaN(date.getTime())) { + console.warn(`⚠️ Invalid timestamp detected:`, { + originalTimestamp: timestamp, + type: typeof timestamp, + processedTimestamp: dateToTry, + fallback + }); + return new Date(fallback); + } + + console.log(`✅ Successfully parsed timestamp:`, { + original: timestamp, + originalType: typeof timestamp, + processed: dateToTry, + wasConverted: dateToTry !== timestamp, + parsed: date.toISOString() + }); + + return date; +}; + +// Helper function to merge and sort messages and logs by timestamp +const mergeMessagesAndLogs = (messages: any[], logs: WorkflowLog[]): Array<{type: 'message' | 'log', item: any, timestamp: Date}> => { + const combined: Array<{type: 'message' | 'log', item: any, timestamp: Date}> = []; + + console.log(`🔄 Starting merge process:`, { + messagesCount: messages.length, + logsCount: logs.length, + firstMessage: messages[0], + firstLog: logs[0] + }); + + // Add messages + messages.forEach((message, index) => { + const rawTimestamp = message.timestamp || message.publishedAt; + + console.log(`📨 Processing message ${index + 1}/${messages.length}:`, { + messageId: message.id, + rawTimestamp, + timestampType: typeof rawTimestamp + }); + + // Use current time minus index for fallback to maintain order + const fallbackTime = Date.now() - ((messages.length + logs.length) - index) * 1000; + const timestamp = safeParseDate(rawTimestamp, fallbackTime); + + combined.push({ + type: 'message', + item: message, + timestamp + }); + }); + + // Add logs + logs.forEach((log, index) => { + console.log(`📋 Processing log ${index + 1}/${logs.length}:`, { + logId: log.id, + rawTimestamp: log.timestamp, + timestampType: typeof log.timestamp, + logObject: log + }); + + // Use current time minus index for fallback to maintain order + const fallbackTime = Date.now() - ((logs.length) - index) * 1000; + const timestamp = safeParseDate(log.timestamp, fallbackTime); + + combined.push({ + type: 'log', + item: log, + timestamp + }); + }); + + // Sort by timestamp (chronological order) + combined.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); + + console.log(`🔄 Final merged timeline:`, { + totalMessages: messages.length, + totalLogs: logs.length, + totalCombined: combined.length, + sortedTimeline: combined.map((item, index) => ({ + index, + type: item.type, + timestamp: item.timestamp.toISOString(), + content: item.type === 'message' ? + item.item.content?.substring(0, 30) + '...' : + item.item.message?.substring(0, 30) + '...' + })) + }); + + return combined; +}; + interface MessageListProps { workflowState: WorkflowState; onFilePreview?: (file: any) => void; @@ -129,6 +343,16 @@ const MessageList: React.FC = ({ const scrollContainerRef = React.useRef(null); const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false); const lastMessageCountRef = React.useRef(0); + + // Analyze workflow progress from all messages + const workflowProgress = React.useMemo(() => { + return analyzeWorkflowProgress(transformedMessages); + }, [transformedMessages]); + + // Create merged timeline of messages and logs + const timeline = React.useMemo(() => { + return mergeMessagesAndLogs(transformedMessages, workflowState.logs || []); + }, [transformedMessages, workflowState.logs]); // Transform messages when workflow messages change React.useEffect(() => { @@ -206,28 +430,28 @@ const MessageList: React.FC = ({ } }, []); - // Auto-scroll when new messages arrive (only if user is near bottom) + // Auto-scroll when new items arrive (only if user is near bottom) React.useEffect(() => { - const currentMessageCount = transformedMessages.length; - const hadMessages = lastMessageCountRef.current > 0; - const hasNewMessages = currentMessageCount > lastMessageCountRef.current; + const currentTimelineCount = timeline.length; + const hadItems = lastMessageCountRef.current > 0; + const hasNewItems = currentTimelineCount > lastMessageCountRef.current; - if (hasNewMessages && hadMessages && !isUserScrolledUp) { - console.log('🆕 New messages detected, auto-scrolling to bottom'); + if (hasNewItems && hadItems && !isUserScrolledUp) { + console.log('🆕 New timeline items detected, auto-scrolling to bottom'); // Small delay to ensure DOM is updated setTimeout(scrollToBottom, 100); } - lastMessageCountRef.current = currentMessageCount; - }, [transformedMessages.length, isUserScrolledUp, scrollToBottom]); + lastMessageCountRef.current = currentTimelineCount; + }, [timeline.length, isUserScrolledUp, scrollToBottom]); // Scroll to bottom on initial load React.useEffect(() => { - if (transformedMessages.length > 0 && lastMessageCountRef.current === 0) { + if (timeline.length > 0 && lastMessageCountRef.current === 0) { console.log('📜 Initial load, scrolling to bottom'); setTimeout(scrollToBottom, 100); } - }, [transformedMessages.length, scrollToBottom]); + }, [timeline.length, scrollToBottom]); const { currentWorkflowId, isLoading, error } = workflowState; @@ -246,22 +470,41 @@ const MessageList: React.FC = ({
- {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 ( - - ); + {timeline.map((timelineItem, index) => { + if (timelineItem.type === 'message') { + const message = timelineItem.item; + console.log(`🎨 Rendering timeline message ${message.id}:`, { + role: message.role, + contentLength: message.content?.length || 0, + hasContent: !!message.content, + documentsCount: message.documents?.length || 0 + }); + + return ( + + ); + } else if (timelineItem.type === 'log') { + const log = timelineItem.item; + console.log(`📋 Rendering timeline log ${log.id}:`, { + logLevel: log.level || log.type, + message: log.message?.substring(0, 50), + timestamp: log.timestamp + }); + + return ( + + ); + } + return null; })}
@@ -271,24 +514,47 @@ const MessageList: React.FC = ({ )} - {transformedMessages.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && ( + {timeline.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
No workflow selected. Start a conversation to create a new workflow.
)} - {transformedMessages.length === 0 && !isLoading && !isTransforming && currentWorkflowId && ( + {timeline.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
- No messages in this workflow yet. + No messages or logs in this workflow yet.
)} + {/* Workflow Progress Bar - positioned outside scrollable area */} + {workflowProgress && ( +
+
+ Workflow Progress + + {workflowProgress.isLoading + ? 'Loading tasks...' + : `${workflowProgress.current}/${workflowProgress.total} Tasks (${workflowProgress.percentage}%)` + } + +
+
+
+
+
+ )} + {/* Scroll to bottom button - positioned relative to message list container */}