Steer generation rules toward direction pools and readable fills.
Direction-first pools, recipes, and grade manifestation replace enum-tag stuffing; spawned products can be deleted and in-flight runs aborted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+470
-54
@@ -197,8 +197,10 @@ function hideFlowNodeMenu() {
|
||||
const flowMenuState = {
|
||||
stepId: null,
|
||||
name: "",
|
||||
label: "",
|
||||
hasArtifact: false,
|
||||
canReenter: false,
|
||||
canDelete: false,
|
||||
node: null,
|
||||
handlers: null,
|
||||
};
|
||||
@@ -224,12 +226,16 @@ function showFlowNodeMenu(node, x, y, handlers) {
|
||||
if (!stepId) return;
|
||||
flowMenuState.stepId = stepId;
|
||||
flowMenuState.name = node.getAttribute("data-flow-done-name") || "";
|
||||
flowMenuState.label =
|
||||
node.getAttribute("data-flow-label") || flowMenuState.name || stepId;
|
||||
flowMenuState.hasArtifact = node.getAttribute("data-has-artifact") === "1";
|
||||
flowMenuState.canReenter = node.getAttribute("data-can-reenter") === "1";
|
||||
flowMenuState.canDelete = node.getAttribute("data-can-delete") === "1";
|
||||
flowMenuState.node = node;
|
||||
flowMenuState.handlers = handlers;
|
||||
const viewBtn = document.getElementById("flow-menu-view");
|
||||
const reenterBtn = document.getElementById("flow-menu-reenter");
|
||||
const deleteBtn = document.getElementById("flow-menu-delete");
|
||||
if (viewBtn) {
|
||||
viewBtn.disabled = !flowMenuState.hasArtifact;
|
||||
viewBtn.title = flowMenuState.hasArtifact ? "" : "这一步还没有产物";
|
||||
@@ -237,6 +243,9 @@ function showFlowNodeMenu(node, x, y, handlers) {
|
||||
if (reenterBtn) {
|
||||
reenterBtn.toggleAttribute("hidden", !flowMenuState.canReenter);
|
||||
}
|
||||
if (deleteBtn) {
|
||||
deleteBtn.toggleAttribute("hidden", !flowMenuState.canDelete);
|
||||
}
|
||||
placeMenu(menu, x, y);
|
||||
}
|
||||
|
||||
@@ -248,11 +257,13 @@ function wireFlowNodeMenu() {
|
||||
const action = e.target.closest("[data-flow-menu-action]")?.getAttribute("data-flow-menu-action");
|
||||
const stepId = flowMenuState.stepId;
|
||||
const name = flowMenuState.name;
|
||||
const label = flowMenuState.label || name;
|
||||
const handlers = flowMenuState.handlers;
|
||||
hideFlowNodeMenu();
|
||||
if (!stepId || !action) return;
|
||||
if (action === "view") handlers?.onViewStepArtifact?.(stepId, name);
|
||||
if (action === "reenter") handlers?.onReenterCreationStep?.(stepId);
|
||||
if (action === "delete") handlers?.onDeleteCreationStep?.(stepId, label);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2406,6 +2417,17 @@ function renderOneGenerationRuleCard(rule, index) {
|
||||
</header>`;
|
||||
|
||||
const blocks = [];
|
||||
if (rule.设计判断 && typeof rule.设计判断 === "object") {
|
||||
const d = rule.设计判断;
|
||||
const unlist = (v) =>
|
||||
Array.isArray(v) && v.length ? v.map(String).join("、") : "";
|
||||
blocks.push(
|
||||
`<div class="skill-sub"><div class="skill-sub-title">设计判断</div>${skillChipRow([
|
||||
unlist(d.不可穷举项) && `不可穷举 · ${unlist(d.不可穷举项)}`,
|
||||
unlist(d.封闭集合) && `封闭 · ${unlist(d.封闭集合)}`,
|
||||
])}${d.正交字段依据 ? `<p class="skill-pool-desc">${esc(String(d.正交字段依据))}</p>` : ""}</div>`,
|
||||
);
|
||||
}
|
||||
if (rule.上下文策略 && typeof rule.上下文策略 === "object") {
|
||||
const flags = Object.entries(rule.上下文策略)
|
||||
.map(([k, v]) => `<span class="ws-slot-chip ${v ? "ws-slot-on" : "ws-slot-off"}">${esc(k)}</span>`)
|
||||
@@ -2424,9 +2446,13 @@ function renderOneGenerationRuleCard(rule, index) {
|
||||
}
|
||||
if (rule.生成与描写 && typeof rule.生成与描写 === "object") {
|
||||
const g = rule.生成与描写;
|
||||
const methodKeys = ["依据", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "校验"];
|
||||
const methodKeys = ["依据", "工作流", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "反模式", "校验"];
|
||||
const extra = [
|
||||
renderGenerationRecipeHtml(g.配方),
|
||||
renderGenerationExpressHtml(g.表现),
|
||||
].filter(Boolean);
|
||||
blocks.push(
|
||||
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${methodKeys
|
||||
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${extra.join("")}${methodKeys
|
||||
.filter((k) => Array.isArray(g[k]) && g[k].length)
|
||||
.map(
|
||||
(k) =>
|
||||
@@ -2435,6 +2461,9 @@ function renderOneGenerationRuleCard(rule, index) {
|
||||
.join("")}</div>`,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(rule.分级表现) && rule.分级表现.length) {
|
||||
blocks.push(renderGenerationGradeHtml(rule.分级表现));
|
||||
}
|
||||
if (rule.产物格式 && typeof rule.产物格式 === "object") {
|
||||
blocks.push(renderProductSchemaCard(rule.产物格式));
|
||||
}
|
||||
@@ -2442,19 +2471,7 @@ function renderOneGenerationRuleCard(rule, index) {
|
||||
if (pools.length) {
|
||||
blocks.push(
|
||||
`<div class="skill-sub"><div class="skill-sub-title">池(${pools.length})</div><div class="skill-pool-grid">${pools
|
||||
.map((p) => {
|
||||
if (!p || typeof p !== "object") return "";
|
||||
const n = Array.isArray(p.条目) ? p.条目.length : 0;
|
||||
return `<article class="skill-pool-card">
|
||||
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
|
||||
${skillChipRow([
|
||||
p.绑定字段 && `绑 · ${p.绑定字段}`,
|
||||
p.用途 && String(p.用途),
|
||||
n ? `${n} 条` : "",
|
||||
])}
|
||||
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 120))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.map((p) => renderGenerationPoolCard(p))
|
||||
.join("")}</div></div>`,
|
||||
);
|
||||
}
|
||||
@@ -2462,6 +2479,137 @@ function renderOneGenerationRuleCard(rule, index) {
|
||||
return `<article class="skill-rule-card">${head}${blocks.join("")}</article>`;
|
||||
}
|
||||
|
||||
function renderGenerationRecipeHtml(rows) {
|
||||
if (!Array.isArray(rows) || !rows.length) return "";
|
||||
const cards = rows
|
||||
.map((row) => {
|
||||
if (!row || typeof row !== "object") return "";
|
||||
const comps = Array.isArray(row.组件) ? row.组件.map(String).join(" × ") : "";
|
||||
return `<article class="skill-pool-card is-direction">
|
||||
<div class="skill-pool-name">配方 · ${esc(String(row.目标字段 || "字段"))}</div>
|
||||
${skillChipRow([comps && `组件 · ${comps}`])}
|
||||
${row.组装 ? `<p class="skill-pool-desc">${esc(String(row.组装))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return cards.length
|
||||
? `<div class="skill-method"><span class="skill-method-label">配方</span><div class="skill-pool-grid">${cards.join("")}</div></div>`
|
||||
: "";
|
||||
}
|
||||
|
||||
function renderGenerationExpressHtml(rows) {
|
||||
if (!Array.isArray(rows) || !rows.length) return "";
|
||||
const cards = rows
|
||||
.map((row) => {
|
||||
if (!row || typeof row !== "object") return "";
|
||||
const ex = Array.isArray(row.例子) ? row.例子.filter(Boolean) : [];
|
||||
return `<article class="skill-pool-card is-direction">
|
||||
<div class="skill-pool-name">${esc(String(row.针对 || "表现"))}</div>
|
||||
${row.概括 ? `<p class="skill-pool-desc">${esc(String(row.概括))}</p>` : ""}
|
||||
${
|
||||
ex.length
|
||||
? `<ul class="artifact-list">${ex.map((x) => `<li>${esc(String(x))}</li>`).join("")}</ul>`
|
||||
: ""
|
||||
}
|
||||
</article>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return cards.length
|
||||
? `<div class="skill-method"><span class="skill-method-label">表现</span><div class="skill-pool-grid">${cards.join("")}</div></div>`
|
||||
: "";
|
||||
}
|
||||
|
||||
function renderGenerationGradeHtml(rows) {
|
||||
const blocks = rows
|
||||
.map((row) => {
|
||||
if (!row || typeof row !== "object") return "";
|
||||
const grades = Array.isArray(row.档位) ? row.档位 : [];
|
||||
const tr = grades
|
||||
.map((g) => {
|
||||
if (!g || typeof g !== "object") return "";
|
||||
return `<tr>
|
||||
<td><code>${esc(String(g.值 || ""))}</code></td>
|
||||
<td>${esc(String(g.概括 || ""))}</td>
|
||||
<td>${esc(String(g.表象 || ""))}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
return `<div class="skill-sub">
|
||||
<div class="skill-sub-title">分级表现 · ${esc(String(row.字段 || ""))}</div>
|
||||
${row.说明 ? `<p class="skill-pool-desc">${esc(String(row.说明))}</p>` : ""}
|
||||
${
|
||||
tr
|
||||
? `<div class="skill-table-wrap"><table class="skill-field-table"><thead><tr><th>档</th><th>概括</th><th>表象</th></tr></thead><tbody>${tr}</tbody></table></div>`
|
||||
: ""
|
||||
}
|
||||
</div>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return blocks.join("");
|
||||
}
|
||||
|
||||
function generationPoolKind(pool) {
|
||||
const raw = String(pool?.池型 || pool?.kind || pool?.类型 || "").trim();
|
||||
if (/方向|类型|type/i.test(raw)) return "方向";
|
||||
if (/元素|枚举|enum/i.test(raw)) return "元素";
|
||||
if (pool?.方向 && typeof pool.方向 === "object") return "方向";
|
||||
if (Array.isArray(pool?.条目) && pool.条目.length) return "元素";
|
||||
return "";
|
||||
}
|
||||
|
||||
function generationPoolDirections(pool) {
|
||||
const dirs = pool?.方向;
|
||||
if (!dirs) return [];
|
||||
if (Array.isArray(dirs)) {
|
||||
return dirs
|
||||
.map((row) => {
|
||||
if (typeof row === "string") return { name: row, text: "" };
|
||||
if (!row || typeof row !== "object") return null;
|
||||
return {
|
||||
name: String(row.名 || row.name || row.方向 || "").trim(),
|
||||
text: String(row.说明 || row.description || row.内容 || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter((x) => x && (x.name || x.text));
|
||||
}
|
||||
if (typeof dirs === "object") {
|
||||
return Object.entries(dirs).map(([name, text]) => ({
|
||||
name,
|
||||
text: text == null ? "" : String(text),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function renderGenerationPoolCard(p) {
|
||||
if (!p || typeof p !== "object") return "";
|
||||
const kind = generationPoolKind(p);
|
||||
const dirs = generationPoolDirections(p);
|
||||
const n = Array.isArray(p.条目) ? p.条目.length : 0;
|
||||
const dirHtml = dirs.length
|
||||
? `<dl class="skill-pool-dirs">${dirs
|
||||
.map(
|
||||
(d) =>
|
||||
`<div class="skill-pool-dir"><dt>${esc(d.name || "方向")}</dt>${
|
||||
d.text ? `<dd>${esc(clampPreviewText(d.text, 160))}</dd>` : ""
|
||||
}</div>`,
|
||||
)
|
||||
.join("")}</dl>`
|
||||
: "";
|
||||
return `<article class="skill-pool-card${kind === "方向" ? " is-direction" : ""}">
|
||||
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
|
||||
${skillChipRow([
|
||||
kind && `${kind}池`,
|
||||
p.绑定字段 && `绑 · ${p.绑定字段}`,
|
||||
p.用途 && String(p.用途),
|
||||
kind === "元素" && n ? `${n} 条` : "",
|
||||
kind === "方向" && dirs.length ? `${dirs.length} 向` : "",
|
||||
])}
|
||||
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 160))}</p>` : ""}
|
||||
${dirHtml}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderProductSchemaCard(fmt) {
|
||||
const schema = fmt.schema && typeof fmt.schema === "object" ? fmt.schema : null;
|
||||
const fields = schema
|
||||
@@ -2474,6 +2622,9 @@ function renderProductSchemaCard(fmt) {
|
||||
const req = spec.required === true ? "必填" : spec.required === false ? "可选" : "—";
|
||||
const desc = spec.description != null ? clampPreviewText(String(spec.description), 80) : "";
|
||||
const extra = [];
|
||||
if (spec.粒度) extra.push(`粒度 ${spec.粒度}`);
|
||||
if (spec.创作) extra.push(`创作 ${spec.创作}`);
|
||||
if (spec.地板) extra.push(clampPreviewText(String(spec.地板), 60));
|
||||
if (Array.isArray(spec.allowed_values) && spec.allowed_values.length) {
|
||||
extra.push(
|
||||
`枚举 ${spec.allowed_values
|
||||
@@ -3576,6 +3727,7 @@ function renderCreationFlowView(flowView, opts = {}) {
|
||||
<div class="flow-dag" data-flow-dag>
|
||||
<svg class="flow-dag-edges" aria-hidden="true"></svg>
|
||||
<div class="flow-dag-rows">${rows}</div>
|
||||
<aside class="flow-dag-hint" hidden role="status"></aside>
|
||||
</div>
|
||||
${suggestionHtml}
|
||||
</section>`;
|
||||
@@ -3670,6 +3822,11 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
||||
const selectable = Boolean(s.selectable) && interactive;
|
||||
const doneMenu = !isProto && runState === "done";
|
||||
const canReenter = doneMenu && interactive;
|
||||
const canDelete =
|
||||
doneMenu &&
|
||||
interactive &&
|
||||
s.role === "instance" &&
|
||||
Boolean(s.repeatable);
|
||||
const runLabel = isProto
|
||||
? selectable
|
||||
? "可增殖"
|
||||
@@ -3685,22 +3842,28 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
||||
? isProto
|
||||
? "增殖"
|
||||
: "进入"
|
||||
: "";
|
||||
: canReenter
|
||||
? "进入"
|
||||
: "";
|
||||
const actionHtml = actionLabel
|
||||
? `<span class="flow-action">${esc(actionLabel)}</span>`
|
||||
: "";
|
||||
const doneMenuBtn = doneMenu
|
||||
? `<span class="flow-node-menu-btn" data-flow-menu-trigger aria-hidden="true">⋯</span>`
|
||||
: "";
|
||||
const blockedBy = Array.isArray(s.blockedBy) ? s.blockedBy : [];
|
||||
const blockedAttr = blockedBy.length
|
||||
? ` data-flow-blocked="${escAttr(JSON.stringify(blockedBy))}"`
|
||||
: "";
|
||||
const blocked =
|
||||
!selectable && !doneMenu && s.blockedBy?.length
|
||||
? ` title="还差:${esc(s.blockedBy.join("、"))}"`
|
||||
!selectable && !doneMenu && blockedBy.length
|
||||
? ` title="还差:${esc(blockedBy.join("、"))}"`
|
||||
: isProto && selectable
|
||||
? ` title="确认当前编排,点一下新开一条产物再进入"`
|
||||
: selectable
|
||||
? ` title="确认当前编排并进入"`
|
||||
: doneMenu
|
||||
? ` title="查看产物或重新进入修改"`
|
||||
: canReenter
|
||||
? ` title="进入这条产物继续改,继承原文;新开一条请点原型上的「增殖」"`
|
||||
: "";
|
||||
const currentAttr = runState === "current" ? ' aria-current="step"' : "";
|
||||
const clickable = selectable || doneMenu;
|
||||
@@ -3708,9 +3871,9 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
||||
const typeAttr = clickable ? ' type="button"' : "";
|
||||
const pickAttr = selectable ? ` data-flow-pick="${esc(s.id)}"` : "";
|
||||
const doneAttr = doneMenu
|
||||
? ` data-flow-done="${esc(s.id)}" data-flow-done-name="${escAttr(s.name)}" data-has-artifact="${s.hasArtifact ? "1" : "0"}" data-can-reenter="${canReenter ? "1" : "0"}" aria-haspopup="menu" aria-label="已完成:${escAttr(s.title || s.name)}"`
|
||||
? ` data-flow-done="${esc(s.id)}" data-flow-done-name="${escAttr(s.name)}" data-flow-label="${escAttr([s.name, s.title].filter(Boolean).join(" · ") || s.id)}" data-has-artifact="${s.hasArtifact ? "1" : "0"}" data-can-reenter="${canReenter ? "1" : "0"}" data-can-delete="${canDelete ? "1" : "0"}" aria-haspopup="menu" aria-label="${canReenter ? "进入继续修改" : "已完成"}:${escAttr(s.title || s.name)}"`
|
||||
: "";
|
||||
const clickableClass = selectable
|
||||
const clickableClass = selectable || canReenter
|
||||
? " is-selectable"
|
||||
: doneMenu
|
||||
? " is-done-menu"
|
||||
@@ -3724,9 +3887,9 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
||||
const originAttr =
|
||||
origin?.fromId ? ` data-flow-origin="${escAttr(origin.fromId)}"` : "";
|
||||
const metaHtml = compact
|
||||
? `<span class="flow-spawn-meta">${actionHtml || doneMenuBtn || `<span class="flow-run">${esc(runLabel)}</span>`}</span>`
|
||||
? `<span class="flow-spawn-meta">${actionHtml}${doneMenuBtn || (!actionHtml ? `<span class="flow-run">${esc(runLabel)}</span>` : "")}</span>`
|
||||
: `${actionHtml}${doneMenuBtn}`;
|
||||
return `<${tag} class="flow-node${protoClass}${spawnClass}${clickableClass}${lockedClass}" data-flow-id="${escAttr(s.id)}" data-flow-name="${escAttr(s.name)}" data-flow-deps="${depsJson}" data-run="${escAttr(runState)}"${fromAttr}${originAttr}${currentAttr}${typeAttr}${pickAttr}${doneAttr}${blocked}>
|
||||
return `<${tag} class="flow-node${protoClass}${spawnClass}${clickableClass}${lockedClass}" data-flow-id="${escAttr(s.id)}" data-flow-name="${escAttr(s.name)}" data-flow-deps="${depsJson}" data-run="${escAttr(runState)}"${fromAttr}${originAttr}${blockedAttr}${currentAttr}${typeAttr}${pickAttr}${doneAttr}${blocked}>
|
||||
<span class="flow-node-main">
|
||||
${compact ? "" : `<span class="flow-run">${esc(runLabel)}</span>`}
|
||||
<span class="flow-name">${nameLabel}${occ}</span>
|
||||
@@ -3741,6 +3904,47 @@ function renderFlowGraphNode(s, interactive, opts = {}) {
|
||||
</${tag}>`;
|
||||
}
|
||||
|
||||
function parseFlowJsonList(raw) {
|
||||
try {
|
||||
const list = JSON.parse(raw || "[]");
|
||||
return Array.isArray(list) ? list.map(String).filter(Boolean) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function flowNodeHintName(el) {
|
||||
if (!el) return "";
|
||||
return (
|
||||
el.getAttribute("data-flow-label") ||
|
||||
el.querySelector(".flow-title")?.textContent?.trim() ||
|
||||
el.getAttribute("data-flow-name") ||
|
||||
el.getAttribute("data-flow-id") ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function flowNodeRunLabel(el) {
|
||||
const run = el?.getAttribute("data-run");
|
||||
if (run === "done") return "已完成";
|
||||
if (run === "current") return "进行中";
|
||||
if (el?.classList.contains("is-selectable")) return "可进入";
|
||||
if (el?.classList.contains("is-locked")) return "待前置";
|
||||
return "未执行";
|
||||
}
|
||||
|
||||
function flowDagNodeIndex(dag) {
|
||||
const byId = new Map();
|
||||
const byName = new Map();
|
||||
for (const el of dag.querySelectorAll("[data-flow-id]")) {
|
||||
const id = el.getAttribute("data-flow-id");
|
||||
const name = el.getAttribute("data-flow-name");
|
||||
if (id) byId.set(id, el);
|
||||
if (name && !byName.has(name)) byName.set(name, el);
|
||||
}
|
||||
return (ref) => byId.get(String(ref)) || byName.get(String(ref)) || null;
|
||||
}
|
||||
|
||||
function paintFlowDagEdges(dag) {
|
||||
if (!dag) return;
|
||||
const svg = dag.querySelector(".flow-dag-edges");
|
||||
@@ -3750,15 +3954,6 @@ function paintFlowDagEdges(dag) {
|
||||
svg.replaceChildren();
|
||||
return;
|
||||
}
|
||||
const byId = new Map();
|
||||
const byName = new Map();
|
||||
for (const el of nodes) {
|
||||
const id = el.getAttribute("data-flow-id");
|
||||
const name = el.getAttribute("data-flow-name");
|
||||
if (id) byId.set(id, el);
|
||||
if (name && !byName.has(name)) byName.set(name, el);
|
||||
}
|
||||
const findNode = (ref) => byId.get(ref) || byName.get(ref) || null;
|
||||
const dagRect = dag.getBoundingClientRect();
|
||||
const w = Math.max(dag.scrollWidth, dag.clientWidth, 1);
|
||||
const h = Math.max(dag.scrollHeight, dag.clientHeight, 1);
|
||||
@@ -3766,20 +3961,17 @@ function paintFlowDagEdges(dag) {
|
||||
svg.setAttribute("height", String(h));
|
||||
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
|
||||
|
||||
const findNode = flowDagNodeIndex(dag);
|
||||
const paths = [];
|
||||
for (const el of nodes) {
|
||||
let deps = [];
|
||||
try {
|
||||
deps = JSON.parse(el.getAttribute("data-flow-deps") || "[]");
|
||||
} catch {
|
||||
deps = [];
|
||||
}
|
||||
if (!Array.isArray(deps) || !deps.length) continue;
|
||||
const deps = parseFlowJsonList(el.getAttribute("data-flow-deps"));
|
||||
if (!deps.length) continue;
|
||||
const toId = el.getAttribute("data-flow-id") || "";
|
||||
const toRect = el.getBoundingClientRect();
|
||||
const x2 = toRect.left - dagRect.left + toRect.width / 2 + dag.scrollLeft;
|
||||
const y2 = toRect.top - dagRect.top + dag.scrollTop;
|
||||
for (const ref of deps) {
|
||||
const from = findNode(String(ref));
|
||||
const from = findNode(ref);
|
||||
if (!from || from === el) continue;
|
||||
const fromId = from.getAttribute("data-flow-id") || "";
|
||||
if (el.getAttribute("data-flow-from") === fromId) continue;
|
||||
@@ -3787,19 +3979,15 @@ function paintFlowDagEdges(dag) {
|
||||
const x1 =
|
||||
fromRect.left - dagRect.left + fromRect.width / 2 + dag.scrollLeft;
|
||||
const y1 = fromRect.bottom - dagRect.top + dag.scrollTop;
|
||||
const dy = Math.max(24, (y2 - y1) * 0.45);
|
||||
const dy = Math.max(28, (y2 - y1) * 0.42);
|
||||
const d = `M ${x1.toFixed(1)} ${y1.toFixed(1)} C ${x1.toFixed(1)} ${(y1 + dy).toFixed(1)}, ${x2.toFixed(1)} ${(y2 - dy).toFixed(1)}, ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
||||
const belong = el.getAttribute("data-flow-origin") === fromId;
|
||||
const active =
|
||||
from.classList.contains("is-selectable") ||
|
||||
el.classList.contains("is-selectable") ||
|
||||
from.getAttribute("data-run") === "done";
|
||||
const cls = `flow-dag-edge${active ? " is-lit" : ""}${belong ? " flow-dag-edge--belong" : ""}`;
|
||||
const cls = `flow-dag-edge${belong ? " flow-dag-edge--belong" : ""}`;
|
||||
const marker = belong
|
||||
? "url(#flow-dag-arrow-belong)"
|
||||
: "url(#flow-dag-arrow)";
|
||||
paths.push(
|
||||
`<path class="${cls}" d="${d}" fill="none" marker-end="${marker}" />`,
|
||||
`<path class="${cls}" d="${d}" fill="none" marker-end="${marker}" data-from="${escAttr(fromId)}" data-to="${escAttr(toId)}" />`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3807,10 +3995,210 @@ function paintFlowDagEdges(dag) {
|
||||
<marker id="flow-dag-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead" />
|
||||
</marker>
|
||||
<marker id="flow-dag-arrow-in" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--in" />
|
||||
</marker>
|
||||
<marker id="flow-dag-arrow-out" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--out" />
|
||||
</marker>
|
||||
<marker id="flow-dag-arrow-belong" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" class="flow-dag-arrowhead flow-dag-arrowhead--belong" />
|
||||
</marker>
|
||||
</defs>${paths.join("")}`;
|
||||
|
||||
const traceId = dag.getAttribute("data-flow-trace");
|
||||
if (traceId) {
|
||||
const node = nodes.find((el) => el.getAttribute("data-flow-id") === traceId);
|
||||
if (node) applyFlowDagTrace(dag, node);
|
||||
else clearFlowDagTrace(dag);
|
||||
}
|
||||
}
|
||||
|
||||
function flowHintItemsHtml(items, emptyText) {
|
||||
if (!items.length) {
|
||||
return `<p class="flow-dag-hint-empty">${esc(emptyText)}</p>`;
|
||||
}
|
||||
return `<ul class="flow-dag-hint-list">${items
|
||||
.map(
|
||||
(item) =>
|
||||
`<li><span class="flow-dag-hint-item-name">${esc(item.name)}</span><span class="flow-dag-hint-item-run">${esc(item.run)}</span></li>`,
|
||||
)
|
||||
.join("")}</ul>`;
|
||||
}
|
||||
|
||||
function placeFlowDagHint(dag, node) {
|
||||
const hint = dag.querySelector(".flow-dag-hint");
|
||||
if (!hint || hint.hidden) return;
|
||||
const dagRect = dag.getBoundingClientRect();
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
const hintW = hint.offsetWidth;
|
||||
const hintH = hint.offsetHeight;
|
||||
const pad = 8;
|
||||
let left = nodeRect.right - dagRect.left + dag.scrollLeft + 12;
|
||||
let top = nodeRect.top - dagRect.top + dag.scrollTop;
|
||||
const maxLeft = Math.max(pad, dag.scrollWidth - hintW - pad);
|
||||
if (left > maxLeft) {
|
||||
left = nodeRect.left - dagRect.left + dag.scrollLeft - hintW - 12;
|
||||
}
|
||||
left = Math.min(maxLeft, Math.max(pad, left));
|
||||
const maxTop = Math.max(pad, dag.scrollHeight - hintH - pad);
|
||||
if (top > maxTop) top = maxTop;
|
||||
if (top < pad) top = pad;
|
||||
hint.style.left = `${left}px`;
|
||||
hint.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
function applyFlowDagTrace(dag, node) {
|
||||
const findNode = flowDagNodeIndex(dag);
|
||||
const nodeId = node.getAttribute("data-flow-id") || "";
|
||||
const nodeName = node.getAttribute("data-flow-name") || "";
|
||||
const refs = new Set([nodeId, nodeName].filter(Boolean));
|
||||
const depRefs = parseFlowJsonList(node.getAttribute("data-flow-deps"));
|
||||
const blocked = new Set(parseFlowJsonList(node.getAttribute("data-flow-blocked")));
|
||||
const relatedIds = new Set([nodeId]);
|
||||
|
||||
dag.querySelectorAll(".flow-dag-edge").forEach((path) => {
|
||||
const fromId = path.getAttribute("data-from") || "";
|
||||
const toId = path.getAttribute("data-to") || "";
|
||||
const isIn = toId === nodeId;
|
||||
const isOut = fromId === nodeId;
|
||||
path.classList.toggle("is-trace-in", isIn);
|
||||
path.classList.toggle("is-trace-out", isOut && !isIn);
|
||||
if (isIn) {
|
||||
path.setAttribute("marker-end", "url(#flow-dag-arrow-in)");
|
||||
relatedIds.add(fromId);
|
||||
} else if (isOut) {
|
||||
path.setAttribute("marker-end", "url(#flow-dag-arrow-out)");
|
||||
relatedIds.add(toId);
|
||||
} else {
|
||||
path.setAttribute(
|
||||
"marker-end",
|
||||
path.classList.contains("flow-dag-edge--belong")
|
||||
? "url(#flow-dag-arrow-belong)"
|
||||
: "url(#flow-dag-arrow)",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
dag.querySelectorAll("[data-flow-id]").forEach((el) => {
|
||||
const id = el.getAttribute("data-flow-id") || "";
|
||||
el.classList.toggle("is-trace-root", el === node);
|
||||
el.classList.toggle("is-trace-related", el !== node && relatedIds.has(id));
|
||||
});
|
||||
|
||||
const deps = depRefs
|
||||
.map((ref) => {
|
||||
const el = findNode(ref);
|
||||
if (el && el.getAttribute("data-flow-from") === nodeId) return null;
|
||||
return {
|
||||
name: el ? flowNodeHintName(el) : ref,
|
||||
run: el
|
||||
? flowNodeRunLabel(el)
|
||||
: blocked.has(ref)
|
||||
? "未齐"
|
||||
: "",
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const downstream = [...dag.querySelectorAll("[data-flow-id]")]
|
||||
.filter((el) => {
|
||||
if (el === node) return false;
|
||||
if (el.getAttribute("data-flow-from") === nodeId) return false;
|
||||
return parseFlowJsonList(el.getAttribute("data-flow-deps")).some((ref) =>
|
||||
refs.has(ref),
|
||||
);
|
||||
})
|
||||
.map((el) => ({
|
||||
name: flowNodeHintName(el),
|
||||
run: flowNodeRunLabel(el),
|
||||
}));
|
||||
|
||||
const hint = dag.querySelector(".flow-dag-hint");
|
||||
if (hint) {
|
||||
hint.innerHTML = `<p class="flow-dag-hint-kicker">依赖关系</p>
|
||||
<p class="flow-dag-hint-name">${esc(flowNodeHintName(node))}</p>
|
||||
<div class="flow-dag-hint-sec">
|
||||
<div class="flow-dag-hint-label">依赖</div>
|
||||
${flowHintItemsHtml(deps, "无前置")}
|
||||
</div>
|
||||
<div class="flow-dag-hint-sec">
|
||||
<div class="flow-dag-hint-label">下游</div>
|
||||
${flowHintItemsHtml(downstream, "无下游")}
|
||||
</div>`;
|
||||
hint.hidden = false;
|
||||
placeFlowDagHint(dag, node);
|
||||
}
|
||||
dag.classList.add("is-tracing");
|
||||
dag.setAttribute("data-flow-trace", nodeId);
|
||||
}
|
||||
|
||||
function clearFlowDagTrace(dag) {
|
||||
if (!dag) return;
|
||||
dag.classList.remove("is-tracing");
|
||||
dag.removeAttribute("data-flow-trace");
|
||||
dag.querySelectorAll(".flow-dag-edge").forEach((path) => {
|
||||
path.classList.remove("is-trace-in", "is-trace-out");
|
||||
path.setAttribute(
|
||||
"marker-end",
|
||||
path.classList.contains("flow-dag-edge--belong")
|
||||
? "url(#flow-dag-arrow-belong)"
|
||||
: "url(#flow-dag-arrow)",
|
||||
);
|
||||
});
|
||||
dag.querySelectorAll("[data-flow-id]").forEach((el) => {
|
||||
el.classList.remove("is-trace-root", "is-trace-related");
|
||||
});
|
||||
const hint = dag.querySelector(".flow-dag-hint");
|
||||
if (hint) {
|
||||
hint.hidden = true;
|
||||
hint.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
function setFlowDagTrace(dag, node) {
|
||||
if (!dag || !node) return;
|
||||
if (dag.getAttribute("data-flow-trace") === node.getAttribute("data-flow-id")) {
|
||||
return;
|
||||
}
|
||||
applyFlowDagTrace(dag, node);
|
||||
}
|
||||
|
||||
function wireFlowDagTrace(dag) {
|
||||
dag.addEventListener("pointerover", (e) => {
|
||||
const node = e.target.closest("[data-flow-id]");
|
||||
if (node && dag.contains(node)) setFlowDagTrace(dag, node);
|
||||
});
|
||||
dag.addEventListener("pointerleave", () => {
|
||||
const focused = dag.querySelector("[data-flow-id]:focus");
|
||||
if (focused) {
|
||||
applyFlowDagTrace(dag, focused);
|
||||
return;
|
||||
}
|
||||
clearFlowDagTrace(dag);
|
||||
});
|
||||
dag.addEventListener("focusin", (e) => {
|
||||
const node = e.target.closest("[data-flow-id]");
|
||||
if (node && dag.contains(node)) setFlowDagTrace(dag, node);
|
||||
});
|
||||
dag.addEventListener("focusout", (e) => {
|
||||
const next = e.relatedTarget;
|
||||
if (next && dag.contains(next) && next.closest("[data-flow-id]")) return;
|
||||
if (dag.matches(":hover")) return;
|
||||
clearFlowDagTrace(dag);
|
||||
});
|
||||
dag.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const id = dag.getAttribute("data-flow-trace");
|
||||
if (!id) return;
|
||||
const node = [...dag.querySelectorAll("[data-flow-id]")].find(
|
||||
(el) => el.getAttribute("data-flow-id") === id,
|
||||
);
|
||||
if (node) placeFlowDagHint(dag, node);
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
|
||||
function wireCreationFlowGraph(root, handlers) {
|
||||
@@ -3825,16 +4213,40 @@ function wireCreationFlowGraph(root, handlers) {
|
||||
});
|
||||
});
|
||||
root.querySelectorAll("[data-flow-done]").forEach((el) => {
|
||||
const stepId = el.getAttribute("data-flow-done");
|
||||
const canReenter = el.getAttribute("data-can-reenter") === "1";
|
||||
const openMenu = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showFlowNodeMenu(el, e.clientX, e.clientY, handlers);
|
||||
};
|
||||
el.addEventListener("click", openMenu);
|
||||
el.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.target.closest("[data-flow-menu-trigger]")) {
|
||||
openMenu(e);
|
||||
return;
|
||||
}
|
||||
if (canReenter && stepId) {
|
||||
handlers.onReenterCreationStep?.(stepId);
|
||||
return;
|
||||
}
|
||||
openMenu(e);
|
||||
});
|
||||
el.addEventListener("contextmenu", openMenu);
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Enter" && e.key !== " " && e.key !== "ContextMenu") return;
|
||||
if (e.key === "ContextMenu") {
|
||||
e.preventDefault();
|
||||
const rect = el.getBoundingClientRect();
|
||||
showFlowNodeMenu(el, rect.left, rect.bottom + 4, handlers);
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
if (canReenter && stepId) {
|
||||
handlers.onReenterCreationStep?.(stepId);
|
||||
return;
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
showFlowNodeMenu(el, rect.left, rect.bottom + 4, handlers);
|
||||
});
|
||||
@@ -3863,6 +4275,7 @@ function wireCreationFlowGraph(root, handlers) {
|
||||
dag.querySelectorAll(".flow-family").forEach((n) => ro.observe(n));
|
||||
}
|
||||
window.addEventListener("resize", paint, { passive: true });
|
||||
wireFlowDagTrace(dag);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3995,6 +4408,7 @@ function renderReviewWorkspace(review, opts = {}) {
|
||||
return `
|
||||
<section class="workspace-review" data-review="1" data-review-kind="${esc(copy.kind)}" data-message-id="${esc(contextId)}" data-can-edit="0" data-has-context="${hasContext}">
|
||||
<header class="workspace-review-head workspace-review-head--slim">
|
||||
${opts.leaveStepHtml || ""}
|
||||
<div class="workspace-review-titles">
|
||||
<span class="workspace-review-kicker">${esc(copy.kicker)}</span>
|
||||
<h2 class="workspace-review-title">${esc(label)}</h2>
|
||||
@@ -4512,6 +4926,7 @@ function renderSpeakWorkspace(view) {
|
||||
: "";
|
||||
return `<section class="workspace-intent">
|
||||
<div class="workspace-intent-head">
|
||||
${leaveStepControlHtml(view)}
|
||||
${
|
||||
flowInteractive
|
||||
? workspaceViewSwitchHtml("flow")
|
||||
@@ -4519,7 +4934,6 @@ function renderSpeakWorkspace(view) {
|
||||
canOfferSaveProduct(view) ? "落档" : "说话"
|
||||
}</span>`
|
||||
}
|
||||
${leaveStepControlHtml(view)}
|
||||
</div>
|
||||
<h2 class="workspace-intent-title">${esc(title)}</h2>
|
||||
<p class="workspace-intent-hint">${esc(hint)}</p>
|
||||
@@ -4534,9 +4948,9 @@ function renderSpeakWorkspace(view) {
|
||||
function renderAnswerWorkspaceShell(view) {
|
||||
return `<section class="workspace-answer" id="workspace-answer-slot">
|
||||
<header class="workspace-answer-head workspace-answer-head--slim">
|
||||
${leaveStepControlHtml(view)}
|
||||
<span class="workspace-surface-kicker">答题</span>
|
||||
<p class="workspace-answer-hint">点字母选中,文案可改;点卡片展开,点顶条收起。</p>
|
||||
${leaveStepControlHtml(view)}
|
||||
</header>
|
||||
</section>`;
|
||||
}
|
||||
@@ -4602,10 +5016,12 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
|
||||
renderReviewWorkspace(view.reviewArtifact, {
|
||||
hideAskSidecar: opts.hideAskSidecar === true,
|
||||
sourceMessage: resolveReviewSourceMessage(view),
|
||||
leaveStepHtml: leaveStepControlHtml(view),
|
||||
}) + askSlot;
|
||||
wireMessageFeedActions(stage, handlers);
|
||||
wireMessageContextMenu(stage, handlers);
|
||||
wireCreationFlowGraph(stage, handlers);
|
||||
wireLeaveStepControl(stage, handlers);
|
||||
wireOpeningPicker();
|
||||
if (opts.mountAskCard) {
|
||||
const askHost = document.getElementById("workspace-review-ask");
|
||||
@@ -4696,13 +5112,13 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
stage.innerHTML = `
|
||||
<div class="workspace-pending" id="msg-live-pending">
|
||||
<header class="workspace-pending-head">
|
||||
${leaveStepControlHtml(view)}
|
||||
<span class="msg-tag">${esc(label)}</span>
|
||||
<span class="msg-live-indicator">流式输出中</span>
|
||||
${renderRetryRunButton()}
|
||||
</header>
|
||||
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
|
||||
</div>`;
|
||||
wireRetryRun(stage, handlers);
|
||||
wireLeaveStepControl(stage, handlers);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+38
-22
@@ -546,7 +546,7 @@ function resolveComposer(view, loading) {
|
||||
|
||||
function composerModeChip(spec) {
|
||||
const id = spec.task?.id;
|
||||
if (id !== "speak" && id !== "answer" && id !== "review") return "";
|
||||
if (id !== "speak" && id !== "answer" && id !== "review" && id !== "busy") return "";
|
||||
const label = spec.task.label || "";
|
||||
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" — ");
|
||||
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
|
||||
@@ -586,13 +586,6 @@ function syncThemeMenuUi() {
|
||||
});
|
||||
}
|
||||
|
||||
function leaveStepButtonHtml(spec) {
|
||||
if (!spec.leaveStep) return "";
|
||||
return `<button type="button" class="btn" data-act="leave_step" title="${esc(
|
||||
spec.leaveStep.title || "先不写这一步,回到节点选择",
|
||||
)}">${esc(spec.leaveStep.label)}</button>`;
|
||||
}
|
||||
|
||||
function stashComposerDraftFromDom() {
|
||||
if (skipComposerStash) {
|
||||
skipComposerStash = false;
|
||||
@@ -650,20 +643,22 @@ function renderComposer(view, loading) {
|
||||
host._enterHandler = null;
|
||||
}
|
||||
const spec = resolveComposer(view, loading);
|
||||
const leaveAct = view.actions?.find((a) => a.type === "leave_step");
|
||||
if (leaveAct && spec.mode !== "waiting" && spec.mode !== "idle") {
|
||||
spec.leaveStep = {
|
||||
label: leaveAct.label || "返回节点",
|
||||
title: "先不写这一步,回到节点选择",
|
||||
};
|
||||
}
|
||||
|
||||
if (spec.mode === "waiting") {
|
||||
root.innerHTML = `<div class="composer-waiting">
|
||||
<span>${esc(spec.text)}</span>
|
||||
<button type="button" class="btn" data-act="retry_run" title="停止当前生成并重试">停止并重试</button>
|
||||
</div>`;
|
||||
const shell = composerInputShell(
|
||||
{ ...spec, task: { ...(spec.task || {}), id: "busy", label: spec.task?.label || "生成中" } },
|
||||
{
|
||||
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="生成中…可提前停止或重roll"></textarea>`,
|
||||
trailing: `<button type="button" class="btn composer-btn-icon" data-act="abort_run" title="提前停止" aria-label="提前停止">□</button>
|
||||
<button type="button" class="btn composer-btn-icon" data-act="retry_run" title="重roll" aria-label="重roll">🔄</button>`,
|
||||
},
|
||||
);
|
||||
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
||||
root.querySelector("#composer-form")?.addEventListener("submit", (e) => e.preventDefault());
|
||||
root.querySelector("[data-act=abort_run]")?.addEventListener("click", () => abortRun());
|
||||
root.querySelector("[data-act=retry_run]")?.addEventListener("click", () => retryRun());
|
||||
applyPendingComposerDraft();
|
||||
const input = $("composer-input");
|
||||
if (input) autosizeComposerInput(input);
|
||||
return;
|
||||
}
|
||||
if (spec.mode === "idle") {
|
||||
@@ -878,7 +873,7 @@ function renderComposer(view, loading) {
|
||||
: spec.placeholder;
|
||||
const shell = composerInputShell(spec, {
|
||||
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="${esc(placeholder)}"></textarea>`,
|
||||
trailing: `${leaveStepButtonHtml(spec)}<button type="button" class="btn composer-btn${
|
||||
trailing: `<button type="button" class="btn composer-btn${
|
||||
sendIsPrimary ? " btn-primary" : ""
|
||||
}" data-act="revise" title="${esc(
|
||||
acceptOnEmpty
|
||||
@@ -889,7 +884,6 @@ function renderComposer(view, loading) {
|
||||
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
|
||||
wireComposerForm({ acceptOnEmpty });
|
||||
root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept"));
|
||||
root.querySelector("[data-act=leave_step]")?.addEventListener("click", () => runAction("leave_step"));
|
||||
applyPendingComposerDraft();
|
||||
}
|
||||
|
||||
@@ -1953,6 +1947,23 @@ async function retryRun() {
|
||||
}
|
||||
}
|
||||
|
||||
async function abortRun() {
|
||||
if (!sessionId) return;
|
||||
const seq = beginUiRequest();
|
||||
try {
|
||||
renderSession(lastView, true);
|
||||
const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/actions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "abort_run" }),
|
||||
});
|
||||
if (isStaleUiRequest(seq)) return;
|
||||
renderSession(view, false);
|
||||
} catch (err) {
|
||||
if (isStaleUiRequest(seq)) return;
|
||||
if (lastView) renderSession({ ...lastView, hints: [err.message] }, false);
|
||||
}
|
||||
}
|
||||
|
||||
function startLivePoll() {
|
||||
stopLivePoll();
|
||||
if (!sessionId) return;
|
||||
@@ -2009,6 +2020,11 @@ function renderSession(view, loading = false) {
|
||||
onReenterCreationStep: (stepId) =>
|
||||
runAction("pick_step", { stepId, reenter: true }),
|
||||
onViewStepArtifact: (stepId, name) => showStepArtifactDialog(stepId, name),
|
||||
onDeleteCreationStep: (stepId, name) => {
|
||||
const label = name ? `${name}` : stepId;
|
||||
if (!confirm(`删除「${label}」这条产物?会从图上拿掉,不可恢复。`)) return;
|
||||
void runAction("delete_step", { stepId });
|
||||
},
|
||||
onSpawnCreationStep: (moduleName) => runAction("spawn_step", { moduleName }),
|
||||
onLeaveCreationStep: () => runAction("leave_step"),
|
||||
onEnterPlay: () => enterPlayNow(),
|
||||
|
||||
+2
-1
@@ -233,7 +233,8 @@
|
||||
|
||||
<menu id="flow-node-menu" class="context-menu" hidden>
|
||||
<button type="button" class="context-menu-item" id="flow-menu-view" data-flow-menu-action="view">查看产物</button>
|
||||
<button type="button" class="context-menu-item" id="flow-menu-reenter" data-flow-menu-action="reenter">重新进入修改</button>
|
||||
<button type="button" class="context-menu-item" id="flow-menu-reenter" data-flow-menu-action="reenter">进入继续修改</button>
|
||||
<button type="button" class="context-menu-item context-menu-danger context-menu-separator-before" id="flow-menu-delete" data-flow-menu-action="delete">删除产物</button>
|
||||
</menu>
|
||||
|
||||
<dialog id="dialog-step-artifact" class="dialog dialog-wide" aria-labelledby="step-artifact-title">
|
||||
|
||||
+168
-21
@@ -479,21 +479,26 @@ body.design-workspace.coord-rail-collapsed .main-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.workspace-review-head .workspace-leave-step {
|
||||
flex-shrink: 0;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.workspace-review-titles {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.workspace-review-nav {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.workspace-review-nav .msg-variant-nav {
|
||||
margin: 0;
|
||||
@@ -860,6 +865,9 @@ body.design-workspace .workspace-pending {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.workspace-pending-head .workspace-leave-step {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.workspace-pending-head .msg-action {
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -948,7 +956,7 @@ body.design-workspace .review-worker-set {
|
||||
.workspace-intent-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
max-width: 46rem;
|
||||
@@ -979,7 +987,7 @@ body.design-workspace .review-worker-set {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.workspace-answer-head--slim .workspace-leave-step {
|
||||
margin-left: auto;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.workspace-intent,
|
||||
.workspace-answer {
|
||||
@@ -1213,7 +1221,7 @@ body.design-workspace .flow-status {
|
||||
body.design-workspace .flow-dag {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
padding: 4px 2px 8px;
|
||||
padding: 8px 8px 16px;
|
||||
}
|
||||
body.design-workspace .flow-dag-edges {
|
||||
position: absolute;
|
||||
@@ -1225,23 +1233,41 @@ body.design-workspace .flow-dag-edges {
|
||||
overflow: visible;
|
||||
}
|
||||
body.design-workspace .flow-dag-edge {
|
||||
stroke: color-mix(in srgb, var(--border) 80%, var(--muted));
|
||||
stroke: color-mix(in srgb, var(--text) 32%, var(--border));
|
||||
stroke-width: 1.5;
|
||||
opacity: 0.85;
|
||||
}
|
||||
body.design-workspace .flow-dag-edge.is-lit {
|
||||
stroke: color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||
opacity: 1;
|
||||
opacity: 0.55;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
body.design-workspace .flow-dag-arrowhead {
|
||||
fill: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||
fill: color-mix(in srgb, var(--text) 38%, var(--border));
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-dag-edge {
|
||||
opacity: 0.12;
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-dag-edge.is-trace-in,
|
||||
body.design-workspace .flow-dag.is-tracing .flow-dag-edge.is-trace-out {
|
||||
opacity: 1;
|
||||
}
|
||||
body.design-workspace .flow-dag-edge.is-trace-in {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2.35;
|
||||
}
|
||||
body.design-workspace .flow-dag-edge.is-trace-out {
|
||||
stroke: color-mix(in srgb, var(--text) 72%, var(--accent));
|
||||
stroke-width: 2.1;
|
||||
}
|
||||
body.design-workspace .flow-dag-arrowhead--in {
|
||||
fill: var(--accent);
|
||||
}
|
||||
body.design-workspace .flow-dag-arrowhead--out {
|
||||
fill: color-mix(in srgb, var(--text) 72%, var(--accent));
|
||||
}
|
||||
body.design-workspace .flow-dag-rows {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
gap: 44px;
|
||||
min-width: 0;
|
||||
}
|
||||
body.design-workspace .flow-dag-row {
|
||||
@@ -1262,8 +1288,96 @@ body.design-workspace .flow-dag-row-nodes {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
gap: 24px 104px;
|
||||
min-width: 0;
|
||||
padding: 0 28px;
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node {
|
||||
opacity: 0.4;
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-root,
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-related,
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node.is-locked.is-trace-root,
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node.is-locked.is-trace-related {
|
||||
opacity: 1;
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-node.is-trace-root {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
}
|
||||
body.design-workspace .flow-dag-hint {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
width: max-content;
|
||||
max-width: min(240px, calc(100% - 24px));
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 8px 22px color-mix(in srgb, var(--text) 12%, transparent);
|
||||
pointer-events: none;
|
||||
color: var(--text);
|
||||
}
|
||||
body.design-workspace .flow-dag-hint[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-kicker {
|
||||
margin: 0 0 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-name {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-sec + .flow-dag-hint-sec {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-label {
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-list li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-item-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
body.design-workspace .flow-dag-hint-item-run {
|
||||
flex-shrink: 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
body.design-workspace .flow-node {
|
||||
display: flex;
|
||||
@@ -1321,10 +1435,13 @@ body.design-workspace .flow-node-menu-btn {
|
||||
line-height: 1;
|
||||
color: var(--muted);
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
cursor: context-menu;
|
||||
}
|
||||
body.design-workspace .flow-node.is-done-menu:hover .flow-node-menu-btn,
|
||||
body.design-workspace .flow-node.is-done-menu:focus-visible .flow-node-menu-btn {
|
||||
body.design-workspace .flow-node.is-done-menu:focus-visible .flow-node-menu-btn,
|
||||
body.design-workspace .flow-node.is-selectable:hover .flow-node-menu-btn,
|
||||
body.design-workspace .flow-node.is-selectable:focus-visible .flow-node-menu-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.step-artifact-body {
|
||||
@@ -1349,14 +1466,12 @@ body.design-workspace .flow-node.is-selectable {
|
||||
0 4px 12px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
body.design-workspace .flow-node.is-selectable:hover {
|
||||
transform: translateY(-2px);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
||||
0 8px 18px color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
}
|
||||
body.design-workspace .flow-node.is-selectable:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent),
|
||||
0 2px 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
@@ -1465,6 +1580,7 @@ body.design-workspace .flow-spawn-meta {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
body.design-workspace .flow-spawn-meta .flow-action {
|
||||
min-height: 24px;
|
||||
@@ -1495,7 +1611,11 @@ body.design-workspace .flow-dag-edge--belong {
|
||||
stroke-width: 1.25;
|
||||
}
|
||||
body.design-workspace .flow-dag-arrowhead--belong {
|
||||
fill: color-mix(in srgb, var(--accent) 38%, var(--border));
|
||||
fill: color-mix(in srgb, var(--text) 28%, var(--border));
|
||||
}
|
||||
body.design-workspace .flow-dag.is-tracing .flow-dag-edge--belong.is-trace-in,
|
||||
body.design-workspace .flow-dag.is-tracing .flow-dag-edge--belong.is-trace-out {
|
||||
stroke-dasharray: 4 4;
|
||||
}
|
||||
body.design-workspace .flow-suggestion-inline {
|
||||
font-size: 12px;
|
||||
@@ -1604,6 +1724,8 @@ body.design-workspace .flow-spawn.is-blocked {
|
||||
body.design-workspace .flow-dag-row-nodes {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
body.design-workspace .flow-node,
|
||||
body.design-workspace .flow-family {
|
||||
@@ -3559,8 +3681,21 @@ body.design-workspace .af-mosaic {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.skill-pool-desc {
|
||||
margin: 6px 0 0;
|
||||
.skill-pool-card.is-direction {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.skill-pool-dirs {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.skill-pool-dir dt {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.skill-pool-dir dd {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
@@ -4821,7 +4956,7 @@ body.design-workspace .af-mosaic {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 10px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.composer-mode-chip {
|
||||
@@ -4861,6 +4996,10 @@ body.design-workspace .af-mosaic {
|
||||
.composer-input-shell[data-task="review"][data-tone="plan"] .composer-mode-chip {
|
||||
color: var(--accent);
|
||||
}
|
||||
.composer-mode-chip[data-task="busy"],
|
||||
.composer-input-shell[data-task="busy"] .composer-mode-chip {
|
||||
color: var(--muted);
|
||||
}
|
||||
.composer-input-shell textarea {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -4917,6 +5056,7 @@ body.design-workspace .af-mosaic {
|
||||
.composer-shell-actions > .btn-primary,
|
||||
.composer-shell-actions > .composer-btn,
|
||||
.composer-shell-actions > .composer-btn-accept,
|
||||
.composer-shell-actions > .composer-btn-icon,
|
||||
.composer-form > .btn,
|
||||
.composer-form > .btn-primary,
|
||||
.composer-form > .composer-btn,
|
||||
@@ -4933,6 +5073,13 @@ body.design-workspace .af-mosaic {
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
.composer-btn-icon {
|
||||
width: calc(var(--composer-control-h) - 8px);
|
||||
min-width: calc(var(--composer-control-h) - 8px);
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
/* 验收态:主按钮承接原「批阅」语义 */
|
||||
.composer-btn-review {
|
||||
background: #15803d;
|
||||
|
||||
Reference in New Issue
Block a user