"""Recipe Editor Dialog for ChefSystem. Dialog for creating and editing recipe prompt templates. """ import logging from typing import Optional from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QFormLayout, QLineEdit, QTextEdit, QDialogButtonBox, QMessageBox, QLabel ) from PyQt6.QtCore import Qt from src.services.recipe_service import RecipeService logger = logging.getLogger(__name__) class RecipeEditor(QDialog): """Dialog for creating and editing recipes. Provides form fields for all recipe attributes with validation. """ def __init__(self, db_manager, recipe_id: Optional[int] = None, parent=None): """Initialize the recipe editor dialog. Args: db_manager: DatabaseManager instance. recipe_id: ID of recipe to edit, or None for new recipe. parent: Parent widget. """ super().__init__(parent) self.db_manager = db_manager self.recipe_service = RecipeService(db_manager) self.recipe_id = recipe_id self.recipe = None # Will be loaded if editing self.setWindowTitle("New Recipe" if recipe_id is None else "Edit Recipe") self.setMinimumSize(700, 600) # Form fields (will be created in _setup_ui) self.name_input: Optional[QLineEdit] = None self.prompt_input: Optional[QTextEdit] = None self.tags_input: Optional[QLineEdit] = None self.description_input: Optional[QTextEdit] = None self.notes_input: Optional[QTextEdit] = None self._setup_ui() self._load_recipe() def _setup_ui(self) -> None: """Set up the form UI components.""" main_layout = QVBoxLayout() # Header header_label = QLabel("

Recipe Details

") main_layout.addWidget(header_label) # Form layout form_layout = QFormLayout() # Name field (required) self.name_input = QLineEdit() self.name_input.setPlaceholderText("Enter a unique name for this recipe") form_layout.addRow("Name*:", self.name_input) # Tags field (comma-separated) self.tags_input = QLineEdit() self.tags_input.setPlaceholderText("e.g., python,code-review,testing") tags_help = QLabel("Comma-separated tags for categorization") tags_help.setStyleSheet("color: gray; font-size: 10px;") form_layout.addRow("Tags:", self.tags_input) form_layout.addRow("", tags_help) # Description field self.description_input = QTextEdit() self.description_input.setMaximumHeight(80) self.description_input.setPlaceholderText("Brief description of what this recipe does") form_layout.addRow("Description:", self.description_input) # Prompt text field (required) self.prompt_input = QTextEdit() self.prompt_input.setMinimumHeight(200) self.prompt_input.setPlaceholderText("Enter the prompt template text here...") prompt_help = QLabel("The AI prompt template - can include placeholders like [TOPIC]") prompt_help.setStyleSheet("color: gray; font-size: 10px;") form_layout.addRow("Prompt Text*:", self.prompt_input) form_layout.addRow("", prompt_help) # Notes field self.notes_input = QTextEdit() self.notes_input.setMaximumHeight(80) self.notes_input.setPlaceholderText("Optional notes about usage, best practices, etc.") form_layout.addRow("Notes:", self.notes_input) # Required field note required_note = QLabel("* Required fields") required_note.setStyleSheet("color: red; font-size: 10px;") form_layout.addRow("", required_note) main_layout.addLayout(form_layout) main_layout.addStretch() # Button box button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) main_layout.addWidget(button_box) self.setLayout(main_layout) def _load_recipe(self) -> None: """Load recipe data if editing existing recipe.""" if self.recipe_id is not None: try: # Load recipe from service self.recipe = self.recipe_service.get_recipe(self.recipe_id) if self.recipe: # Populate form fields self.name_input.setText(self.recipe.name) self.prompt_input.setPlainText(self.recipe.prompt_text) self.tags_input.setText(self.recipe.tags if self.recipe.tags else "") self.description_input.setPlainText( self.recipe.description if self.recipe.description else "" ) self.notes_input.setPlainText( self.recipe.notes if self.recipe.notes else "" ) logger.info(f"Loaded recipe for editing: {self.recipe.name}") else: # Recipe not found QMessageBox.critical( self, "Error", f"Recipe ID {self.recipe_id} not found in database." ) self.reject() except Exception as e: logger.error(f"Error loading recipe {self.recipe_id}: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to load recipe: {e}" ) self.reject() def validate_form(self) -> tuple[bool, str]: """Validate form inputs. Returns: tuple[bool, str]: (is_valid, error_message) """ # Validate name is not empty name = self.name_input.text().strip() if not name: return False, "Recipe name is required." # Validate name length if len(name) > 200: return False, "Recipe name must be 200 characters or less." # Validate prompt_text is not empty prompt_text = self.prompt_input.toPlainText().strip() if not prompt_text: return False, "Prompt text is required." # Validate name is unique (if new or name changed) if self.recipe_id is None: # New recipe: check uniqueness if not self.recipe_service.check_name_unique(name): return False, f"A recipe named '{name}' already exists. Please choose a different name." else: # Editing recipe: check uniqueness excluding current recipe if self.recipe and name != self.recipe.name: if not self.recipe_service.check_name_unique(name, exclude_id=self.recipe_id): return False, f"A recipe named '{name}' already exists. Please choose a different name." return True, "" def save_recipe(self) -> bool: """Save the recipe to database. Returns: bool: True if successful, False otherwise. """ # Validate form is_valid, error_message = self.validate_form() if not is_valid: QMessageBox.warning( self, "Validation Error", error_message ) return False # Get form values name = self.name_input.text().strip() prompt_text = self.prompt_input.toPlainText() tags = self.tags_input.text().strip() description = self.description_input.toPlainText().strip() notes = self.notes_input.toPlainText().strip() try: if self.recipe_id is None: # Create new recipe recipe = self.recipe_service.create_recipe( name=name, prompt_text=prompt_text, tags=tags, description=description, notes=notes ) if recipe: self.recipe_id = recipe.id self.recipe = recipe logger.info(f"Created new recipe: {recipe.name} (ID: {recipe.id})") QMessageBox.information( self, "Success", f"Recipe '{recipe.name}' created successfully!" ) return True else: QMessageBox.critical( self, "Error", "Failed to create recipe. The name may already exist." ) return False else: # Update existing recipe success = self.recipe_service.update_recipe( self.recipe_id, name=name, prompt_text=prompt_text, tags=tags, description=description, notes=notes ) if success: logger.info(f"Updated recipe ID {self.recipe_id}") QMessageBox.information( self, "Success", f"Recipe '{name}' updated successfully!" ) return True else: QMessageBox.critical( self, "Error", "Failed to update recipe. The name may already exist." ) return False except Exception as e: logger.error(f"Error saving recipe: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"An error occurred while saving: {e}" ) return False def accept(self) -> None: """Handle dialog acceptance (Save button).""" if self.save_recipe(): super().accept() def get_recipe_id(self) -> Optional[int]: """Get the ID of the saved recipe. Returns: int: Recipe ID, or None if not saved yet. """ return self.recipe_id