diff --git a/src/frontend/src/components/content/PlanChatBody.tsx b/src/frontend/src/components/content/PlanChatBody.tsx index 5d9a8869..ea1c60fb 100644 --- a/src/frontend/src/components/content/PlanChatBody.tsx +++ b/src/frontend/src/components/content/PlanChatBody.tsx @@ -1,11 +1,12 @@ import ChatInput from "@/coral/modules/ChatInput"; import { PlanChatProps } from "@/models"; -import { Button } from "@fluentui/react-components"; -import { SendRegular } from "@fluentui/react-icons"; +import { Button, Caption1 } from "@fluentui/react-components"; +import { Send } from "@/coral/imports/bundleicons"; interface SimplifiedPlanChatProps extends PlanChatProps { waitingForPlan: boolean; } + const PlanChatBody: React.FC = ({ planData, input, @@ -15,38 +16,91 @@ const PlanChatBody: React.FC = ({ waitingForPlan }) => { return ( -
- OnChatSubmit(input)} - disabledChat={submittingChatDisableInput} - placeholder={ - waitingForPlan - ? "Creating plan..." - : "Add more info to this plan..." - } +
+ {/* Chat Input Container */} +
+ OnChatSubmit(input)} + disabledChat={submittingChatDisableInput} + placeholder={ + waitingForPlan + ? "Creating plan..." + : "Tell us what needs planning, building, or connecting—we'll handle the rest." + } + style={{ + minHeight: '56px', + fontSize: '16px', + borderRadius: '8px', + border: '2px solid var(--colorNeutralStroke2)', + backgroundColor: 'var(--colorNeutralBackground1)', + padding: '16px 60px 16px 20px', + width: '100%', + boxSizing: 'border-box', + alignItems: 'flex-start', + textAlign: 'left', + verticalAlign: 'top' + }} + > +
+
+
-
); + +
+ + ); } export default PlanChatBody; \ No newline at end of file diff --git a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx index efd1f34e..81b62af2 100644 --- a/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingAgentMessage.tsx @@ -1,94 +1,278 @@ -import { AgentMessageData, AgentMessageType, role } from "@/models"; +import React from "react"; +import { AgentMessageData, AgentMessageType } from "@/models"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypePrism from "rehype-prism"; -import { Body1, Button, Tag } from "@fluentui/react-components"; +import { Body1, Button, Tag, makeStyles, tokens } from "@fluentui/react-components"; import { TaskService } from "@/services"; import { Copy } from "@/coral/imports/bundleicons"; -import { DiamondRegular, HeartRegular } from "@fluentui/react-icons"; +import { PersonRegular, Code20Regular } from "@fluentui/react-icons"; +import { TeamService } from "@/services/TeamService"; +import { iconMap } from "@/models/homeInput"; +const useStyles = makeStyles({ + container: { + maxWidth: '800px', + margin: '0 auto 32px auto', + padding: '0 24px', + display: 'flex', + alignItems: 'flex-start', + gap: '16px', + fontFamily: tokens.fontFamilyBase + }, + avatar: { + width: '32px', + height: '32px', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + }, + humanAvatar: { + backgroundColor: 'var(--colorBrandBackground)' + }, + botAvatar: { + backgroundColor: 'var(--colorNeutralBackground3)' + }, + messageContent: { + flex: 1, + maxWidth: 'calc(100% - 48px)', + display: 'flex', + flexDirection: 'column' + }, + humanMessageContent: { + alignItems: 'flex-end' + }, + botMessageContent: { + alignItems: 'flex-start' + }, + agentHeader: { + display: 'flex', + alignItems: 'center', + gap: '12px', + marginBottom: '8px' + }, + agentName: { + fontWeight: '600', + fontSize: '14px', + color: 'var(--colorNeutralForeground1)', + lineHeight: '20px' + }, + botBadge: { + fontSize: '11px', + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: '0.5px', + backgroundColor: 'var(--colorNeutralBackground3)', + color: 'var(--colorNeutralForeground1)', + border: '1px solid var(--colorNeutralStroke2)', + padding: '2px 8px', + borderRadius: '4px' + }, + messageBubble: { + padding: '12px 16px', + borderRadius: '8px', + fontSize: '14px', + lineHeight: '1.5', + wordWrap: 'break-word' + }, + humanBubble: { + backgroundColor: 'var(--colorBrandBackground)', + color: 'var(--colorNeutralForegroundInverted)', + maxWidth: '80%', + alignSelf: 'flex-end' + }, + botBubble: { + backgroundColor: 'var(--colorNeutralBackground2)', + color: 'var(--colorNeutralForeground1)', + maxWidth: '100%', + alignSelf: 'flex-start' + }, + + clarificationBubble: { + backgroundColor: 'var(--colorNeutralBackground2)', + color: 'var(--colorNeutralForeground1)', + padding: '6px 8px', // Reduced from 12px 16px + borderRadius: '8px', + fontSize: '14px', + lineHeight: '1.5', + wordWrap: 'break-word', + maxWidth: '100%', + alignSelf: 'flex-start' + }, + actionContainer: { + display: 'flex', + alignItems: 'center', + marginTop: '12px', + paddingTop: '8px', + borderTop: '1px solid var(--colorNeutralStroke2)' + }, + copyButton: { + height: '28px', + width: '28px' + }, + sampleTag: { + fontSize: '11px', + opacity: 0.7 + } +}); -const StreamingAgentMessage = (agentMessages: AgentMessageData[]) => { +// Function to get agent icon from team configuration +const getAgentIconFromTeam = (agentName: string): React.ReactNode => { + const storedTeam = TeamService.getStoredTeam(); + + if (!storedTeam?.agents) { + return ; + } + + const cleanAgentName = TaskService.cleanTextToSpaces(agentName); + + const agent = storedTeam.agents.find(a => + TaskService.cleanTextToSpaces(a.name).toLowerCase().includes(cleanAgentName.toLowerCase()) || + a.type.toLowerCase().includes(cleanAgentName.toLowerCase()) || + a.input_key.toLowerCase().includes(cleanAgentName.toLowerCase()) + ); + + if (agent?.icon && iconMap[agent.icon]) { + return React.cloneElement(iconMap[agent.icon] as React.ReactElement, { + style: { fontSize: '16px', color: 'var(--colorNeutralForeground2)' } + }); + } + + return ; +}; + +// Check if message is a clarification request +const isClarificationMessage = (content: string): boolean => { + const clarificationKeywords = [ + 'need clarification', + 'please clarify', + 'could you provide more details', + 'i need more information', + 'please specify', + 'what do you mean by', + 'clarification about' + ]; + + const lowerContent = content.toLowerCase(); + return clarificationKeywords.some(keyword => lowerContent.includes(keyword)); +}; + +const renderAgentMessages = (agentMessages: AgentMessageData[]) => { + const styles = useStyles(); + if (!agentMessages?.length) return null; // Filter out messages with empty content const validMessages = agentMessages.filter(msg => msg.content?.trim()); - const messages = validMessages; if (!validMessages.length) return null; return ( - - -
- {messages.map((msg, index) => { + <> + {validMessages.map((msg, index) => { const isHuman = msg.agent_type === AgentMessageType.HUMAN_AGENT; + const isClarification = !isHuman && isClarificationMessage(msg.content || ''); return (
- {!isHuman && ( -
-
- + {/* Avatar */} +
+ {isHuman ? ( + + ) : ( + getAgentIconFromTeam(msg.agent) + )} +
+ + {/* Message Content */} +
+ {/* Agent Header (only for bots) */} + {!isHuman && ( +
+ {TaskService.cleanTextToSpaces(msg.agent)} BOT
-
- )} + )} - -
+ {/* Message Bubble */} +
); })} -
- + ); }; -export default StreamingAgentMessage; \ No newline at end of file +export default renderAgentMessages; \ No newline at end of file diff --git a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx index 5710b891..8e888d27 100644 --- a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx @@ -19,103 +19,124 @@ const renderBufferMessage = (streamingMessageBuffer: string) => { return ( + > + {previewText} + +
+
+ )} - {!isExpanded && ( -
- + {/* Full content when expanded */} + {isExpanded && (
- {previewText} + {streamingMessageBuffer} -
-
- )} - - {isExpanded && ( -
- - {streamingMessageBuffer} - -
- )} + )} +
); }; diff --git a/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx b/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx index 723a4cec..2e2d91b6 100644 --- a/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx +++ b/src/frontend/src/components/content/streaming/StreamingPlanResponse.tsx @@ -1,86 +1,120 @@ import { MPlanData } from "@/models"; import { Button, - Text, - Title3, + Text, 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"; -import rehypePrism from "rehype-prism"; import React, { useState } from 'react'; +import { TeamService } from "@/services/TeamService"; +import { TaskService } from "@/services"; +import { iconMap } from "@/models/homeInput"; +import { Desktop20Regular } from "@fluentui/react-icons"; -// FluentUI styles using design tokens with 14px max font size +// Function to get agent icon from team configuration +const getAgentIconFromTeam = (agentName: string): React.ReactNode => { + const storedTeam = TeamService.getStoredTeam(); + + if (!storedTeam?.agents) { + return ; + } + + const cleanAgentName = TaskService.cleanTextToSpaces(agentName); + + const agent = storedTeam.agents.find(a => + TaskService.cleanTextToSpaces(a.name).toLowerCase().includes(cleanAgentName.toLowerCase()) || + a.type.toLowerCase().includes(cleanAgentName.toLowerCase()) || + a.input_key.toLowerCase().includes(cleanAgentName.toLowerCase()) + ); + + if (agent?.icon && iconMap[agent.icon]) { + return React.cloneElement(iconMap[agent.icon] as React.ReactElement, { + style: { fontSize: '16px', color: 'var(--colorNeutralForeground2)' } + }); + } + + // Use Desktop icon for AI agents instead of Person icon + return ; +}; + +// Updated styles to match consistent spacing and remove brand colors from bot elements const useStyles = makeStyles({ container: { maxWidth: '800px', - margin: '0 auto', - padding: `${tokens.spacingVerticalXXL} ${tokens.spacingHorizontalXL}`, - backgroundColor: tokens.colorNeutralBackground1, + margin: '0 auto 32px auto', + padding: '0 24px', fontFamily: tokens.fontFamilyBase }, agentHeader: { display: 'flex', alignItems: 'center', - gap: tokens.spacingHorizontalM, - marginBottom: tokens.spacingVerticalXL, - padding: tokens.spacingVerticalM, - backgroundColor: tokens.colorNeutralBackground2, - borderRadius: tokens.borderRadiusMedium + gap: '16px', + marginBottom: '8px' }, agentAvatar: { width: '32px', height: '32px', borderRadius: '50%', - backgroundColor: tokens.colorBrandForeground1, + backgroundColor: 'var(--colorNeutralBackground3)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }, + hiddenAvatar: { + width: '32px', + height: '32px', + visibility: 'hidden', + flexShrink: 0 + }, agentInfo: { display: 'flex', alignItems: 'center', - gap: tokens.spacingHorizontalM, + gap: '12px', flex: 1 }, agentName: { fontSize: '14px', - fontWeight: tokens.fontWeightSemibold, - color: tokens.colorNeutralForeground1, - lineHeight: tokens.lineHeightBase400 + fontWeight: '600', + color: 'var(--colorNeutralForeground1)', + lineHeight: '20px' }, botBadge: { - border: 'none', fontSize: '11px', - fontWeight: tokens.fontWeightSemibold, + fontWeight: '600', textTransform: 'uppercase', - letterSpacing: '0.5px' + letterSpacing: '0.5px', + backgroundColor: 'var(--colorNeutralBackground3)', + color: 'var(--colorNeutralForeground1)', + border: '1px solid var(--colorNeutralStroke2)', + padding: '2px 8px', + borderRadius: '4px' + }, + messageContainer: { + backgroundColor: 'var(--colorNeutralBackground2)', + padding: '12px 16px', + borderRadius: '8px', + fontSize: '14px', + lineHeight: '1.5', + wordWrap: 'break-word' }, factsSection: { - marginBottom: tokens.spacingVerticalXL, - marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})`, - backgroundColor: tokens.colorNeutralBackground2, - border: `1px solid ${tokens.colorNeutralStroke1}`, + backgroundColor: 'var(--colorNeutralBackground2)', + border: '1px solid var(--colorNeutralStroke2)', borderRadius: '8px', - padding: '16px' + padding: '16px', + marginBottom: '16px' }, factsHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', - marginBottom: '8px' + marginBottom: '12px' }, factsHeaderLeft: { display: 'flex', @@ -89,78 +123,87 @@ const useStyles = makeStyles({ }, factsTitle: { fontWeight: '500', - color: tokens.colorNeutralForeground1, + color: 'var(--colorNeutralForeground1)', fontSize: '14px', lineHeight: '20px' }, factsButton: { - backgroundColor: tokens.colorNeutralBackground3, - border: `1px solid ${tokens.colorNeutralStroke2}`, + backgroundColor: 'var(--colorNeutralBackground3)', + border: '1px solid var(--colorNeutralStroke2)', borderRadius: '16px', padding: '4px 12px', - fontSize: '12px' + fontSize: '14px', + fontWeight: '500', + cursor: 'pointer' }, factsPreview: { - display: 'flex', - alignItems: 'flex-start', - gap: '8px', - marginLeft: '32px' + fontSize: '14px', + lineHeight: '1.4', + color: 'var(--colorNeutralForeground2)', + marginTop: '8px' }, factsContent: { - padding: '12px', - marginTop: '8px', fontSize: '14px', - lineHeight: '1.4' + lineHeight: '1.5', + color: 'var(--colorNeutralForeground2)', + marginTop: '8px', + whiteSpace: 'pre-wrap' + }, + planTitle: { + marginBottom: '20px', + fontSize: '18px', + fontWeight: '600', + color: 'var(--colorNeutralForeground1)', + lineHeight: '24px' }, stepsList: { - marginBottom: tokens.spacingVerticalXXL, - marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})` + marginBottom: '16px' }, stepItem: { - marginBottom: tokens.spacingVerticalL, display: 'flex', alignItems: 'flex-start', - gap: tokens.spacingHorizontalS + gap: '12px', + marginBottom: '12px' }, stepNumber: { - fontSize: '14px', - fontWeight: tokens.fontWeightSemibold, - color: tokens.colorNeutralForeground1, minWidth: '24px', + height: '24px', + borderRadius: '50%', + color: 'var(--colorNeutralForeground1)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '12px', + fontWeight: '600', 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, + color: 'var(--colorNeutralForeground1)', lineHeight: '1.5', flex: 1, wordWrap: 'break-word', overflowWrap: 'break-word' }, + stepHeading: { + marginBottom: '12px', + fontSize: '16px', + fontWeight: '600', + color: 'var(--colorNeutralForeground1)', + lineHeight: '22px' + }, instructionText: { - marginBottom: tokens.spacingVerticalXL, - color: tokens.colorNeutralForeground2, + color: 'var(--colorNeutralForeground2)', fontSize: '14px', - lineHeight: tokens.lineHeightBase400, - textAlign: 'left', - marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})` + lineHeight: '1.5', + marginBottom: '16px' }, buttonContainer: { display: 'flex', - gap: tokens.spacingHorizontalM, - paddingTop: tokens.spacingVerticalM, + gap: '12px', alignItems: 'center', - justifyContent: 'flex-start', - marginLeft: `calc(32px + ${tokens.spacingHorizontalM} + ${tokens.spacingVerticalM})` + marginTop: '20px' } }); @@ -272,7 +315,7 @@ const getFactsPreview = (content: string): string => { return content.length > 200 ? content.substring(0, 200) + "..." : content; }; -// FluentUI-based plan response component +// FluentUI-based plan response component with consistent spacing and proper colors const renderPlanResponse = ( planApprovalRequest: MPlanData | null, handleApprovePlan: () => void, @@ -289,133 +332,143 @@ const renderPlanResponse = ( const { factsContent, planSteps } = extractDynamicContent(planApprovalRequest); const factsPreview = getFactsPreview(factsContent); + // Check if this is a "creating plan" state + const isCreatingPlan = !planSteps.length && !factsContent; + let stepCounter = 0; return (
{/* Agent Header */}
-
- -
+ {/* Hide avatar when creating plan */} + {isCreatingPlan ? ( +
+ ) : ( +
+ {getAgentIconFromTeam(agentName)} +
+ )}
{agentName} - - BOT - + {!isCreatingPlan && ( + + BOT + + )}
- {/* Facts Section */} - {factsContent && ( -
-
-
- - - Analysis & Context - + {/* Message Container */} +
+ {/* Facts Section */} + {factsContent && ( +
+
+
+ + + Analysis + +
+ +
- -
- - {!isFactsExpanded && ( -
-
+ {!isFactsExpanded && ( +
{factsPreview}
-
- )} - - {isFactsExpanded && ( -
-
+ )} + + {isFactsExpanded && ( +
{factsContent}
-
- )} + )} +
+ )} + + {/* Plan Title */} +
+ {isCreatingPlan ? 'Creating plan...' : `Proposed Plan for ${planApprovalRequest.user_request || 'Task'}`}
- )} - {/* 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}. -
-
+ {/* Plan Steps */} + {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. - + {/* Instruction Text */} + {!isCreatingPlan && ( + + If the plan looks good we can move forward with the first step. + + )} - {/* Action Buttons */} - {showApprovalButtons && ( -
- - -
- )} + {/* Action Buttons */} + {showApprovalButtons && !isCreatingPlan && ( +
+ + +
+ )} +
); }; diff --git a/src/frontend/src/components/content/streaming/StreamingPlanState.tsx b/src/frontend/src/components/content/streaming/StreamingPlanState.tsx index 67d02e60..f881131e 100644 --- a/src/frontend/src/components/content/streaming/StreamingPlanState.tsx +++ b/src/frontend/src/components/content/streaming/StreamingPlanState.tsx @@ -1,79 +1,86 @@ -import { Spinner, tokens } from "@fluentui/react-components"; -import { BotRegular } from "@fluentui/react-icons"; +import { Spinner } from "@fluentui/react-components"; -// Render AI thinking/planning state +// Simple thinking message to show while creating plan const renderThinkingState = (waitingForPlan: boolean) => { if (!waitingForPlan) return null; return (
- {/* AI Avatar */}
- -
- - {/* Thinking Message */} -
-
- - Creating your plan... +
+
*/} + + {/* Thinking Message */} +
+
+ + Creating your plan... +
); }; - // Simple message to show while executing the plan - const renderPlanExecutionMessage = () => { return (
- - - Processing your plan and coordinating with AI agents... - + + + Processing your plan and coordinating with AI agents... + +
); }; - export { renderPlanExecutionMessage, renderThinkingState }; \ No newline at end of file diff --git a/src/frontend/src/components/content/streaming/StreamingUserPlanMessage.tsx b/src/frontend/src/components/content/streaming/StreamingUserPlanMessage.tsx index 23bb8e06..736ea8f6 100644 --- a/src/frontend/src/components/content/streaming/StreamingUserPlanMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingUserPlanMessage.tsx @@ -2,19 +2,23 @@ import { PersonRegular } from "@fluentui/react-icons"; import getUserTask from "./StreamingUserPlan"; import { MPlanData, ProcessedPlanData } from "@/models"; -// Render user task message +// Render user task message with exact styling from image const renderUserPlanMessage = (planApprovalRequest: MPlanData | null, initialTask?: string, planData?: ProcessedPlanData) => { const userPlan = getUserTask(planApprovalRequest, initialTask, planData); + if (!userPlan) return null; + return (
{/* User Avatar */}
{/* User Message */} -
+
{userPlan}
@@ -44,4 +57,5 @@ const renderUserPlanMessage = (planApprovalRequest: MPlanData | null,
); }; + export default renderUserPlanMessage; \ No newline at end of file