Profiles allow users to inject system instructions into prompts, customizing the AI's behavior and persona for tool execution. Features: - Profile dataclass with name, description, system_prompt - 8 built-in profiles: None, Comedian, Technical Writer, Teacher, Concise, Creative, Code Reviewer, Analyst - Custom profile creation and storage in ~/.cmdforge/profiles/ - Profile selector in Prompt Step dialog - Profile injection during tool execution - Profiles page in GUI (Ctrl+4) for viewing and managing profiles Co-Authored-By: Claude Opus 4.5 <[email protected]>
251 lines
8.1 KiB
Python
251 lines
8.1 KiB
Python
"""Profiles page - manage AI persona profiles."""
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
|
|
QPushButton, QLabel, QGroupBox, QTextEdit, QLineEdit,
|
|
QFormLayout, QMessageBox, QSplitter
|
|
)
|
|
from PySide6.QtCore import Qt
|
|
|
|
from ...profiles import list_profiles, save_profile, delete_profile, Profile
|
|
|
|
|
|
class ProfilesPage(QWidget):
|
|
"""Profiles management page."""
|
|
|
|
def __init__(self, main_window):
|
|
super().__init__()
|
|
self.main_window = main_window
|
|
self._setup_ui()
|
|
self.refresh()
|
|
|
|
def _setup_ui(self):
|
|
"""Set up the UI."""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(24, 24, 24, 24)
|
|
layout.setSpacing(16)
|
|
|
|
# Header
|
|
header = QHBoxLayout()
|
|
title = QLabel("Profiles")
|
|
title.setObjectName("heading")
|
|
header.addWidget(title)
|
|
|
|
header.addStretch()
|
|
|
|
self.btn_new = QPushButton("New Profile")
|
|
self.btn_new.clicked.connect(self._new_profile)
|
|
header.addWidget(self.btn_new)
|
|
|
|
layout.addLayout(header)
|
|
|
|
# Description
|
|
desc = QLabel(
|
|
"AI persona profiles inject system instructions into prompts. "
|
|
"Select a profile when creating a prompt step to customize the AI's behavior."
|
|
)
|
|
desc.setWordWrap(True)
|
|
desc.setStyleSheet("color: #718096;")
|
|
layout.addWidget(desc)
|
|
|
|
# Main content splitter
|
|
splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Left: Profile list
|
|
left = QWidget()
|
|
left_layout = QVBoxLayout(left)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.profile_list = QListWidget()
|
|
self.profile_list.currentItemChanged.connect(self._on_profile_selected)
|
|
left_layout.addWidget(self.profile_list)
|
|
|
|
splitter.addWidget(left)
|
|
|
|
# Right: Profile details/editor
|
|
right = QWidget()
|
|
right_layout = QVBoxLayout(right)
|
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Details group
|
|
details_box = QGroupBox("Profile Details")
|
|
details_layout = QFormLayout(details_box)
|
|
details_layout.setSpacing(12)
|
|
|
|
self.name_input = QLineEdit()
|
|
self.name_input.setPlaceholderText("Profile name")
|
|
details_layout.addRow("Name:", self.name_input)
|
|
|
|
self.desc_input = QLineEdit()
|
|
self.desc_input.setPlaceholderText("Brief description")
|
|
details_layout.addRow("Description:", self.desc_input)
|
|
|
|
right_layout.addWidget(details_box)
|
|
|
|
# System prompt group
|
|
prompt_box = QGroupBox("System Prompt")
|
|
prompt_layout = QVBoxLayout(prompt_box)
|
|
|
|
self.prompt_input = QTextEdit()
|
|
self.prompt_input.setPlaceholderText(
|
|
"Enter the system instructions that will be injected into prompts.\n\n"
|
|
"Example: You are a helpful assistant who explains things clearly..."
|
|
)
|
|
prompt_layout.addWidget(self.prompt_input)
|
|
|
|
right_layout.addWidget(prompt_box, 1)
|
|
|
|
# Action buttons
|
|
actions = QHBoxLayout()
|
|
|
|
self.btn_save = QPushButton("Save")
|
|
self.btn_save.clicked.connect(self._save_profile)
|
|
self.btn_save.setEnabled(False)
|
|
actions.addWidget(self.btn_save)
|
|
|
|
self.btn_delete = QPushButton("Delete")
|
|
self.btn_delete.setObjectName("danger")
|
|
self.btn_delete.clicked.connect(self._delete_profile)
|
|
self.btn_delete.setEnabled(False)
|
|
actions.addWidget(self.btn_delete)
|
|
|
|
actions.addStretch()
|
|
|
|
right_layout.addLayout(actions)
|
|
|
|
splitter.addWidget(right)
|
|
splitter.setSizes([300, 500])
|
|
|
|
layout.addWidget(splitter, 1)
|
|
|
|
# Status label
|
|
self.status_label = QLabel("")
|
|
self.status_label.setStyleSheet("color: #718096; font-style: italic;")
|
|
layout.addWidget(self.status_label)
|
|
|
|
def refresh(self):
|
|
"""Refresh the profile list."""
|
|
self.profile_list.clear()
|
|
profiles = list_profiles()
|
|
|
|
for profile in profiles:
|
|
item = QListWidgetItem()
|
|
if profile.builtin:
|
|
item.setText(f"{profile.name} (built-in)")
|
|
else:
|
|
item.setText(profile.name)
|
|
item.setData(Qt.UserRole, profile)
|
|
self.profile_list.addItem(item)
|
|
|
|
self._clear_form()
|
|
self.status_label.setText(f"{len(profiles)} profiles available")
|
|
|
|
def _on_profile_selected(self, current, previous):
|
|
"""Handle profile selection."""
|
|
if not current:
|
|
self._clear_form()
|
|
return
|
|
|
|
profile = current.data(Qt.UserRole)
|
|
self._load_profile(profile)
|
|
|
|
def _load_profile(self, profile: Profile):
|
|
"""Load profile into form."""
|
|
self.name_input.setText(profile.name)
|
|
self.desc_input.setText(profile.description)
|
|
self.prompt_input.setPlainText(profile.system_prompt)
|
|
|
|
# Built-in profiles can't be edited
|
|
is_builtin = profile.builtin
|
|
self.name_input.setEnabled(not is_builtin)
|
|
self.desc_input.setEnabled(not is_builtin)
|
|
self.prompt_input.setEnabled(not is_builtin)
|
|
self.btn_save.setEnabled(not is_builtin)
|
|
self.btn_delete.setEnabled(not is_builtin and profile.name != "None")
|
|
|
|
if is_builtin:
|
|
self.status_label.setText("Built-in profile (read-only)")
|
|
else:
|
|
self.status_label.setText("Custom profile")
|
|
|
|
def _clear_form(self):
|
|
"""Clear the form."""
|
|
self.name_input.clear()
|
|
self.name_input.setEnabled(True)
|
|
self.desc_input.clear()
|
|
self.desc_input.setEnabled(True)
|
|
self.prompt_input.clear()
|
|
self.prompt_input.setEnabled(True)
|
|
self.btn_save.setEnabled(False)
|
|
self.btn_delete.setEnabled(False)
|
|
|
|
def _new_profile(self):
|
|
"""Start creating a new profile."""
|
|
self.profile_list.clearSelection()
|
|
self._clear_form()
|
|
self.name_input.setFocus()
|
|
self.btn_save.setEnabled(True)
|
|
self.status_label.setText("Creating new profile...")
|
|
|
|
def _save_profile(self):
|
|
"""Save the current profile."""
|
|
name = self.name_input.text().strip()
|
|
if not name:
|
|
QMessageBox.warning(self, "Validation", "Profile name is required")
|
|
return
|
|
|
|
if name == "None":
|
|
QMessageBox.warning(self, "Validation", "'None' is a reserved profile name")
|
|
return
|
|
|
|
description = self.desc_input.text().strip()
|
|
system_prompt = self.prompt_input.toPlainText().strip()
|
|
|
|
if not system_prompt:
|
|
QMessageBox.warning(self, "Validation", "System prompt is required")
|
|
return
|
|
|
|
profile = Profile(
|
|
name=name,
|
|
description=description,
|
|
system_prompt=system_prompt
|
|
)
|
|
|
|
try:
|
|
save_profile(profile)
|
|
self.main_window.show_status(f"Saved profile '{name}'")
|
|
self.refresh()
|
|
# Select the saved profile
|
|
for i in range(self.profile_list.count()):
|
|
item = self.profile_list.item(i)
|
|
if item.data(Qt.UserRole).name == name:
|
|
self.profile_list.setCurrentItem(item)
|
|
break
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save profile:\n{e}")
|
|
|
|
def _delete_profile(self):
|
|
"""Delete the selected profile."""
|
|
current = self.profile_list.currentItem()
|
|
if not current:
|
|
return
|
|
|
|
profile = current.data(Qt.UserRole)
|
|
if profile.builtin:
|
|
QMessageBox.warning(self, "Error", "Cannot delete built-in profiles")
|
|
return
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Confirm Delete",
|
|
f"Delete profile '{profile.name}'?\n\nThis cannot be undone.",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
if delete_profile(profile.name):
|
|
self.main_window.show_status(f"Deleted profile '{profile.name}'")
|
|
self.refresh()
|
|
else:
|
|
QMessageBox.warning(self, "Error", "Failed to delete profile")
|