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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user