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