fix: complete approved layout and prompt cancellation safeguards

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-10-24 23:42:46 +08:00
co-authored by multica-agent
parent 4f4a75b3da
commit 7ddd1d32d0
13 changed files with 114 additions and 66 deletions
@@ -26,6 +26,7 @@
min-width: 0;
}
.mdl-sum {
flex: none;
display: flex;
align-items: center;
gap: 10px;
@@ -55,8 +56,10 @@
color: var(--tx-2);
font-size: 11.5px;
margin-top: 2px;
overflow-wrap: anywhere;
}
.mlist {
flex: none;
border-radius: 12px;
border: 1px solid var(--line);
background: var(--bg-1);
@@ -189,6 +192,7 @@
opacity: 0.3;
}
}
html[data-motion="calm"] .spill.run i,
html[data-motion="off"] .spill.run i {
animation: none;
}
@@ -214,6 +218,7 @@ html[data-motion="off"] .spill.run i {
border-color: color-mix(in srgb, var(--sc-image) 40%, transparent);
}
.role-t {
flex: none;
border-radius: 12px;
border: 1px solid var(--line);
background: var(--bg-1);
@@ -257,6 +262,7 @@ html[data-motion="off"] .spill.run i {
height: 28px;
}
.soon-row {
flex: none;
display: flex;
align-items: center;
gap: 10px;
@@ -399,11 +405,7 @@ html[data-motion="off"] .spill.run i {
@media (max-width: 1280px) {
.mlist-h,
.mrow {
grid-template-columns: minmax(0, 1.3fr) 150px 84px;
}
.mlist-h .c-use,
.mrow .use {
display: none;
grid-template-columns: minmax(0, 1fr) 150px 84px;
}
.role-g {
grid-template-columns: 1fr;
@@ -414,6 +416,10 @@ html[data-motion="off"] .spill.run i {
.role:nth-last-child(2) {
border-bottom: 1px solid var(--line);
}
.mlist-h .use,
.mrow .use {
display: none;
}
}
.mdl-body small,
@@ -457,21 +463,8 @@ html[data-motion="off"] .spill.run i {
width: 100%;
font-size: 12.5px;
}
@media (max-width: 1280px) {
.mlist-h,
.mrow {
grid-template-columns: minmax(0, 1fr) 150px 84px;
}
.mlist-h .use,
.mrow .use {
display: none;
}
}
.role > span {
font-size: 12px;
color: var(--tx-2);
}
.mdl-alert small {
overflow-wrap: anywhere;
}
@@ -5,7 +5,7 @@
import ToolButtons from "./ToolButtons.svelte";
import ResultBand from "./ResultBand.svelte";
let {
value = $bindable(""),
value = $bindable(),
identity,
label = "提示词",
rows = 2,
@@ -18,7 +18,7 @@
placeholder?: string;
} = $props();
const tool = promptTool(
() => value,
() => value ?? "",
() => `${page.url.pathname}|${identity}`,
(v) => (value = v),
untrack(() => label),
@@ -16,7 +16,10 @@
<svelte:window
onkeydown={(e) => {
if (e.key === "Escape" && tool.state !== "idle") tool.close();
if (e.key === "Escape" && tool.state !== "idle") {
if (tool.state === "loading") tool.cancel();
else tool.close();
}
}}
/>
{#if tool.state !== "idle"}
@@ -36,7 +39,7 @@
{#if tool.state === "loading"}<Button
size="sm"
variant="ghost"
onclick={tool.close}>取消</Button
onclick={tool.cancel}>取消</Button
>
{:else if tool.state === "result"}
{#if tool.confirm}<span class="pt-confirm">替换会覆盖你刚才的修改</span
@@ -68,7 +71,8 @@
type="button"
class="pt-btn"
aria-label="关闭提示词结果"
onclick={tool.close}>×</button
onclick={() =>
tool.state === "loading" ? tool.cancel() : tool.close()}>×</button
>
</div>
<div class="pt-b">
@@ -55,6 +55,10 @@ export function promptTool(
state = "idle";
confirm = false;
}
function cancel() {
close();
toast.info("已取消,原提示词未改动");
}
function edit() {
++textVersion;
confirm = false;
@@ -63,9 +67,12 @@ export function promptTool(
const context = getContext();
const text = getText();
if (context !== observedContext) {
const hadResult = state !== "idle";
observedContext = context;
++contextVersion;
close();
if (hadResult)
toast.info("已切换工作流或项目,旧结果已作废;提示词未改动");
}
if (text !== observedText) {
observedText = text;
@@ -87,8 +94,7 @@ export function promptTool(
sync();
if (!getText().trim()) return;
if (state === "loading" && next === action) {
close();
toast.info("已取消,原提示词未改动");
cancel();
return;
}
onOpen?.();
@@ -230,6 +236,7 @@ export function promptTool(
},
run,
close,
cancel,
edit,
replace,
cancelConfirm() {
+14 -12
View File
@@ -422,22 +422,24 @@
/>{:else}<span class="cover-blank">暂无封面</span>{/if}
</div>
<div class="copy">
<small
<span class="eyebrow"
>继续创作 · {new Date(
parseDbTs(recent.updated_at),
).toLocaleDateString()}</small
).toLocaleDateString()}</span
>
<h2 class="t1" title={recent.title}>《{recent.title}》</h2>
<b class="resume-stage"
>{recentStage < 0
? "已完成"
: stageNames[recentStage] +
" · " +
(recent.stats?.stage_status?.[stageKeys[recentStage]] ===
"awaiting_confirm"
? "等你确认"
: "继续创作")}</b
>
<h2 class="t2">
<em
>{recentStage < 0
? "已完成"
: stageNames[recentStage] +
" · " +
(recent.stats?.stage_status?.[stageKeys[recentStage]] ===
"awaiting_confirm"
? "等你确认"
: "继续创作")}</em
>
</h2>
<p class="next">草稿确认后,才会交给下一阶段使用。</p>
<div class="studio-stages">
{#each stageKeys as key, i (key)}<span
+2 -1
View File
@@ -272,7 +272,8 @@
</div>
<div class="lgrid" class:dense class:fill>
{#each group.items as i (i.path)}<article
class="lt"
class="lt a-viewfinder"
class:a-in-frame={focused?.path === i.path}
class:on={focused?.path === i.path}
class:picked={selected.has(i.path)}
class:broken={broken[i.path]}
@@ -3,6 +3,7 @@
box-sizing: border-box;
}
.lib2 {
--viewfinder-position-duration: var(--t-base);
flex: 1;
display: grid;
grid-template-columns: var(--lib-filter) minmax(0, 1fr) var(--lib-detail);
@@ -494,6 +495,8 @@
}
.lt .mat img,
.lt .mat video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
@@ -503,6 +506,8 @@
}
.ld-pv img,
.ld-pv video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
@@ -537,3 +542,17 @@
height: 60px;
object-fit: contain;
}
html[data-motion="full"] .selbar.on {
animation: library-selection var(--t-base) var(--ease-out);
}
@keyframes library-selection {
from {
opacity: 0;
transform: translate(-50%, 12px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
+32 -15
View File
@@ -72,7 +72,7 @@
display: flex;
flex-direction: column;
min-width: 0;
gap: 6px;
gap: 8px;
}
.resume h2 {
font-size: 22px;
@@ -381,17 +381,11 @@
font: 11px var(--mono);
color: var(--tx-3);
}
.resume .copy > small {
font-size: 11px;
color: var(--tx-2);
}
.resume-stage {
font-size: 14px;
color: var(--gold);
}
.studio-stages {
display: flex;
gap: 6px;
gap: 4px;
margin-top: 4px;
max-width: 460px;
}
.studio-stages > span {
display: flex;
@@ -405,17 +399,21 @@
.studio-stages i {
width: 100%;
height: 3px;
border-radius: 50%;
background: var(--tx-3);
border-radius: 3px;
background: rgb(var(--ink) / 0.12);
}
.studio-stages .done i {
background: var(--ok);
background: var(--gold);
}
.studio-stages .cur {
color: var(--gold);
color: var(--tx-1);
}
.studio-stages .cur i {
background: var(--gold);
background: linear-gradient(
90deg,
var(--gold) 50%,
rgb(var(--ink) / 0.12) 50%
);
}
.start > span:last-child {
min-width: 0;
@@ -513,3 +511,22 @@
.st2 :is(h2, h3, p) {
margin: 0;
}
.resume .eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
height: 22px;
padding: 0 9px;
border-radius: var(--r-pill);
font-size: 11px;
letter-spacing: 0.5px;
width: max-content;
color: color-mix(in srgb, var(--scene) 75%, var(--hi));
border: 1px solid color-mix(in srgb, var(--scene) 45%, transparent);
background: color-mix(in srgb, var(--scene) 10%, transparent);
}
.studio-stages .done {
color: var(--tx-1);
}
+1 -1
View File
@@ -15,7 +15,7 @@ HEL-840:第二版布局与提示词工具
视觉与动效
采用原有颜色/字体/圆角/动效 token,新增批准的几何 token;共享页头样式抽到唯一来源。创作台外壳/舞台不改,结果条沿现有 grid 重排。小窗角标仅防换行和给工作流名省略,不改变外壳。双环源码未变。
模型列表/分工按内容高度在主容器滚动,窄窗不收缩丢行;素材媒体固定在比例容器内,默认 contain 不裁切;复用现有取景框,并按批准档位控制选择条与测试状态动效。采用原有颜色/字体/圆角/动效 token,新增批准的几何 token;共享页头样式抽到唯一来源。创作台外壳/舞台不改,结果条沿现有 grid 重排。小窗角标仅防换行和给工作流名省略,不改变外壳。双环源码未变。
| 宽度 | 工作室右栏 | 素材筛选 / 详情 | 模型详情 |
| --- | --- | --- | --- |
+2
View File
@@ -16,3 +16,5 @@ Additional verification:
- `python3 scripts/hel840/compare_reference.py --reference /path/to/approved-v2.html --output ./evidence/hel840/tokens.json` compares token values; it does not claim screenshot pixel equality.
The approved original HTML and PNGs must be supplied unchanged from the approved attachment. Keep originals, mapping and actual application captures separate. For cancellation/timeout screenshots the isolated fixture accelerates the total timeout to 1.5 seconds; the product uses 120 seconds. Complete recognized weighted expressions are preserved verbatim so their weights cannot move to another operand. Closing a request does not promise that a supplier stops charging.
Review packaging supports `--matrix-run finalized --states-run finalized` when the final full capture was recorded under that directory. This keeps a single source capture for the delivered matrix and states.
+3 -1
View File
@@ -118,6 +118,8 @@ try:
1
).click()
page.wait_for_timeout(150)
assert page.locator(".mlist").bounding_box()["height"] >= 212
assert page.locator(".mrow").first.bounding_box()["y"] >= 44
if label.startswith("04"):
page.locator("#desk-prompt").wait_for()
page.locator("#desk-prompt").fill(
@@ -137,7 +139,7 @@ try:
"height": height,
"page": label,
"dom": page.evaluate(
"""()=>{const rect=s=>{const n=document.querySelector(s);if(!n)return null;const r=n.getBoundingClientRect(),c=getComputedStyle(n);return {x:r.x,y:r.y,width:r.width,height:r.height,bottom:r.bottom,font:c.font,fontFamily:c.fontFamily,color:c.color,background:c.backgroundColor}};return {overflow:document.documentElement.scrollWidth>innerWidth,theme:document.documentElement.dataset.theme,main:rect('main'),resume:rect('.resume'),library:rect('.lib2'),filter:rect('.mg-side'),detail:rect('.lib-detail'),models:rect('.mdl'),modelDetail:rect('.mdl-detail'),band:rect('.ptband'),dock:rect('.studio-form'),media:rect('.stage-media .print'),generate:rect('.gen')}}"""
"""()=>{const rect=s=>{const n=document.querySelector(s);if(!n)return null;const r=n.getBoundingClientRect(),c=getComputedStyle(n);return {x:r.x,y:r.y,width:r.width,height:r.height,bottom:r.bottom,font:c.font,fontFamily:c.fontFamily,color:c.color,background:c.backgroundColor}};return {overflow:document.documentElement.scrollWidth>innerWidth,theme:document.documentElement.dataset.theme,main:rect('main'),resume:rect('.resume'),library:rect('.lib2'),filter:rect('.mg-side'),detail:rect('.lib-detail'),models:rect('.mdl'),modelList:rect('.mlist'),modelRow:rect('.mrow'),modelDetail:rect('.mdl-detail'),band:rect('.ptband'),dock:rect('.studio-form'),media:rect('.stage-media .print'),generate:rect('.gen')}}"""
),
}
)
+12 -10
View File
@@ -12,6 +12,8 @@ parser = argparse.ArgumentParser()
parser.add_argument("--approved", type=Path, required=True)
parser.add_argument("--evidence", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--matrix-run", default="delivery")
parser.add_argument("--states-run", default="default-verification")
args = parser.parse_args()
root = Path(__file__).resolve().parents[2]
evidence = args.evidence.resolve()
@@ -36,19 +38,19 @@ for name in [
"HEL-837-交互样本-v2.html",
]:
copy(args.approved / name, Path("originals") / name)
for p in (evidence / "delivery/matrix").rglob("*.png"):
copy(p, Path("product") / p.relative_to(evidence / "delivery/matrix"))
for p in (evidence / "default-verification/states").glob("*.png"):
for p in (evidence / args.matrix_run / "matrix").rglob("*.png"):
copy(p, Path("product") / p.relative_to(evidence / args.matrix_run / "matrix"))
for p in (evidence / args.states_run / "states").glob("*.png"):
copy(p, Path("states") / p.name)
for p in (evidence / "motion-hd/motion").glob("*.png"):
copy(p, Path("motion") / p.name)
for run, name in [("default-verification", "interactions"), ("motion-hd", "motion-hd")]:
for run, name in [(args.states_run, "interactions"), ("motion-hd", "motion-hd")]:
for i, p in enumerate((evidence / run / "recordings").glob("*.webm")):
copy(p, Path("recordings") / f"{name}-{i}.webm")
for run, name in [
("delivery", "geometry.json"),
("delivery", "token-comparison.json"),
("default-verification", "interaction-checks.json"),
(args.matrix_run, "geometry.json"),
(args.matrix_run, "token-comparison.json"),
(args.states_run, "interaction-checks.json"),
("motion-hd", "motion-checks.json"),
("restart", "restart-check.json"),
]:
@@ -67,7 +69,7 @@ for name in [
"motion-840.log",
]:
copy(root / name, Path("logs") / name)
for run in ["delivery", "default-verification", "motion-hd"]:
for run in dict.fromkeys([args.matrix_run, args.states_run, "motion-hd"]):
copy(evidence / run / "server.log", Path("logs") / f"{run}-server.log")
copy(
evidence / run / "fixture/provider.jsonl",
@@ -79,12 +81,12 @@ for p in (root / "scripts/hel840").glob("*"):
copy(p, Path("reproduction/scripts/hel840") / p.name)
copy(root / "docs/changelogs/2026-10-24-hel840-v2.md", Path("实现与自测说明.md"))
# Fake-only snapshots; never include daemon roster/state or installed configuration.
for p in (evidence / "default-verification/fixture/assets/uploads").glob("*"):
for p in (evidence / args.states_run / "fixture/assets/uploads").glob("*"):
if p.is_file():
copy(p, Path("reproduction/fixture/assets/uploads") / p.name)
for name in ["before-empty.db", "calliope_config.json"]:
copy(
evidence / "default-verification/fixture" / name,
evidence / args.states_run / "fixture" / name,
Path("reproduction/fixture") / name,
)
head = subprocess.check_output(
-1
View File
@@ -318,7 +318,6 @@ if not conn.execute("SELECT 1 FROM projects LIMIT 1").fetchone():
"name": "暮色",
"description": "隔离夹具",
"prompt": "夜景中的猫",
"negative_prompt": "",
},
]
},