feat: refresh creative recommendations on source changes
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
这个文件用于给 Gitea 仓库保留阶段性版本更新记录,方便直接查看每一轮里程碑,不用只依赖零散 commit。
|
||||
|
||||
## 2026-04-07
|
||||
|
||||
### 创作表单开始跟随来源任务动态刷新推荐值
|
||||
|
||||
- `生成文案 / 创建 AI 视频 / 创建实拍剪辑 / 写复盘` 这四类创作表单,现在不只会在打开时算一次默认值;如果你在表单里切换来源任务,平台、标题、受众、画幅、时长、目标这些推荐值会继续跟着刷新。
|
||||
- 这套联动只会在字段还处于“自动推荐”状态时继续接管;一旦用户手动改过,就会尊重手改内容,不会再被来源任务覆盖。
|
||||
- `来源任务` 摘要区也会跟着联动更新,切换任务后第一眼就能看到当前承接的是哪条任务。
|
||||
- 为了支持这层联动,输入型表单里的 HTML 字段现在也带了稳定的 `data-action-field` 标记,后续继续做表单智能化和回归锁定会更稳。
|
||||
|
||||
## 2026-04-06
|
||||
|
||||
### 主 Agent 高注意图动作统一切到直执行入口
|
||||
|
||||
@@ -503,6 +503,96 @@ function bindManualIntakeTitleRecommendation(fields, kind, options = {}) {
|
||||
sync();
|
||||
}
|
||||
|
||||
function getCompletedJobById(jobId = "") {
|
||||
const normalizedId = String(jobId || "").trim();
|
||||
if (!normalizedId) return null;
|
||||
return safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === normalizedId)
|
||||
|| (appState.lastJobDetail?.job?.id === normalizedId ? appState.lastJobDetail.job : null)
|
||||
|| null;
|
||||
}
|
||||
|
||||
function bindCreativeSourceJobRecommendations(fields, options = {}) {
|
||||
const sourceJobSelect = fields.querySelector('[data-action-field="sourceJobId"]');
|
||||
if (!(sourceJobSelect instanceof HTMLSelectElement)) return;
|
||||
const sourceContext = fields.querySelector('[data-action-field="sourceJobContext"] .sheet-html');
|
||||
const platformSelect = fields.querySelector(`[data-action-field="${options.platformField || "platform"}"]`);
|
||||
const defaultPlatform = options.defaultPlatform || "douyin";
|
||||
const managedFields = [];
|
||||
const resolveSourceJob = () => getCompletedJobById(sourceJobSelect.value) || options.sourceJob || null;
|
||||
const recommendedPlatform = (job) => normalizePlatformValue(job?.platform || getPreferredPlatform(), defaultPlatform);
|
||||
const registerManagedField = (field, compute, eventName = field instanceof HTMLSelectElement ? "change" : "input") => {
|
||||
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement)) return;
|
||||
field.dataset.recommendationMode = field.dataset.recommendationMode || "auto";
|
||||
field.addEventListener(eventName, () => {
|
||||
if (field.dataset.recommendationApplying === "1") return;
|
||||
field.dataset.recommendationMode = "manual";
|
||||
});
|
||||
managedFields.push({ field, compute });
|
||||
};
|
||||
const applyManagedValue = (field, value) => {
|
||||
if (value == null || value === "") return;
|
||||
field.dataset.recommendationApplying = "1";
|
||||
field.value = String(value);
|
||||
field.dataset.recommendationMode = "auto";
|
||||
delete field.dataset.recommendationApplying;
|
||||
};
|
||||
const titleInput = fields.querySelector('[data-action-field="title"]');
|
||||
if (options.titleSuffix && titleInput instanceof HTMLInputElement) {
|
||||
registerManagedField(titleInput, (job) => recommendDerivativeJobTitle(job, options.titleSuffix, options.titleFallback || ""));
|
||||
}
|
||||
if (platformSelect instanceof HTMLSelectElement) {
|
||||
registerManagedField(platformSelect, (job) => recommendedPlatform(job), "change");
|
||||
}
|
||||
const audienceInput = fields.querySelector('[data-action-field="audience"]');
|
||||
if (audienceInput instanceof HTMLInputElement) {
|
||||
registerManagedField(audienceInput, (job) => {
|
||||
const platformValue = platformSelect instanceof HTMLSelectElement ? platformSelect.value : recommendedPlatform(job);
|
||||
return recommendAudienceForPlatform(platformValue);
|
||||
});
|
||||
}
|
||||
const styleInput = fields.querySelector('[data-action-field="style"]');
|
||||
if (styleInput instanceof HTMLInputElement) {
|
||||
registerManagedField(styleInput, (job) => recommendCreativeStyle(job));
|
||||
}
|
||||
const aspectRatioField = fields.querySelector('[data-action-field="aspectRatio"]');
|
||||
if (aspectRatioField instanceof HTMLInputElement || aspectRatioField instanceof HTMLSelectElement) {
|
||||
registerManagedField(
|
||||
aspectRatioField,
|
||||
(job) => {
|
||||
const platformValue = platformSelect instanceof HTMLSelectElement ? platformSelect.value : recommendedPlatform(job);
|
||||
return job?.artifacts?.aspect_ratio || recommendAspectRatioForPlatform(platformValue);
|
||||
},
|
||||
aspectRatioField instanceof HTMLSelectElement ? "change" : "input"
|
||||
);
|
||||
}
|
||||
const durationField = fields.querySelector('[data-action-field="duration"]');
|
||||
if (durationField instanceof HTMLInputElement) {
|
||||
registerManagedField(durationField, (job) => recommendAiVideoShotDuration(job));
|
||||
}
|
||||
const targetDurationField = fields.querySelector('[data-action-field="targetDurationSec"]');
|
||||
if (targetDurationField instanceof HTMLInputElement) {
|
||||
registerManagedField(targetDurationField, (job) => recommendRealCutDuration(job));
|
||||
}
|
||||
const objectiveField = fields.querySelector('[data-action-field="objective"]');
|
||||
if (objectiveField instanceof HTMLTextAreaElement) {
|
||||
registerManagedField(objectiveField, (job) => recommendRealCutObjective(job));
|
||||
}
|
||||
const sync = () => {
|
||||
const sourceJob = resolveSourceJob();
|
||||
if (sourceContext instanceof HTMLElement) {
|
||||
sourceContext.innerHTML = sourceJob ? renderSourceJobContextHtml(sourceJob) : "";
|
||||
sourceContext.parentElement?.classList.toggle("hidden", !sourceJob);
|
||||
}
|
||||
managedFields.forEach(({ field, compute }) => {
|
||||
if (field.dataset.recommendationMode === "manual") return;
|
||||
applyManagedValue(field, compute(sourceJob));
|
||||
});
|
||||
};
|
||||
sourceJobSelect.addEventListener("change", sync);
|
||||
platformSelect?.addEventListener("change", sync);
|
||||
sync();
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
@@ -1071,7 +1161,7 @@ function renderActionFields(fields) {
|
||||
const common = `data-action-field="${escapeHtml(field.name)}"`;
|
||||
if (field.type === "html") {
|
||||
return `
|
||||
<div class="field-stack">
|
||||
<div class="field-stack" ${field.name ? common : ""}>
|
||||
<label>${escapeHtml(field.label || "")}</label>
|
||||
<div class="sheet-html">${field.html || ""}</div>
|
||||
</div>
|
||||
@@ -11564,6 +11654,9 @@ function openGenerateCopyAction(defaults = {}) {
|
||||
{ name: "audience", label: "受众", value: defaults.audience || recommendAudienceForPlatform(defaultPlatform) },
|
||||
{ name: "extraRequirements", label: "额外要求", placeholder: "例如:强结论开头,结尾带 CTA" }
|
||||
],
|
||||
onOpen: ({ fields }) => {
|
||||
bindCreativeSourceJobRecommendations(fields, { sourceJob, defaultPlatform });
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
if (!values.brief?.trim()) throw new Error("请填写创作需求");
|
||||
const result = await storyforgeFetch(`/v2/assistants/${encodeURIComponent(assistant.id)}/generate`, {
|
||||
@@ -11670,6 +11763,9 @@ function openCreateAiVideoAction(defaults = {}) {
|
||||
placeholder: "例如:避免过暗,人物手部自然,保持真实商业质感",
|
||||
}
|
||||
],
|
||||
onOpen: ({ fields }) => {
|
||||
bindCreativeSourceJobRecommendations(fields, { sourceJob, defaultPlatform, titleSuffix: "AI 视频" });
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写任务标题");
|
||||
if (!values.brief?.trim()) throw new Error("请填写视频 brief");
|
||||
@@ -11737,6 +11833,9 @@ function openCreateRealCutAction(defaults = {}) {
|
||||
{ name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || sourceJob?.artifacts?.aspect_ratio || recommendAspectRatioForPlatform(defaultPlatform) },
|
||||
{ name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || recommendRealCutObjective(sourceJob), placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" }
|
||||
],
|
||||
onOpen: ({ fields }) => {
|
||||
bindCreativeSourceJobRecommendations(fields, { sourceJob, defaultPlatform, titleSuffix: "实拍剪辑" });
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写任务标题");
|
||||
if (!values.sourceJobId) throw new Error("请先选择一个已完成的源任务");
|
||||
@@ -12030,6 +12129,9 @@ function openReviewAction(defaults = {}) {
|
||||
{ name: "nextActions", label: "下一步", type: "textarea", rows: 4, value: existingReview?.next_actions || "", placeholder: "例如:保留结构,换一个细分人群再做一条" },
|
||||
{ name: "notes", label: "备注", type: "textarea", rows: 4, value: existingReview?.notes || "", placeholder: "补充团队讨论、平台环境、发布时间段等信息" }
|
||||
],
|
||||
onOpen: ({ fields }) => {
|
||||
bindCreativeSourceJobRecommendations(fields, { sourceJob, defaultPlatform: normalizePlatformValue(existingReview?.platform || defaults.platform || sourceJob?.platform || "douyin"), titleSuffix: "复盘" });
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写复盘标题");
|
||||
const payload = {
|
||||
|
||||
@@ -1196,6 +1196,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", (
|
||||
assert.match(APP, /function recommendLiveRecorderImportSamples\(platform\)/);
|
||||
assert.match(APP, /function recommendManualIntakeTitle\(project, platform, kind\)/);
|
||||
assert.match(APP, /function bindManualIntakeTitleRecommendation\(fields, kind, options = \{\}\)/);
|
||||
assert.match(APP, /function bindCreativeSourceJobRecommendations\(fields, options = \{\}\)/);
|
||||
assert.match(importHomepage, /label: "当前上下文", type: "html"/);
|
||||
assert.match(importHomepage, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/);
|
||||
assert.match(importHomepage, /const defaultPlatform = normalizePlatformValue\(getPreferredPlatform\(\), "douyin"\)/);
|
||||
@@ -1236,6 +1237,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", (
|
||||
assert.match(generateCopy, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/);
|
||||
assert.match(generateCopy, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/);
|
||||
assert.match(generateCopy, /value: defaults\.audience \|\| recommendAudienceForPlatform\(defaultPlatform\)/);
|
||||
assert.match(generateCopy, /onOpen:\s*\(\{ fields \}\) => \{\s*bindCreativeSourceJobRecommendations\(fields, \{/);
|
||||
assert.match(createAiVideo, /label: "当前上下文", type: "html"/);
|
||||
assert.match(createAiVideo, /const defaultAssistantId = assistant\?\.id \|\| ""/);
|
||||
assert.match(createAiVideo, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/);
|
||||
@@ -1245,6 +1247,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", (
|
||||
assert.match(createAiVideo, /value: defaults\.style \|\| recommendCreativeStyle\(sourceJob\)/);
|
||||
assert.match(createAiVideo, /recommendAspectRatioForPlatform\(defaultPlatform\)/);
|
||||
assert.match(createAiVideo, /value: defaults\.duration \|\| recommendAiVideoShotDuration\(sourceJob\)/);
|
||||
assert.match(createAiVideo, /onOpen:\s*\(\{ fields \}\) => \{\s*bindCreativeSourceJobRecommendations\(fields, \{/);
|
||||
assert.match(createRealCut, /label: "当前上下文", type: "html"/);
|
||||
assert.match(createRealCut, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/);
|
||||
assert.match(createRealCut, /renderIntakeActionContextHtml\(project\.id, assistant\?\.id \|\| ""\)/);
|
||||
@@ -1253,6 +1256,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", (
|
||||
assert.match(createRealCut, /value: defaults\.targetDurationSec \|\| recommendRealCutDuration\(sourceJob\)/);
|
||||
assert.match(createRealCut, /recommendAspectRatioForPlatform\(defaultPlatform\)/);
|
||||
assert.match(createRealCut, /value: defaults\.objective \|\| recommendRealCutObjective\(sourceJob\)/);
|
||||
assert.match(createRealCut, /onOpen:\s*\(\{ fields \}\) => \{\s*bindCreativeSourceJobRecommendations\(fields, \{/);
|
||||
assert.match(liveRecorderCreate, /label: "当前上下文", type: "html"/);
|
||||
assert.match(liveRecorderCreate, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/);
|
||||
assert.match(liveRecorderCreate, /const defaultPlatform = normalizePlatformValue\(getPreferredPlatform\(\), "kuaishou"\)/);
|
||||
@@ -1276,6 +1280,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", (
|
||||
assert.match(reviewAction, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/);
|
||||
assert.match(reviewAction, /value: existingReview\?\.title \|\| defaults\.title \|\| recommendDerivativeJobTitle\(sourceJob, "复盘", ""\)/);
|
||||
assert.match(reviewAction, /normalizePlatformValue\(existingReview\?\.platform \|\| defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/);
|
||||
assert.match(reviewAction, /onOpen:\s*\(\{ fields \}\) => \{\s*bindCreativeSourceJobRecommendations\(fields, \{/);
|
||||
});
|
||||
|
||||
test("discovery analysis actions focus the most relevant detail tab after success", () => {
|
||||
|
||||
Reference in New Issue
Block a user