feature: show workflow stats
This commit is contained in:
parent
401c0885a1
commit
ae6a634274
6 changed files with 125 additions and 57 deletions
|
|
@ -107,6 +107,34 @@
|
|||
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 */
|
||||
[data-theme="dark"] .workflowStatusContainer {
|
||||
background-color: var(--color-surface-dark);
|
||||
|
|
@ -151,3 +179,11 @@
|
|||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .statLabel {
|
||||
color: var(--color-text-secondary-dark);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .statValue {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,43 +88,28 @@ const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round
|
|||
};
|
||||
};
|
||||
|
||||
// Helper function to group logs by round and get latest progress
|
||||
const getLatestRoundProgress = (logs: any[]): { round: number | null; progress: number | undefined } => {
|
||||
if (!logs || logs.length === 0) {
|
||||
return { round: null, progress: undefined };
|
||||
// Helper function to format bytes to KB or MB
|
||||
const formatBytes = (bytes?: number): string => {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
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
|
||||
let currentRound = 1;
|
||||
let latestProgress: number | undefined = undefined;
|
||||
let latestRound = 1;
|
||||
// Helper function to format price
|
||||
const formatPrice = (price?: number): string => {
|
||||
if (price === undefined || price === null) return '-';
|
||||
return `$${price.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const sortedLogs = [...logs].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
||||
|
||||
sortedLogs.forEach((log) => {
|
||||
const message = (log.message || '').toLowerCase();
|
||||
|
||||
// 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 };
|
||||
// Helper function to format processing time
|
||||
const formatProcessingTime = (time?: number): string => {
|
||||
if (time === undefined || time === null) return '-';
|
||||
return `${time.toFixed(2)}s`;
|
||||
};
|
||||
|
||||
const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
||||
|
|
@ -132,7 +117,8 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
logs = [],
|
||||
workflowStatus: workflowStatusFromApi,
|
||||
currentRound: currentRoundFromApi,
|
||||
isRunning
|
||||
isRunning,
|
||||
latestStats
|
||||
}) => {
|
||||
// Use workflow status and round from API response, fallback to extracting from logs
|
||||
const workflowStatus = useMemo(() => {
|
||||
|
|
@ -173,21 +159,12 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
return extractWorkflowStatus(logs);
|
||||
}, [workflowStatusFromApi, currentRoundFromApi, logs]);
|
||||
|
||||
// Get latest round progress
|
||||
const latestProgress = useMemo(() => getLatestRoundProgress(logs), [logs]);
|
||||
|
||||
// Determine if workflow is running (show spinner)
|
||||
// Show spinner if explicitly running OR if status indicates running state
|
||||
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)
|
||||
if (!showSpinner && !workflowStatus.status && workflowStatus.round === null && progressValue === undefined) {
|
||||
// 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 && !latestStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -208,16 +185,33 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{progressValue !== undefined && (
|
||||
<div className={styles.progressBarContainer}>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressBarFill}
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.progressBarLabel}>{progressPercent}%</div>
|
||||
{/* Stats Display */}
|
||||
{latestStats && (
|
||||
<div className={styles.statsContainer}>
|
||||
{latestStats.priceUsd !== undefined && (
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>Price:</span>
|
||||
<span className={styles.statValue}>{formatPrice(latestStats.priceUsd)}</span>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,16 @@ export interface WorkflowStatusProps {
|
|||
* Whether the workflow is currently running (shows spinner)
|
||||
*/
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1471,6 +1471,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
|||
workflowStatus={hookData?.workflowStatus}
|
||||
currentRound={hookData?.currentRound || hookData?.workflowData?.currentRound}
|
||||
isRunning={hookData?.isRunning || false}
|
||||
latestStats={hookData?.latestStats || null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function useDashboardInputForm() {
|
|||
logs,
|
||||
dashboardLogs,
|
||||
unifiedContentLogs,
|
||||
latestStats,
|
||||
startWorkflow,
|
||||
stopWorkflow,
|
||||
resetWorkflow,
|
||||
|
|
@ -755,7 +756,8 @@ export function useDashboardInputForm() {
|
|||
setIsFileAttachmentPopupOpen,
|
||||
allUserFiles: fileContext.files || [],
|
||||
handleFileAttach,
|
||||
handleFileUploadAndAttach
|
||||
handleFileUploadAndAttach,
|
||||
latestStats
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function useWorkflowLifecycle() {
|
|||
const [dashboardLogs, setDashboardLogs] = useState<WorkflowLog[]>([]);
|
||||
const [unifiedContentLogs, setUnifiedContentLogs] = useState<WorkflowLog[]>([]);
|
||||
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 statusRef = useRef<string>('idle');
|
||||
const statusChangedFromRunningAtRef = useRef<number | null>(null);
|
||||
|
|
@ -266,6 +267,25 @@ export function useWorkflowLifecycle() {
|
|||
|
||||
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]);
|
||||
|
||||
// Poll workflow data using unified chat data endpoint
|
||||
|
|
@ -346,6 +366,7 @@ export function useWorkflowLifecycle() {
|
|||
setLogs([]);
|
||||
setDashboardLogs([]);
|
||||
setUnifiedContentLogs([]);
|
||||
setLatestStats(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +425,7 @@ export function useWorkflowLifecycle() {
|
|||
setLogs(prev => prev.length > 0 ? [] : prev);
|
||||
setDashboardLogs(prev => prev.length > 0 ? [] : prev);
|
||||
setUnifiedContentLogs(prev => prev.length > 0 ? [] : prev);
|
||||
setLatestStats(null);
|
||||
setCurrentRound(prev => prev !== undefined ? undefined : prev);
|
||||
if (statusChangedFromRunningAt !== null) {
|
||||
setStatusChangedFromRunningAt(null);
|
||||
|
|
@ -493,6 +515,7 @@ export function useWorkflowLifecycle() {
|
|||
statusRef.current = 'idle';
|
||||
updateWorkflowStatus('idle');
|
||||
setCurrentRound(undefined);
|
||||
setLatestStats(null);
|
||||
setStatusChangedFromRunningAt(null);
|
||||
statusChangedFromRunningAtRef.current = null;
|
||||
lastRenderedTimestampRef.current = null;
|
||||
|
|
@ -512,6 +535,7 @@ export function useWorkflowLifecycle() {
|
|||
setLogs([]);
|
||||
setDashboardLogs([]);
|
||||
setUnifiedContentLogs([]);
|
||||
setLatestStats(null);
|
||||
updateWorkflowStatus('idle');
|
||||
return;
|
||||
}
|
||||
|
|
@ -579,6 +603,7 @@ export function useWorkflowLifecycle() {
|
|||
logs,
|
||||
dashboardLogs,
|
||||
unifiedContentLogs,
|
||||
latestStats,
|
||||
startWorkflow: handleStartWorkflow,
|
||||
stopWorkflow: handleStopWorkflow,
|
||||
resetWorkflow,
|
||||
|
|
|
|||
Loading…
Reference in a new issue