diff --git a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx
index 09ba32d9..7839f54f 100644
--- a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx
+++ b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx
@@ -1,39 +1,39 @@
import { AgentMessageData } from "@/models";
-
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypePrism from "rehype-prism";
-// Render AI thinking/planning state
-const renderAgentMessages = (agentMessages: AgentMessageData[]) => {
- if (!agentMessages || agentMessages.length === 0) return null;
- return (
-
{
+ if (!agentMessages?.length) return null;
+
+ // Filter out messages with empty content
+ const validMessages = agentMessages.filter(msg => msg.raw_content?.trim());
- >
- {agentMessages.map((msg, index) => {
- const trimmed = msg.raw_content?.trim();
- if (!trimmed) return null; // skip if empty, null, or whitespace
- return (
-
- {msg.agent} :
-
- {trimmed}
-
-
- );
- })}
+ if (!validMessages.length) return null;
+
+ return (
+
+ {validMessages.map((message, index) => (
+
+
+ {message.agent} :
+
+
+
+ {message.raw_content.trim()}
+
+
- );
+ ))}
+
+ );
};
-export default renderAgentMessages;
\ No newline at end of file
+
+export default StreamingAgentMessage;
\ No newline at end of file
diff --git a/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx b/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx
index 644b57a9..723a4cec 100644
--- a/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx
+++ b/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx
@@ -1,252 +1,421 @@
import { MPlanData } from "@/models";
-import { Button, Spinner, Tag } from "@fluentui/react-components";
-import { BotRegular, CheckmarkRegular, DismissRegular } from "@fluentui/react-icons";
+import {
+ Button,
+ Text,
+ Title3,
+ Body1,
+ Caption1,
+ Badge,
+ makeStyles,
+ tokens
+} from "@fluentui/react-components";
+import {
+ BotRegular,
+ Copy20Regular,
+ InfoRegular,
+ ClockRegular,
+ CheckmarkCircle20Regular
+} from "@fluentui/react-icons";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
-// Render the complete plan with all information
-const renderPlanResponse = (planApprovalRequest: MPlanData | null, handleApprovePlan: () => void, handleRejectPlan: () => void, processingApproval: boolean, showApprovalButtons: boolean) => {
+import rehypePrism from "rehype-prism";
+import React, { useState } from 'react';
+
+// FluentUI styles using design tokens with 14px max font size
+const useStyles = makeStyles({
+ container: {
+ maxWidth: '800px',
+ margin: '0 auto',
+ padding: `${tokens.spacingVerticalXXL} ${tokens.spacingHorizontalXL}`,
+ backgroundColor: tokens.colorNeutralBackground1,
+ fontFamily: tokens.fontFamilyBase
+ },
+ agentHeader: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: tokens.spacingHorizontalM,
+ marginBottom: tokens.spacingVerticalXL,
+ padding: tokens.spacingVerticalM,
+ backgroundColor: tokens.colorNeutralBackground2,
+ borderRadius: tokens.borderRadiusMedium
+ },
+ agentAvatar: {
+ width: '32px',
+ height: '32px',
+ borderRadius: '50%',
+ backgroundColor: tokens.colorBrandForeground1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0
+ },
+ agentInfo: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: tokens.spacingHorizontalM,
+ flex: 1
+ },
+ agentName: {
+ fontSize: '14px',
+ fontWeight: tokens.fontWeightSemibold,
+ color: tokens.colorNeutralForeground1,
+ lineHeight: tokens.lineHeightBase400
+ },
+ botBadge: {
+ border: 'none',
+ fontSize: '11px',
+ fontWeight: tokens.fontWeightSemibold,
+ textTransform: 'uppercase',
+ letterSpacing: '0.5px'
+ },
+ factsSection: {
+ marginBottom: tokens.spacingVerticalXL,
+ marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`,
+ backgroundColor: tokens.colorNeutralBackground2,
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
+ borderRadius: '8px',
+ padding: '16px'
+ },
+ factsHeader: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: '8px'
+ },
+ factsHeaderLeft: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '12px'
+ },
+ factsTitle: {
+ fontWeight: '500',
+ color: tokens.colorNeutralForeground1,
+ fontSize: '14px',
+ lineHeight: '20px'
+ },
+ factsButton: {
+ backgroundColor: tokens.colorNeutralBackground3,
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
+ borderRadius: '16px',
+ padding: '4px 12px',
+ fontSize: '12px'
+ },
+ factsPreview: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ gap: '8px',
+ marginLeft: '32px'
+ },
+ factsContent: {
+ padding: '12px',
+ marginTop: '8px',
+ fontSize: '14px',
+ lineHeight: '1.4'
+ },
+ stepsList: {
+ marginBottom: tokens.spacingVerticalXXL,
+ marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`
+ },
+ stepItem: {
+ marginBottom: tokens.spacingVerticalL,
+ display: 'flex',
+ alignItems: 'flex-start',
+ gap: tokens.spacingHorizontalS
+ },
+ stepNumber: {
+ fontSize: '14px',
+ fontWeight: tokens.fontWeightSemibold,
+ color: tokens.colorNeutralForeground1,
+ minWidth: '24px',
+ flexShrink: 0,
+ marginTop: '2px'
+ },
+ stepHeading: {
+ marginBottom: tokens.spacingVerticalM,
+ marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`,
+ fontSize: '14px',
+ fontWeight: tokens.fontWeightSemibold,
+ color: tokens.colorNeutralForeground1,
+ lineHeight: '1.5'
+ },
+ stepText: {
+ fontSize: '14px',
+ color: tokens.colorNeutralForeground1,
+ lineHeight: '1.5',
+ flex: 1,
+ wordWrap: 'break-word',
+ overflowWrap: 'break-word'
+ },
+ instructionText: {
+ marginBottom: tokens.spacingVerticalXL,
+ color: tokens.colorNeutralForeground2,
+ fontSize: '14px',
+ lineHeight: tokens.lineHeightBase400,
+ textAlign: 'left',
+ marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`
+ },
+ buttonContainer: {
+ display: 'flex',
+ gap: tokens.spacingHorizontalM,
+ paddingTop: tokens.spacingVerticalM,
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`
+ }
+});
+
+// Function to get agent name from backend data
+const getAgentDisplayName = (planApprovalRequest: MPlanData | null): string => {
+ if (planApprovalRequest?.steps?.length) {
+ const firstAgent = planApprovalRequest.steps.find(step => step.agent)?.agent;
+ if (firstAgent) {
+ return firstAgent.replace(/Agent$/, '').replace(/([A-Z])/g, ' $1').trim();
+ }
+ }
+ return 'Assistant';
+};
+
+// Dynamically extract content from whatever fields contain data
+const extractDynamicContent = (planApprovalRequest: MPlanData): {
+ factsContent: string;
+ planSteps: Array<{ type: 'heading' | 'substep'; text: string }>
+} => {
+ if (!planApprovalRequest) return { factsContent: '', planSteps: [] };
+
+ let factsContent = '';
+ let planSteps: Array<{ type: 'heading' | 'substep'; text: string }> = [];
+
+ // Build facts content from available sources
+ const factsSources: string[] = [];
+
+ // Add team assembly if available
+ if (planApprovalRequest.context?.participant_descriptions &&
+ Object.keys(planApprovalRequest.context.participant_descriptions).length > 0) {
+ let teamContent = 'Team Assembly:\n\n';
+ Object.entries(planApprovalRequest.context.participant_descriptions).forEach(([agent, description]) => {
+ teamContent += `${agent}: ${description}\n\n`;
+ });
+ factsSources.push(teamContent);
+ }
+
+ // Add facts field if it contains substantial content
+ if (planApprovalRequest.facts && planApprovalRequest.facts.trim().length > 10) {
+ factsSources.push(planApprovalRequest.facts.trim());
+ }
+
+ // Combine all facts sources
+ factsContent = factsSources.join('\n---\n\n');
+
+ // Extract plan steps from multiple possible sources
+ if (planApprovalRequest.steps && planApprovalRequest.steps.length > 0) {
+ planApprovalRequest.steps.forEach(step => {
+ // Use whichever action field has content
+ const action = step.action || step.cleanAction || '';
+ if (action.trim()) {
+ // Check if it ends with colon (heading) or is a regular step
+ if (action.trim().endsWith(':')) {
+ planSteps.push({ type: 'heading', text: action.trim() });
+ } else {
+ planSteps.push({ type: 'substep', text: action.trim() });
+ }
+ }
+ });
+ }
+
+ // If no steps found in steps array, try to extract from other fields
+ if (planSteps.length === 0) {
+ // Look in user_request or facts for plan content
+ const searchContent = planApprovalRequest.user_request || planApprovalRequest.facts || '';
+ const lines = searchContent.split('\n');
+
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ // Skip empty lines and section headers
+ if (!trimmedLine ||
+ trimmedLine.toLowerCase().includes('plan created') ||
+ trimmedLine.toLowerCase().includes('user request') ||
+ trimmedLine.toLowerCase().includes('team assembly') ||
+ trimmedLine.toLowerCase().includes('fact sheet')) {
+ continue;
+ }
+
+ // Look for bullet points, dashes, or numbered items
+ if (trimmedLine.match(/^[-•*]\s+/) ||
+ trimmedLine.match(/^\d+\.\s+/) ||
+ trimmedLine.match(/^[a-zA-Z][\w\s]*:$/)) {
+
+ // Remove bullet/number prefixes for clean display
+ let cleanText = trimmedLine
+ .replace(/^[-•*]\s+/, '')
+ .replace(/^\d+\.\s+/, '')
+ .trim();
+
+ if (cleanText.length > 3) {
+ // Determine if it's a heading (ends with colon) or substep
+ if (cleanText.endsWith(':')) {
+ planSteps.push({ type: 'heading', text: cleanText });
+ } else {
+ planSteps.push({ type: 'substep', text: cleanText });
+ }
+ }
+ }
+ }
+ }
+
+ return { factsContent, planSteps };
+};
+
+// Process facts for preview
+const getFactsPreview = (content: string): string => {
+ if (!content) return '';
+ return content.length > 200 ? content.substring(0, 200) + "..." : content;
+};
+
+// FluentUI-based plan response component
+const renderPlanResponse = (
+ planApprovalRequest: MPlanData | null,
+ handleApprovePlan: () => void,
+ handleRejectPlan: () => void,
+ processingApproval: boolean,
+ showApprovalButtons: boolean
+) => {
+ const styles = useStyles();
+ const [isFactsExpanded, setIsFactsExpanded] = useState(false);
+
if (!planApprovalRequest) return null;
- return (
-
- {/* AI Avatar */}
-
-
-
+ const agentName = getAgentDisplayName(planApprovalRequest);
+ const { factsContent, planSteps } = extractDynamicContent(planApprovalRequest);
+ const factsPreview = getFactsPreview(factsContent);
- {/* Plan Content */}
-
-
- {/* Plan Header */}
-
-
- 📋 Plan Generated
-
-
- Plan ID: {planApprovalRequest.id}
-
- {planApprovalRequest.status?.replace(/^.*'([^']*)'.*$/, '$1') || planApprovalRequest.status || 'Pending'}
-
-
+ let stepCounter = 0;
+
+ return (
+
+ {/* Agent Header */}
+
+
+
+
+
+ {agentName}
+
+
+ BOT
+
+
+
- {/* Analysis Section */}
- {planApprovalRequest.facts && (
-
-
- 🔍 Analysis & Context
-
-
-
- {planApprovalRequest.facts}
-
-
-
- )}
-
- {/* Action Steps */}
- {planApprovalRequest.steps && planApprovalRequest.steps.length > 0 && (
-
-
- 📝 Action Plan ({planApprovalRequest.steps.length} steps)
-
-
- {planApprovalRequest.steps.map((step, index) => (
-
-
- {step.id}
-
-
-
-
- {step.cleanAction || step.action}
-
-
- {step.agent && step.agent !== 'System' && (
-
- {step.agent}
-
- )}
-
-
- ))}
-
-
- )}
-
- {/* Team Assignment */}
- {planApprovalRequest.team && planApprovalRequest.team.length > 0 && (
-
-
- 👥 Assigned Team
-
-
- {planApprovalRequest.team.map((member, index) => (
-
- {member}
-
- ))}
+ {/* Facts Section */}
+ {factsContent && (
+
+
+
+
+
+ Analysis & Context
+
+
+
setIsFactsExpanded(!isFactsExpanded)}
+ className={styles.factsButton}
+ >
+ {isFactsExpanded ? 'Hide' : 'Details'}
+
- )}
-
- {/* Agent Capabilities */}
- {planApprovalRequest.context?.participant_descriptions &&
- Object.keys(planApprovalRequest.context.participant_descriptions).length > 0 && (
-
-
- Agent Capabilities
-
+
+ {!isFactsExpanded && (
+
- {Object.entries(planApprovalRequest.context.participant_descriptions).map(([agent, description]) => (
-
-
{agent}:
-
{description}
-
- ))}
+ {factsPreview}
)}
-
-
- {/* Action Buttons - Separate section */}
- {showApprovalButtons && <>
-
-
-
Ready for approval
-
+
+ {isFactsExpanded && (
+
+ )}
+
+ )}
-
:
}
- onClick={handleApprovePlan}
- disabled={processingApproval}
- size="medium"
- style={{ minWidth: '140px' }}
- >
- {processingApproval ? 'Processing...' : 'Approve'}
-
-
}
- onClick={handleRejectPlan}
- disabled={processingApproval}
- size="medium"
- style={{ minWidth: '100px' }}
- >
- Cancel
-
+ {/* Plan Steps - Better formatting like the image */}
+ {planSteps.length > 0 && (
+
+ {planSteps.map((step, index) => {
+ if (step.type === 'heading') {
+ return (
+
+ {step.text}
+
+ );
+ } else {
+ stepCounter++;
+ return (
+
+
+ {stepCounter}.
+
+
+ {step.text}
+
+
+ );
+ }
+ })}
+
+ )}
-
- >}
-
+ {/* Instruction Text */}
+
+ If the plan looks good we can move forward with the first step.
+
+
+ {/* Action Buttons */}
+ {showApprovalButtons && (
+
+
+ {processingApproval ? 'Processing...' : 'Approve Task Plan'}
+
+
+ Cancel
+
+
+ )}
);
};