ui-nyla/src/components/FlowEditor/editor/RunTracingPanel.tsx
2026-04-07 00:49:12 +02:00

116 lines
3.6 KiB
TypeScript

/**
* RunTracingPanel
*
* Shows AutoStepLog entries for a workflow run with live-update capability.
* Displays per-node status (running/completed/failed/skipped) with timing info.
*/
import React, { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from '../../../hooks/useApi';
import type { AutoStepLog } from '../../../api/workflowApi';
interface RunTracingPanelProps {
instanceId: string;
runId: string | null;
onNodeSelect?: (nodeId: string) => void;
}
const STATUS_COLORS: Record<string, string> = {
pending: '#999',
running: '#f0ad4e',
completed: '#28a745',
failed: '#dc3545',
skipped: '#6c757d',
};
const STATUS_ICONS: Record<string, string> = {
pending: '○',
running: '◉',
completed: '✓',
failed: '✗',
skipped: '—',
};
export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
instanceId,
runId,
onNodeSelect,
}) => {
const [steps, setSteps] = useState<AutoStepLog[]>([]);
const [loading, setLoading] = useState(false);
const { request } = useApiRequest();
const loadSteps = useCallback(async () => {
if (!runId || !instanceId) return;
setLoading(true);
try {
const data = await request({
url: `/api/workflows/${instanceId}/runs/${runId}/steps`,
method: 'get',
});
setSteps(data?.steps || []);
} catch (e) {
console.error('[RunTracing] Failed to load steps:', e);
} finally {
setLoading(false);
}
}, [runId, instanceId, request]);
useEffect(() => {
loadSteps();
const interval = setInterval(loadSteps, 3000);
return () => clearInterval(interval);
}, [loadSteps]);
if (!runId) {
return (
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}>
Select a run to see tracing details.
</div>
);
}
return (
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}>
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
</div>
{steps.length === 0 && !loading && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>No steps recorded yet.</div>
)}
{steps.map((step) => (
<div
key={step.id}
onClick={() => onNodeSelect?.(step.nodeId)}
style={{
padding: '8px 12px',
marginBottom: '6px',
borderRadius: '6px',
border: `1px solid ${STATUS_COLORS[step.status] || '#ddd'}`,
background: 'var(--bg-primary, #fff)',
cursor: 'pointer',
fontSize: '13px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>
<span style={{ color: STATUS_COLORS[step.status] || '#999', marginRight: '6px' }}>
{STATUS_ICONS[step.status] || '?'}
</span>
<strong>{step.nodeType}</strong>
<span style={{ color: '#888', marginLeft: '6px' }}>({step.nodeId})</span>
</span>
{step.durationMs != null && (
<span style={{ color: '#888', fontSize: '12px' }}>{step.durationMs}ms</span>
)}
</div>
{step.error && (
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
)}
{step.tokensUsed > 0 && (
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div>
)}
</div>
))}
</div>
);
};