diff --git a/codeaide/__main__.py b/codeaide/__main__.py index 40e2574..fb83584 100644 --- a/codeaide/__main__.py +++ b/codeaide/__main__.py @@ -3,7 +3,6 @@ from PyQt5.QtWidgets import QApplication from codeaide.logic.chat_handler import ChatHandler -from codeaide.ui.chat_window import ChatWindow from codeaide.utils import api_utils @@ -20,10 +19,8 @@ def main(): print("Error:", message) else: app = QApplication(sys.argv) - chat_window = ChatWindow(chat_handler) - chat_handler.set_main_window(chat_window) - chat_window.show() - app.exec_() + chat_handler.start_application() + sys.exit(app.exec_()) if __name__ == "__main__": diff --git a/codeaide/logic/chat_handler.py b/codeaide/logic/chat_handler.py index 98549a0..5ff5674 100644 --- a/codeaide/logic/chat_handler.py +++ b/codeaide/logic/chat_handler.py @@ -26,10 +26,18 @@ from codeaide.utils.logging_config import get_logger, setup_logger from PyQt5.QtWidgets import QMessageBox, QTextEdit from PyQt5.QtGui import QFont -from PyQt5.QtCore import QObject, QMetaObject, Qt, Q_ARG, pyqtSlot +from PyQt5.QtCore import QObject, QMetaObject, Qt, Q_ARG, pyqtSlot, pyqtSignal +from codeaide.ui.traceback_dialog import TracebackDialog class ChatHandler(QObject): + # Define custom signals for updating the chat and showing code + update_chat_signal = pyqtSignal( + str, str + ) # Signal to update chat with (role, message) + show_code_signal = pyqtSignal(str, str) # Signal to show code with (code, version) + traceback_occurred = pyqtSignal(str) + def __init__(self): super().__init__() """ @@ -50,7 +58,7 @@ def __init__(self): self.logger = get_logger() self.conversation_history = self.file_handler.load_chat_history() self.terminal_manager = TerminalManager( - traceback_callback=self.show_traceback_dialog + traceback_callback=self.emit_traceback_signal ) self.latest_version = "0.0" self.api_client = None @@ -64,7 +72,21 @@ def __init__(self): self.api_key_valid, self.api_key_message = self.check_api_key() self.logger.info(f"New session started with ID: {self.session_id}") self.logger.info(f"Session directory: {self.session_dir}") - self.main_window = None # We'll set this later + self.chat_window = None + + def start_application(self): + from codeaide.ui.chat_window import ( + ChatWindow, + ) # Import here to avoid circular imports + + self.chat_window = ChatWindow(self) + self.connect_signals() + self.chat_window.show() + + def connect_signals(self): + self.update_chat_signal.connect(self.chat_window.add_to_chat) + self.show_code_signal.connect(self.chat_window.show_code) + self.traceback_occurred.connect(self.chat_window.show_traceback_dialog) def check_api_key(self): """ @@ -570,60 +592,22 @@ def load_previous_session(self, session_id, chat_window): self.logger.info(f"Loaded previous session with ID: {self.session_id}") - def set_main_window(self, main_window): - self.main_window = main_window - self.logger.info(f"Main window set: {self.main_window}") - - def show_traceback_dialog(self, traceback_text): - self.logger.info(f"show_traceback_dialog called with: {traceback_text}") - if self.main_window: - QMetaObject.invokeMethod( - self, - "_display_traceback_dialog", - Qt.QueuedConnection, - Q_ARG(str, traceback_text), - ) - else: - self.logger.info("self.main_window is None") - - @pyqtSlot(str) - def _display_traceback_dialog(self, traceback_text): - self.logger.info(f"_display_traceback_dialog called with: {traceback_text}") - msg_box = QMessageBox(self.main_window) - msg_box.setWindowTitle("Error Detected") - msg_box.setText("An error was detected in the running script:") - msg_box.setInformativeText(traceback_text) - msg_box.setIcon(QMessageBox.Warning) - - # Create custom buttons - send_button = msg_box.addButton("Request a fix", QMessageBox.ActionRole) - ignore_button = msg_box.addButton("Ignore", QMessageBox.RejectRole) - - # Set a fixed width for the dialog - msg_box.setFixedWidth(600) - - # Make the dialog resizable - msg_box.setSizeGripEnabled(True) - - # Set a monospace font for the traceback text - text_browser = msg_box.findChild(QTextEdit) - if text_browser: - font = QFont("Courier") - font.setStyleHint(QFont.Monospace) - font.setFixedPitch(True) - font.setPointSize(10) - text_browser.setFont(font) - - msg_box.exec_() - - if msg_box.clickedButton() == send_button: - self.send_traceback_to_agent(traceback_text) + def emit_traceback_signal(self, traceback_text): + self.logger.info( + f"ChatHandler: Emitting traceback signal with text: {traceback_text[:50]}..." + ) + self.traceback_occurred.emit(traceback_text) def send_traceback_to_agent(self, traceback_text): + self.logger.info( + f"ChatHandler: Sending traceback to agent: {traceback_text[:50]}..." + ) message = ( "The following error occurred when running the code you just provided:\n\n" f"```\n{traceback_text}\n```\n\n" "Please provide a solution that avoids this error." ) - self.main_window.input_text.setPlainText(message) - self.main_window.on_submit() + self.logger.info(f"ChatHandler: Setting input text in chat window") + self.chat_window.input_text.setPlainText(message) + self.logger.info(f"ChatHandler: Calling on_submit in chat window") + self.chat_window.on_submit() diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index 1ab2e18..aecda4a 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -39,6 +39,7 @@ MODEL_SWITCH_MESSAGE, ) from codeaide.utils.logging_config import get_logger +from codeaide.ui.traceback_dialog import TracebackDialog class ChatWindow(QMainWindow): @@ -160,15 +161,18 @@ def eventFilter(self, obj, event): def on_submit(self): user_input = self.input_text.toPlainText().strip() + self.logger.info( + f"ChatWindow: on_submit called with input: {user_input[:50]}..." + ) if not user_input: + self.logger.info("ChatWindow: Empty input, returning") return - self.logger.info(f"User input: {user_input}") - - # Clear the input field immediately + self.logger.info(f"ChatWindow: Processing user input") self.input_text.clear() if self.waiting_for_api_key: + self.logger.info("ChatWindow: Handling API key input") ( success, message, @@ -179,17 +183,21 @@ def on_submit(self): if success: self.enable_ui_elements() else: - # Immediately display user input and "Thinking..." message + self.logger.info("ChatWindow: Adding user input to chat") self.add_to_chat("User", user_input) self.disable_ui_elements() self.add_to_chat("AI", "Thinking... 🤔") - - # Use QTimer to process the input after the UI has updated + self.logger.info("ChatWindow: Scheduling call_process_input_async") QTimer.singleShot(100, lambda: self.call_process_input_async(user_input)) def call_process_input_async(self, user_input): - # Process the input + self.logger.info( + f"ChatWindow: call_process_input_async called with input: {user_input[:50]}..." + ) response = self.chat_handler.process_input(user_input) + self.logger.info( + f"ChatWindow: Received response from chat handler: {str(response)[:50]}..." + ) self.handle_response(response) def on_modify(self): @@ -270,6 +278,7 @@ def update_or_create_code_popup(self, response): self.code_popup = CodePopup( self, self.chat_handler.file_handler, + self.chat_handler.terminal_manager, code, requirements, self.chat_handler.run_generated_code, @@ -398,3 +407,31 @@ def load_chat_contents(self): for item in self.chat_contents: self.add_to_chat(item["sender"], item["message"]) self.logger.info(f"Loaded {len(self.chat_contents)} messages from chat log") + + def show_code(self, code, version): + if not self.code_popup: + self.code_popup = CodePopup( + self, + self.chat_handler.file_handler, + code, + [], + self.chat_handler.run_generated_code, + chat_handler=self.chat_handler, + ) + else: + self.code_popup.update_with_new_version(code, []) + self.code_popup.show() + self.code_popup.raise_() + self.code_popup.activateWindow() + + def show_traceback_dialog(self, traceback_text): + self.logger.info( + f"ChatWindow: show_traceback_dialog called with text: {traceback_text[:50]}..." + ) + dialog = TracebackDialog(self, traceback_text) + self.logger.info("ChatWindow: Showing TracebackDialog") + if dialog.exec_(): + self.logger.info("ChatWindow: User requested to fix the traceback") + self.chat_handler.send_traceback_to_agent(traceback_text) + else: + self.logger.info("ChatWindow: User chose to ignore the traceback") diff --git a/codeaide/ui/code_popup.py b/codeaide/ui/code_popup.py index 7b3e75e..53146ae 100644 --- a/codeaide/ui/code_popup.py +++ b/codeaide/ui/code_popup.py @@ -20,6 +20,7 @@ QWidget, QPlainTextEdit, QMessageBox, + QDialog, ) from pygments import highlight from pygments.lexers import PythonLexer @@ -33,7 +34,6 @@ CODE_WINDOW_HEIGHT, CODE_WINDOW_WIDTH, ) -from codeaide.utils.terminal_manager import TerminalManager class LineNumberArea(QWidget): @@ -242,14 +242,22 @@ def highlightBlock(self, text): self.setCurrentBlockState(0) -class CodePopup(QWidget): +class CodePopup(QDialog): def __init__( - self, parent, file_handler, code, requirements, run_callback, chat_handler + self, + parent, + file_handler, + terminal_manager, + code, + requirements, + run_callback, + chat_handler, ): - super().__init__(parent, Qt.Window) + super().__init__(parent) self.setWindowTitle("💻 Generated Code 💻") self.resize(CODE_WINDOW_WIDTH, CODE_WINDOW_HEIGHT) self.file_handler = file_handler + self.terminal_manager = terminal_manager self.run_callback = run_callback self.setup_ui() self.load_versions() @@ -261,14 +269,6 @@ def __init__( # Use the chat_handler passed as an argument, or try to get it from the parent self.chat_handler = chat_handler or (parent.chat_handler if parent else None) - # Create TerminalManager with a safe traceback callback - self.terminal_manager = TerminalManager( - traceback_callback=self.safe_show_traceback_dialog - ) - - def safe_show_traceback_dialog(self, traceback_text): - self.chat_handler.show_traceback_dialog(traceback_text) - def setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(5) @@ -370,7 +370,6 @@ def on_run(self): with open(req_path, "w") as f: f.write("\n".join(requirements)) - # Assuming you have a TerminalManager instance available as self.terminal_manager self.terminal_manager.run_script(code_path, req_path) def on_copy_code(self): diff --git a/codeaide/ui/traceback_dialog.py b/codeaide/ui/traceback_dialog.py new file mode 100644 index 0000000..1b5c135 --- /dev/null +++ b/codeaide/ui/traceback_dialog.py @@ -0,0 +1,44 @@ +from PyQt5.QtWidgets import QMessageBox, QTextEdit +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt +import logging + + +def get_logger(): + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + return logger + + +class TracebackDialog(QMessageBox): + def __init__(self, parent, traceback_text): + super().__init__(parent) + self.logger = get_logger() + self.logger.info( + f"TracebackDialog: Initializing with text: {traceback_text[:50]}..." + ) + self.setWindowTitle("Error Detected") + self.setText("An error was detected in the running script:") + self.setInformativeText(traceback_text) + self.setIcon(QMessageBox.Warning) + + self.setFixedWidth(600) + self.setSizeGripEnabled(True) + + self.send_button = self.addButton("Request a fix", QMessageBox.ActionRole) + self.ignore_button = self.addButton("Ignore", QMessageBox.RejectRole) + + text_browser = self.findChild(QTextEdit) + if text_browser: + font = QFont("Courier") + font.setStyleHint(QFont.Monospace) + font.setFixedPitch(True) + font.setPointSize(10) + text_browser.setFont(font) + + def exec_(self): + self.logger.info("TracebackDialog: Executing dialog") + result = super().exec_() + user_choice = "fix" if self.clickedButton() == self.send_button else "ignore" + self.logger.info(f"TracebackDialog: User chose to {user_choice} the traceback") + return self.clickedButton() == self.send_button diff --git a/codeaide/utils/terminal_manager.py b/codeaide/utils/terminal_manager.py index 55ec7b8..23fce5c 100644 --- a/codeaide/utils/terminal_manager.py +++ b/codeaide/utils/terminal_manager.py @@ -150,11 +150,11 @@ def process_line(self, line): def show_traceback_if_any(self): if self.traceback_buffer: traceback_text = "\n".join(self.traceback_buffer) - self.logger.info("\nERROR DETECTED:") - self.logger.info("-----------------------") - self.logger.info(traceback_text) - self.logger.info("-----------------------") + self.logger.info( + f"ScriptRunner: Traceback detected: {traceback_text[:50]}..." + ) if self.traceback_callback: + self.logger.info("ScriptRunner: Calling traceback callback") self.traceback_callback(traceback_text) self.traceback_buffer = [] diff --git a/tests/ui/test_chat_window.py b/tests/ui/test_chat_window.py index a5484a4..fec9255 100644 --- a/tests/ui/test_chat_window.py +++ b/tests/ui/test_chat_window.py @@ -1,6 +1,7 @@ import sys from unittest.mock import Mock, patch, MagicMock import pytest +import logging import os from PyQt5.QtWidgets import QApplication @@ -29,105 +30,92 @@ @pytest.fixture def mock_chat_handler(): mock_handler = Mock(spec=ChatHandler) - mock_handler.cost_tracker = Mock() - mock_handler.api_key_valid = True - mock_handler.api_key_message = "API key is valid" - mock_handler.file_handler = Mock() mock_handler.process_input = Mock( return_value={"type": "message", "message": "AI response"} ) + mock_handler.file_handler = Mock() + mock_handler.file_handler.get_versions_dict = Mock(return_value={}) + mock_handler.get_latest_version = Mock(return_value="1.0") + mock_handler.terminal_manager = Mock() + + # Add these new attributes + mock_handler.api_key_valid = True + mock_handler.api_key_message = "API key is valid" + mock_handler.cost_tracker = Mock() + + # Mock the set_model method to return a tuple mock_handler.set_model = Mock(return_value=(True, "Model set successfully")) - mock_handler.get_latest_version = Mock( - return_value="1.0" - ) # Ensure this returns a string + return mock_handler @pytest.fixture def chat_window(mock_chat_handler): - window = ChatWindow(mock_chat_handler) - # Ensure that QTimer.singleShot calls are executed immediately - QTimer.singleShot = lambda ms, callback: callback() - return window + def _create_window(api_key_valid=True): + mock_chat_handler.api_key_valid = api_key_valid + window = ChatWindow(mock_chat_handler) + # Ensure that QTimer.singleShot calls are executed immediately + QTimer.singleShot = lambda ms, callback: callback() + return window + + return _create_window def test_chat_window_initialization(chat_window): - assert chat_window.windowTitle() == "🤖 CodeAIde 🤖" - assert chat_window.chat_handler is not None + window = chat_window() # Creates a window with a valid API key + assert window.windowTitle() == "🤖 CodeAIde 🤖" + assert window.chat_handler is not None def test_send_message(chat_window, mock_chat_handler): + window = chat_window() # Create the window # Simulate typing a message - QTest.keyClicks(chat_window.input_text, "Hello, AI!") - + QTest.keyClicks(window.input_text, "Hello, AI!") # Simulate pressing the submit button - QTest.mouseClick(chat_window.submit_button, Qt.LeftButton) - + QTest.mouseClick(window.submit_button, Qt.LeftButton) # Check if the chat_handler's process_input method was called mock_chat_handler.process_input.assert_called_once_with("Hello, AI!") -def test_model_switching(chat_window, mock_chat_handler): - print("Initial provider:", chat_window.provider_dropdown.currentText()) - print("Initial model:", chat_window.model_dropdown.currentText()) +def test_model_switching(chat_window, mock_chat_handler, caplog): + caplog.set_level(logging.INFO) + window = chat_window() - # Get a non-default provider test_provider = next( provider for provider in AI_PROVIDERS.keys() if provider != DEFAULT_PROVIDER ) - - # Change to the test provider - chat_window.provider_dropdown.setCurrentText(test_provider) - print(f"Provider after change: {chat_window.provider_dropdown.currentText()}") - print(f"Model after provider change: {chat_window.model_dropdown.currentText()}") - - # Print all available models for the test provider - print(f"Available models for {test_provider}:") - for model in AI_PROVIDERS[test_provider]["models"].keys(): - print(model) - - # Select a non-default model for the test provider test_model = next( model for model in AI_PROVIDERS[test_provider]["models"].keys() if model != DEFAULT_MODEL ) - chat_window.model_dropdown.setCurrentText(test_model) - print( - f"Provider after model selection: {chat_window.provider_dropdown.currentText()}" - ) - print(f"Model after model selection: {chat_window.model_dropdown.currentText()}") - # Manually trigger the update_chat_handler method - chat_window.update_chat_handler() + window.provider_dropdown.setCurrentText(test_provider) + window.model_dropdown.setCurrentText(test_model) - print("All calls to set_model:") - for call in mock_chat_handler.set_model.call_args_list: - print(call) + window.update_chat_handler() - print(f"Final provider: {chat_window.provider_dropdown.currentText()}") - print(f"Final model: {chat_window.model_dropdown.currentText()}") + # Log important information + logging.info(f"Final provider: {window.provider_dropdown.currentText()}") + logging.info(f"Final model: {window.model_dropdown.currentText()}") # Check if the chat_handler's set_model method was called with the correct arguments - mock_chat_handler.set_model.assert_any_call(test_provider, test_model) - - print("Chat display content:") - print(chat_window.chat_display.toPlainText()) + mock_chat_handler.set_model.assert_called_with(test_provider, test_model) # Check if the correct message was added to the chat - current_version = "1.0" # This should match what's set in the mock_chat_handler - new_version = general_utils.increment_version( - current_version, major_or_minor="major", increment=1 - ) expected_message = MODEL_SWITCH_MESSAGE.format( - provider=test_provider, - model=test_model, + provider=test_provider, model=test_model ) - assert expected_message in chat_window.chat_display.toPlainText() + assert expected_message in window.chat_display.toPlainText() + + # Assert that the important information was logged + assert f"Final provider: {test_provider}" in caplog.text + assert f"Final model: {test_model}" in caplog.text @patch("codeaide.ui.chat_window.CodePopup") def test_handle_code_response(mock_code_popup, chat_window, mock_chat_handler): + window = chat_window() # Create the window response = { "type": "code", "message": "Here's your code", @@ -135,23 +123,32 @@ def test_handle_code_response(mock_code_popup, chat_window, mock_chat_handler): "requirements": [], } - chat_window.handle_response(response) - - # Check if CodePopup was created - mock_code_popup.assert_called_once() + window.handle_response(response) + + # Check if CodePopup was created with the correct arguments + mock_code_popup.assert_called_once_with( + window, + mock_chat_handler.file_handler, + mock_chat_handler.terminal_manager, + "print('Hello, World!')", + [], + mock_chat_handler.run_generated_code, + chat_handler=mock_chat_handler, + ) # Check if the message was added to the chat display - assert "Here's your code" in chat_window.chat_display.toPlainText() + assert "Here's your code" in window.chat_display.toPlainText() def test_load_example(chat_window, monkeypatch): + window = chat_window() # Create the window # Mock the show_example_dialog function monkeypatch.setattr( "codeaide.ui.chat_window.show_example_dialog", lambda _: "Example code" ) # Simulate clicking the load example button - QTest.mouseClick(chat_window.example_button, Qt.LeftButton) + QTest.mouseClick(window.example_button, Qt.LeftButton) # Check if the example was loaded into the input text - assert chat_window.input_text.toPlainText() == "Example code" + assert window.input_text.toPlainText() == "Example code"