feat(product-suite): autosave prompt edits

This commit is contained in:
chengma
2026-07-16 16:39:40 +08:00
parent a9acfee460
commit f5892bb310
4 changed files with 433 additions and 10 deletions
+157 -8
View File
@@ -64,8 +64,72 @@ from ..workers import (
ORIGINAL_DOWNLOAD_CONCURRENCY = 2
ORIGINAL_CHECK_STATE_ROLE = Qt.UserRole + 1
PROMPT_AUTOSAVE_DELAY_MS = 500
_PRODUCT_SUITE_THREAD_REFS = {}
_URL_RE = re.compile(r"https?://[^\s,,;;))\]]+", re.IGNORECASE)
class AutoHeightPlainTextEdit(QPlainTextEdit):
"""A plain-text editor that delegates scrolling to its containing page."""
def __init__(self, parent=None, minimum_height=96):
super().__init__(parent)
self._minimum_content_height = max(1, int(minimum_height))
self._height_update_pending = False
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFixedHeight(self._minimum_content_height)
self.document().contentsChanged.connect(self.schedule_height_update)
self.document().documentLayout().documentSizeChanged.connect(
self.schedule_height_update
)
self.schedule_height_update()
def schedule_height_update(self, *args):
if self._height_update_pending:
return
self._height_update_pending = True
QTimer.singleShot(0, self.refresh_height)
def refresh_height(self):
self._height_update_pending = False
text_height = 0.0
block = self.document().firstBlock()
while block.isValid():
layout = block.layout()
line_count = max(1, layout.lineCount())
for index in range(line_count):
line = layout.lineAt(index)
text_height += (
line.height()
if line.isValid()
else self.fontMetrics().lineSpacing()
)
block = block.next()
margins = self.contentsMargins()
target = int(
text_height
+ (self.document().documentMargin() * 2)
+ margins.top()
+ margins.bottom()
+ (self.frameWidth() * 2)
+ 6
)
target = max(self._minimum_content_height, target)
if self.height() != target:
self.setFixedHeight(target)
self.updateGeometry()
def resizeEvent(self, event):
super().resizeEvent(event)
self.schedule_height_update()
def showEvent(self, event):
super().showEvent(event)
self.schedule_height_update()
def _asset_usable(asset):
path = str(getattr(asset, "local_path", "") or "")
return (
@@ -618,6 +682,7 @@ class SuiteTaskState:
project_id: int = None
project_binding_state: str = ""
prompt: str = ""
last_saved_prompt: str = ""
settings: dict = field(default_factory=product_suite.default_suite_settings)
current_job_ids: list = field(default_factory=list)
show_history: bool = False
@@ -673,6 +738,7 @@ class ProductSuiteTab(QWidget):
self._next_key = 1
self._next_serial = 1
self._displayed_state = None
self._prompt_save_timers = {}
self._original_list_context = None
self._loading = False
self._result_refresh_pending = False
@@ -985,11 +1051,9 @@ class ProductSuiteTab(QWidget):
self.prompt_settings_button.setToolTip("编辑并预览套图最终提示词")
title_row.addWidget(self.prompt_settings_button)
layout.addLayout(title_row)
self.prompt_edit = QPlainTextEdit()
self.prompt_edit = AutoHeightPlainTextEdit()
self.prompt_edit.setObjectName("suitePromptEdit")
self.prompt_edit.setPlaceholderText("输入产品名称、卖点、目标人群、使用场景和禁用元素")
self.prompt_edit.setMinimumHeight(96)
self.prompt_edit.setMaximumHeight(112)
layout.addWidget(self.prompt_edit)
helper = QLabel("内容越具体,生成的商品套图越稳定。AI 帮写不会阻塞其他套图任务。")
helper.setWordWrap(True)
@@ -1198,6 +1262,7 @@ class ProductSuiteTab(QWidget):
project_id=int(project.id),
project_binding_state=project.binding_state,
prompt=str(project.draft_prompt or ""),
last_saved_prompt=str(project.draft_prompt or ""),
settings=product_suite.normalize_suite_settings(
image_studio.project_suite_settings(project)
),
@@ -1240,6 +1305,9 @@ class ProductSuiteTab(QWidget):
state = self._states.get(key)
if state is None:
return
if state is self._displayed_state:
self._save_controls_to_state(state)
self._flush_prompt_save(state)
project = self._state_project(state)
draft_action = None
if image_studio.is_draft_project(project) and image_studio.project_has_content(
@@ -1278,6 +1346,7 @@ class ProductSuiteTab(QWidget):
image_studio.discard_empty_draft_project(project.id, path=self.db_path)
except Exception as exc:
self._status("清理空临时草稿失败:%s" % _user_error(exc), "danger")
self._release_prompt_save_timer(state)
self._retired_states.append(state)
self._states.pop(state.key, None)
self.task_tabs.removeTab(index)
@@ -1313,6 +1382,7 @@ class ProductSuiteTab(QWidget):
return
if self._displayed_state is not None:
self._save_controls_to_state(self._displayed_state)
self._flush_prompt_save(self._displayed_state)
state = self._state_for_index(index)
self._displayed_state = state
if state is not None:
@@ -1433,6 +1503,7 @@ class ProductSuiteTab(QWidget):
self._loading = False
self._update_context_actions(state)
return
self._flush_prompt_save(state)
try:
project = image_studio.bind_draft_project(
state.project_id,
@@ -1484,8 +1555,10 @@ class ProductSuiteTab(QWidget):
self._update_context_actions(state)
def _clear_project_binding(self, state):
self._flush_prompt_save(state)
state.project_id = None
state.project_binding_state = ""
state.last_saved_prompt = ""
state.current_job_ids = []
state.done = state.failed = state.total = 0
state.started_at = None
@@ -1547,6 +1620,7 @@ class ProductSuiteTab(QWidget):
if state is None:
return None
if state.project_id is not None:
self._flush_prompt_save(state)
return self._state_project(state)
if state.item_id:
return self._bind_project(state)
@@ -1564,6 +1638,7 @@ class ProductSuiteTab(QWidget):
project = image_studio.create_or_get_project(
account,
item_id=state.item_id,
draft_prompt=state.prompt,
path=self.db_path,
)
except Exception as exc:
@@ -1571,13 +1646,17 @@ class ProductSuiteTab(QWidget):
return None
state.project_id = int(project.id)
state.project_binding_state = project.binding_state
stored_prompt = str(project.draft_prompt or "")
if load_existing and previous_id != state.project_id:
state.prompt = str(project.draft_prompt or "")
state.prompt = stored_prompt
state.settings = product_suite.normalize_suite_settings(
image_studio.project_suite_settings(project)
)
if state is self._displayed_state:
self._load_state(state)
state.last_saved_prompt = stored_prompt
if state.prompt != stored_prompt:
self._flush_prompt_save(state)
return project
def _create_draft_project(self, state):
@@ -1596,6 +1675,7 @@ class ProductSuiteTab(QWidget):
state.project_id = int(project.id)
state.project_binding_state = project.binding_state
state.item_id = ""
state.last_saved_prompt = str(project.draft_prompt or "")
self._persist_state(state)
self._set_task_title(state)
if state is self._displayed_state:
@@ -1603,15 +1683,75 @@ class ProductSuiteTab(QWidget):
self._status("已创建临时草稿,可继续添加本地图片", "info")
return project
def _persist_state(self, state):
if state.project_id is None:
def _prompt_save_timer(self, state):
timer = self._prompt_save_timers.get(state.key)
if timer is None:
timer = QTimer(self)
timer.setSingleShot(True)
timer.setInterval(PROMPT_AUTOSAVE_DELAY_MS)
timer.timeout.connect(
lambda key=state.key: self._on_prompt_autosave_timeout(key)
)
self._prompt_save_timers[state.key] = timer
return timer
def _schedule_prompt_save(self, state):
if state is None or state.project_id is None:
return
if state.prompt == state.last_saved_prompt:
self._cancel_prompt_save(state)
return
self._prompt_save_timer(state).start()
def _cancel_prompt_save(self, state):
timer = self._prompt_save_timers.get(state.key) if state is not None else None
if timer is not None:
timer.stop()
def _release_prompt_save_timer(self, state):
timer = (
self._prompt_save_timers.pop(state.key, None)
if state is not None
else None
)
if timer is not None:
timer.stop()
timer.deleteLater()
def _on_prompt_autosave_timeout(self, state_key):
state = self._states.get(state_key)
if state is not None:
self._persist_prompt(state)
def _persist_prompt(self, state):
if state is None or state.project_id is None:
return True
prompt = str(state.prompt or "")
if prompt == state.last_saved_prompt:
return True
try:
image_studio.update_project_prompt(
project = image_studio.update_project_prompt(
state.project_id,
state.prompt,
prompt,
path=self.db_path,
)
if project is None:
raise image_studio.ImageStudioError("商品套图项目不存在")
except Exception as exc:
self._status("商品卖点自动保存失败:%s" % _user_error(exc), "danger")
return False
state.last_saved_prompt = prompt
return True
def _flush_prompt_save(self, state):
self._cancel_prompt_save(state)
return self._persist_prompt(state)
def _persist_state(self, state):
if state.project_id is None:
return True
prompt_saved = self._flush_prompt_save(state)
try:
image_studio.update_project_suite_settings(
state.project_id,
state.settings,
@@ -1619,6 +1759,8 @@ class ProductSuiteTab(QWidget):
)
except Exception as exc:
self._status("商品套图设置保存失败:%s" % _user_error(exc), "danger")
return False
return prompt_saved
def _on_settings_changed(self, value=None):
if self._loading or self._displayed_state is None:
@@ -1655,6 +1797,7 @@ class ProductSuiteTab(QWidget):
return
state = self._displayed_state
state.prompt = self.prompt_edit.toPlainText()
self._schedule_prompt_save(state)
def open_prompt_settings(self, checked=False):
state = self._displayed_state
@@ -2754,10 +2897,16 @@ class ProductSuiteTab(QWidget):
self._refresh_results(self._displayed_state)
def closeEvent(self, event):
if self._displayed_state is not None:
self._save_controls_to_state(self._displayed_state)
for state in list(self._states.values()):
self._flush_prompt_save(state)
for state in list(self._states.values()) + list(self._retired_states):
for worker in (state.worker, state.pull_worker, state.import_worker, state.ai_worker):
if worker is not None and hasattr(worker, "cancel"):
worker.cancel()
for worker, thread in list(state.downloads.values()):
worker.cancel()
for state in list(self._states.values()) + list(self._retired_states):
self._release_prompt_save_timer(state)
super().closeEvent(event)