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,
|
QAbstractItemView,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QDoubleSpinBox,
|
QDoubleSpinBox,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
@@ -129,63 +127,6 @@ class _OutfitWorker(QObject):
|
|||||||
self.finished.emit(summary)
|
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
|
# Main panel
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -255,29 +196,51 @@ class AiOutfitPanel(QWidget):
|
|||||||
prm = QGroupBox("通用话术")
|
prm = QGroupBox("通用话术")
|
||||||
pv = QVBoxLayout(prm)
|
pv = QVBoxLayout(prm)
|
||||||
self._prompt_edit = QPlainTextEdit()
|
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)
|
pv.addWidget(self._prompt_edit)
|
||||||
prow = QHBoxLayout()
|
prow = QHBoxLayout()
|
||||||
insert_title_btn = QPushButton("插入标题")
|
insert_title_btn = QPushButton("插入标题")
|
||||||
insert_title_btn.clicked.connect(
|
insert_title_btn.clicked.connect(
|
||||||
lambda: self._prompt_edit.insertPlainText("{title}"))
|
lambda: self._prompt_edit.insertPlainText("{title}"))
|
||||||
prow.addWidget(insert_title_btn)
|
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 = QPushButton("保存话术")
|
||||||
save_btn.clicked.connect(self._save_prompt)
|
save_btn.clicked.connect(self._save_prompt)
|
||||||
pv.addWidget(save_btn)
|
prow.addWidget(save_btn)
|
||||||
|
pv.addLayout(prow)
|
||||||
col.addWidget(prm)
|
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("生成设置")
|
gen = QGroupBox("生成设置")
|
||||||
gv = QVBoxLayout(gen)
|
gv = QVBoxLayout(gen)
|
||||||
self._retry_failed_chk = QCheckBox("重试上次失败的行")
|
self._retry_failed_chk = QCheckBox("重试上次失败的行")
|
||||||
gv.addWidget(self._retry_failed_chk)
|
gv.addWidget(self._retry_failed_chk)
|
||||||
|
|
||||||
from PySide6.QtWidgets import QFormLayout
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
self._concurrency = QSpinBox()
|
self._concurrency = QSpinBox()
|
||||||
self._concurrency.setRange(1, 16)
|
self._concurrency.setRange(1, 16)
|
||||||
@@ -300,11 +263,7 @@ class AiOutfitPanel(QWidget):
|
|||||||
form.addRow("分辨率", self._resolution)
|
form.addRow("分辨率", self._resolution)
|
||||||
form.addRow("JPG 质量", self._quality)
|
form.addRow("JPG 质量", self._quality)
|
||||||
gv.addLayout(form)
|
gv.addLayout(form)
|
||||||
col.addWidget(gen)
|
return gen
|
||||||
|
|
||||||
col.addStretch()
|
|
||||||
scroll.setWidget(inner)
|
|
||||||
return scroll
|
|
||||||
|
|
||||||
def _path_row(self, line_edit, on_browse):
|
def _path_row(self, line_edit, on_browse):
|
||||||
row = QHBoxLayout()
|
row = QHBoxLayout()
|
||||||
@@ -345,11 +304,13 @@ class AiOutfitPanel(QWidget):
|
|||||||
|
|
||||||
def _build_right(self):
|
def _build_right(self):
|
||||||
wrap = QWidget()
|
wrap = QWidget()
|
||||||
wrap.setMinimumWidth(280)
|
wrap.setMinimumWidth(300)
|
||||||
col = QVBoxLayout(wrap)
|
col = QVBoxLayout(wrap)
|
||||||
col.setContentsMargins(12, 12, 12, 12)
|
col.setContentsMargins(12, 12, 12, 12)
|
||||||
col.setSpacing(10)
|
col.setSpacing(10)
|
||||||
|
|
||||||
|
col.addWidget(self._create_settings_group())
|
||||||
|
|
||||||
col.addWidget(QLabel("本次进度"))
|
col.addWidget(QLabel("本次进度"))
|
||||||
self._progress = QProgressBar()
|
self._progress = QProgressBar()
|
||||||
self._progress.setValue(0)
|
self._progress.setValue(0)
|
||||||
@@ -409,6 +370,9 @@ class AiOutfitPanel(QWidget):
|
|||||||
self._model_combo.addItem(m.get("name") or m.get("model") or "(未命名)")
|
self._model_combo.addItem(m.get("name") or m.get("model") or "(未命名)")
|
||||||
self._set_combo(self._model_combo, config.get("outfit_model", ""))
|
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):
|
def _set_combo(self, combo, value):
|
||||||
idx = combo.findText(str(value))
|
idx = combo.findText(str(value))
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
@@ -436,6 +400,7 @@ class AiOutfitPanel(QWidget):
|
|||||||
if path:
|
if path:
|
||||||
self._excel_edit.setText(path)
|
self._excel_edit.setText(path)
|
||||||
self._emit_config()
|
self._emit_config()
|
||||||
|
self._reload_sample_rows()
|
||||||
|
|
||||||
def _browse_output(self):
|
def _browse_output(self):
|
||||||
path = QFileDialog.getExistingDirectory(
|
path = QFileDialog.getExistingDirectory(
|
||||||
@@ -448,25 +413,48 @@ class AiOutfitPanel(QWidget):
|
|||||||
save_outfit_prompt(self._prompt_edit.toPlainText())
|
save_outfit_prompt(self._prompt_edit.toPlainText())
|
||||||
self.statusBar_message("话术已保存")
|
self.statusBar_message("话术已保存")
|
||||||
|
|
||||||
def _preview_prompt(self):
|
# -- inline prompt preview ------------------------------------------
|
||||||
tasks = self._peek_tasks()
|
|
||||||
if tasks is None:
|
|
||||||
return
|
|
||||||
dlg = _PromptPreviewDialog(self, self._prompt_edit.toPlainText(), tasks)
|
|
||||||
dlg.exec()
|
|
||||||
|
|
||||||
def _peek_tasks(self):
|
def _fill_sample_combo(self, tasks):
|
||||||
"""Load a few tasks just for preview; returns [] if none, None on error."""
|
"""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()
|
excel = self._excel_edit.text().strip()
|
||||||
if not excel:
|
tasks = []
|
||||||
QMessageBox.information(self, "提示", "请先选择 Excel 文件再预览。")
|
if excel:
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
from services.excel_service import load_outfit_tasks
|
from services.excel_service import load_outfit_tasks
|
||||||
return load_outfit_tasks(excel, retry_failed=self._retry_failed_chk.isChecked())
|
tasks = load_outfit_tasks(
|
||||||
except Exception as exc: # noqa: BLE001
|
excel, retry_failed=self._retry_failed_chk.isChecked())
|
||||||
QMessageBox.warning(self, "读取失败", "无法读取 Excel:{}".format(exc))
|
except Exception as exc: # noqa: BLE001 - silent for preview
|
||||||
return None
|
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 ----------------------------------------------------
|
# -- run control ----------------------------------------------------
|
||||||
|
|
||||||
@@ -575,6 +563,7 @@ class AiOutfitPanel(QWidget):
|
|||||||
self._set_cell(row, 5, "—")
|
self._set_cell(row, 5, "—")
|
||||||
self._progress.setMaximum(max(1, len(tasks)))
|
self._progress.setMaximum(max(1, len(tasks)))
|
||||||
self._update_stats(0, 0, len(tasks))
|
self._update_stats(0, 0, len(tasks))
|
||||||
|
self._fill_sample_combo(tasks)
|
||||||
self._append_log("已加载 {} 行待处理任务".format(len(tasks)))
|
self._append_log("已加载 {} 行待处理任务".format(len(tasks)))
|
||||||
|
|
||||||
def _on_progress(self, completed, total, result):
|
def _on_progress(self, completed, total, result):
|
||||||
|
|||||||
Reference in New Issue
Block a user