fetching logs

This commit is contained in:
Ida Dittrich 2025-08-20 16:57:41 +02:00
parent 1e46d8f93e
commit 28f0293ada
7 changed files with 710 additions and 54 deletions

View file

@ -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;

View file

@ -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) => {

View file

@ -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
}`}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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
};

View file

@ -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