feature: show workflow stats

This commit is contained in:
Ida Dittrich 2026-01-02 12:02:25 +01:00
parent 401c0885a1
commit ae6a634274
6 changed files with 125 additions and 57 deletions

View file

@ -107,6 +107,34 @@
text-align: right; text-align: right;
} }
.statsContainer {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.statItem {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.statLabel {
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 11px;
}
.statValue {
font-weight: 600;
color: var(--color-text);
font-family: 'Courier New', monospace;
}
/* Dark theme support */ /* Dark theme support */
[data-theme="dark"] .workflowStatusContainer { [data-theme="dark"] .workflowStatusContainer {
background-color: var(--color-surface-dark); background-color: var(--color-surface-dark);
@ -151,3 +179,11 @@
color: var(--color-text-dark); color: var(--color-text-dark);
} }
[data-theme="dark"] .statLabel {
color: var(--color-text-secondary-dark);
}
[data-theme="dark"] .statValue {
color: var(--color-text-dark);
}

View file

@ -88,43 +88,28 @@ const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round
}; };
}; };
// Helper function to group logs by round and get latest progress // Helper function to format bytes to KB or MB
const getLatestRoundProgress = (logs: any[]): { round: number | null; progress: number | undefined } => { const formatBytes = (bytes?: number): string => {
if (!logs || logs.length === 0) { if (bytes === undefined || bytes === null) return '-';
return { round: null, progress: undefined }; if (bytes === 0) return '0 B';
const kb = bytes / 1024;
if (kb < 1024) {
return `${kb.toFixed(2)} KB`;
} }
const mb = kb / 1024;
return `${mb.toFixed(2)} MB`;
};
// Find the latest round // Helper function to format price
let currentRound = 1; const formatPrice = (price?: number): string => {
let latestProgress: number | undefined = undefined; if (price === undefined || price === null) return '-';
let latestRound = 1; return `$${price.toFixed(2)}`;
};
const sortedLogs = [...logs].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); // Helper function to format processing time
const formatProcessingTime = (time?: number): string => {
sortedLogs.forEach((log) => { if (time === undefined || time === null) return '-';
const message = (log.message || '').toLowerCase(); return `${time.toFixed(2)}s`;
// Check if this is a workflow status message that indicates a round change
if (message.includes('workflow started') || message.includes('workflow resumed')) {
const roundMatch = message.match(/\(?round\s+(\d+)\)?/i);
if (roundMatch) {
currentRound = parseInt(roundMatch[1], 10);
latestRound = currentRound;
} else if (message.includes('workflow started')) {
currentRound = 1;
latestRound = 1;
}
}
// Update progress for current round
if (log.progress !== undefined && log.progress !== null) {
if (currentRound === latestRound) {
latestProgress = log.progress;
}
}
});
return { round: latestRound, progress: latestProgress };
}; };
const WorkflowStatus: React.FC<WorkflowStatusProps> = ({ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
@ -132,7 +117,8 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
logs = [], logs = [],
workflowStatus: workflowStatusFromApi, workflowStatus: workflowStatusFromApi,
currentRound: currentRoundFromApi, currentRound: currentRoundFromApi,
isRunning isRunning,
latestStats
}) => { }) => {
// Use workflow status and round from API response, fallback to extracting from logs // Use workflow status and round from API response, fallback to extracting from logs
const workflowStatus = useMemo(() => { const workflowStatus = useMemo(() => {
@ -173,21 +159,12 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
return extractWorkflowStatus(logs); return extractWorkflowStatus(logs);
}, [workflowStatusFromApi, currentRoundFromApi, logs]); }, [workflowStatusFromApi, currentRoundFromApi, logs]);
// Get latest round progress
const latestProgress = useMemo(() => getLatestRoundProgress(logs), [logs]);
// Determine if workflow is running (show spinner) // Determine if workflow is running (show spinner)
// Show spinner if explicitly running OR if status indicates running state // Show spinner if explicitly running OR if status indicates running state
const showSpinner = isRunning === true || workflowStatus.status === 'started' || workflowStatus.status === 'resumed'; const showSpinner = isRunning === true || workflowStatus.status === 'started' || workflowStatus.status === 'resumed';
// Calculate progress percentage
const progressValue = latestProgress.progress !== undefined
? Math.min(Math.max(latestProgress.progress, 0), 1)
: undefined;
const progressPercent = progressValue !== undefined ? Math.round(progressValue * 100) : undefined;
// Don't render if no status information (but always show if spinner should be visible) // Don't render if no status information and no stats (but always show if spinner should be visible)
if (!showSpinner && !workflowStatus.status && workflowStatus.round === null && progressValue === undefined) { if (!showSpinner && !workflowStatus.status && workflowStatus.round === null && !latestStats) {
return null; return null;
} }
@ -208,16 +185,33 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
)} )}
</div> </div>
{/* Progress Bar */} {/* Stats Display */}
{progressValue !== undefined && ( {latestStats && (
<div className={styles.progressBarContainer}> <div className={styles.statsContainer}>
<div className={styles.progressBar}> {latestStats.priceUsd !== undefined && (
<div <div className={styles.statItem}>
className={styles.progressBarFill} <span className={styles.statLabel}>Price:</span>
style={{ width: `${progressPercent}%` }} <span className={styles.statValue}>{formatPrice(latestStats.priceUsd)}</span>
/> </div>
</div> )}
<div className={styles.progressBarLabel}>{progressPercent}%</div> {latestStats.processingTime !== undefined && (
<div className={styles.statItem}>
<span className={styles.statLabel}>Time:</span>
<span className={styles.statValue}>{formatProcessingTime(latestStats.processingTime)}</span>
</div>
)}
{latestStats.bytesSent !== undefined && (
<div className={styles.statItem}>
<span className={styles.statLabel}>Sent:</span>
<span className={styles.statValue}>{formatBytes(latestStats.bytesSent)}</span>
</div>
)}
{latestStats.bytesReceived !== undefined && (
<div className={styles.statItem}>
<span className={styles.statLabel}>Received:</span>
<span className={styles.statValue}>{formatBytes(latestStats.bytesReceived)}</span>
</div>
)}
</div> </div>
)} )}
</div> </div>

