diff --git a/src/backend/common/models/messages_kernel.py b/src/backend/common/models/messages_kernel.py index f27f8cb44..e6b98beff 100644 --- a/src/backend/common/models/messages_kernel.py +++ b/src/backend/common/models/messages_kernel.py @@ -3,8 +3,9 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional -from semantic_kernel.kernel_pydantic import Field, KernelBaseModel +from semantic_kernel.kernel_pydantic import Field, KernelBaseModel +from dataclasses import dataclass class DataType(str, Enum): """Enumeration of possible data types for documents in the database.""" @@ -16,7 +17,7 @@ class DataType(str, Enum): team_config = "team_config" user_current_team = "user_current_team" m_plan = "m_plan" - m_plan_step = "m_plan_step" + m_plan_message = "m_plan_message" class AgentType(str, Enum): @@ -53,6 +54,7 @@ class PlanStatus(str, Enum): in_progress = "in_progress" completed = "completed" failed = "failed" + canceled = "canceled" class HumanFeedbackStatus(str, Enum): @@ -93,6 +95,7 @@ class AgentMessage(BaseDataModel): step_id: Optional[str] = None + class Session(BaseDataModel): """Represents a user session.""" @@ -119,6 +122,7 @@ class Plan(BaseDataModel): user_id: str initial_goal: str overall_status: PlanStatus = PlanStatus.in_progress + approved: bool = False source: str = AgentType.PLANNER.value summary: Optional[str] = None team_id: Optional[str] = None @@ -250,3 +254,14 @@ class InputTask(KernelBaseModel): class UserLanguage(KernelBaseModel): language: str + + +class AgentMessageType(str, Enum): + HUMAN_AGENT = "Human_Agent", + AI_AGENT = "AI_Agent", + + +class AgentMessageData (BaseDataModel): + agent: str + agent_type: AgentMessageType = AgentMessageType.AI_AGENT + content: str \ No newline at end of file diff --git a/src/backend/v3/models/messages.py b/src/backend/v3/models/messages.py index 489b21dda..3aec8697b 100644 --- a/src/backend/v3/models/messages.py +++ b/src/backend/v3/models/messages.py @@ -123,7 +123,6 @@ class ApprovalRequest(KernelBaseModel): agent_name: str - class WebsocketMessageType(str, Enum): """Types of WebSocket messages.""" SYSTEM_MESSAGE = "system_message" diff --git a/src/backend/v3/models/models.py b/src/backend/v3/models/models.py index 7a764a71f..19a2b9f33 100644 --- a/src/backend/v3/models/models.py +++ b/src/backend/v3/models/models.py @@ -1,10 +1,12 @@ import uuid from datetime import datetime, timezone from enum import Enum -from typing import List, Optional +from typing import List, Literal, Optional from pydantic import BaseModel, Field +from common.models.messages_kernel import DataType + class PlanStatus(str, Enum): CREATED = "created" @@ -22,6 +24,7 @@ class MStep(BaseModel): class MPlan(BaseModel): """model of a plan""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) + data_type: Literal[DataType.m_plan] = Field(DataType.m_plan, Literal=True) user_id: str = "" team_id: str = "" plan_id: str = "" diff --git a/src/frontend/src/components/common/TeamSelected.tsx b/src/frontend/src/components/common/TeamSelected.tsx index 8aedbe7d3..29609b593 100644 --- a/src/frontend/src/components/common/TeamSelected.tsx +++ b/src/frontend/src/components/common/TeamSelected.tsx @@ -1,19 +1,18 @@ import { TeamConfig } from "@/models"; import { Body1, Caption1 } from "@fluentui/react-components"; - +import styles from '../../styles/TeamSelector.module.css'; export interface TeamSelectedProps { selectedTeam?: TeamConfig | null; - styles: { [key: string]: string }; } -const TeamSelected: React.FC = ({ selectedTeam, styles }) => { +const TeamSelected: React.FC = ({ selectedTeam }) => { return (
- Current Team +   Current Team - {selectedTeam ? selectedTeam.name : 'No team selected'} +   {selectedTeam ? selectedTeam.name : 'No team selected'}
); diff --git a/src/frontend/src/components/content/PlanChat.tsx b/src/frontend/src/components/content/PlanChat.tsx index 207aa80ba..34a1527d1 100644 --- a/src/frontend/src/components/content/PlanChat.tsx +++ b/src/frontend/src/components/content/PlanChat.tsx @@ -43,7 +43,11 @@ interface SimplifiedPlanChatProps extends PlanChatProps { streamingMessageBuffer: string; agentMessages: AgentMessageData[]; showProcessingPlanSpinner: boolean; - setShowProcessingPlanSpinner: (show: boolean) => void; + showApprovalButtons: boolean; + handleApprovePlan: () => Promise; + handleRejectPlan: () => Promise; + processingApproval: boolean; + } const PlanChat: React.FC = ({ @@ -61,7 +65,11 @@ const PlanChat: React.FC = ({ streamingMessageBuffer, agentMessages, showProcessingPlanSpinner, - setShowProcessingPlanSpinner + showApprovalButtons, + handleApprovePlan, + handleRejectPlan, + processingApproval + }) => { const navigate = useNavigate(); @@ -69,71 +77,11 @@ const PlanChat: React.FC = ({ const { showToast, dismissToast } = useInlineToaster(); // States - const [processingApproval, setProcessingApproval] = useState(false); - - const [showApprovalButtons, setShowApprovalButtons] = useState(true); - - - - - // Listen for m_plan streaming - - - // Handle plan approval - const handleApprovePlan = useCallback(async () => { - if (!planApprovalRequest) return; - - setProcessingApproval(true); - let id = showToast("Submitting Approval", "progress"); - try { - await apiService.approvePlan({ - m_plan_id: planApprovalRequest.id, - plan_id: planData?.plan?.id, - approved: true, - feedback: 'Plan approved by user' - }); - - dismissToast(id); - onPlanApproval?.(true); - setShowProcessingPlanSpinner?.(true); - setShowApprovalButtons(false); - - } catch (error) { - dismissToast(id); - showToast("Failed to submit approval", "error"); - console.error('❌ Failed to approve plan:', error); - } finally { - setProcessingApproval(false); - } - }, [planApprovalRequest, planData, onPlanApproval, setShowProcessingPlanSpinner]); - - // Handle plan rejection - const handleRejectPlan = useCallback(async () => { - if (!planApprovalRequest) return; - - setProcessingApproval(true); - let id = showToast("Submitting cancellation", "progress"); - try { - await apiService.approvePlan({ - m_plan_id: planApprovalRequest.id, - plan_id: planData?.plan?.id, - approved: false, - feedback: 'Plan rejected by user' - }); - - dismissToast(id); - onPlanApproval?.(false); - navigate('/'); - - } catch (error) { - dismissToast(id); - showToast("Failed to submit cancellation", "error"); - console.error('❌ Failed to reject plan:', error); - navigate('/'); - } finally { - setProcessingApproval(false); - } - }, [planApprovalRequest, planData, onPlanApproval, navigate]); + + + + + if (!planData) return ( diff --git a/src/frontend/src/components/content/PlanPanelLeft.tsx b/src/frontend/src/components/content/PlanPanelLeft.tsx index 7e86971d6..3cb75676f 100644 --- a/src/frontend/src/components/content/PlanPanelLeft.tsx +++ b/src/frontend/src/components/content/PlanPanelLeft.tsx @@ -30,6 +30,8 @@ import PanelUserCard from "../../coral/components/Panels/UserCard"; import { getUserInfoGlobal } from "@/api/config"; import TeamSelector from "../common/TeamSelector"; import { TeamConfig } from "../../models/Team"; +import TeamSelected from "../common/TeamSelected"; +import TeamService from "@/services/TeamService"; const PlanPanelLeft: React.FC = ({ reloadTasks, @@ -172,13 +174,23 @@ const PlanPanelLeft: React.FC = ({ {/* Team Selector right under the toolbar */} +
- + {isHomePage && ( + + )} + + {!isHomePage && ( + + )} +
{ if (!agentMessages?.length) return null; // Filter out messages with empty content - const validMessages = agentMessages.filter(msg => msg.raw_content?.trim()); - + const validMessages = agentMessages.filter(msg => msg.content?.trim()); + const messages = validMessages; if (!validMessages.length) return null; return ( -
- {validMessages.map((message, index) => ( -
-
- {message.agent}: -
-
- - {message.raw_content.trim()} - + + +
+ {messages.map((msg, index) => { + const isHuman = msg.agent_type === AgentMessageType.HUMAN_AGENT; + + return ( +
+ {!isHuman && ( +
+
+ + {TaskService.cleanTextToSpaces(msg.agent)} + + + BOT + +
+
+ )} + + +
+ + {TaskService.cleanHRAgent(msg.content) || ""} + + + {!isHuman && ( +
+
+
+
+ + } + appearance="filled" + size="extra-small" + > + Sample data for demonstration purposes only. + +
+
+ )} +
+
-
- ))} + ); + })}
+ ); }; diff --git a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx index f5bee37f8..5710b891e 100644 --- a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx @@ -12,9 +12,10 @@ const renderBufferMessage = (streamingMessageBuffer: string) => { if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null; - const previewText = streamingMessageBuffer.length > 500 - ? streamingMessageBuffer.substring(0, 500) + "..." - : streamingMessageBuffer; + const start = Math.max(0, streamingMessageBuffer.length - 500); + const previewText = start === 0 + ? streamingMessageBuffer + : "..." + streamingMessageBuffer.substring(start); return (
{ backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusMedium, border: `1px solid ${tokens.colorNeutralStroke1}`, - marginBottom: tokens.spacingVerticalXL + marginBottom: tokens.spacingVerticalXL, + marginTop: tokens.spacingVerticalXL }}> { } } - } else { - throw new Error('Invalid response from init_team endpoint'); } } catch (error) { console.error('Error initializing team from backend:', error); - showToast("Team initialization failed, using fallback", "warning"); + showToast("Team initialization failed", "warning"); // Fallback to the old client-side method - try { - console.log('Using fallback: client-side team loading...'); - const teams = await TeamService.getUserTeams(); - if (teams.length > 0) { - const hrTeam = teams.find(team => team.name === "Human Resources Team"); - const defaultTeam = hrTeam || teams[0]; - setSelectedTeam(defaultTeam); - TeamService.storageTeam(defaultTeam); - showToast( - `${defaultTeam.name} team loaded (fallback mode)`, - "info" - ); - } else { - console.log('No teams found - user needs to upload a team configuration'); - showToast( - "No teams found. Please upload a team configuration.", - "warning" - ); - } - } catch (fallbackError) { - console.error('Fallback team loading also failed:', fallbackError); - showToast("Failed to load team configuration", "error"); - } } finally { setIsLoadingTeam(false); } @@ -155,20 +130,6 @@ const HomePage: React.FC = () => { `${initializedTeam.name} team initialized successfully with ${initializedTeam.agents?.length || 0} agents`, "success" ); - } else { - // Fallback: if we can't find the specific team, use HR team or first available - console.log('Specific team not found, using default selection logic'); - const hrTeam = teams.find(team => team.name === "Human Resources Team"); - const defaultTeam = hrTeam || teams[0]; - - if (defaultTeam) { - setSelectedTeam(defaultTeam); - TeamService.storageTeam(defaultTeam); - showToast( - `${defaultTeam.name} team loaded as default`, - "success" - ); - } } } else { diff --git a/src/frontend/src/pages/PlanPage.tsx b/src/frontend/src/pages/PlanPage.tsx index 4517e384c..5d88755c9 100644 --- a/src/frontend/src/pages/PlanPage.tsx +++ b/src/frontend/src/pages/PlanPage.tsx @@ -45,11 +45,12 @@ const PlanPage: React.FC = () => { const [submittingChatDisableInput, setSubmittingChatDisableInput] = useState(true); const [error, setError] = useState(null); const [clarificationMessage, setClarificationMessage] = useState(null); - + const [processingApproval, setProcessingApproval] = useState(false); const [planApprovalRequest, setPlanApprovalRequest] = useState(null); const [reloadLeftList, setReloadLeftList] = useState(true); const [waitingForPlan, setWaitingForPlan] = useState(true); const [showProcessingPlanSpinner, setShowProcessingPlanSpinner] = useState(false); + const [showApprovalButtons, setShowApprovalButtons] = useState(true); // WebSocket connection state const [wsConnected, setWsConnected] = useState(false); const [streamingMessages, setStreamingMessages] = useState([]); @@ -146,7 +147,7 @@ const PlanPage: React.FC = () => { timestamp: clarificationMessage.timestamp || Date.now(), steps: [], // intentionally always empty next_steps: [], // intentionally always empty - raw_content: clarificationMessage.data.question || '', + content: clarificationMessage.data.question || '', raw_data: clarificationMessage.data || '', } as AgentMessageData; console.log('✅ Parsed clarification message:', agentMessageData); @@ -183,7 +184,7 @@ const PlanPage: React.FC = () => { timestamp: Date.now(), steps: [], // intentionally always empty next_steps: [], // intentionally always empty - raw_content: finalMessage.data.content || '', + content: finalMessage.data.content || '', raw_data: finalMessage.data || '', } as AgentMessageData; console.log('✅ Parsed final result message:', agentMessageData); @@ -362,6 +363,60 @@ const PlanPage: React.FC = () => { loadPlanDataRef.current = loadPlanData; }, [loadPlanData]); + // Handle plan approval + const handleApprovePlan = useCallback(async () => { + if (!planApprovalRequest) return; + + setProcessingApproval(true); + let id = showToast("Submitting Approval", "progress"); + try { + await apiService.approvePlan({ + m_plan_id: planApprovalRequest.id, + plan_id: planData?.plan?.id, + approved: true, + feedback: 'Plan approved by user' + }); + + dismissToast(id); + setShowProcessingPlanSpinner(true); + setShowApprovalButtons(false); + + } catch (error) { + dismissToast(id); + showToast("Failed to submit approval", "error"); + console.error('❌ Failed to approve plan:', error); + } finally { + setProcessingApproval(false); + } + }, [planApprovalRequest, planData, setProcessingApproval]); + + // Handle plan rejection + const handleRejectPlan = useCallback(async () => { + if (!planApprovalRequest) return; + + setProcessingApproval(true); + let id = showToast("Submitting cancellation", "progress"); + try { + await apiService.approvePlan({ + m_plan_id: planApprovalRequest.id, + plan_id: planData?.plan?.id, + approved: false, + feedback: 'Plan rejected by user' + }); + + dismissToast(id); + + navigate('/'); + + } catch (error) { + dismissToast(id); + showToast("Failed to submit cancellation", "error"); + console.error('❌ Failed to reject plan:', error); + navigate('/'); + } finally { + setProcessingApproval(false); + } + }, [planApprovalRequest, planData, navigate, setProcessingApproval]); // Chat submission handler - updated for v3 backend compatibility const handleOnchatSubmit = useCallback( async (chatInput: string) => { @@ -395,7 +450,7 @@ const PlanPage: React.FC = () => { timestamp: Date.now(), steps: [], // intentionally always empty next_steps: [], // intentionally always empty - raw_content: chatInput || '', + content: chatInput || '', raw_data: chatInput || '', } as AgentMessageData; @@ -518,13 +573,16 @@ const PlanPage: React.FC = () => { streamingMessages={streamingMessages} wsConnected={wsConnected} onPlanApproval={(approved) => setPlanApproved(approved)} - onPlanProcessing={(showProcessingPlanSpinner) => setShowProcessingPlanSpinner(showProcessingPlanSpinner)} planApprovalRequest={planApprovalRequest} waitingForPlan={waitingForPlan} messagesContainerRef={messagesContainerRef} streamingMessageBuffer={streamingMessageBuffer} agentMessages={agentMessages} showProcessingPlanSpinner={showProcessingPlanSpinner} + showApprovalButtons={showApprovalButtons} + processingApproval={processingApproval} + handleApprovePlan={handleApprovePlan} + handleRejectPlan={handleRejectPlan} /> diff --git a/src/frontend/src/services/PlanDataService.tsx b/src/frontend/src/services/PlanDataService.tsx index d70e2acf9..1449cfe9e 100644 --- a/src/frontend/src/services/PlanDataService.tsx +++ b/src/frontend/src/services/PlanDataService.tsx @@ -376,7 +376,7 @@ export class PlanDataService { raw_block: string; }>; next_steps: string[]; - raw_content: string; + content: string; raw_data: any; } | null { try { @@ -401,9 +401,9 @@ export class PlanDataService { // Extract content='...' const contentMatch = source.match(/content='((?:\\'|[^'])*)'/); - let raw_content = contentMatch ? contentMatch[1] : ''; + let content = contentMatch ? contentMatch[1] : ''; // Unescape - raw_content = raw_content + content = content .replace(/\\n/g, '\n') .replace(/\\'/g, "'") .replace(/\\"/g, '"') @@ -411,7 +411,7 @@ export class PlanDataService { // Parse sections of the form "##### Title Completed" // Each block ends at --- line or next "##### " or end. - const lines = raw_content.split('\n'); + const lines = content.split('\n'); const steps: Array<{ title: string; fields: Record; summary?: string; raw_block: string; }> = []; let i = 0; while (i < lines.length) { @@ -473,7 +473,7 @@ export class PlanDataService { timestamp, steps, next_steps: nextSteps, - raw_content, + content, raw_data: rawData }; } catch (e) {