fetching logs
This commit is contained in:
parent
1e46d8f93e
commit
28f0293ada
7 changed files with 710 additions and 54 deletions
|
|
@ -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<LogItemProps> = ({ 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 (
|
||||||
|
<div className={`${messageStyles.log_container} ${messageStyles[logLevel]}`}>
|
||||||
|
<div className={messageStyles.log_header}>
|
||||||
|
<span className={`${messageStyles.log_level} ${messageStyles[logLevel]}`}>
|
||||||
|
{logLevel.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className={messageStyles.log_timestamp}>
|
||||||
|
{formatTimestamp(log.timestamp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={messageStyles.log_message}>
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{log.source && (
|
||||||
|
<div className={messageStyles.log_source}>
|
||||||
|
Source: {log.source}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{log.progress !== undefined && log.progress >= 0 && (
|
||||||
|
<div className={messageStyles.log_progress}>
|
||||||
|
Progress: {log.progress}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(log.details || log.performance) && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: 'var(--color-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '11px',
|
||||||
|
padding: '4px 0',
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showDetails ? 'Hide Details' : 'Show Details'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showDetails && (
|
||||||
|
<div className={messageStyles.log_details}>
|
||||||
|
{log.details && (
|
||||||
|
<div>
|
||||||
|
<strong>Details:</strong>
|
||||||
|
<pre>{JSON.stringify(log.details, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{log.performance && (
|
||||||
|
<div>
|
||||||
|
<strong>Performance:</strong>
|
||||||
|
<pre>{JSON.stringify(log.performance, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogItem;
|
||||||
|
|
@ -17,6 +17,7 @@ const formatFileSize = (bytes?: number): string => {
|
||||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Helper function to get file icon based on type or extension
|
// Helper function to get file icon based on type or extension
|
||||||
const getFileIcon = (type?: string, ext?: string): string => {
|
const getFileIcon = (type?: string, ext?: string): string => {
|
||||||
// Use extension first if available, then fall back to MIME type
|
// Use extension first if available, then fall back to MIME type
|
||||||
|
|
@ -76,7 +77,8 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, onFilePreview }) =>
|
||||||
// Timestamp debugging
|
// Timestamp debugging
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
hasTimestamp: !!message.timestamp,
|
hasTimestamp: !!message.timestamp,
|
||||||
timestampType: typeof message.timestamp
|
timestampType: typeof message.timestamp,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDocumentClick = (document: Document) => {
|
const handleDocumentClick = (document: Document) => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,223 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import MessageItem from './DashboardChatAreaMessageItem';
|
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';
|
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 {
|
interface MessageListProps {
|
||||||
workflowState: WorkflowState;
|
workflowState: WorkflowState;
|
||||||
onFilePreview?: (file: any) => void;
|
onFilePreview?: (file: any) => void;
|
||||||
|
|
@ -129,6 +343,16 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
|
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
|
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
|
||||||
const lastMessageCountRef = React.useRef(0);
|
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
|
// Transform messages when workflow messages change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -206,28 +430,28 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 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(() => {
|
React.useEffect(() => {
|
||||||
const currentMessageCount = transformedMessages.length;
|
const currentTimelineCount = timeline.length;
|
||||||
const hadMessages = lastMessageCountRef.current > 0;
|
const hadItems = lastMessageCountRef.current > 0;
|
||||||
const hasNewMessages = currentMessageCount > lastMessageCountRef.current;
|
const hasNewItems = currentTimelineCount > lastMessageCountRef.current;
|
||||||
|
|
||||||
if (hasNewMessages && hadMessages && !isUserScrolledUp) {
|
if (hasNewItems && hadItems && !isUserScrolledUp) {
|
||||||
console.log('🆕 New messages detected, auto-scrolling to bottom');
|
console.log('🆕 New timeline items detected, auto-scrolling to bottom');
|
||||||
// Small delay to ensure DOM is updated
|
// Small delay to ensure DOM is updated
|
||||||
setTimeout(scrollToBottom, 100);
|
setTimeout(scrollToBottom, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessageCountRef.current = currentMessageCount;
|
lastMessageCountRef.current = currentTimelineCount;
|
||||||
}, [transformedMessages.length, isUserScrolledUp, scrollToBottom]);
|
}, [timeline.length, isUserScrolledUp, scrollToBottom]);
|
||||||
|
|
||||||
// Scroll to bottom on initial load
|
// Scroll to bottom on initial load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (transformedMessages.length > 0 && lastMessageCountRef.current === 0) {
|
if (timeline.length > 0 && lastMessageCountRef.current === 0) {
|
||||||
console.log('📜 Initial load, scrolling to bottom');
|
console.log('📜 Initial load, scrolling to bottom');
|
||||||
setTimeout(scrollToBottom, 100);
|
setTimeout(scrollToBottom, 100);
|
||||||
}
|
}
|
||||||
}, [transformedMessages.length, scrollToBottom]);
|
}, [timeline.length, scrollToBottom]);
|
||||||
|
|
||||||
const { currentWorkflowId, isLoading, error } = workflowState;
|
const { currentWorkflowId, isLoading, error } = workflowState;
|
||||||
|
|
||||||
|
|
@ -246,22 +470,41 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
|
|
||||||
|
|
||||||
<div className={messageStyles.messages_container}>
|
<div className={messageStyles.messages_container}>
|
||||||
{transformedMessages.map((message, index) => {
|
{timeline.map((timelineItem, index) => {
|
||||||
console.log(`🎨 Rendering transformed message ${message.id}:`, {
|
if (timelineItem.type === 'message') {
|
||||||
role: message.role,
|
const message = timelineItem.item;
|
||||||
contentLength: message.content?.length || 0,
|
console.log(`🎨 Rendering timeline message ${message.id}:`, {
|
||||||
hasContent: !!message.content,
|
role: message.role,
|
||||||
documentsCount: message.documents?.length || 0
|
contentLength: message.content?.length || 0,
|
||||||
});
|
hasContent: !!message.content,
|
||||||
|
documentsCount: message.documents?.length || 0
|
||||||
return (
|
});
|
||||||
<MessageItem
|
|
||||||
key={message.id}
|
return (
|
||||||
message={message}
|
<MessageItem
|
||||||
index={index}
|
key={`message-${message.id}`}
|
||||||
onFilePreview={onFilePreview}
|
message={message}
|
||||||
/>
|
index={index}
|
||||||
);
|
onFilePreview={onFilePreview}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} 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 (
|
||||||
|
<LogItem
|
||||||
|
key={`log-${log.id}`}
|
||||||
|
log={log}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -271,24 +514,47 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{transformedMessages.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
|
{timeline.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
|
||||||
<div className={messageStyles.message_empty_state}>
|
<div className={messageStyles.message_empty_state}>
|
||||||
No workflow selected. Start a conversation to create a new workflow.
|
No workflow selected. Start a conversation to create a new workflow.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{transformedMessages.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
|
{timeline.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
|
||||||
<div className={messageStyles.message_empty_state}>
|
<div className={messageStyles.message_empty_state}>
|
||||||
No messages in this workflow yet.
|
No messages or logs in this workflow yet.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Workflow Progress Bar - positioned outside scrollable area */}
|
||||||
|
{workflowProgress && (
|
||||||
|
<div className={messageStyles.workflow_progress_container}>
|
||||||
|
<div className={messageStyles.workflow_progress_label}>
|
||||||
|
<span>Workflow Progress</span>
|
||||||
|
<span>
|
||||||
|
{workflowProgress.isLoading
|
||||||
|
? 'Loading tasks...'
|
||||||
|
: `${workflowProgress.current}/${workflowProgress.total} Tasks (${workflowProgress.percentage}%)`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={messageStyles.workflow_progress_bar}>
|
||||||
|
<div
|
||||||
|
className={`${messageStyles.workflow_progress_fill} ${
|
||||||
|
workflowProgress.isLoading ? messageStyles.loading : ''
|
||||||
|
}`}
|
||||||
|
style={{ width: workflowProgress.isLoading ? '0%' : `${workflowProgress.percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Scroll to bottom button - positioned relative to message list container */}
|
{/* Scroll to bottom button - positioned relative to message list container */}
|
||||||
<button
|
<button
|
||||||
className={`${messageStyles.scroll_to_bottom_btn} ${
|
className={`${messageStyles.scroll_to_bottom_btn} ${
|
||||||
(isUserScrolledUp && transformedMessages.length > 0)
|
(isUserScrolledUp && timeline.length > 0)
|
||||||
? messageStyles.visible
|
? messageStyles.visible
|
||||||
: messageStyles.hidden
|
: messageStyles.hidden
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@
|
||||||
/* Scroll to Bottom Button */
|
/* Scroll to Bottom Button */
|
||||||
.scroll_to_bottom_btn {
|
.scroll_to_bottom_btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 80px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -339,3 +339,182 @@
|
||||||
.polling_indicator {
|
.polling_indicator {
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { left: -100%; }
|
||||||
|
100% { left: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingSlide {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
50% { transform: translateX(0%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow Progress Bar (fixed at bottom, outside scrollable area) */
|
||||||
|
.workflow_progress_container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
border-top: 1px solid var(--color-gray-disabled);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow_progress_label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow_progress_bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--color-gray-disabled);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow_progress_fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--color-secondary) 0%, var(--color-secondary-hover) 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow_progress_fill.loading {
|
||||||
|
width: 100% !important;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--color-gray-disabled), transparent);
|
||||||
|
animation: loadingSlide 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced shimmer animation for workflow progress */
|
||||||
|
.workflow_progress_fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||||
|
animation: shimmer 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide shimmer during loading state */
|
||||||
|
.workflow_progress_fill.loading::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow Log Messages */
|
||||||
|
.log_container {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
border-left: 3px solid;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_container.info {
|
||||||
|
border-left-color: var(--color-secondary);
|
||||||
|
background-color: rgba(59, 130, 246, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_container.warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
background-color: rgba(245, 158, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_container.error {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background-color: rgba(239, 68, 68, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_container.debug {
|
||||||
|
border-left-color: var(--color-gray);
|
||||||
|
background-color: rgba(107, 114, 128, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_level {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_level.info {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_level.warning {
|
||||||
|
background-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_level.error {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_level.debug {
|
||||||
|
background-color: var(--color-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_timestamp {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_message {
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_source {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
margin-top: 4px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_progress {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log details (expandable) */
|
||||||
|
.log_details {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_details pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ export interface WorkflowLog {
|
||||||
status: string;
|
status: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
performance: any;
|
performance: any;
|
||||||
|
level?: 'info' | 'warning' | 'error' | 'debug';
|
||||||
|
source?: string;
|
||||||
|
details?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowAction {
|
export interface WorkflowAction {
|
||||||
|
|
@ -119,6 +122,7 @@ export interface WorkflowState {
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
workflow: Workflow | null;
|
workflow: Workflow | null;
|
||||||
messages: WorkflowMessage[];
|
messages: WorkflowMessage[];
|
||||||
|
logs: WorkflowLog[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowLogs, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
||||||
import { WorkflowState, WorkflowActions } from './dashboardChatAreaTypes';
|
import { WorkflowState, WorkflowActions } from './dashboardChatAreaTypes';
|
||||||
|
|
||||||
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowState, WorkflowActions] {
|
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowState, WorkflowActions] {
|
||||||
|
|
@ -12,6 +12,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const { workflow, loading: workflowLoading, error: workflowError } = useWorkflow(currentWorkflowId);
|
const { workflow, loading: workflowLoading, error: workflowError } = useWorkflow(currentWorkflowId);
|
||||||
const { status: workflowStatus, loading: statusLoading, error: statusError, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
const { status: workflowStatus, loading: statusLoading, error: statusError, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
||||||
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
||||||
|
const { logs, loading: logsLoading, error: logsError, refetch: refetchLogs } = useWorkflowLogs(currentWorkflowId);
|
||||||
const { startWorkflow, stopWorkflow: stopWorkflowRequest } = useWorkflowOperations();
|
const { startWorkflow, stopWorkflow: stopWorkflowRequest } = useWorkflowOperations();
|
||||||
|
|
||||||
// Use status for real-time updates, fallback to workflow for initial data
|
// Use status for real-time updates, fallback to workflow for initial data
|
||||||
|
|
@ -20,8 +21,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
|
|
||||||
|
|
||||||
// Combined loading and error states
|
// Combined loading and error states
|
||||||
const isLoading = workflowLoading || statusLoading || messagesLoading;
|
const isLoading = workflowLoading || statusLoading || messagesLoading || logsLoading;
|
||||||
const error = workflowError || statusError || messagesError;
|
const error = workflowError || statusError || messagesError || logsError;
|
||||||
|
|
||||||
// Auto-polling for active workflows and message updates
|
// Auto-polling for active workflows and message updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -32,6 +33,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
refetchMessages();
|
refetchMessages();
|
||||||
|
refetchLogs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
@ -43,7 +45,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
pollingIntervalRef.current = null;
|
pollingIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages]);
|
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages, refetchLogs]);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const loadWorkflow = useCallback(async (workflowId: string) => {
|
const loadWorkflow = useCallback(async (workflowId: string) => {
|
||||||
|
|
@ -62,7 +64,10 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
const newWorkflowId = result.data.id;
|
const newWorkflowId = result.data.id;
|
||||||
setCurrentWorkflowId(newWorkflowId);
|
setCurrentWorkflowId(newWorkflowId);
|
||||||
setIsPolling(true);
|
setIsPolling(true);
|
||||||
setTimeout(() => refetchMessages(), 500);
|
setTimeout(() => {
|
||||||
|
refetchMessages();
|
||||||
|
refetchLogs();
|
||||||
|
}, 500);
|
||||||
return newWorkflowId;
|
return newWorkflowId;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -80,11 +85,14 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsPolling(true);
|
setIsPolling(true);
|
||||||
setTimeout(() => refetchMessages(), 500);
|
setTimeout(() => {
|
||||||
|
refetchMessages();
|
||||||
|
refetchLogs();
|
||||||
|
}, 500);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
}, [currentWorkflowId, startWorkflow, refetchMessages, refetchLogs]);
|
||||||
|
|
||||||
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
||||||
if (!currentWorkflowId) return false;
|
if (!currentWorkflowId) return false;
|
||||||
|
|
@ -95,11 +103,12 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refetchStatus();
|
refetchStatus();
|
||||||
refetchMessages();
|
refetchMessages();
|
||||||
|
refetchLogs();
|
||||||
}, 500);
|
}, 500);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages]);
|
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages, refetchLogs]);
|
||||||
|
|
||||||
const clearWorkflow = useCallback(() => {
|
const clearWorkflow = useCallback(() => {
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
|
|
@ -132,6 +141,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
workflow: currentWorkflow,
|
workflow: currentWorkflow,
|
||||||
messages,
|
messages,
|
||||||
|
logs,
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
|
||||||
import { useApiRequest } from './useApi';
|
import { useApiRequest } from './useApi';
|
||||||
|
|
||||||
// Import the centralized workflow interfaces
|
// Import the centralized workflow interfaces
|
||||||
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
||||||
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument };
|
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog };
|
||||||
|
|
||||||
export interface StartWorkflowRequest {
|
export interface StartWorkflowRequest {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
@ -248,32 +248,57 @@ export function useWorkflow(workflowId: string | null) {
|
||||||
return { workflow, loading, error, refetch: fetchWorkflow };
|
return { workflow, loading, error, refetch: fetchWorkflow };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow logs hook
|
// Enhanced workflow logs hook with better typing
|
||||||
export function useWorkflowLogs(workflowId: string | null, logId?: string) {
|
export function useWorkflowLogs(workflowId: string | null) {
|
||||||
const [logs, setLogs] = useState<any[]>([]);
|
const [logs, setLogs] = useState<WorkflowLog[]>([]);
|
||||||
const { request, isLoading: loading, error } = useApiRequest<null, any[]>();
|
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
|
||||||
|
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowLog[]>();
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
if (!workflowId) return;
|
if (!workflowId) {
|
||||||
|
setLogs([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log(`📡 Fetching logs for workflow: ${workflowId}`);
|
||||||
const data = await request({
|
const data = await request({
|
||||||
url: `/api/workflows/${workflowId}/logs`,
|
url: `/api/workflows/${workflowId}/logs`,
|
||||||
method: 'get',
|
method: 'get'
|
||||||
params: logId ? { logId } : undefined
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLogs(data);
|
console.log(`📋 Raw API response for logs:`, {
|
||||||
|
url: `/api/workflows/${workflowId}/logs`,
|
||||||
|
responseType: typeof data,
|
||||||
|
isArray: Array.isArray(data),
|
||||||
|
length: data?.length,
|
||||||
|
rawData: data,
|
||||||
|
firstLog: data?.[0],
|
||||||
|
firstLogKeys: data?.[0] ? Object.keys(data[0]) : []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update if data has actually changed
|
||||||
|
const hasChanged = JSON.stringify(data) !== JSON.stringify(logs);
|
||||||
|
if (hasChanged) {
|
||||||
|
console.log(`📋 Logs updated: ${logs.length} → ${data?.length || 0}`);
|
||||||
|
setLogs(data || []);
|
||||||
|
setLastFetchTime(Date.now());
|
||||||
|
} else {
|
||||||
|
console.log(`📋 No changes in logs (${data?.length || 0} logs)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error is already handled by useApiRequest
|
console.error('Failed to fetch workflow logs:', error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}, [workflowId, logId]);
|
}, [workflowId]);
|
||||||
|
|
||||||
return { logs, loading, error, refetch: fetchLogs };
|
return { logs, loading, error, refetch: fetchLogs, lastFetchTime };
|
||||||
}
|
}
|
||||||
|
|
||||||
// File preview hook
|
// File preview hook
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue