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];
|
||||
};
|
||||
|
||||
|
||||
// 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<MessageItemProps> = ({ message, onFilePreview }) =>
|
|||
// Timestamp debugging
|
||||
timestamp: message.timestamp,
|
||||
hasTimestamp: !!message.timestamp,
|
||||
timestampType: typeof message.timestamp
|
||||
timestampType: typeof message.timestamp,
|
||||
|
||||
});
|
||||
|
||||
const handleDocumentClick = (document: Document) => {
|
||||
|
|
|
|||
|
|
@ -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<MessageListProps> = ({
|
|||
const scrollContainerRef = React.useRef<HTMLDivElement>(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<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(() => {
|
||||
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<MessageListProps> = ({
|
|||
|
||||
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
{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 (
|
||||
<MessageItem
|
||||
key={`message-${message.id}`}
|
||||
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>
|
||||
|
||||
|
|
@ -271,24 +514,47 @@ const MessageList: React.FC<MessageListProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{transformedMessages.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
|
||||
{timeline.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 && (
|
||||
{timeline.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
|
||||
<div className={messageStyles.message_empty_state}>
|
||||
No messages in this workflow yet.
|
||||
No messages or logs in this workflow yet.
|
||||
</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 */}
|
||||
<button
|
||||
className={`${messageStyles.scroll_to_bottom_btn} ${
|
||||
(isUserScrolledUp && transformedMessages.length > 0)
|
||||
(isUserScrolledUp && timeline.length > 0)
|
||||
? messageStyles.visible
|
||||
: messageStyles.hidden
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@
|
|||
/* Scroll to Bottom Button */
|
||||
.scroll_to_bottom_btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
bottom: 80px;
|
||||
right: 20px;
|
||||
background-color: var(--color-secondary);
|
||||
color: white;
|
||||
|
|
@ -339,3 +339,182 @@
|
|||
.polling_indicator {
|
||||
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;
|
||||
progress: number;
|
||||
performance: any;
|
||||
level?: 'info' | 'warning' | 'error' | 'debug';
|
||||
source?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
export interface WorkflowAction {
|
||||
|
|
@ -119,6 +122,7 @@ export interface WorkflowState {
|
|||
currentWorkflowId: string | null;
|
||||
workflow: Workflow | null;
|
||||
messages: WorkflowMessage[];
|
||||
logs: WorkflowLog[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
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 { status: workflowStatus, loading: statusLoading, error: statusError, refetch: refetchStatus } = useWorkflowStatus(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();
|
||||
|
||||
// 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
|
||||
const isLoading = workflowLoading || statusLoading || messagesLoading;
|
||||
const error = workflowError || statusError || messagesError;
|
||||
const isLoading = workflowLoading || statusLoading || messagesLoading || logsLoading;
|
||||
const error = workflowError || statusError || messagesError || logsError;
|
||||
|
||||
// Auto-polling for active workflows and message updates
|
||||
useEffect(() => {
|
||||
|
|
@ -32,6 +33,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||
if (isActive) {
|
||||
refetchMessages();
|
||||
refetchLogs();
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
|
@ -43,7 +45,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
pollingIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages]);
|
||||
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages, refetchLogs]);
|
||||
|
||||
// Actions
|
||||
const loadWorkflow = useCallback(async (workflowId: string) => {
|
||||
|
|
@ -62,7 +64,10 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
const newWorkflowId = result.data.id;
|
||||
setCurrentWorkflowId(newWorkflowId);
|
||||
setIsPolling(true);
|
||||
setTimeout(() => refetchMessages(), 500);
|
||||
setTimeout(() => {
|
||||
refetchMessages();
|
||||
refetchLogs();
|
||||
}, 500);
|
||||
return newWorkflowId;
|
||||
}
|
||||
return null;
|
||||
|
|
@ -80,11 +85,14 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
|
||||
if (result.success) {
|
||||
setIsPolling(true);
|
||||
setTimeout(() => refetchMessages(), 500);
|
||||
setTimeout(() => {
|
||||
refetchMessages();
|
||||
refetchLogs();
|
||||
}, 500);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
||||
}, [currentWorkflowId, startWorkflow, refetchMessages, refetchLogs]);
|
||||
|
||||
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
||||
if (!currentWorkflowId) return false;
|
||||
|
|
@ -95,11 +103,12 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
setTimeout(() => {
|
||||
refetchStatus();
|
||||
refetchMessages();
|
||||
refetchLogs();
|
||||
}, 500);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages]);
|
||||
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus, refetchMessages, refetchLogs]);
|
||||
|
||||
const clearWorkflow = useCallback(() => {
|
||||
setCurrentWorkflowId(null);
|
||||
|
|
@ -132,6 +141,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
currentWorkflowId,
|
||||
workflow: currentWorkflow,
|
||||
messages,
|
||||
logs,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
|
|||
import { useApiRequest } from './useApi';
|
||||
|
||||
// Import the centralized workflow interfaces
|
||||
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
||||
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument };
|
||||
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
|
||||
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog };
|
||||
|
||||
export interface StartWorkflowRequest {
|
||||
prompt: string;
|
||||
|
|
@ -248,32 +248,57 @@ export function useWorkflow(workflowId: string | null) {
|
|||
return { workflow, loading, error, refetch: fetchWorkflow };
|
||||
}
|
||||
|
||||
// Workflow logs hook
|
||||
export function useWorkflowLogs(workflowId: string | null, logId?: string) {
|
||||
const [logs, setLogs] = useState<any[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, any[]>();
|
||||
// Enhanced workflow logs hook with better typing
|
||||
export function useWorkflowLogs(workflowId: string | null) {
|
||||
const [logs, setLogs] = useState<WorkflowLog[]>([]);
|
||||
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowLog[]>();
|
||||
|
||||
const fetchLogs = async () => {
|
||||
if (!workflowId) return;
|
||||
if (!workflowId) {
|
||||
setLogs([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`📡 Fetching logs for workflow: ${workflowId}`);
|
||||
const data = await request({
|
||||
url: `/api/workflows/${workflowId}/logs`,
|
||||
method: 'get',
|
||||
params: logId ? { logId } : undefined
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
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) {
|
||||
// Error is already handled by useApiRequest
|
||||
console.error('Failed to fetch workflow logs:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [workflowId, logId]);
|
||||
}, [workflowId]);
|
||||
|
||||
return { logs, loading, error, refetch: fetchLogs };
|
||||
return { logs, loading, error, refetch: fetchLogs, lastFetchTime };
|
||||
}
|
||||
|
||||
// File preview hook
|
||||
|
|
|
|||
Loading…
Reference in a new issue