diff --git a/src/app/widgets/ai_outfit_panel.py b/src/app/widgets/ai_outfit_panel.py index c08768d..60afc3a 100644 --- a/src/app/widgets/ai_outfit_panel.py +++ b/src/app/widgets/ai_outfit_panel.py @@ -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):