View file

@ -44,6 +44,16 @@ export interface WorkflowStatusProps {
* Whether the workflow is currently running (shows spinner) * Whether the workflow is currently running (shows spinner)
*/ */
isRunning?: boolean; isRunning?: boolean;
/**
* Latest statistics from the workflow (price, processing time, bytes sent/received)
*/
latestStats?: {
priceUsd?: number;
processingTime?: number;
bytesSent?: number;
bytesReceived?: number;
} | null;
} }
export type WorkflowStatusType = 'started' | 'resumed' | 'stopped' | 'failed' | 'completed' | null; export type WorkflowStatusType = 'started' | 'resumed' | 'stopped' | 'failed' | 'completed' | null;

View file

@ -1471,6 +1471,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
workflowStatus={hookData?.workflowStatus} workflowStatus={hookData?.workflowStatus}
currentRound={hookData?.currentRound || hookData?.workflowData?.currentRound} currentRound={hookData?.currentRound || hookData?.workflowData?.currentRound}
isRunning={hookData?.isRunning || false} isRunning={hookData?.isRunning || false}
latestStats={hookData?.latestStats || null}
/> />
</div> </div>
)} )}

View file

@ -48,6 +48,7 @@ export function useDashboardInputForm() {
logs, logs,
dashboardLogs, dashboardLogs,
unifiedContentLogs, unifiedContentLogs,
latestStats,
startWorkflow, startWorkflow,
stopWorkflow, stopWorkflow,
resetWorkflow, resetWorkflow,
@ -755,7 +756,8 @@ export function useDashboardInputForm() {
setIsFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen,
allUserFiles: fileContext.files || [], allUserFiles: fileContext.files || [],
handleFileAttach, handleFileAttach,
handleFileUploadAndAttach handleFileUploadAndAttach,
latestStats
}; };
} }

View file

