1+ #!/usr/bin/env python3
2+ """
3+ Agent Status Widget SAFE VERSION - Displays agent overview information
4+
5+ SAFE VERSION: Uses QGridLayout instead of nested QHBoxLayout inside QVBoxLayout
6+ to avoid macOS Qt layout calculation crash in external terminals.
7+
8+ Shows the current agent state, connection status, conversation metrics,
9+ and general system health in a compact overview panel.
10+ """
11+
12+ import time
13+ from datetime import datetime , timedelta
14+ from collections import deque
15+
16+ from python_qt_binding .QtWidgets import (
17+ QWidget , QVBoxLayout , QHBoxLayout , QLabel ,
18+ QFrame , QGridLayout , QProgressBar
19+ )
20+ from python_qt_binding .QtCore import Qt , QTimer
21+ from python_qt_binding .QtGui import QFont , QPalette , QColor
22+
23+ from coffee_voice_agent_msgs .msg import AgentStatus
24+
25+
26+ class AgentStatusWidgetSafe (QWidget ):
27+ """SAFE VERSION: Widget for displaying agent overview and status information
28+
29+ Uses QGridLayout instead of nested QHBoxLayouts to avoid macOS crash.
30+ """
31+
32+ # State colors for visual indication
33+ STATE_COLORS = {
34+ 'dormant' : '#dc3545' , # Red
35+ 'connecting' : '#ffc107' , # Yellow
36+ 'active' : '#28a745' , # Green
37+ 'speaking' : '#007bff' , # Blue
38+ 'disconnecting' : '#6c757d' # Gray
39+ }
40+
41+ def __init__ (self ):
42+ super ().__init__ ()
43+ self .setFixedSize (350 , 250 )
44+
45+ # Data tracking
46+ self .current_status = None
47+ self .connection_status = False
48+ self .session_start_time = None
49+ self .conversation_count = 0
50+ self .uptime_start = datetime .now ()
51+
52+ # Status history for analytics
53+ self .status_history = deque (maxlen = 100 )
54+
55+ self ._setup_ui ()
56+
57+ # Update timer for dynamic elements
58+ self .update_timer = QTimer ()
59+ self .update_timer .timeout .connect (self ._update_dynamic_elements )
60+ self .update_timer .start (1000 ) # Update every second
61+
62+ def _setup_ui (self ):
63+ """Set up the widget UI using SAFE layout patterns"""
64+ layout = QVBoxLayout ()
65+ self .setLayout (layout )
66+
67+ # Title
68+ title = QLabel ("🤖 AGENT STATUS (SAFE)" )
69+ title .setAlignment (Qt .AlignCenter )
70+ font = QFont ()
71+ font .setBold (True )
72+ font .setPointSize (12 )
73+ title .setFont (font )
74+ layout .addWidget (title )
75+
76+ # Status frame - KEEP this as it works fine
77+ self .status_frame = QFrame ()
78+ self .status_frame .setFrameStyle (QFrame .Box )
79+ layout .addWidget (self .status_frame )
80+
81+ # SAFE CHANGE: Use QGridLayout instead of QVBoxLayout with nested QHBoxLayouts
82+ status_layout = QGridLayout ()
83+ self .status_frame .setLayout (status_layout )
84+
85+ # Row 0: Current state display
86+ status_layout .addWidget (QLabel ("State:" ), 0 , 0 )
87+ self .state_label = QLabel ("UNKNOWN" )
88+ self .state_label .setAlignment (Qt .AlignCenter )
89+ # SAFE CHANGE: Use QFont.setBold() instead of font-weight in stylesheet
90+ font = self .state_label .font ()
91+ font .setBold (True )
92+ self .state_label .setFont (font )
93+ self .state_label .setStyleSheet ("padding: 4px 8px; border-radius: 4px;" ) # No font-weight
94+ status_layout .addWidget (self .state_label , 0 , 1 )
95+
96+ # Row 1: Connection status
97+ status_layout .addWidget (QLabel ("Connection:" ), 1 , 0 )
98+ self .connection_label = QLabel ("❌ Disconnected" )
99+ status_layout .addWidget (self .connection_label , 1 , 1 )
100+
101+ # Row 2: Session info
102+ status_layout .addWidget (QLabel ("Session:" ), 2 , 0 )
103+ self .session_label = QLabel ("No active session" )
104+ status_layout .addWidget (self .session_label , 2 , 1 )
105+
106+ # Row 3: Uptime
107+ status_layout .addWidget (QLabel ("Uptime:" ), 3 , 0 )
108+ self .uptime_label = QLabel ("00:00:00" )
109+ status_layout .addWidget (self .uptime_label , 3 , 1 )
110+
111+ # Row 4: Separator - span across both columns
112+ separator = QFrame ()
113+ separator .setFrameShape (QFrame .HLine )
114+ separator .setFrameShadow (QFrame .Sunken )
115+ status_layout .addWidget (separator , 4 , 0 , 1 , 2 ) # span 2 columns
116+
117+ # Row 5-7: Additional info - continue with grid layout
118+ status_layout .addWidget (QLabel ("Phase:" ), 5 , 0 )
119+ self .phase_label = QLabel ("-" )
120+ status_layout .addWidget (self .phase_label , 5 , 1 )
121+
122+ status_layout .addWidget (QLabel ("Last Tool:" ), 6 , 0 )
123+ self .tool_label = QLabel ("-" )
124+ status_layout .addWidget (self .tool_label , 6 , 1 )
125+
126+ status_layout .addWidget (QLabel ("Conversations:" ), 7 , 0 )
127+ self .conversation_count_label = QLabel ("0" )
128+ status_layout .addWidget (self .conversation_count_label , 7 , 1 )
129+
130+ def update_status (self , status : AgentStatus ):
131+ """Update the widget with new agent status"""
132+ self .current_status = status
133+ self .status_history .append ((datetime .now (), status ))
134+
135+ # Update state display with color coding
136+ state = status .behavioral_mode .upper ()
137+ self .state_label .setText (state )
138+
139+ # Set state color - SAFE: Keep stylesheet simple, no font-weight
140+ color = self .STATE_COLORS .get (status .behavioral_mode , '#6c757d' )
141+ self .state_label .setStyleSheet (f"""
142+ padding: 4px 8px;
143+ border-radius: 4px;
144+ background-color: { color } ;
145+ color: white;
146+ """ )
147+
148+ # Update conversation phase
149+ phase = status .conversation_phase if status .conversation_phase else "idle"
150+ self .phase_label .setText (phase .title ())
151+
152+ # Update last tool used
153+ tool = status .last_tool_used if status .last_tool_used else "none"
154+ self .tool_label .setText (tool )
155+
156+ # Track session changes
157+ if status .behavioral_mode in ['active' , 'speaking' ]:
158+ if self .session_start_time is None :
159+ self .session_start_time = datetime .now ()
160+ self .conversation_count += 1
161+ self .conversation_count_label .setText (str (self .conversation_count ))
162+ elif status .behavioral_mode == 'dormant' :
163+ if self .session_start_time is not None :
164+ self .session_start_time = None
165+
166+ def update_connection (self , connected : bool ):
167+ """Update connection status"""
168+ self .connection_status = connected
169+
170+ if connected :
171+ self .connection_label .setText ("✅ Connected" )
172+ self .connection_label .setStyleSheet ("color: green;" )
173+ else :
174+ self .connection_label .setText ("❌ Disconnected" )
175+ self .connection_label .setStyleSheet ("color: red;" )
176+
177+ def _update_dynamic_elements (self ):
178+ """Update time-based elements"""
179+ # Update uptime
180+ uptime = datetime .now () - self .uptime_start
181+ uptime_str = str (uptime ).split ('.' )[0 ] # Remove microseconds
182+ self .uptime_label .setText (uptime_str )
183+
184+ # Update session duration
185+ if self .session_start_time :
186+ session_duration = datetime .now () - self .session_start_time
187+ session_str = str (session_duration ).split ('.' )[0 ]
188+ self .session_label .setText (f"Active: { session_str } " )
189+ else :
190+ self .session_label .setText ("No active session" )
191+
192+ def get_analytics_data (self ):
193+ """Get analytics data for the analytics widget"""
194+ return {
195+ 'uptime' : datetime .now () - self .uptime_start ,
196+ 'conversation_count' : self .conversation_count ,
197+ 'current_state' : self .current_status .behavioral_mode if self .current_status else 'unknown' ,
198+ 'connection_status' : self .connection_status ,
199+ 'status_history' : list (self .status_history ),
200+ 'session_active' : self .session_start_time is not None
201+ }
0 commit comments