"""Step editor dialogs.""" import ast from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QFormLayout, QLineEdit, QComboBox, QPushButton, QHBoxLayout, QLabel, QPlainTextEdit, QSplitter, QGroupBox, QTextEdit, QMessageBox ) from PySide6.QtCore import Qt, QThread, Signal from ...tool import PromptStep, CodeStep from ...providers import load_providers, call_provider from ...profiles import list_profiles class PromptStepDialog(QDialog): """Dialog for editing prompt steps.""" def __init__(self, parent, step: PromptStep = None): super().__init__(parent) self.setWindowTitle("Edit Prompt Step" if step else "Add Prompt Step") self.setMinimumSize(500, 400) self._step = step self._setup_ui() if step: self._load_step(step) def _setup_ui(self): """Set up the UI.""" layout = QVBoxLayout(self) layout.setSpacing(16) # Form form = QFormLayout() form.setSpacing(12) # Step name (optional) self.name_input = QLineEdit() self.name_input.setPlaceholderText("Optional display name") form.addRow("Step name:", self.name_input) # Provider selection self.provider_combo = QComboBox() providers = load_providers() for provider in sorted(providers, key=lambda p: p.name): self.provider_combo.addItem(provider.name) # Add common defaults if not present for default in ["claude", "gpt", "mock"]: if self.provider_combo.findText(default) < 0: self.provider_combo.addItem(default) form.addRow("Provider:", self.provider_combo) # Profile selection self.profile_combo = QComboBox() profiles = list_profiles() for profile in profiles: self.profile_combo.addItem(profile.name) # Set tooltip with description idx = self.profile_combo.count() - 1 self.profile_combo.setItemData(idx, profile.description, Qt.ToolTipRole) form.addRow("Profile:", self.profile_combo) # Output variable self.output_input = QLineEdit() self.output_input.setPlaceholderText("response") self.output_input.setText("response") form.addRow("Output variable:", self.output_input) layout.addLayout(form) # Prompt text prompt_label = QLabel("Prompt template:") layout.addWidget(prompt_label) self.prompt_input = QPlainTextEdit() self.prompt_input.setPlaceholderText( "Enter your prompt here...\n\n" "Use {input} for stdin input.\n" "Use {variable} for argument values.\n" "Use {prev_output} for previous step output." ) layout.addWidget(self.prompt_input, 1) # Buttons buttons = QHBoxLayout() buttons.addStretch() self.btn_cancel = QPushButton("Cancel") self.btn_cancel.setObjectName("secondary") self.btn_cancel.clicked.connect(self.reject) buttons.addWidget(self.btn_cancel) self.btn_ok = QPushButton("OK") self.btn_ok.clicked.connect(self._validate_and_accept) buttons.addWidget(self.btn_ok) layout.addLayout(buttons) def _load_step(self, step: PromptStep): """Load step data into form.""" # Load name if step.name: self.name_input.setText(step.name) idx = self.provider_combo.findText(step.provider) if idx >= 0: self.provider_combo.setCurrentIndex(idx) else: self.provider_combo.setCurrentText(step.provider) # Load profile if step.profile: idx = self.profile_combo.findText(step.profile) if idx >= 0: self.profile_combo.setCurrentIndex(idx) self.output_input.setText(step.output_var) self.prompt_input.setPlainText(step.prompt) def _validate_and_accept(self): """Validate and accept.""" prompt = self.prompt_input.toPlainText().strip() if not prompt: self.prompt_input.setFocus() return output = self.output_input.text().strip() if not output: self.output_input.setFocus() return self.accept() def get_step(self) -> PromptStep: """Get the step from form data.""" profile = self.profile_combo.currentText() # Don't store "None" profile if profile == "None": profile = None # Get name, use None if empty name = self.name_input.text().strip() or None return PromptStep( prompt=self.prompt_input.toPlainText(), provider=self.provider_combo.currentText(), output_var=self.output_input.text().strip(), profile=profile, name=name ) class AIGenerateWorker(QThread): """Background worker for AI code generation.""" finished = Signal(str) error = Signal(str) def __init__(self, provider: str, prompt: str): super().__init__() self.provider = provider self.prompt = prompt def run(self): try: result = call_provider(self.provider, self.prompt) if result.success: self.finished.emit(result.text) else: self.error.emit(result.error or "Unknown error") except Exception as e: self.error.emit(str(e)) class CodeStepDialog(QDialog): """Dialog for editing code steps with AI assist.""" def __init__(self, parent, step: CodeStep = None, available_vars: list = None): super().__init__(parent) self.setWindowTitle("Edit Code Step" if step else "Add Code Step") self.setMinimumSize(900, 600) self._step = step self._available_vars = available_vars or ["input"] self._worker = None self._setup_ui() if step: self._load_step(step) def _setup_ui(self): """Set up the UI with code editor and AI assist panel.""" layout = QVBoxLayout(self) layout.setSpacing(12) # Top: Step name and Output variable form = QFormLayout() form.setSpacing(8) # Step name (optional) self.name_input = QLineEdit() self.name_input.setPlaceholderText("Optional display name") form.addRow("Step name:", self.name_input) self.output_input = QLineEdit() self.output_input.setPlaceholderText("result") self.output_input.setText("result") form.addRow("Output variable:", self.output_input) layout.addLayout(form) # Available variables display vars_text = ", ".join(self._available_vars) vars_label = QLabel(f"Available variables: {vars_text}") vars_label.setStyleSheet("color: #718096; font-size: 11px;") layout.addWidget(vars_label) # Main splitter: Code editor | AI Assist splitter = QSplitter(Qt.Horizontal) # Left: Code editor code_group = QGroupBox("Code") code_layout = QVBoxLayout(code_group) self.code_input = QPlainTextEdit() self.code_input.setPlaceholderText( "# Python code here\n" "# Access input with: input\n" "# Access args with their variable names\n\n" "result = input.upper()" ) font = self.code_input.font() font.setFamily("Consolas, Monaco, monospace") self.code_input.setFont(font) code_layout.addWidget(self.code_input) splitter.addWidget(code_group) # Right: AI Assist panel ai_group = QGroupBox("AI Assisted Code Generation") ai_layout = QVBoxLayout(ai_group) ai_layout.setSpacing(8) # Provider selector provider_layout = QHBoxLayout() provider_layout.addWidget(QLabel("Provider:")) self.ai_provider_combo = QComboBox() providers = load_providers() for provider in sorted(providers, key=lambda p: p.name): self.ai_provider_combo.addItem(provider.name) # Add common defaults if not present for default in ["claude", "gpt", "mock"]: if self.ai_provider_combo.findText(default) < 0: self.ai_provider_combo.addItem(default) self.ai_provider_combo.setMinimumWidth(150) provider_layout.addWidget(self.ai_provider_combo) provider_layout.addStretch() ai_layout.addLayout(provider_layout) # Prompt editor prompt_label = QLabel("Describe what you want the code to do:") ai_layout.addWidget(prompt_label) self.ai_prompt_input = QPlainTextEdit() self.ai_prompt_input.setPlaceholderText( "Example: Parse the input as JSON and extract the 'name' field" ) # Set default prompt template self._set_default_prompt() ai_layout.addWidget(self.ai_prompt_input, 2) # Generate button btn_layout = QHBoxLayout() self.btn_generate = QPushButton("Generate Code") self.btn_generate.clicked.connect(self._generate_code) self.btn_generate.setMinimumHeight(32) btn_layout.addStretch() btn_layout.addWidget(self.btn_generate) btn_layout.addStretch() ai_layout.addLayout(btn_layout) # Output/feedback area feedback_label = QLabel("AI Response:") ai_layout.addWidget(feedback_label) self.ai_output = QTextEdit() self.ai_output.setReadOnly(True) self.ai_output.setPlaceholderText("AI response will appear here...") self.ai_output.setMaximumHeight(100) ai_layout.addWidget(self.ai_output, 1) splitter.addWidget(ai_group) splitter.setSizes([450, 450]) layout.addWidget(splitter, 1) # Bottom: OK/Cancel buttons buttons = QHBoxLayout() buttons.addStretch() self.btn_cancel = QPushButton("Cancel") self.btn_cancel.setObjectName("secondary") self.btn_cancel.clicked.connect(self.reject) buttons.addWidget(self.btn_cancel) self.btn_ok = QPushButton("OK") self.btn_ok.clicked.connect(self._validate_and_accept) buttons.addWidget(self.btn_ok) layout.addLayout(buttons) def _set_default_prompt(self): """Set the default AI prompt template.""" vars_formatted = ', '.join(f'"{{{v}}}"' for v in self._available_vars) default_prompt = f"""Write inline Python code (NOT a function definition) according to my instruction. The code runs directly with variable substitution. Assign any available variables to a local variable first using triple quotes for multi-line content safety. Example: my_var = \"\"\"{{input}}\"\"\" result = my_var.upper() INSTRUCTION: [Describe what you want the code to do] CURRENT CODE: {{code}} AVAILABLE VARIABLES: {vars_formatted} IMPORTANT: Return ONLY executable inline Python code. No function definitions, no markdown fencing, no explanations - just the code.""" self.ai_prompt_input.setPlainText(default_prompt) def _generate_code(self): """Generate code using AI.""" prompt_template = self.ai_prompt_input.toPlainText().strip() if not prompt_template: self.ai_output.setHtml("Please enter a prompt") return # Replace {code} placeholder with current code current_code = self.code_input.toPlainText().strip() or "# No code yet" prompt = prompt_template.replace("{code}", current_code) provider = self.ai_provider_combo.currentText() # Disable button and show loading state self.btn_generate.setEnabled(False) self.btn_generate.setText("Generating...") self.ai_output.setHtml(f"Calling {provider}...") # Start worker thread self._worker = AIGenerateWorker(provider, prompt) self._worker.finished.connect(self._on_generate_finished) self._worker.error.connect(self._on_generate_error) self._worker.start() def _on_generate_finished(self, result: str): """Handle successful AI generation.""" self.btn_generate.setEnabled(True) self.btn_generate.setText("Generate Code") # Clean up the result - strip markdown code fences if present code = result.strip() if code.startswith("```python"): code = code[9:] elif code.startswith("```"): code = code[3:] if code.endswith("```"): code = code[:-3] code = code.strip() # Update code editor self.code_input.setPlainText(code) # Show success message self.ai_output.setHtml( f"Code generated successfully!
" f"Response length: {len(result)} chars" ) def _on_generate_error(self, error: str): """Handle AI generation error.""" self.btn_generate.setEnabled(True) self.btn_generate.setText("Generate Code") self.ai_output.setHtml(f"Error: {error}") def _load_step(self, step: CodeStep): """Load step data into form.""" # Load name if step.name: self.name_input.setText(step.name) self.output_input.setText(step.output_var) self.code_input.setPlainText(step.code) def _validate_and_accept(self): """Validate and accept.""" code = self.code_input.toPlainText().strip() if not code: self.code_input.setFocus() return output = self.output_input.text().strip() if not output: self.output_input.setFocus() return # Syntax check the Python code try: ast.parse(code) except SyntaxError as e: line_info = f" (line {e.lineno})" if e.lineno else "" QMessageBox.warning( self, "Syntax Error", f"Python syntax error{line_info}:\n\n{e.msg}" ) self.code_input.setFocus() return self.accept() def get_step(self) -> CodeStep: """Get the step from form data.""" # Get name, use None if empty name = self.name_input.text().strip() or None return CodeStep( code=self.code_input.toPlainText(), output_var=self.output_input.text().strip(), name=name )