feat(ai-outfit): inline prompt preview + move settings to right (layout A)

- Drop the prompt-preview dialog; add a live inline preview pinned at the
  bottom of the left column: sample-row dropdown + read-only rendered text,
  refreshing on prompt edit / sample change, with a missing-{title} hint.
- Sample rows load from the chosen Excel (on browse, on startup from the
  remembered path, and from the run's loaded tasks).
- Move 生成设置 out of the left column into the right run column
  (_create_settings_group), so left = 数据源 + 话术 + 预览.
- Matches docs d79f881. Full suite green; offscreen window smoke passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:49:21 +08:00
co-authored by Claude Opus 4.8
parent d79f881ec7
commit db28a39ebc
+79 -90
View File
@@ -14,8 +14,6 @@ from PySide6.QtWidgets import (
QAbstractItemView,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFileDialog,
QGroupBox,
@@ -129,63 +127,6 @@ class _OutfitWorker(QObject):
self.finished.emit(summary)
# ---------------------------------------------------------------------------
# Preview dialog (最终提示词预览)
# ---------------------------------------------------------------------------
class _PromptPreviewDialog(QDialog):
def __init__(self, parent, template, tasks):
super().__init__(parent)
self.setWindowTitle("最终提示词预览")
self.resize(560, 420)
self._template = template
self._tasks = tasks
col = QVBoxLayout(self)
row = QHBoxLayout()
row.addWidget(QLabel("样本行:"))
self._combo = QComboBox()
for t in tasks:
self._combo.addItem("第 {} 行 · {} · {}".format(
t.row_index, t.product_id, t.title), t)
self._combo.currentIndexChanged.connect(self._render)
row.addWidget(self._combo, stretch=1)
col.addLayout(row)
self._text = QPlainTextEdit()
self._text.setReadOnly(True)
col.addWidget(self._text, stretch=1)
self._warn = QLabel("")
self._warn.setStyleSheet("color:#b87a00;")
self._warn.setVisible(False)
col.addWidget(self._warn)
buttons = QDialogButtonBox()
copy_btn = buttons.addButton("复制", QDialogButtonBox.ActionRole)
buttons.addButton("关闭", QDialogButtonBox.RejectRole)
copy_btn.clicked.connect(self._copy)
buttons.rejected.connect(self.reject)
col.addWidget(buttons)
if "{title}" not in (template or ""):
self._warn.setText("⚠ 话术中未检测到 {title} 占位符")
self._warn.setVisible(True)
self._render()
def _render(self):
from core.ai_outfit import render_prompt
task = self._combo.currentData()
if task is None:
self._text.setPlainText(self._template or "")
return
self._text.setPlainText(render_prompt(self._template or "", task))
def _copy(self):
from PySide6.QtWidgets import QApplication
QApplication.clipboard().setText(self._text.toPlainText())
# ---------------------------------------------------------------------------
# Main panel
# ---------------------------------------------------------------------------
@@ -255,29 +196,51 @@ class AiOutfitPanel(QWidget):
prm = QGroupBox("通用话术")
pv = QVBoxLayout(prm)
self._prompt_edit = QPlainTextEdit()
self._prompt_edit.setFixedHeight(130)
self._prompt_edit.setFixedHeight(120)
self._prompt_edit.textChanged.connect(self._refresh_preview)
pv.addWidget(self._prompt_edit)
prow = QHBoxLayout()
insert_title_btn = QPushButton("插入标题")
insert_title_btn.clicked.connect(
lambda: self._prompt_edit.insertPlainText("{title}"))
prow.addWidget(insert_title_btn)
preview_btn = QPushButton("预览最终提示词")
preview_btn.clicked.connect(self._preview_prompt)
prow.addWidget(preview_btn)
pv.addLayout(prow)
save_btn = QPushButton("保存话术")
save_btn.clicked.connect(self._save_prompt)
pv.addWidget(save_btn)
prow.addWidget(save_btn)
pv.addLayout(prow)
col.addWidget(prm)
# 生成设置
# 最终提示词预览(内嵌、实时;替换样本行的占位符)
prev = QGroupBox("最终提示词预览")
pvw = QVBoxLayout(prev)
srow = QHBoxLayout()
srow.addWidget(QLabel("样本行"))
self._sample_combo = QComboBox()
self._sample_combo.currentIndexChanged.connect(self._refresh_preview)
srow.addWidget(self._sample_combo, stretch=1)
pvw.addLayout(srow)
self._preview_warn = QLabel("")
self._preview_warn.setStyleSheet("color:#b87a00;")
self._preview_warn.setVisible(False)
pvw.addWidget(self._preview_warn)
self._preview_view = QPlainTextEdit()
self._preview_view.setReadOnly(True)
self._preview_view.setMinimumHeight(140)
pvw.addWidget(self._preview_view)
col.addWidget(prev)
col.addStretch()
scroll.setWidget(inner)
return scroll
def _create_settings_group(self):
"""生成设置 group (lives in the right run column)."""
from PySide6.QtWidgets import QFormLayout
gen = QGroupBox("生成设置")
gv = QVBoxLayout(gen)
self._retry_failed_chk = QCheckBox("重试上次失败的行")
gv.addWidget(self._retry_failed_chk)
from PySide6.QtWidgets import QFormLayout
form = QFormLayout()
self._concurrency = QSpinBox()
self._concurrency.setRange(1, 16)
@@ -300,11 +263,7 @@ class AiOutfitPanel(QWidget):
form.addRow("分辨率", self._resolution)
form.addRow("JPG 质量", self._quality)
gv.addLayout(form)
col.addWidget(gen)
col.addStretch()
scroll.setWidget(inner)
return scroll
return gen
def _path_row(self, line_edit, on_browse):
row = QHBoxLayout()
@@ -345,11 +304,13 @@ class AiOutfitPanel(QWidget):
def _build_right(self):
wrap = QWidget()
wrap.setMinimumWidth(280)
wrap.setMinimumWidth(300)
col = QVBoxLayout(wrap)
col.setContentsMargins(12, 12, 12, 12)
col.setSpacing(10)
col.addWidget(self._create_settings_group())
col.addWidget(QLabel("本次进度"))
self._progress = QProgressBar()
self._progress.setValue(0)
@@ -409,6 +370,9 @@ class AiOutfitPanel(QWidget):
self._model_combo.addItem(m.get("name") or m.get("model") or "(未命名)")
self._set_combo(self._model_combo, config.get("outfit_model", ""))
# Fill the preview's sample-row dropdown from the remembered Excel.
self._reload_sample_rows()
def _set_combo(self, combo, value):
idx = combo.findText(str(value))
if idx >= 0:
@@ -436,6 +400,7 @@ class AiOutfitPanel(QWidget):
if path:
self._excel_edit.setText(path)
self._emit_config()
self._reload_sample_rows()
def _browse_output(self):
path = QFileDialog.getExistingDirectory(
@@ -448,25 +413,48 @@ class AiOutfitPanel(QWidget):
save_outfit_prompt(self._prompt_edit.toPlainText())
self.statusBar_message("话术已保存")
def _preview_prompt(self):
tasks = self._peek_tasks()
if tasks is None:
return
dlg = _PromptPreviewDialog(self, self._prompt_edit.toPlainText(), tasks)
dlg.exec()
# -- inline prompt preview ------------------------------------------
def _peek_tasks(self):
"""Load a few tasks just for preview; returns [] if none, None on error."""
def _fill_sample_combo(self, tasks):
"""Populate the sample-row dropdown (None data = no sample)."""
self._sample_combo.blockSignals(True)
self._sample_combo.clear()
if tasks:
for t in tasks:
self._sample_combo.addItem(
"第 {} 行 · {} · {}".format(t.row_index, t.product_id, t.title), t)
else:
self._sample_combo.addItem("(选 Excel 后显示替换效果)", None)
self._sample_combo.blockSignals(False)
self._refresh_preview()
def _reload_sample_rows(self):
"""Best-effort: read the chosen Excel to fill the sample dropdown."""
excel = self._excel_edit.text().strip()
if not excel:
QMessageBox.information(self, "提示", "请先选择 Excel 文件再预览。")
return None
try:
from services.excel_service import load_outfit_tasks
return load_outfit_tasks(excel, retry_failed=self._retry_failed_chk.isChecked())
except Exception as exc: # noqa: BLE001
QMessageBox.warning(self, "读取失败", "无法读取 Excel:{}".format(exc))
return None
tasks = []
if excel:
try:
from services.excel_service import load_outfit_tasks
tasks = load_outfit_tasks(
excel, retry_failed=self._retry_failed_chk.isChecked())
except Exception as exc: # noqa: BLE001 - silent for preview
logger.info("Sample rows unavailable: %s", exc)
self._fill_sample_combo(tasks)
def _refresh_preview(self):
if not hasattr(self, "_preview_view"):
return
template = self._prompt_edit.toPlainText()
self._preview_warn.setVisible("{title}" not in template)
if "{title}" not in template:
self._preview_warn.setText("⚠ 话术缺少 {title} 占位符")
task = self._sample_combo.currentData() if hasattr(self, "_sample_combo") else None
if task is None:
self._preview_view.setPlainText(template)
else:
rendered = template.replace("{title}", task.title).replace(
"{product_id}", task.product_id)
self._preview_view.setPlainText(rendered)
# -- run control ----------------------------------------------------
@@ -575,6 +563,7 @@ class AiOutfitPanel(QWidget):
self._set_cell(row, 5, "—")
self._progress.setMaximum(max(1, len(tasks)))
self._update_stats(0, 0, len(tasks))
self._fill_sample_combo(tasks)
self._append_log("已加载 {} 行待处理任务".format(len(tasks)))
def _on_progress(self, completed, total, result):