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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue