feat(product-suite): autosave prompt edits
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user