diff --git a/distiller_cm5_python/client/ui/App.py b/distiller_cm5_python/client/ui/App.py index 8706211..83d3be8 100644 --- a/distiller_cm5_python/client/ui/App.py +++ b/distiller_cm5_python/client/ui/App.py @@ -397,11 +397,6 @@ def handle_quit(self): if config.get("display").get("eink_enabled"): self._cleanup_eink() - # Disconnect SAM - if self.sam: - self.sam.disconnect() - self.sam = None - # Schedule bridge shutdown try: self.bridge.shutdown() @@ -602,12 +597,6 @@ def _emergency_exit_handler(self): except Exception as e: logger.error(f"Failed to send emergency shutdown signal to UART: {e}") - try: - # Disconnect SAM if possible - if self.sam: - self.sam.disconnect() - except Exception: - pass # Ignore errors during emergency exit try: # Make one final attempt to flush logs import logging diff --git a/distiller_cm5_python/client/ui/Components/ConversationView.qml b/distiller_cm5_python/client/ui/Components/ConversationView.qml index 704eaad..5330759 100644 --- a/distiller_cm5_python/client/ui/Components/ConversationView.qml +++ b/distiller_cm5_python/client/ui/Components/ConversationView.qml @@ -1,83 +1,181 @@ import QtQuick +import QtQuick.Layouts ListView { id: conversationView - // Properties for state tracking - simplified + // Core properties property bool responseInProgress: false - // Track if a response is being generated property bool navigable: true - // Make focusable for keyboard navigation property bool visualFocus: false - // For focus management property bool scrollModeActive: false - // Track if scroll mode is active - // Expose scrolling animation for FocusManager property alias scrollAnimation: smoothScrollAnimation + property int currentMessageIndex: -1 + property var messagePaginationInfo: ({}) + property int currentPaginationPage: 0 - // Signal to notify when scroll mode changes + // Signal for scroll mode changes signal scrollModeChanged(bool active) - // Update function for external callers + // Set response progress and scroll to bottom when needed function setResponseInProgress(inProgress) { - // Force scroll to bottom when response starts - responseInProgress = inProgress; if (inProgress) positionViewAtEnd(); - } - // Force scroll to bottom immediately + // Force scroll to bottom function scrollToBottom() { positionViewAtEnd(); } - // Method to update the model and scroll to bottom conditionally + // Update model and handle scrolling function updateModel(newModel) { - // Record current position state var wasAtEnd = atYEnd; - // Update model model = newModel; - // Position view based on context if (responseInProgress || wasAtEnd) positionViewAtEnd(); + // Reset pagination info + messagePaginationInfo = {}; + currentPaginationPage = 0; + // Calculate pagination for new model + Qt.callLater(calculateMessagePagination); + } + + // Calculate pagination for large messages + function calculateMessagePagination() { + messagePaginationInfo = {}; + for (var i = 0; i < count; i++) { + var item = itemAtIndex(i); + if (item && item.height > height * 0.8) { + // If message is larger than 80% of visible area + var pages = Math.ceil(item.height / (height * 0.8)); + messagePaginationInfo[i] = pages; + } + } + } + + // Ensure pagination is calculated for current index + function ensureCurrentMessagePagination() { + if (currentMessageIndex >= 0 && currentMessageIndex < count) { + var item = itemAtIndex(currentMessageIndex); + if (item && item.height > height * 0.8) { + var pages = Math.ceil(item.height / (height * 0.8)); + messagePaginationInfo[currentMessageIndex] = pages; + } + } + } + + // Navigate to next message or page + function navigateDown() { + // Already at the last message and page, do nothing + if (currentMessageIndex >= count - 1 && currentPaginationPage >= (messagePaginationInfo[currentMessageIndex] || 0) - 1) { + return; + } + + // Ensure pagination is calculated for current message + ensureCurrentMessagePagination(); + + // Has pagination and not at last page + if (messagePaginationInfo[currentMessageIndex] && currentPaginationPage < messagePaginationInfo[currentMessageIndex] - 1) { + // Next page within same message + currentPaginationPage++; + var item = itemAtIndex(currentMessageIndex); + if (item) { + var pageSize = height * 0.8; + var targetY = item.y + (currentPaginationPage * pageSize); + contentY = Math.min(targetY, contentHeight - height); + } + } else { + // Next message + currentMessageIndex = Math.min(currentMessageIndex + 1, count - 1); + currentPaginationPage = 0; + positionViewAtIndex(currentMessageIndex, 0); + // Ensure pagination is calculated for the new current message + ensureCurrentMessagePagination(); + } + } + // Navigate to previous message or page + function navigateUp() { + // Already at first message and page, do nothing + if (currentMessageIndex <= 0 && currentPaginationPage <= 0) { + return; + } + + // Ensure pagination is calculated for current message + ensureCurrentMessagePagination(); + + // On a paginated message and not at first page + if (currentPaginationPage > 0) { + // Previous page within same message + currentPaginationPage--; + var item = itemAtIndex(currentMessageIndex); + if (item) { + var pageSize = height * 0.8; + var targetY = item.y + (currentPaginationPage * pageSize); + contentY = Math.min(targetY, contentHeight - height); + } + } else { + // Previous message + currentMessageIndex = Math.max(currentMessageIndex - 1, 0); + currentPaginationPage = 0; + positionViewAtIndex(currentMessageIndex, 0); + // Ensure pagination is calculated for the new current message + ensureCurrentMessagePagination(); + } } + // ListView configuration objectName: "conversationView" focus: visualFocus clip: true spacing: ThemeManager.spacingSmall interactive: true boundsBehavior: Flickable.StopAtBounds - // Use ListView's built-in positioning features + + // Auto-scroll handling onContentHeightChanged: { if (responseInProgress || atYEnd) positionViewAtEnd(); - + Qt.callLater(calculateMessagePagination); + if (scrollModeActive && currentMessageIndex >= 0) { + // Recalculate for current message if we're in scroll mode + Qt.callLater(ensureCurrentMessagePagination); + } } - // Automatically scroll to the end when model changes during a response + onModelChanged: { if (responseInProgress || atYEnd || count === 0) positionViewAtEnd(); - + currentMessageIndex = -1; + currentPaginationPage = 0; } - // Simplified scroll mode handling + + // Scroll mode handling onScrollModeActiveChanged: { activeScrollModeInstructions.visible = scrollModeActive; + if (scrollModeActive && currentMessageIndex === -1 && count > 0) { + // Initialize to last message + currentMessageIndex = count - 1; + positionViewAtIndex(currentMessageIndex, 0); + // Ensure pagination is calculated for the initial current message + Qt.callLater(ensureCurrentMessagePagination); + } else if (!scrollModeActive) { + // Reset when exiting + currentMessageIndex = -1; + currentPaginationPage = 0; + } } - // Add keyboard handling for scroll mode - Keys.onPressed: function(event) { + + // Key navigation handling + Keys.onPressed: function (event) { if (scrollModeActive) { - var scrollAmount = 50; // Pixels to scroll per key press if (event.key === Qt.Key_Down) { - // Simplified scrolling down with bounds protection - contentY = Math.min(contentY + scrollAmount, Math.max(0, contentHeight - height)); + navigateDown(); event.accepted = true; } else if (event.key === Qt.Key_Up) { - // Simplified scrolling up with bounds protection - contentY = Math.max(contentY - scrollAmount, 0); + navigateUp(); event.accepted = true; } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { // Exit scroll mode @@ -86,7 +184,7 @@ ListView { event.accepted = true; } } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - // Enter scroll mode if there's content to scroll + // Enter scroll mode if needed if (contentHeight > height) { FocusManager.enterScrollMode(); scrollModeChanged(true); @@ -95,25 +193,23 @@ ListView { } } - // Animation with zero duration for compatibility with code expecting the animation + // Zero-duration animation for compatibility NumberAnimation { id: smoothScrollAnimation - target: conversationView property: "contentY" duration: 0 // No animation for e-ink easing.type: Easing.Linear } - // Visual instruction when in focus but not in scroll mode + // Entry scroll instruction Rectangle { id: scrollModeInstructions - anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: ThemeManager.spacingNormal - height: scrollModeText.height + ThemeManager.spacingLarge - width: scrollModeText.width + ThemeManager.spacingSmall * 3 + width: parent.width + height: scrollInstructionLayout.height + ThemeManager.spacingLarge color: ThemeManager.textColor border.width: ThemeManager.borderWidth border.color: ThemeManager.textColor @@ -121,26 +217,35 @@ ListView { visible: visualFocus && !scrollModeActive && conversationView.contentHeight > conversationView.height z: 2 - Text { - id: scrollModeText - + ColumnLayout { + id: scrollInstructionLayout anchors.centerIn: parent - text: "Press Enter to enable scroll mode" - color: ThemeManager.backgroundColor - font: FontManager.small - } + width: parent.width - ThemeManager.spacingNormal * 2 + spacing: 0 + Text { + id: scrollModeText + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + text: "Press Enter to enable scroll mode" + color: ThemeManager.backgroundColor + font: FontManager.small + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideNone + maximumLineCount: 2 + } + } } - // Visual instruction when in scroll mode + // Active scroll instruction Rectangle { id: activeScrollModeInstructions - anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: ThemeManager.spacingNormal - height: activeScrollModeText.height + ThemeManager.spacingLarge - width: activeScrollModeText.width + ThemeManager.spacingSmall * 3 + width: parent.width + height: activescrollInstructionLayout.height + ThemeManager.spacingLarge color: ThemeManager.textColor border.width: ThemeManager.borderWidth border.color: ThemeManager.textColor @@ -148,23 +253,65 @@ ListView { visible: scrollModeActive z: 2 - Text { - id: activeScrollModeText - + ColumnLayout { + id: activescrollInstructionLayout anchors.centerIn: parent - text: "Use ↑/↓ to scroll, Enter to exit" - color: ThemeManager.backgroundColor - font: FontManager.small - } + width: parent.width - ThemeManager.spacingNormal * 2 + spacing: 0 + Text { + id: activeScrollModeText + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + text: { + // Ensure we refresh pagination info when showing text + if (scrollModeActive && currentMessageIndex >= 0) { + ensureCurrentMessagePagination(); + } + + if (messagePaginationInfo[currentMessageIndex] && messagePaginationInfo[currentMessageIndex] > 1) { + return "Message " + (currentMessageIndex + 1) + "/" + count + " (Page " + (currentPaginationPage + 1) + "/" + messagePaginationInfo[currentMessageIndex] + "), Enter to exit"; + } else { + return "Message " + (currentMessageIndex + 1) + "/" + count + ", Enter to exit"; + } + } + color: ThemeManager.backgroundColor + font: FontManager.small + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideNone + maximumLineCount: 2 + } + } } - // Delegate for message items + // Message item delegate delegate: MessageItem { + id: delegateItem width: ListView.view.width messageText: typeof modelData === "string" ? modelData : "" isLastMessage: index === conversationView.count - 1 isResponding: conversationView.responseInProgress && index === conversationView.count - 1 + navigable: !conversationView.scrollModeActive + + // Current message highlight + Rectangle { + anchors.fill: parent + visible: conversationView.scrollModeActive && conversationView.currentMessageIndex === index + color: ThemeManager.transparentColor + border.color: ThemeManager.white + border.width: ThemeManager.borderWidth + radius: ThemeManager.borderRadius + } + + onClicked: { + if (!conversationView.scrollModeActive) { + conversationView.positionViewAtIndex(index, 0); + } + } } + Component.onCompleted: { + Qt.callLater(calculateMessagePagination); + } } diff --git a/distiller_cm5_python/client/ui/Components/FocusManager.qml b/distiller_cm5_python/client/ui/Components/FocusManager.qml index e471381..7db9760 100644 --- a/distiller_cm5_python/client/ui/Components/FocusManager.qml +++ b/distiller_cm5_python/client/ui/Components/FocusManager.qml @@ -34,7 +34,7 @@ QtObject { property int currentFocusIndex: -1 // Current scroll view - can be set by pages to enable scroll support property var currentScrollView: null - // Scroll step size + // Scroll step size - this is now used only for non-ConversationView scrolling property int scrollStep: 40 // Track scroll mode specifically for ConversationView property bool scrollModeActive: false @@ -182,10 +182,16 @@ QtObject { // Enter scroll mode for ConversationView function enterScrollMode(item) { if (!item) - return ; + return; scrollModeActive = true; scrollTargetItem = item; + + // Initialize current message index if it's ConversationView + if (item.objectName === "conversationView" && item.currentMessageIndex === -1 && item.count > 0) { + item.currentMessageIndex = item.count - 1; + item.positionViewAtIndex(item.currentMessageIndex, 0); + } } // Exit scroll mode @@ -203,7 +209,6 @@ QtObject { // Force one more update as a failsafe if (oldTarget) oldTarget.scrollModeActive = false; - } scrollModeActive = false; } @@ -212,21 +217,24 @@ QtObject { function moveFocusUp() { // If in scroll mode, scroll the target view instead of changing focus if (scrollModeActive && scrollTargetItem) { - if (scrollTargetItem.contentY !== undefined) { + if (scrollTargetItem.objectName === "conversationView") { + // Use the ConversationView's custom navigation for messages + scrollTargetItem.navigateUp(); + } else if (scrollTargetItem.contentY !== undefined) { + // Standard scrolling for other types of scroll views var newY = Math.max(0, scrollTargetItem.contentY - scrollStep); scrollTargetItem.contentY = newY; if (scrollTargetItem.checkIfAtBottom) scrollTargetItem.checkIfAtBottom(); - } - return ; + return; } if (currentMode === sliderMode) { // In slider mode, increase the value if (currentFocusItems[currentFocusIndex] && currentFocusItems[currentFocusIndex].increaseValue) currentFocusItems[currentFocusIndex].increaseValue(); - return ; + return; } // Make sure we have focusable items before proceeding if (currentFocusItems.length === 0) { @@ -235,18 +243,18 @@ QtObject { if (currentScrollView) scrollUp(); - return ; + return; } // Priority #1: Navigate to previous item first (if not at first item) if (currentFocusIndex > 0) { currentFocusIndex--; setFocusToItem(currentFocusItems[currentFocusIndex]); - return ; + return; } // Priority #2: If at first item, try to scroll if not at top if (currentScrollView && currentScrollView.contentItem.contentY > 0) { scrollUp(); - return ; + return; } // Priority #3: If at first item and top of scroll, wrap to last item currentFocusIndex = currentFocusItems.length - 1; @@ -256,7 +264,7 @@ QtObject { // Helper function to scroll up function scrollUp() { if (!currentScrollView) - return ; + return; var newY = Math.max(0, currentScrollView.contentItem.contentY - scrollStep); if (currentScrollView.scrollAnimation) { @@ -273,22 +281,25 @@ QtObject { function moveFocusDown() { // If in scroll mode, scroll the target view instead of changing focus if (scrollModeActive && scrollTargetItem) { - if (scrollTargetItem.contentY !== undefined) { + if (scrollTargetItem.objectName === "conversationView") { + // Use the ConversationView's custom navigation for messages + scrollTargetItem.navigateDown(); + } else if (scrollTargetItem.contentY !== undefined) { + // Standard scrolling for other types of scroll views var maxY = Math.max(0, scrollTargetItem.contentHeight - scrollTargetItem.height); var newY = Math.min(maxY, scrollTargetItem.contentY + scrollStep); scrollTargetItem.contentY = newY; if (scrollTargetItem.checkIfAtBottom) scrollTargetItem.checkIfAtBottom(); - } - return ; + return; } if (currentMode === sliderMode) { // In slider mode, decrease the value if (currentFocusItems[currentFocusIndex] && currentFocusItems[currentFocusIndex].decreaseValue) currentFocusItems[currentFocusIndex].decreaseValue(); - return ; + return; } // Make sure we have focusable items before proceeding if (currentFocusItems.length === 0) { @@ -297,20 +308,20 @@ QtObject { if (currentScrollView) scrollDown(); - return ; + return; } // Priority #1: Navigate to next item first (if not at last item) if (currentFocusIndex < currentFocusItems.length - 1) { currentFocusIndex++; setFocusToItem(currentFocusItems[currentFocusIndex]); - return ; + return; } // Priority #2: If at last item, try to scroll if not at bottom if (currentScrollView) { var maxY = Math.max(0, currentScrollView.contentItem.contentHeight - currentScrollView.height); if (currentScrollView.contentItem.contentY < maxY) { scrollDown(); - return ; + return; } } // Priority #3: If at last item and bottom of scroll, wrap to first item diff --git a/distiller_cm5_python/client/ui/Components/MessageItem.qml b/distiller_cm5_python/client/ui/Components/MessageItem.qml index 52759b4..f10f733 100644 --- a/distiller_cm5_python/client/ui/Components/MessageItem.qml +++ b/distiller_cm5_python/client/ui/Components/MessageItem.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Layouts -Rectangle { +NavigableItem { id: messageItem property string messageText: "" @@ -18,20 +18,26 @@ Rectangle { width: parent.width height: messageLayout.implicitHeight + ThemeManager.spacingNormal - radius: ThemeManager.borderRadius - color: ThemeManager.backgroundColor - border.color: ThemeManager.black // Always black border for contrast - border.width: ThemeManager.borderWidth // Don't show the message if it's empty or only contains timestamp brackets visible: messageText !== "" && content.trim() !== "" + // Background rectangle for message + Rectangle { + id: messageBackground + anchors.fill: parent + radius: ThemeManager.borderRadius + color: messageItem.backgroundColor + border.color: ThemeManager.black // Always black border for contrast + border.width: ThemeManager.borderWidth + } + // Subtle highlight effect for the latest message during response generation // Only applied to the actual message rectangle, not extending beyond it Rectangle { id: highlightEffect anchors.fill: parent - radius: parent.radius + radius: messageBackground.radius color: isLastMessage && isResponding ? ThemeManager.black : ThemeManager.transparentColor // Solid black highlight instead of subtle color visible: isLastMessage && isResponding z: -1 // Behind text content @@ -49,7 +55,7 @@ Rectangle { Text { text: sender font: FontManager.small - color: ThemeManager.textColor + color: messageItem.textColor Layout.fillWidth: true visible: sender !== "" } @@ -58,7 +64,7 @@ Rectangle { MarkdownText { markdownText: content textFont: FontManager.normal - textColor: ThemeManager.textColor + textColor: messageItem.textColor Layout.fillWidth: true Layout.preferredHeight: implicitHeight } @@ -70,7 +76,7 @@ Rectangle { // Display the message type at the bottom left Text { text: messageType - color: ThemeManager.textColor + color: messageItem.textColor horizontalAlignment: Text.AlignLeft Layout.fillWidth: true visible: messageType !== "" @@ -83,7 +89,6 @@ Rectangle { switch (messageType.toLowerCase()) { case "error": case "warning": - case "ssh info": return Font.Bold; default: return Font.Normal; @@ -109,7 +114,7 @@ Rectangle { Text { text: timestamp font: FontManager.small - color: ThemeManager.textColor + color: messageItem.textColor horizontalAlignment: Text.AlignRight Layout.fillWidth: true visible: timestamp !== "" diff --git a/distiller_cm5_python/client/ui/Components/ThemeManager.qml b/distiller_cm5_python/client/ui/Components/ThemeManager.qml index 94ad448..78df3c0 100644 --- a/distiller_cm5_python/client/ui/Components/ThemeManager.qml +++ b/distiller_cm5_python/client/ui/Components/ThemeManager.qml @@ -45,15 +45,15 @@ QtObject { // Initialize theme from bridge settings function initializeTheme() { - if (!themeCached && bridge && bridge.ready) { - var savedTheme = bridge.getConfigValue("display", "dark_mode"); - if (savedTheme !== "") - setDarkMode(savedTheme === "true" || savedTheme === "True"); - - themeCached = true; - return true; - } - return false; + // if (!themeCached && bridge && bridge.ready) { + // var savedTheme = bridge.getConfigValue("display", "dark_mode"); + // if (savedTheme !== "") + // setDarkMode(savedTheme === "true" || savedTheme === "True"); + // + // themeCached = true; + // return true; + // } + // return false; } // Helper function to get theme-appropriate icon path diff --git a/distiller_cm5_python/client/ui/Components/VoiceAssistantPageHeader.qml b/distiller_cm5_python/client/ui/Components/VoiceAssistantPageHeader.qml index 81b8652..76f6a59 100644 --- a/distiller_cm5_python/client/ui/Components/VoiceAssistantPageHeader.qml +++ b/distiller_cm5_python/client/ui/Components/VoiceAssistantPageHeader.qml @@ -7,7 +7,6 @@ Rectangle { property string serverName: "NO SERVER" property string statusText: "Ready" property bool isConnected: false - property bool showStatusText: false property bool wifiConnected: false property string ipAddress: "" property string wifiName: "" @@ -17,7 +16,6 @@ Rectangle { property alias closeButton: closeBtn property alias statusButton: statusBtn // System stats properties - property bool showSystemStats: bridge && bridge.ready ? bridge.getShowSystemStats() : true property var systemStats: { "cpu": "N/A", "ram": "N/A", @@ -51,7 +49,7 @@ Rectangle { // Update system stats from bridge function updateSystemStats() { - if (bridge && bridge.ready && showSystemStats) { + if (bridge && bridge.ready) { systemStats = bridge.getSystemStats(); // Also update WiFi status when updating stats updateWifiStatus(); diff --git a/distiller_cm5_python/client/ui/VoiceAssistantPage.qml b/distiller_cm5_python/client/ui/VoiceAssistantPage.qml index 7178865..69ea8ed 100644 --- a/distiller_cm5_python/client/ui/VoiceAssistantPage.qml +++ b/distiller_cm5_python/client/ui/VoiceAssistantPage.qml @@ -14,7 +14,7 @@ PageBase { property bool isListening: state === "listening" property bool isProcessing: ["processing", "thinking", "toolExecution", "cacheRestoring"].includes(state) property bool isServerConnected: bridge && bridge.ready ? bridge.isConnected : false - property string statusText: conversationView && conversationView.scrollModeActive ? "Scroll Mode (↑↓ to scroll)" : _statusText + property string statusText: conversationView && conversationView.scrollModeActive ? "Scroll Mode" : _statusText property string _statusText: getStatusTextForState(state) property var focusableItems: [] property var previousFocusedItem: null @@ -584,20 +584,6 @@ PageBase { transcriptionInProgress = false; } - // Handler for SSH information events - function onSshInfoReceived(content, eventId, timestamp) { - console.log("SSH Info received: " + content); - // Add to conversation if not empty - if (content && content.trim().length > 0) { - // Update the UI to show SSH connection info - if (conversationView) - conversationView.updateModel(bridge.get_conversation()); - - // Show a toast notification for the SSH info - messageToast.showMessage("SSH Connection Info: " + content, 5000); - } - } - // Handler for function events function onFunctionReceived(content, eventId, timestamp) { console.log("Function info received: " + content); @@ -771,7 +757,7 @@ PageBase { serverName: _serverName statusText: voiceAssistantPage.statusText isConnected: bridge && bridge.ready ? bridge.isConnected : false - showStatusText: true + Component.onCompleted: { // Add high contrast border for visibility var headerRect = findChild(header, "headerBackground"); @@ -1120,7 +1106,7 @@ PageBase { dialogTitle: "Server Connection" message: "Server connection lost. Do you want to reconnect?" standardButtonTypes: DialogButtonBox.Yes | DialogButtonBox.No - yesButtonText: "Reconnect" + yesButtonText: "Sure" noButtonText: "Cancel" acceptButtonColor: ThemeManager.backgroundColor onAccepted: { diff --git a/distiller_cm5_python/client/ui/bridge/ConversationManager.py b/distiller_cm5_python/client/ui/bridge/ConversationManager.py index c838edc..c1a35c5 100644 --- a/distiller_cm5_python/client/ui/bridge/ConversationManager.py +++ b/distiller_cm5_python/client/ui/bridge/ConversationManager.py @@ -70,24 +70,6 @@ def get_messages(self): """ return self._conversation - def get_messages_copy(self): - """Get a copy of the conversation messages. - - Returns: - A copy of the internal conversation list - """ - return self._conversation.copy() - - def set_messages(self, messages): - """Set the conversation messages list. - - Args: - messages: The new conversation list to use - """ - self._conversation = messages - # Immediate update for set operation (not batched) - self._bridge.conversationChanged.emit() - def get_formatted_messages(self): """Format the conversation messages for display in the UI. diff --git a/distiller_cm5_python/client/ui/bridge/MCPClientBridge.py b/distiller_cm5_python/client/ui/bridge/MCPClientBridge.py index d1a6a92..976e8e4 100644 --- a/distiller_cm5_python/client/ui/bridge/MCPClientBridge.py +++ b/distiller_cm5_python/client/ui/bridge/MCPClientBridge.py @@ -139,8 +139,6 @@ def __init__(self, parent=None): # MCPClientBridge-specific caches self._last_server_discovery_time = 0 self._server_discovery_cache_timeout = 5 # seconds - self._config_cache = {} - self._config_dirty = False # Audio recording and transcription methods override the base class # to provide App instance-specific functionality @@ -178,33 +176,6 @@ def _handle_event(self, event: Union[dict, object]) -> None: self.event_handler.handle_event(event) # MCPClientBridge-specific methods not present in BridgeCore - @pyqtSlot(result=str) - def getWifiMacAddress(self): - """Get the WiFi MAC address of the system.""" - try: - return self.network_utils.get_wifi_mac_address() - except Exception as e: - logger.error(f"Error getting WiFi MAC address: {e}") - return "Error getting MAC address" - - @pyqtSlot(result=str) - def getWifiSignalStrength(self): - """Get the WiFi signal strength.""" - try: - return self.network_utils.get_wifi_signal_strength() - except Exception as e: - logger.error(f"Error getting WiFi signal strength: {e}") - return "Error getting signal strength" - - @pyqtSlot(result="QVariant") - def getNetworkDetails(self): - """Get detailed information about the network.""" - try: - return self.network_utils.get_network_details() - except Exception as e: - logger.error(f"Error getting network details: {e}") - return {"error": "Failed to get network details"} - def _on_connection_changed(self, value): """Handle connection state changes from the connection manager""" self.is_connected = value # This will emit the signal @@ -230,35 +201,17 @@ def getPrimaryFontPath(self): logger.error(f"Error getting primary font path: {e}") return "fonts/MonoramaNerdFont-Medium.ttf" # Default fallback - @pyqtSlot(result=bool) - def getShowSystemStats(self): - """Check if system stats display is enabled in config.""" - try: - # Import here to avoid circular imports - from distiller_cm5_python.client.ui.display_config import ( - config as display_config, - ) - - if ( - "display" in display_config - and "show_system_stats" in display_config["display"] - ): - return display_config["display"]["show_system_stats"] - - return True # Default to enabled if missing - except Exception as e: - logger.error(f"Error checking system stats flag: {e}") - return True # Default to enabled if error - @pyqtSlot(result="QVariantMap") def getSystemStats(self): """Get system statistics (CPU, RAM, temperature, LLM).""" try: # Lazy import to avoid circular imports - from distiller_cm5_python.client.ui.system_monitor import system_monitor + from distiller_cm5_python.client.ui.system_monitor import SystemMonitor # Return formatted stats dictionary - return system_monitor.get_formatted_stats() + return SystemMonitor( + self.mcp_client.llm_provider.provider_type + ).get_formatted_stats() except Exception as e: logger.error(f"Error getting system stats: {e}") return {"cpu": "N/A", "ram": "N/A", "temp": "N/A", "llm": "Local"} @@ -273,24 +226,25 @@ def closeApplication(self): try: # Signal application power down via UART from distiller_cm5_python.utils.uart_utils import signal_app_shutdown + signal_app_shutdown() logger.info("Sent shutdown signal to UART device") - + # Perform cleanup first - if hasattr(self, 'cleanup') and callable(self.cleanup): + if hasattr(self, "cleanup") and callable(self.cleanup): try: # Run cleanup synchronously - asyncio.run_coroutine_threadsafe( - self.cleanup(), self._loop - ).result(timeout=2.0) # 2-second timeout for cleanup + asyncio.run_coroutine_threadsafe(self.cleanup(), self._loop).result( + timeout=2.0 + ) # 2-second timeout for cleanup logger.info("Cleanup completed successfully") except Exception as e: logger.error(f"Error during cleanup: {e}") - + # Schedule application exit with a short delay to allow cleanup to complete QApplication.instance().quit() logger.info("Application exit scheduled") - + except Exception as e: logger.error(f"Error during application close: {e}") # Force quit if normal exit fails @@ -299,52 +253,3 @@ def closeApplication(self): except: # Last resort: terminate process os._exit(1) - - @pyqtSlot(str) - def executeSystemCommand(self, command: str): - """ - Execute a system command with proper security checks. - - Args: - command: The system command to execute - """ - # Only allow specific system commands - allowed_commands = { - "shutdown now": "sudo shutdown now", - "poweroff": "sudo poweroff" - } - - logger.info(f"Received system command request: {command}") - - if command not in allowed_commands: - logger.error(f"Unauthorized system command attempt: {command}") - return - - try: - # Signal application power down via UART first - from distiller_cm5_python.utils.uart_utils import signal_app_shutdown - signal_app_shutdown() - logger.info(f"Sent shutdown signal to UART device before system {command}") - - # Execute the allowed command - import subprocess - logger.info(f"Executing system command: {allowed_commands[command]}") - - # Run the command in a separate process - subprocess.Popen(allowed_commands[command], shell=True) - - # Return immediately to allow the command to complete - return - - except Exception as e: - logger.error(f"Error executing system command: {e}") - - @pyqtSlot(str) - def setLlmModel(self, model_name): - """Set the current LLM model name.""" - try: - from distiller_cm5_python.client.ui.system_monitor import system_monitor - - system_monitor.set_llm_model(model_name) - except Exception as e: - logger.error(f"Error setting LLM model: {e}") diff --git a/distiller_cm5_python/client/ui/bridge/ServerDiscovery.py b/distiller_cm5_python/client/ui/bridge/ServerDiscovery.py index b37c610..0bc1b62 100644 --- a/distiller_cm5_python/client/ui/bridge/ServerDiscovery.py +++ b/distiller_cm5_python/client/ui/bridge/ServerDiscovery.py @@ -142,8 +142,3 @@ def _process_server_file(self, file_path): f"Failed to process server file {file_path}: {e}", exc_info=True ) return False - - def cleanup(self): - """Clean up resources used by server discovery""" - logger.debug("Server discovery cleanup") - self._available_servers = [] diff --git a/distiller_cm5_python/client/ui/bridge/StatusManager.py b/distiller_cm5_python/client/ui/bridge/StatusManager.py index ca7216b..33f9f96 100644 --- a/distiller_cm5_python/client/ui/bridge/StatusManager.py +++ b/distiller_cm5_python/client/ui/bridge/StatusManager.py @@ -105,24 +105,6 @@ def is_processing(self): ] return self._current_status in processing_states - def is_ready(self): - """ - Check if the system is in ready state - - Returns: - True if status is READY - """ - return self._current_status == self.STATUS_READY - - def is_connected(self): - """ - Check if the system is connected - - Returns: - True if status is CONNECTED - """ - return self._current_status == self.STATUS_CONNECTED - def cleanup(self): """Clean up resources and reset status""" self.update_status(self.STATUS_IDLE) diff --git a/distiller_cm5_python/client/ui/bridge/components/bridge_core.py b/distiller_cm5_python/client/ui/bridge/components/bridge_core.py index ed131d5..49bef90 100644 --- a/distiller_cm5_python/client/ui/bridge/components/bridge_core.py +++ b/distiller_cm5_python/client/ui/bridge/components/bridge_core.py @@ -61,7 +61,6 @@ class BridgeCore(QObject): transcriptionComplete = pyqtSignal( str, arguments=["full_text"] ) # Signal for completed transcription - sshInfoReceived = pyqtSignal(str, str, str) # Signal for SSH info events functionReceived = pyqtSignal(str, str, str) # Signal for function events observationReceived = pyqtSignal(str, str, str) # Signal for observation events planReceived = pyqtSignal(str, str, str) # Signal for plan events @@ -220,19 +219,6 @@ async def process_query(self, query: str): user_friendly_msg="Failed to process query. Please try again.", ) - @pyqtSlot(str, str, result="QVariant") - def getConfigValue(self, section: str, key: str) -> str: - """Get a configuration value (Stub method).""" - logger.warning("ConfigManager removed: getConfigValue returning empty string") - return "" - - @pyqtSlot(str, str, "QVariant") - def setConfigValue(self, section: str, key: str, value): - """Set a configuration value (Stub method).""" - logger.warning( - f"ConfigManager removed: setConfigValue({section}, {key}, {value}) ignored" - ) - async def connect_to_server(self): """Ask the user to select a server from the list of available servers.""" success = await self.connection_manager.connect_to_server() diff --git a/distiller_cm5_python/client/ui/bridge/components/event_handler.py b/distiller_cm5_python/client/ui/bridge/components/event_handler.py index 99debb9..8320b23 100644 --- a/distiller_cm5_python/client/ui/bridge/components/event_handler.py +++ b/distiller_cm5_python/client/ui/bridge/components/event_handler.py @@ -219,19 +219,8 @@ def _handle_message_schema(self, event: MessageSchema) -> None: logger.debug( f"Handling INFO event: content='{event.content}', id={event.id}" ) - # if event.content: - # self.signals.infoReceived.emit( - # event.content, str(event.id), timestamp_str - # ) - # Add to conversation history - # if conversation_manager: - # message = { - # "timestamp": self._get_formatted_timestamp(), - # "content": f"{event.content}", - # "type": "Info", - # } - # conversation_manager.add_message(message) - + # INFO events are handled via status updates only - the UI doesn't + # need to show these in the chat status_value = ( event.status.value if hasattr(event.status, "value") else event.status ) @@ -295,14 +284,7 @@ def _handle_message_schema(self, event: MessageSchema) -> None: elif status_value == StatusType.FAILED: self.status_manager.update_status("error", event.content) - # Add to conversation history - # if conversation_manager: - # message = { - # "timestamp": self._get_formatted_timestamp(), - # "content": f"{event.content}", - # "type": "Cache Operation", - # } - # conversation_manager.add_message(message) + # Cache operations don't need to be added to the conversation history elif event.type == EventType.OBSERVATION: # Handle observation events @@ -345,6 +327,27 @@ def _handle_message_schema(self, event: MessageSchema) -> None: "type": "Plan", } conversation_manager.add_message(message) + + elif event.type == EventType.FUNCTION: + # Handle function events + if hasattr(self.signals, "functionReceived"): + self.signals.functionReceived.emit( + event.content, str(event.id), timestamp_str + ) + else: + # Fall back to info message if no dedicated handler + self.signals.infoReceived.emit( + event.content, str(event.id), timestamp_str + ) + + # Add to conversation history + if conversation_manager: + message = { + "timestamp": self._get_formatted_timestamp(), + "content": f"{event.content}", + "type": "Function", + } + conversation_manager.add_message(message) elif event.type == EventType.STATUS: # Check for specific component status events @@ -371,24 +374,6 @@ def _handle_message_schema(self, event: MessageSchema) -> None: else: logger.warning(f"Unknown event type: {event.type}") - def create_error_event(self, error_msg: str) -> MessageSchema: - """ - Create an error event. - - Args: - error_msg: The error message - - Returns: - A MessageSchema representing the error - """ - return MessageSchema( - id=str(uuid.uuid4()), - type=EventType.ERROR, - content=error_msg, - status=StatusType.FAILED, - timestamp=time.time(), - ) - def _get_formatted_timestamp(self): """Get a formatted timestamp string for messages.""" from datetime import datetime diff --git a/distiller_cm5_python/client/ui/system_monitor.py b/distiller_cm5_python/client/ui/system_monitor.py index dd38bf0..90c7a36 100644 --- a/distiller_cm5_python/client/ui/system_monitor.py +++ b/distiller_cm5_python/client/ui/system_monitor.py @@ -4,39 +4,16 @@ class SystemMonitor: - def __init__(self): + def __init__(self, llm_model: str): self.ram_usage = 0.0 self.cpu_usage = 0.0 self.temperature = 0.0 - self.llm_model = "Local" # Default LLM model name + self.llm_model = llm_model or "Local" self.last_update_time = 0 self.update_interval = ( 2.0 # Update every 2 seconds to avoid excessive resource usage ) - def get_ram_usage(self): - """Return current RAM usage as a percentage.""" - self._update_if_needed() - return self.ram_usage - - def get_cpu_usage(self): - """Return current CPU usage as a percentage.""" - self._update_if_needed() - return self.cpu_usage - - def get_temperature(self): - """Return current CPU temperature in Celsius.""" - self._update_if_needed() - return self.temperature - - def get_llm_model(self): - """Return the current LLM model name.""" - return self.llm_model - - def set_llm_model(self, model_name): - """Set the current LLM model name.""" - self.llm_model = model_name - def get_formatted_stats(self): """Return all stats in a formatted dictionary.""" self._update_if_needed() @@ -97,7 +74,3 @@ def _read_temp_from_zone(self): self.temperature = 0.0 except Exception: self.temperature = 0.0 - - -# Create a singleton instance -system_monitor = SystemMonitor() diff --git a/requirements.txt b/requirements.txt index f1785c8..0026467 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ aiohttp colorama nest_asyncio psutil>=5.9.0 -PyQt6>=6.4.0 +PyQt6==6.7.1 qasync pyaudio faster_whisper