@ -27,6 +27,7 @@ export function useWorkflowLifecycle() {
const [dashboardLogs, setDashboardLogs] = useState<WorkflowLog[]>([]); const [dashboardLogs, setDashboardLogs] = useState<WorkflowLog[]>([]);
const [unifiedContentLogs, setUnifiedContentLogs] = useState<WorkflowLog[]>([]); const [unifiedContentLogs, setUnifiedContentLogs] = useState<WorkflowLog[]>([]);
const [statusChangedFromRunningAt, setStatusChangedFromRunningAt] = useState<number | null>(null); const [statusChangedFromRunningAt, setStatusChangedFromRunningAt] = useState<number | null>(null);
const [latestStats, setLatestStats] = useState<{ priceUsd?: number; processingTime?: number; bytesSent?: number; bytesReceived?: number } | null>(null);
const prevStatusRef = useRef<string>('idle'); const prevStatusRef = useRef<string>('idle');
const statusRef = useRef<string>('idle'); const statusRef = useRef<string>('idle');
const statusChangedFromRunningAtRef = useRef<number | null>(null); const statusChangedFromRunningAtRef = useRef<number | null>(null);
@ -266,6 +267,25 @@ export function useWorkflowLifecycle() {
return [...allLogs].sort(sortLogs); return [...allLogs].sort(sortLogs);
}); });
// Process stats and keep the latest one (highest createdAt)
const statsItems = timeline.filter(item => item.type === 'stat');
if (statsItems.length > 0) {
// Sort by createdAt descending to get the latest
const sortedStats = [...statsItems].sort((a, b) => b.createdAt - a.createdAt);
const latestStatItem = sortedStats[0];
const statData = latestStatItem.item || latestStatItem;
if (statData && (statData.priceUsd !== undefined || statData.processingTime !== undefined ||
statData.bytesSent !== undefined || statData.bytesReceived !== undefined)) {
setLatestStats({
priceUsd: statData.priceUsd,
processingTime: statData.processingTime,
bytesSent: statData.bytesSent,
bytesReceived: statData.bytesReceived
});
}
}
}, [convertLogToFrontendFormat]); }, [convertLogToFrontendFormat]);
// Poll workflow data using unified chat data endpoint // Poll workflow data using unified chat data endpoint
@ -346,6 +366,7 @@ export function useWorkflowLifecycle() {
setLogs([]); setLogs([]);
setDashboardLogs([]); setDashboardLogs([]);
setUnifiedContentLogs([]); setUnifiedContentLogs([]);
setLatestStats(null);
return; return;
} }
@ -404,6 +425,7 @@ export function useWorkflowLifecycle() {
setLogs(prev => prev.length > 0 ? [] : prev); setLogs(prev => prev.length > 0 ? [] : prev);
setDashboardLogs(prev => prev.length > 0 ? [] : prev); setDashboardLogs(prev => prev.length > 0 ? [] : prev);
setUnifiedContentLogs(prev => prev.length > 0 ? [] : prev); setUnifiedContentLogs(prev => prev.length > 0 ? [] : prev);
setLatestStats(null);
setCurrentRound(prev => prev !== undefined ? undefined : prev); setCurrentRound(prev => prev !== undefined ? undefined : prev);
if (statusChangedFromRunningAt !== null) { if (statusChangedFromRunningAt !== null) {
setStatusChangedFromRunningAt(null); setStatusChangedFromRunningAt(null);
@ -493,6 +515,7 @@ export function useWorkflowLifecycle() {
statusRef.current = 'idle'; statusRef.current = 'idle';
updateWorkflowStatus('idle'); updateWorkflowStatus('idle');
setCurrentRound(undefined); setCurrentRound(undefined);
setLatestStats(null);
setStatusChangedFromRunningAt(null); setStatusChangedFromRunningAt(null);
statusChangedFromRunningAtRef.current = null; statusChangedFromRunningAtRef.current = null;
lastRenderedTimestampRef.current = null; lastRenderedTimestampRef.current = null;
@ -512,6 +535,7 @@ export function useWorkflowLifecycle() {
setLogs([]); setLogs([]);
setDashboardLogs([]); setDashboardLogs([]);
setUnifiedContentLogs([]); setUnifiedContentLogs([]);
setLatestStats(null);
updateWorkflowStatus('idle'); updateWorkflowStatus('idle');
return; return;
} }
@ -579,6 +603,7 @@ export function useWorkflowLifecycle() {
logs, logs,
dashboardLogs, dashboardLogs,
unifiedContentLogs, unifiedContentLogs,
latestStats,
startWorkflow: handleStartWorkflow, startWorkflow: handleStartWorkflow,
stopWorkflow: handleStopWorkflow, stopWorkflow: handleStopWorkflow,
resetWorkflow, resetWorkflow,