"""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