feat(workshop): AI创作弹窗时长改功能选项下拉(30/60/120/240/自定义,移出需求栏) + 一句话创意与选题改名一句话创意
This commit is contained in:
1 parent
3be2ff152d
commit
17acaf1740
3 files changed
+50
-11
No files matched your search
@@ -170,8 +170,9 @@ function bindBatchBar(container, opts) {
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
// 需求单字段顺序:弹窗需求框展示、AI 输出模板、创作 prompt 共用(值空 = 待 AI 按账号设定与选题自决)
|
// 需求单字段顺序:弹窗需求框展示、AI 输出模板、创作 prompt 共用(值空 = 待 AI 按账号设定与选题自决)
|
||||||
// 09-02 v10:需求框定位=选题补充——只留本条视频选题侧字段 + 兜底「其他」;账号设定类(情绪基调/受众/结构性符号/广告植入)由人设卡提供,不在此重复收集
|
// 09-02 v10:需求框定位=选题补充——只留本条视频选题侧字段 + 兜底「其他」;账号设定类(情绪基调/受众/结构性符号/广告植入)由人设卡提供,不在此重复收集
|
||||||
|
// 09-02 v11:dur 时长目标 移出需求栏 → 需求栏下方「功能选项」下拉(30/60/120/240/自定义),值由 w.dur 单独承载
|
||||||
const REQ_FIELDS = [
|
const REQ_FIELDS = [
|
||||||
['idea', '一句话创意与选题'], ['form', '叙事形态'], ['dur', '时长目标'],
|
['idea', '一句话创意'], ['form', '叙事形态'],
|
||||||
['plot', '核心情节'], ['end', '结尾落点'], ['note', '其他约束'],
|
['plot', '核心情节'], ['end', '结尾落点'], ['note', '其他约束'],
|
||||||
];
|
];
|
||||||
// 需求 md 行 → 向导字段映射(parseReqSheet 用)
|
// 需求 md 行 → 向导字段映射(parseReqSheet 用)
|
||||||
@@ -188,13 +189,16 @@ function parseReqSheet(md) {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
// 状态 → 6 行完整需求清单(供 AI 每轮保持同模板输出;空字段留空值)
|
// 状态 → 需求清单 md(供 AI 每轮保持同模板输出;空字段留空值;时长由选项条承载,不走 AI 模板)
|
||||||
function reqMdOf(w) {
|
function reqMdOf(w) {
|
||||||
return REQ_FIELDS.map(([k, label]) => `- ${label}:${String(w[k] || '').trim()}`).join('\n');
|
return REQ_FIELDS.map(([k, label]) => `- ${label}:${String(w[k] || '').trim()}`).join('\n');
|
||||||
}
|
}
|
||||||
// 状态 → 已填需求行(创作 prompt 用,非空才输出)
|
// 状态 → 已填需求行(创作 prompt / 确认弹窗用,非空才输出;时长=选项条独立值,追加在清单尾部)
|
||||||
function reqLines(w) {
|
function reqLines(w) {
|
||||||
return REQ_FIELDS.filter(([k]) => String(w[k] || '').trim()).map(([k, label]) => `- ${label}:${String(w[k]).trim()}`);
|
const lines = REQ_FIELDS.filter(([k]) => String(w[k] || '').trim()).map(([k, label]) => `- ${label}:${String(w[k]).trim()}`);
|
||||||
|
const dur = String(w.dur || '').trim();
|
||||||
|
if (dur) lines.push(`- 时长目标:${dur}`);
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
// 组装创作 prompt:人设块在前(既有铁律),需求+附件素材在后;空维度不输出由 AI 自主决策
|
// 组装创作 prompt:人设块在前(既有铁律),需求+附件素材在后;空维度不输出由 AI 自主决策
|
||||||
function buildCreatePrompt(accountName, w, personaBlock, hasPersona, attachText) {
|
function buildCreatePrompt(accountName, w, personaBlock, hasPersona, attachText) {
|
||||||
@@ -209,7 +213,7 @@ function buildCreatePrompt(accountName, w, personaBlock, hasPersona, attachText)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* AI创作 弹窗(09-02 v2:需求框 + 输入框 对话式打磨)——
|
/* AI创作 弹窗(09-02 v2:需求框 + 输入框 对话式打磨)——
|
||||||
顶部=创作需求(6 行选题字段,随对话逐轮被 AI 更新,空值标「待定·由 AI 按账号设定与选题自决」)
|
顶部=创作需求(5 行选题字段,随对话逐轮被 AI 更新,空值标「待定·由 AI 按账号设定与选题自决」)+ 下方时长选项条
|
||||||
中部=对话流(用户输入 vs AI 整理/追问),底部=输入框 + 提交创作 */
|
中部=对话流(用户输入 vs AI 整理/追问),底部=输入框 + 提交创作 */
|
||||||
async function confirmAiScript(accountName, btn) {
|
async function confirmAiScript(accountName, btn) {
|
||||||
const accId = btn && btn.dataset.accId ? btn.dataset.accId : '';
|
const accId = btn && btn.dataset.accId ? btn.dataset.accId : '';
|
||||||
@@ -241,6 +245,20 @@ async function confirmAiScript(accountName, btn) {
|
|||||||
</button>
|
</button>
|
||||||
<div class="req-sheet" id="reqSheet"></div>
|
<div class="req-sheet" id="reqSheet"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="req-opts">
|
||||||
|
<span class="opts-label">时长目标</span>
|
||||||
|
<select id="reqDur" class="input-select req-dur-sel">
|
||||||
|
<option value="">未指定(由 AI 定)</option>
|
||||||
|
<option value="30">30 秒</option>
|
||||||
|
<option value="60">60 秒</option>
|
||||||
|
<option value="120">120 秒</option>
|
||||||
|
<option value="240">240 秒</option>
|
||||||
|
<option value="custom">自定义…</option>
|
||||||
|
</select>
|
||||||
|
<span class="req-dur-custom" id="reqDurCustom" hidden>
|
||||||
|
<input id="reqDurNum" class="input-select req-dur-num" type="number" min="1" max="900" placeholder="秒数"> 秒
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="req-dialog" id="reqDialog"></div>
|
<div class="req-dialog" id="reqDialog"></div>
|
||||||
<div class="req-attach-row" id="attachRow"></div>
|
<div class="req-attach-row" id="attachRow"></div>
|
||||||
<div class="req-bar">
|
<div class="req-bar">
|
||||||
@@ -262,7 +280,7 @@ async function confirmAiScript(accountName, btn) {
|
|||||||
mask.querySelector('[data-x]').addEventListener('click', close);
|
mask.querySelector('[data-x]').addEventListener('click', close);
|
||||||
const q = (s) => mask.querySelector(s);
|
const q = (s) => mask.querySelector(s);
|
||||||
|
|
||||||
/* —— 需求框渲染:6 个选题字段,维度名作标题、内容在下方 —— */
|
/* —— 需求框渲染:5 个选题字段,维度名作标题、内容在下方 —— */
|
||||||
const renderSheet = () => {
|
const renderSheet = () => {
|
||||||
const rows = REQ_FIELDS.map(([k, label]) => {
|
const rows = REQ_FIELDS.map(([k, label]) => {
|
||||||
const v = String(w[k] || '').trim();
|
const v = String(w[k] || '').trim();
|
||||||
@@ -283,6 +301,21 @@ async function confirmAiScript(accountName, btn) {
|
|||||||
const open = box.classList.toggle('open');
|
const open = box.classList.toggle('open');
|
||||||
sheetToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
sheetToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
|
/* —— 时长选项条(09-02 v11):下拉 30/60/120/240/自定义;值写 w.dur,随 reqLines 进入确认弹窗与创作 prompt —— */
|
||||||
|
const durSel = q('#reqDur');
|
||||||
|
const durCustom = q('#reqDurCustom');
|
||||||
|
const durNum = q('#reqDurNum');
|
||||||
|
const syncDur = () => {
|
||||||
|
const sel = durSel.value;
|
||||||
|
if (sel === 'custom') { durCustom.hidden = false; durNum.focus(); w.dur = ''; return; }
|
||||||
|
durCustom.hidden = true;
|
||||||
|
w.dur = sel ? sel + '秒' : '';
|
||||||
|
};
|
||||||
|
durSel.addEventListener('change', syncDur);
|
||||||
|
durNum.addEventListener('input', () => {
|
||||||
|
const n = Math.round(Number(durNum.value));
|
||||||
|
w.dur = (Number.isFinite(n) && n > 0) ? n + '秒' : '';
|
||||||
|
});
|
||||||
/* —— 对话流渲染 —— */
|
/* —— 对话流渲染 —— */
|
||||||
const renderDialog = () => {
|
const renderDialog = () => {
|
||||||
const el = q('#reqDialog');
|
const el = q('#reqDialog');
|
||||||
@@ -290,7 +323,7 @@ async function confirmAiScript(accountName, btn) {
|
|||||||
? convo.map((c) => (c.role === 'u'
|
? convo.map((c) => (c.role === 'u'
|
||||||
? `<div class="req-msg mine"><div class="req-bubble">${esc(c.text)}</div></div>`
|
? `<div class="req-msg mine"><div class="req-bubble">${esc(c.text)}</div></div>`
|
||||||
: `<div class="req-msg ai"><div class="req-bubble">${esc(c.text)}</div></div>`)).join('')
|
: `<div class="req-msg ai"><div class="req-bubble">${esc(c.text)}</div></div>`)).join('')
|
||||||
: `<div class="req-dialog-empty">在这里输入一个初步想法,AI 会引导你补齐创意 / 形态 / 时长 / 情节 / 结尾等选题需求;每条输入都会即时更新上方的创作需求框。账号级设定(情绪基调 / 受众 / 植入等)已由账号设定提供,无需重复填写。也可点回形针上传需求文档(doc / docx / txt / pdf)。Enter 发送,Shift+Enter 换行。</div>`;
|
: `<div class="req-dialog-empty">在这里输入一个初步想法,AI 会引导你补齐创意 / 形态 / 情节 / 结尾等选题需求;每条输入都会即时更新上方的创作需求框。账号级设定(情绪基调 / 受众 / 植入等)已由账号设定提供,无需重复填写。也可点回形针上传需求文档(doc / docx / txt / pdf)。Enter 发送,Shift+Enter 换行。</div>`;
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
};
|
};
|
||||||
const setBusy = (b) => {
|
const setBusy = (b) => {
|
||||||
@@ -420,7 +453,7 @@ async function confirmAiScript(accountName, btn) {
|
|||||||
ck.innerHTML = `<div class="modal">
|
ck.innerHTML = `<div class="modal">
|
||||||
<div class="modal-title-row"><span>确认提交创作</span><button class="btn btn-icon" data-x title="取消">${svgIcon('x', 16)}</button></div>
|
<div class="modal-title-row"><span>确认提交创作</span><button class="btn btn-icon" data-x title="取消">${svgIcon('x', 16)}</button></div>
|
||||||
<div class="dim">将为达人「${esc(accountName)}」创建一条完整脚本创作任务(S1-S11 全流程,自动模式)。</div>
|
<div class="dim">将为达人「${esc(accountName)}」创建一条完整脚本创作任务(S1-S11 全流程,自动模式)。</div>
|
||||||
<div class="req-confirm-label">创作需求(${lines.length}/${REQ_FIELDS.length} 项已填)</div>
|
<div class="req-confirm-label">创作需求(已填 ${lines.length} 项,含时长选项)</div>
|
||||||
<div class="req-confirm-box">${reqTxt}</div>
|
<div class="req-confirm-box">${reqTxt}</div>
|
||||||
<div class="req-confirm-attach">${perTxt} · ${atTxt}</div>
|
<div class="req-confirm-attach">${perTxt} · ${atTxt}</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
|
|||||||
@@ -248,6 +248,12 @@ main { height: calc(100vh - 54px); }
|
|||||||
.req-sheet-box.open .rst-caret svg { transform: rotate(0deg); }
|
.req-sheet-box.open .rst-caret svg { transform: rotate(0deg); }
|
||||||
.req-sheet-box .req-sheet { max-height: 0; overflow: hidden; padding: 0; transition: max-height .2s ease; }
|
.req-sheet-box .req-sheet { max-height: 0; overflow: hidden; padding: 0; transition: max-height .2s ease; }
|
||||||
.req-sheet-box.open .req-sheet { max-height: 340px; overflow: auto; padding: 2px; }
|
.req-sheet-box.open .req-sheet { max-height: 340px; overflow: auto; padding: 2px; }
|
||||||
|
/* 需求栏下方功能选项条(09-02 v11):时长目标下拉(30/60/120/240/自定义),与需求栏同宽同视觉层级 */
|
||||||
|
.req-opts { flex: none; display: flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 7px 10px; }
|
||||||
|
.req-opts .opts-label { flex: none; font-size: 13px; font-weight: 600; color: var(--text); }
|
||||||
|
.req-dur-sel { flex: none; width: 168px; }
|
||||||
|
.req-dur-num { flex: none; width: 76px; }
|
||||||
|
.req-dur-custom { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--dim); }
|
||||||
/* 提交创作确认弹窗 */
|
/* 提交创作确认弹窗 */
|
||||||
.req-confirm-label { font-size: 13px; color: var(--dim); margin: 10px 0 6px; }
|
.req-confirm-label { font-size: 13px; color: var(--dim); margin: 10px 0 6px; }
|
||||||
.req-confirm-box {
|
.req-confirm-box {
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ const server = http.createServer((req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- AI 创作需求打磨(09-02 弹窗内对话:Dify chat-messages 同步、无状态多轮) ----------
|
// ---------- AI 创作需求打磨(09-02 弹窗内对话:Dify chat-messages 同步、无状态多轮) ----------
|
||||||
// body: { accountName, turn(本轮输入), reqMd(当前需求6行md), history(最近轮[{u,a}]) } → { answer }
|
// body: { accountName, turn(本轮输入), reqMd(当前需求5行md), history(最近轮[{u,a}]) } → { answer }
|
||||||
if (p === '/api/ai/clarify' && req.method === 'POST') {
|
if (p === '/api/ai/clarify' && req.method === 'POST') {
|
||||||
let body = '';
|
let body = '';
|
||||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||||
@@ -704,8 +704,8 @@ const server = http.createServer((req, res) => {
|
|||||||
+ '3. 若信息已基本齐备:用一两句话总结并提示用户可点击「提交创作」。\n'
|
+ '3. 若信息已基本齐备:用一两句话总结并提示用户可点击「提交创作」。\n'
|
||||||
+ '4. 输出必须严格分两段:第一段是对用户说的话(120 字以内,含必要时的问题);\n'
|
+ '4. 输出必须严格分两段:第一段是对用户说的话(120 字以内,含必要时的问题);\n'
|
||||||
+ '然后另起一行单独输出 ==REQ==;\n'
|
+ '然后另起一行单独输出 ==REQ==;\n'
|
||||||
+ '接着输出 6 行需求清单(仍未确定的项留空值,严禁编造),模板:\n'
|
+ '接着输出 5 行需求清单(仍未确定的项留空值,严禁编造),模板:\n'
|
||||||
+ '- 一句话创意与选题:\n- 叙事形态:\n- 时长目标:\n- 核心情节:\n- 结尾落点:\n- 其他约束:\n'
|
+ '- 一句话创意:\n- 叙事形态:\n- 核心情节:\n- 结尾落点:\n- 其他约束:\n'
|
||||||
+ '5. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。';
|
+ '5. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。';
|
||||||
// 4) Dify blocking 调用
|
// 4) Dify blocking 调用
|
||||||
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
|
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
|
||||||
|
|||||||
Reference in new issue
Block a user