feat(product-suite): add prompt template settings
This commit is contained in:
@@ -39,6 +39,7 @@ if QT_IMPORT_ERROR is None:
|
||||
from .tabs.collect import CollectTab
|
||||
from .tabs.generate import GenerateTab
|
||||
from .tabs.image_studio import ImageStudioPreviewDialog, ImageStudioTab
|
||||
from .product_suite_prompt_dialog import ProductSuitePromptDialog
|
||||
from .tabs.product_suite import ProductSuitePreviewDialog, ProductSuiteTab
|
||||
from .tabs.settings import SettingsTab
|
||||
from .main_window import MainWindow
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
"""Product-suite prompt template editor and final prompt preview."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QFontDatabase
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSplitter,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .. import product_suite, prompts
|
||||
|
||||
|
||||
class ProductSuitePromptDialog(QDialog):
|
||||
"""Edit one global product-suite template and preview the final request."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prompt_path,
|
||||
base_prompt,
|
||||
settings,
|
||||
item_id,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("productSuitePromptDialog")
|
||||
self.setWindowTitle("套图提示词设置")
|
||||
self.setMinimumSize(760, 480)
|
||||
self.resize(920, 600)
|
||||
self.prompt_path = str(prompt_path)
|
||||
self.base_prompt = str(base_prompt or "")
|
||||
self.settings = product_suite.normalize_suite_settings(settings)
|
||||
self.item_id = str(item_id or "")
|
||||
self._saved_text = ""
|
||||
self._allow_close = False
|
||||
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
self._load_current_template()
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(12, 12, 12, 12)
|
||||
root.setSpacing(10)
|
||||
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.setSpacing(8)
|
||||
self.save_button = QPushButton("保存")
|
||||
self.save_button.setObjectName("suitePromptSaveButton")
|
||||
self.save_button.setMinimumHeight(32)
|
||||
toolbar.addWidget(self.save_button)
|
||||
self.restore_button = QPushButton("恢复默认")
|
||||
self.restore_button.setObjectName("suitePromptRestoreButton")
|
||||
self.restore_button.setMinimumHeight(32)
|
||||
toolbar.addWidget(self.restore_button)
|
||||
self.feedback_label = QLabel("")
|
||||
self.feedback_label.setObjectName("suitePromptFeedbackLabel")
|
||||
self.feedback_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
toolbar.addWidget(self.feedback_label, 1)
|
||||
toolbar.addWidget(QLabel("预览分类"))
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.setObjectName("suitePromptCategoryCombo")
|
||||
self.category_combo.setMinimumWidth(128)
|
||||
for category in product_suite.category_order(self.settings):
|
||||
self.category_combo.addItem(category, category)
|
||||
white_index = self.category_combo.findData("白底图")
|
||||
if white_index >= 0:
|
||||
self.category_combo.setCurrentIndex(white_index)
|
||||
toolbar.addWidget(self.category_combo)
|
||||
root.addLayout(toolbar)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal)
|
||||
self.splitter.setObjectName("suitePromptSplitter")
|
||||
self.splitter.addWidget(self._build_editor_panel())
|
||||
self.splitter.addWidget(self._build_preview_panel())
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.splitter.setStretchFactor(1, 1)
|
||||
self.splitter.setSizes([440, 440])
|
||||
root.addWidget(self.splitter, 1)
|
||||
|
||||
def _build_editor_panel(self):
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout(panel)
|
||||
layout.setContentsMargins(0, 0, 4, 0)
|
||||
layout.setSpacing(6)
|
||||
title_row = QHBoxLayout()
|
||||
title = QLabel("基础模板")
|
||||
title.setStyleSheet("font-weight: 600;")
|
||||
title_row.addWidget(title)
|
||||
title_row.addStretch(1)
|
||||
self.variable_button = QToolButton()
|
||||
self.variable_button.setObjectName("suitePromptVariableButton")
|
||||
self.variable_button.setText("插入变量")
|
||||
self.variable_button.setToolTip("在当前光标位置插入提示词变量")
|
||||
self.variable_button.setAccessibleName("插入提示词变量")
|
||||
self.variable_button.setPopupMode(QToolButton.InstantPopup)
|
||||
menu = QMenu(self.variable_button)
|
||||
for name in product_suite.PRODUCT_SUITE_PLACEHOLDERS:
|
||||
action = menu.addAction("{%s}" % name)
|
||||
action.triggered.connect(
|
||||
lambda checked=False, variable=name: self.insert_variable(variable)
|
||||
)
|
||||
self.variable_button.setMenu(menu)
|
||||
title_row.addWidget(self.variable_button)
|
||||
layout.addLayout(title_row)
|
||||
|
||||
self.template_edit = QPlainTextEdit()
|
||||
self.template_edit.setObjectName("suitePromptTemplateEdit")
|
||||
self.template_edit.setPlaceholderText("输入套图提示词模板")
|
||||
self.template_edit.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
|
||||
layout.addWidget(self.template_edit, 1)
|
||||
|
||||
self.validation_label = QLabel("")
|
||||
self.validation_label.setObjectName("suitePromptValidationLabel")
|
||||
self.validation_label.setWordWrap(True)
|
||||
self.validation_label.setStyleSheet("color: #cf222e;")
|
||||
layout.addWidget(self.validation_label)
|
||||
return panel
|
||||
|
||||
def _build_preview_panel(self):
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout(panel)
|
||||
layout.setContentsMargins(4, 0, 0, 0)
|
||||
layout.setSpacing(6)
|
||||
title = QLabel("最终提示词预览")
|
||||
title.setStyleSheet("font-weight: 600;")
|
||||
layout.addWidget(title)
|
||||
self.preview_edit = QPlainTextEdit()
|
||||
self.preview_edit.setObjectName("suitePromptPreviewEdit")
|
||||
self.preview_edit.setReadOnly(True)
|
||||
self.preview_edit.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
|
||||
self.preview_edit.setStyleSheet(
|
||||
"QPlainTextEdit#suitePromptPreviewEdit { "
|
||||
"background: #f6f8fa; color: #24292f; border: 1px solid #d0d7de; "
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self.preview_edit, 1)
|
||||
return panel
|
||||
|
||||
def _connect_signals(self):
|
||||
self.save_button.clicked.connect(self.save_template)
|
||||
self.restore_button.clicked.connect(self.restore_default)
|
||||
self.category_combo.currentIndexChanged.connect(self.schedule_preview)
|
||||
self.template_edit.textChanged.connect(self.schedule_preview)
|
||||
self.preview_timer = QTimer(self)
|
||||
self.preview_timer.setSingleShot(True)
|
||||
self.preview_timer.setInterval(200)
|
||||
self.preview_timer.timeout.connect(self.refresh_preview)
|
||||
self.feedback_timer = QTimer(self)
|
||||
self.feedback_timer.setSingleShot(True)
|
||||
self.feedback_timer.setInterval(3000)
|
||||
self.feedback_timer.timeout.connect(lambda: self.feedback_label.setText(""))
|
||||
|
||||
def _load_current_template(self):
|
||||
try:
|
||||
text = prompts.read_product_suite_prompt(self.prompt_path)
|
||||
except prompts.PromptError as exc:
|
||||
text = ""
|
||||
self._set_feedback(str(exc), "danger")
|
||||
self._saved_text = text
|
||||
self.template_edit.setPlainText(text)
|
||||
self.refresh_preview()
|
||||
|
||||
def insert_variable(self, name):
|
||||
if name not in product_suite.PRODUCT_SUITE_PLACEHOLDERS:
|
||||
return
|
||||
cursor = self.template_edit.textCursor()
|
||||
cursor.insertText("{%s}" % name)
|
||||
self.template_edit.setTextCursor(cursor)
|
||||
self.template_edit.setFocus()
|
||||
|
||||
def schedule_preview(self, value=None):
|
||||
self.preview_timer.start()
|
||||
|
||||
def preview_context(self):
|
||||
category = str(self.category_combo.currentData() or "白底图")
|
||||
return product_suite.product_suite_prompt_context(
|
||||
self.base_prompt,
|
||||
self.settings,
|
||||
category,
|
||||
self.item_id or "未绑定商品",
|
||||
source_index=1,
|
||||
)
|
||||
|
||||
def refresh_preview(self):
|
||||
text = self.template_edit.toPlainText()
|
||||
errors = product_suite.product_suite_prompt_errors(text)
|
||||
self.save_button.setEnabled(not errors)
|
||||
self.validation_label.setText(";".join(errors))
|
||||
if errors:
|
||||
self.preview_edit.setPlainText(
|
||||
"套图提示词模板无效:\n%s"
|
||||
% "\n".join("- %s" % error for error in errors)
|
||||
)
|
||||
return
|
||||
try:
|
||||
rendered = product_suite.render_product_suite_prompt(
|
||||
text,
|
||||
self.preview_context(),
|
||||
)
|
||||
except product_suite.ProductSuitePromptError as exc:
|
||||
self.save_button.setEnabled(False)
|
||||
self.validation_label.setText(str(exc))
|
||||
self.preview_edit.setPlainText("套图提示词模板无效:\n%s" % exc)
|
||||
return
|
||||
self.preview_edit.setPlainText(rendered)
|
||||
|
||||
def save_template(self):
|
||||
text = self.template_edit.toPlainText()
|
||||
try:
|
||||
prompts.save_product_suite_prompt(text, self.prompt_path)
|
||||
except prompts.PromptError as exc:
|
||||
self._message("提示词保存失败", str(exc))
|
||||
return False
|
||||
self._saved_text = text
|
||||
self.refresh_preview()
|
||||
self._set_feedback("套图提示词模板已保存", "success")
|
||||
return True
|
||||
|
||||
def restore_default(self):
|
||||
try:
|
||||
prompts.load_default_product_suite_prompt()
|
||||
except prompts.PromptError as exc:
|
||||
self._message("恢复默认失败", str(exc))
|
||||
return False
|
||||
if not self._confirm_restore():
|
||||
return False
|
||||
try:
|
||||
text = prompts.restore_default_product_suite_prompt(self.prompt_path)
|
||||
except prompts.PromptError as exc:
|
||||
self._message("恢复默认失败", str(exc))
|
||||
return False
|
||||
self._saved_text = text
|
||||
previous = self.template_edit.blockSignals(True)
|
||||
try:
|
||||
self.template_edit.setPlainText(text)
|
||||
finally:
|
||||
self.template_edit.blockSignals(previous)
|
||||
self.refresh_preview()
|
||||
self._set_feedback("已恢复默认套图提示词", "success")
|
||||
return True
|
||||
|
||||
def is_dirty(self):
|
||||
return self.template_edit.toPlainText() != self._saved_text
|
||||
|
||||
def _confirm_restore(self):
|
||||
box = QMessageBox(self)
|
||||
box.setIcon(QMessageBox.Warning)
|
||||
box.setWindowTitle("恢复默认提示词")
|
||||
box.setText("恢复默认会丢弃当前未保存的模板修改。确认继续吗?")
|
||||
restore_button = box.addButton("恢复默认", QMessageBox.AcceptRole)
|
||||
box.addButton("取消", QMessageBox.RejectRole)
|
||||
box.exec()
|
||||
return box.clickedButton() is restore_button
|
||||
|
||||
def _unsaved_action(self):
|
||||
box = QMessageBox(self)
|
||||
box.setIcon(QMessageBox.Question)
|
||||
box.setWindowTitle("提示词尚未保存")
|
||||
box.setText("当前套图提示词模板有未保存修改。")
|
||||
save_button = box.addButton("保存", QMessageBox.AcceptRole)
|
||||
discard_button = box.addButton("不保存", QMessageBox.DestructiveRole)
|
||||
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
|
||||
box.setDefaultButton(save_button)
|
||||
box.exec()
|
||||
clicked = box.clickedButton()
|
||||
if clicked is save_button:
|
||||
return "save"
|
||||
if clicked is discard_button:
|
||||
return "discard"
|
||||
if clicked is cancel_button:
|
||||
return "cancel"
|
||||
return "cancel"
|
||||
|
||||
def _can_close(self):
|
||||
if not self.is_dirty():
|
||||
return True
|
||||
action = self._unsaved_action()
|
||||
if action == "save":
|
||||
return self.save_template()
|
||||
return action == "discard"
|
||||
|
||||
def reject(self):
|
||||
if self._can_close():
|
||||
self._allow_close = True
|
||||
super().reject()
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self._allow_close or self._can_close():
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def _set_feedback(self, message, level):
|
||||
color = "#1a7f37" if level == "success" else "#cf222e"
|
||||
self.feedback_label.setStyleSheet("color: %s; font-weight: 600;" % color)
|
||||
self.feedback_label.setText(str(message))
|
||||
self.feedback_timer.start()
|
||||
|
||||
def _message(self, title, message):
|
||||
box = QMessageBox(self)
|
||||
box.setIcon(QMessageBox.Warning)
|
||||
box.setWindowTitle(str(title))
|
||||
box.setText(str(message))
|
||||
box.exec()
|
||||
@@ -40,9 +40,18 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ... import accounts, appconfig, diagnostics, image_studio, image_studio_images, product_suite
|
||||
from ... import (
|
||||
accounts,
|
||||
appconfig,
|
||||
diagnostics,
|
||||
image_studio,
|
||||
image_studio_images,
|
||||
product_suite,
|
||||
prompts,
|
||||
)
|
||||
from .. import file_manager
|
||||
from ..image_preview import ImagePreviewDialog
|
||||
from ..product_suite_prompt_dialog import ProductSuitePromptDialog
|
||||
from ..widgets import _emit_status, run_worker
|
||||
from ..workers import (
|
||||
ImageStudioDownloadOriginalWorker,
|
||||
@@ -57,13 +66,6 @@ ORIGINAL_DOWNLOAD_CONCURRENCY = 2
|
||||
ORIGINAL_CHECK_STATE_ROLE = Qt.UserRole + 1
|
||||
_PRODUCT_SUITE_THREAD_REFS = {}
|
||||
_URL_RE = re.compile(r"https?://[^\s,,;;))\]]+", re.IGNORECASE)
|
||||
FIXED_CATEGORY_HELPERS = {
|
||||
"白底图": "白底主图,多角度呈现商品细节",
|
||||
"场景图": "生活化场景展示商品使用方式",
|
||||
"卖点图": "突出核心卖点和差异化优势",
|
||||
}
|
||||
|
||||
|
||||
def _asset_usable(asset):
|
||||
path = str(getattr(asset, "local_path", "") or "")
|
||||
return (
|
||||
@@ -663,6 +665,7 @@ class ProductSuiteTab(QWidget):
|
||||
self.db_path = db_path or appconfig.db_path(self.config)
|
||||
self.config_path = config_path or self.config.get("config_path") or appconfig.CONFIG_PATH
|
||||
self.cmhub_config_path = self.config.get("cmhub_config_path") or appconfig.cmhub_config_file_path(self.config)
|
||||
self.product_suite_prompt_path = appconfig.product_suite_prompt_path(self.config)
|
||||
self.status_callback = status_callback
|
||||
self.accounts = []
|
||||
self._states = {}
|
||||
@@ -673,6 +676,12 @@ class ProductSuiteTab(QWidget):
|
||||
self._original_list_context = None
|
||||
self._loading = False
|
||||
self._result_refresh_pending = False
|
||||
self._prompt_template_init_error = ""
|
||||
|
||||
try:
|
||||
prompts.ensure_default_product_suite_prompt(self.product_suite_prompt_path)
|
||||
except prompts.PromptError as exc:
|
||||
self._prompt_template_init_error = str(exc)
|
||||
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
@@ -684,6 +693,8 @@ class ProductSuiteTab(QWidget):
|
||||
self.elapsed_timer.setInterval(1000)
|
||||
self.elapsed_timer.timeout.connect(self._refresh_elapsed)
|
||||
self.elapsed_timer.start()
|
||||
if self._prompt_template_init_error:
|
||||
self._status(self._prompt_template_init_error, "danger")
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
@@ -956,17 +967,23 @@ class ProductSuiteTab(QWidget):
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
title_row = QHBoxLayout()
|
||||
self.prompt_title_layout = title_row
|
||||
title = QLabel("商品卖点与要求")
|
||||
self.prompt_title_label = title
|
||||
title.setStyleSheet("font-weight: 600;")
|
||||
title_row.addWidget(title)
|
||||
title_row.addStretch(1)
|
||||
self.ai_write_button = QPushButton("AI 帮写")
|
||||
self.ai_write_button.setObjectName("suiteAiWriteButton")
|
||||
title_row.addWidget(self.ai_write_button)
|
||||
self.ai_cancel_button = QPushButton("取消")
|
||||
self.ai_cancel_button.setObjectName("suiteAiCancelButton")
|
||||
self.ai_cancel_button.hide()
|
||||
title_row.addWidget(self.ai_cancel_button)
|
||||
self.ai_write_button = QPushButton("AI 帮写")
|
||||
self.ai_write_button.setObjectName("suiteAiWriteButton")
|
||||
title_row.addWidget(self.ai_write_button)
|
||||
title_row.addStretch(1)
|
||||
self.prompt_settings_button = QPushButton("提示词设置")
|
||||
self.prompt_settings_button.setObjectName("suitePromptSettingsButton")
|
||||
self.prompt_settings_button.setToolTip("编辑并预览套图最终提示词")
|
||||
title_row.addWidget(self.prompt_settings_button)
|
||||
layout.addLayout(title_row)
|
||||
self.prompt_edit = QPlainTextEdit()
|
||||
self.prompt_edit.setObjectName("suitePromptEdit")
|
||||
@@ -1098,6 +1115,7 @@ class ProductSuiteTab(QWidget):
|
||||
self.prompt_edit.textChanged.connect(self._on_prompt_changed)
|
||||
self.ai_write_button.clicked.connect(self.start_ai_write)
|
||||
self.ai_cancel_button.clicked.connect(self.cancel_ai_write)
|
||||
self.prompt_settings_button.clicked.connect(self.open_prompt_settings)
|
||||
self.add_category_button.clicked.connect(self.add_custom_category)
|
||||
self.custom_category_edit.returnPressed.connect(self._commit_custom_category)
|
||||
self.custom_category_edit.editingFinished.connect(self._finish_custom_category_edit)
|
||||
@@ -1638,6 +1656,27 @@ class ProductSuiteTab(QWidget):
|
||||
state = self._displayed_state
|
||||
state.prompt = self.prompt_edit.toPlainText()
|
||||
|
||||
def open_prompt_settings(self, checked=False):
|
||||
state = self._displayed_state
|
||||
if state is not None:
|
||||
self._save_controls_to_state(state)
|
||||
settings = state.settings
|
||||
base_prompt = state.prompt
|
||||
item_id = state.item_id or "未绑定商品"
|
||||
else:
|
||||
settings = product_suite.default_suite_settings()
|
||||
base_prompt = ""
|
||||
item_id = "未绑定商品"
|
||||
dialog = ProductSuitePromptDialog(
|
||||
prompt_path=self.product_suite_prompt_path,
|
||||
base_prompt=base_prompt,
|
||||
settings=settings,
|
||||
item_id=item_id,
|
||||
parent=self,
|
||||
)
|
||||
self.prompt_settings_dialog = dialog
|
||||
dialog.exec()
|
||||
|
||||
def _update_context_actions(self, state):
|
||||
is_draft = self._is_draft_state(state)
|
||||
self.pull_button.setEnabled(
|
||||
@@ -2093,7 +2132,7 @@ class ProductSuiteTab(QWidget):
|
||||
row = SuiteCategoryRow(
|
||||
name,
|
||||
categories.get(name, 0),
|
||||
helper=FIXED_CATEGORY_HELPERS.get(name, ""),
|
||||
helper=product_suite.category_helper(name),
|
||||
custom=custom,
|
||||
)
|
||||
row.countChangeRequested.connect(self.change_category_count)
|
||||
@@ -2327,6 +2366,18 @@ class ProductSuiteTab(QWidget):
|
||||
return False
|
||||
if state is self._displayed_state:
|
||||
self._save_controls_to_state(state)
|
||||
template_text = None
|
||||
if specs is None:
|
||||
try:
|
||||
template_text = prompts.load_product_suite_prompt(
|
||||
self.product_suite_prompt_path
|
||||
)
|
||||
except prompts.PromptError:
|
||||
self._message(
|
||||
"套图提示词模板无效",
|
||||
"套图提示词模板无效,请在提示词设置中修复或恢复默认。",
|
||||
)
|
||||
return False
|
||||
if self._ensure_project_for_local_work(state) is None:
|
||||
return False
|
||||
local_assets = [asset for asset in self._original_assets(state) if _asset_usable(asset)]
|
||||
@@ -2341,6 +2392,7 @@ class ProductSuiteTab(QWidget):
|
||||
state.prompt,
|
||||
state.settings,
|
||||
state.item_id or "未绑定商品",
|
||||
template_text=template_text,
|
||||
))
|
||||
if not specs:
|
||||
self._message("生成数量为0", "请至少把一个套图分类的数量设为1。")
|
||||
|
||||
Reference in New Issue
Block a user