- {apiData.agents?.find(agent => agent.includes('speaking') || agent.includes('thinking')) ||
- `Working on ${currentPhase?.toLowerCase()} phase...`}
+
+ Current Activity:
+ {(() => {
+ // Show step-level elapsed time from step_timings
+ const stepTimings = apiData.step_timings || {};
+ const currentStep = (apiData.step || '').toLowerCase();
+ const timing = stepTimings[currentStep];
+ if (timing?.started_at) {
+ try {
+ const started = new Date(timing.started_at.replace(' UTC', 'Z'));
+ const diffSec = Math.max(0, Math.floor((Date.now() - started.getTime()) / 1000));
+ let elapsed = '';
+ if (diffSec < 60) elapsed = `${diffSec}s`;
+ else if (diffSec < 3600) elapsed = `${Math.floor(diffSec / 60)}m ${diffSec % 60}s`;
+ else elapsed = `${Math.floor(diffSec / 3600)}h ${Math.floor((diffSec % 3600) / 60)}m`;
+ return (
+
+ ⏱ {elapsed}
+
+ );
+ } catch { /* ignore */ }
+ }
+ return null;
+ })()}
- {apiData.active_agent_count && apiData.total_agents && (
+ {(() => {
+ // Parse all active agents from raw telemetry strings
+ const activeAgents = (apiData.agents || []).filter((agent: string) =>
+ agent.startsWith('✓')
+ );
+
+ if (activeAgents.length === 0) {
+ return (
+
+ Working on {currentPhase?.toLowerCase()} phase...
+
+ );
+ }
+
+ return activeAgents.map((raw: string, idx: number) => {
+ // Strip prefix: "✓[🤔🔥] " → ""
+ const cleaned = raw.replace(/^[✓✗]\[.*?\]\s*/, '');
+ // Agent name: everything before first ":"
+ const colonIdx = cleaned.indexOf(':');
+ const agentName = colonIdx > 0 ? cleaned.substring(0, colonIdx).trim() : 'Agent';
+
+ // Determine action from status keywords
+ const actionIcons: Record
= {
+ 'speaking': { icon: '🗣️', label: 'Speaking' },
+ 'thinking': { icon: '💭', label: 'Thinking' },
+ 'using_tool':{ icon: '🔧', label: 'Invoking Tool' },
+ 'analyzing': { icon: '🔍', label: 'Analyzing' },
+ 'responded': { icon: '✅', label: 'Responded' },
+ 'ready': { icon: '⏳', label: 'Ready' },
+ };
+ const statusPart = colonIdx > 0 ? cleaned.substring(colonIdx + 1) : cleaned;
+ let actionInfo = { icon: '⚡', label: 'Working' };
+ for (const [key, info] of Object.entries(actionIcons)) {
+ if (statusPart.toLowerCase().includes(key)) {
+ actionInfo = info;
+ break;
+ }
+ }
+
+ // Extract tool name(s) from 🔧 segment
+ const toolMatch = raw.match(/🔧\s*([^|]+)/);
+ let toolName = '';
+ if (toolMatch) {
+ // Clean up: take tool names, strip long JSON args
+ toolName = toolMatch[1]
+ .trim()
+ .replace(/\{[^}]*\}\.*/g, '') // remove JSON snippets
+ .replace(/\(.*?\)/g, '') // remove parenthesized args
+ .replace(/,\s*$/, '')
+ .trim();
+ if (toolName) {
+ actionInfo = { icon: '🔧', label: 'Invoking Tool' };
+ }
+ }
+
+ // Extract action count from 📊 segment
+ const actionsMatch = raw.match(/📊\s*(\d+)\s*actions?/);
+ const actionCount = actionsMatch ? parseInt(actionsMatch[1]) : 0;
+
+ // Extract blocking info from 🚧 segment
+ const blockingMatch = raw.match(/🚧\s*Blocking\s*(\d+)/);
+ const blockingCount = blockingMatch ? parseInt(blockingMatch[1]) : 0;
+
+ // Special handling for Coordinator: parse routing info from message
+ let coordinatorTarget = '';
+ let coordinatorInstruction = '';
+ if (agentName === 'Coordinator') {
+ const agentData = apiData.agent_activities?.['Coordinator'];
+ // Try full message first (preview is truncated to 300 chars which breaks JSON parsing)
+ const fullMsg = agentData?.last_full_message || '';
+ const preview = agentData?.last_message_preview || '';
+ const msgToParse = fullMsg || preview;
+ let parsedInstruction = '';
+ try {
+ const parsed = JSON.parse(msgToParse);
+ if (parsed.selected_participant) {
+ coordinatorTarget = parsed.selected_participant;
+ }
+ if (parsed.instruction) {
+ parsedInstruction = parsed.instruction;
+ coordinatorInstruction = parsedInstruction
+ .replace(/^Phase\s+\d+\s*:\s*.+?\s+-\s+/i, '')
+ .trim();
+ }
+ // Dynamic badge based on instruction content
+ if (parsed.finish && parsed.instruction === 'complete') {
+ actionInfo = { icon: '✅', label: 'Completed' };
+ } else if (parsed.finish && parsed.instruction === 'hard_blocked') {
+ actionInfo = { icon: '🚫', label: 'Blocked' };
+ } else {
+ // Extract phase name for badge: "Phase X : Phase Title - ..."
+ const phaseMatch = parsedInstruction.match(/^Phase\s+\d+\s*:\s*(.+?)\s+-\s+/i);
+ if (phaseMatch) {
+ actionInfo = { icon: '⚡', label: phaseMatch[1].trim() };
+ } else {
+ actionInfo = { icon: '⚡', label: 'Routing' };
+ }
+ }
+ } catch {
+ actionInfo = { icon: '⚡', label: 'Routing' };
+ const text = fullMsg || preview;
+ const nameMatch = text.match(/selected_participant['":\s]+([^'",$}]+)/);
+ if (nameMatch) coordinatorTarget = nameMatch[1].trim();
+ const instrMatch = text.match(/instruction['":\s]+([^"]+?)(?:"|$)/);
+ if (instrMatch) {
+ coordinatorInstruction = instrMatch[1]
+ .replace(/^Phase\s+\d+\s*:\s*.+?\s+-\s+/i, '')
+ .trim();
+ }
+ }
+ // Truncate long instructions for readability
+ if (coordinatorInstruction.length > 120) {
+ coordinatorInstruction = coordinatorInstruction.substring(0, 120) + '...';
+ }
+ }
+
+ return (
+
+ {/* Agent name + action */}
+
+ {actionInfo.icon}
+ {agentName}
+
+ {actionInfo.label}
+
+
+ {/* Coordinator routing target + instruction */}
+ {(coordinatorTarget || coordinatorInstruction) && (
+
+ {coordinatorTarget && → {coordinatorTarget}}
+ {coordinatorTarget && coordinatorInstruction && · }
+ {coordinatorInstruction && {coordinatorInstruction}}
+
+ )}
+ {/* Tool + metrics row */}
+
+ {toolName && (
+
+ 🔧 {toolName}
+
+ )}
+ {actionCount > 0 && (
+
+ 📊 {actionCount} action{actionCount !== 1 ? 's' : ''}
+
+ )}
+
+
+ );
+ });
+ })()}
+ {apiData.active_agent_count != null && apiData.total_agents != null && (
{apiData.active_agent_count}/{apiData.total_agents} agents active
{apiData.health_status?.includes('🟢') && ' 🟢'}
@@ -196,12 +382,13 @@ const ProgressModal: React.FC
= ({
{/* Recent Steps */}
{phaseSteps.length > 0 && (
-
+
Recent Activity:
+
- {phaseSteps.slice(-5).map((step, index) => (
+ {[...phaseSteps].reverse().slice(0, 5).map((step, index) => (
= ({
))}
+
)}
diff --git a/src/frontend/src/pages/batchView.tsx b/src/frontend/src/pages/batchView.tsx
index e681ecf..b09fb7b 100644
--- a/src/frontend/src/pages/batchView.tsx
+++ b/src/frontend/src/pages/batchView.tsx
@@ -79,6 +79,7 @@ const BatchStoryPage = () => {
const [batchSummary, setBatchSummary] = useState(null);
const [selectedFileContent, setSelectedFileContent] = useState("");
const [selectedFileTranslatedContent, setSelectedFileTranslatedContent] = useState("");
+ const [telemetryData, setTelemetryData] = useState(null);
// Helper function to determine file type and language for syntax highlighting
const getFileLanguageAndType = (fileName: string) => {
@@ -198,6 +199,16 @@ const BatchStoryPage = () => {
setSelectedFileId("summary"); // Default to summary view
setDataLoaded(true);
setLoading(false);
+
+ // Fetch telemetry data for the summary page
+ try {
+ const telemetry = await apiService.get(`/process/status/${batchId}/render/`);
+ if (telemetry) {
+ setTelemetryData(telemetry);
+ }
+ } catch (telErr) {
+ console.warn("Could not load telemetry data:", telErr);
+ }
} catch (err) {
console.error("Error fetching batch data:", err);
setError(err instanceof Error ? err.message : "An unknown error occurred");
@@ -474,28 +485,221 @@ const BatchStoryPage = () => {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
- marginTop: '60px',
- height: '70vh',
- width: "100%", // Ensures full visibility
- maxWidth: "800px", // Prevents content from stretching
- margin: "auto", // Keeps it centered
+ marginTop: '24px',
+ width: "100%",
+ maxWidth: "800px",
+ margin: "auto",
transition: "width 0.3s ease-in-out",
}}>
-
+
{getJsonYamlFileCount() === 0 ? "No files to process!" : "No errors! Your files are ready to download."}
-
+
{getJsonYamlFileCount() === 0
? "No files were found in this migration batch. Please upload files to proceed with the migration process."
: "Your files have been successfully migrated with no errors. All files are now ready for download. Click 'Download' to save them to your local drive."
}
+
+ {/* Migration Telemetry Dashboard */}
+ {telemetryData && (
+
+
+ {/* Migration Overview Card */}
+
+
+ Migration Overview
+
+ {telemetryData.conversion_metrics?.platform_detected || 'Unknown'} → Azure Kubernetes Service
+
+
+
+ {/* Total Time */}
+
+
Total Time
+
+ {(() => {
+ const timings = telemetryData.step_timings || {};
+ const total = Object.values(timings).reduce((sum: number, t: any) => sum + (t?.elapsed_seconds || 0), 0);
+ const mins = Math.floor(total / 60);
+ const secs = Math.floor(total % 60);
+ return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
+ })()}
+
+
+ {/* Platform */}
+
+
Source Platform
+
+ {telemetryData.conversion_metrics?.platform_detected || 'N/A'}
+
+
+ {/* Accuracy */}
+ {telemetryData.step_results?.yaml?.result && (
+
+
Conversion Accuracy
+
+ {(() => {
+ const yamlResult = Array.isArray(telemetryData.step_results.yaml.result)
+ ? telemetryData.step_results.yaml.result[0]
+ : telemetryData.step_results.yaml.result;
+ return yamlResult?.termination_output?.overall_conversion_metrics?.overall_accuracy || 'N/A';
+ })()}
+
+
+ )}
+ {/* Enterprise Readiness */}
+
+
Readiness
+
+ {telemetryData.conversion_metrics?.enterprise_readiness?.split('–')[0]?.trim() || 'N/A'}
+
+
+
+
+
+ {/* Step Timeline */}
+ {telemetryData.step_timings && Object.keys(telemetryData.step_timings).length > 0 && (
+
+ Step Timeline
+
+ {(() => {
+ const stepOrder = ['analysis', 'design', 'yaml', 'yaml_conversion', 'documentation'];
+ const stepLabels: Record
= {
+ 'analysis': 'Analysis', 'design': 'Design', 'yaml': 'YAML Conversion',
+ 'yaml_conversion': 'YAML Conversion', 'documentation': 'Documentation'
+ };
+ const stepIcons: Record = {
+ 'analysis': '🔍', 'design': '📐', 'yaml': '📄',
+ 'yaml_conversion': '📄', 'documentation': '📝'
+ };
+ const timings = telemetryData.step_timings;
+ const totalElapsed = Object.values(timings).reduce((sum: number, t: any) => sum + (t?.elapsed_seconds || 0), 0);
+ const seen = new Set();
+
+ return stepOrder
+ .filter(key => {
+ if (!timings[key] || seen.has(stepLabels[key])) return false;
+ seen.add(stepLabels[key]);
+ return true;
+ })
+ .map(key => {
+ const t = timings[key];
+ const elapsed = t?.elapsed_seconds || 0;
+ const pct = totalElapsed > 0 ? (elapsed / totalElapsed) * 100 : 0;
+ const mins = Math.floor(elapsed / 60);
+ const secs = Math.floor(elapsed % 60);
+ const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
+
+ // Get step summary from step_results
+ const stepResult = telemetryData.step_results?.[key];
+ let summary = '';
+ if (stepResult?.result) {
+ const r = Array.isArray(stepResult.result) ? stepResult.result[0] : stepResult.result;
+ if (key === 'analysis') {
+ summary = `${r?.output?.platform_detected || ''} detected (${r?.output?.confidence_score || ''})`;
+ } else if (key === 'yaml' || key === 'yaml_conversion') {
+ const metrics = r?.termination_output?.overall_conversion_metrics;
+ if (metrics) summary = `${metrics.successful_conversions}/${metrics.total_files} files converted (${metrics.overall_accuracy})`;
+ } else if (key === 'design') {
+ const services = r?.termination_output?.azure_services?.length || 0;
+ const decisions = r?.termination_output?.architecture_decisions?.length || 0;
+ if (services) summary = `${services} Azure services, ${decisions} architecture decisions`;
+ } else if (key === 'documentation') {
+ summary = 'Migration report finalized, all sign-offs PASS';
+ }
+ }
+
+ return (
+
+
{stepIcons[key] || '✅'}
+
+
+ {stepLabels[key]}
+ {timeStr}
+
+
+ {summary && (
+
{summary}
+ )}
+
+
+ );
+ });
+ })()}
+
+
+ )}
+
+ {/* Agent Participation */}
+ {telemetryData.agent_activities && Object.keys(telemetryData.agent_activities).length > 0 && (
+
+ Agent Participation
+
+ {(() => {
+ const agents = telemetryData.agent_activities;
+ const agentIcons: Record
= {
+ 'Coordinator': '⚡', 'Chief Architect': '👷', 'AKS Expert': '☁️',
+ 'EKS Expert': '🔍', 'GKE Expert': '🔍', 'YAML Expert': '🔧',
+ 'Technical Writer': '📝', 'QA Engineer': '✅', 'Azure Architect': '🏗️'
+ };
+ const stepLabels: Record = {
+ 'analysis': 'Analysis', 'design': 'Design', 'yaml': 'YAML',
+ 'yaml_conversion': 'YAML', 'documentation': 'Docs'
+ };
+
+ return Object.entries(agents)
+ .filter(([name]) => name !== 'Coordinator')
+ .map(([name, agent]: [string, any]) => {
+ const history = agent.activity_history || [];
+ const actionCount = history.length;
+ const steps = [...new Set(history.map((h: any) => stepLabels[h.step] || h.step).filter(Boolean))];
+ const toolCount = history.filter((h: any) => h.tool_used).length;
+ return { name, actionCount, steps, toolCount };
+ })
+ .sort((a, b) => b.actionCount - a.actionCount)
+ .map(({ name, actionCount, steps, toolCount }) => (
+
+
+ {agentIcons[name] || '🤖'}
+
+ {name}
+
+ {actionCount} actions
+
+ {toolCount > 0 && (
+ 🔧 {toolCount} tool calls
+ )}
+
+ {steps.join(', ')}
+
+
+ ));
+ })()}
+
+
+ )}
+
+
+ )}
>
);
diff --git a/src/frontend/src/pages/processPage.tsx b/src/frontend/src/pages/processPage.tsx
index 6869182..19ea7f0 100644
--- a/src/frontend/src/pages/processPage.tsx
+++ b/src/frontend/src/pages/processPage.tsx
@@ -130,13 +130,23 @@ const ProcessPage: React.FC = () => {
const getPhaseMessage = (apiResponse: any) => {
if (!apiResponse) return "";
- const { phase, active_agent_count, total_agents, health_status, agents } = apiResponse;
+ const { step, phase, active_agent_count, total_agents, health_status, agents } = apiResponse;
+
+ // Map step identifiers to human-readable step names
+ const stepDisplayNames: Record