Files
ttstd-infinite-canvas/docs/design/creative-agent-phase-one-prototype.html

236 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>影策 · 第一期创作 Agent 交互设计</title>
<style>
:root { color-scheme:light; --yc-bg:#f1f1f1; --yc-surface:#fff; --yc-panel:#f7f7f7; --yc-hover:#efefef; --yc-text:#202020; --yc-muted:#727272; --yc-border:#e5e5e5; --yc-strong:#181818; --yc-on-strong:#fff; --yc-success:#26754a; --yc-success-bg:#eef8f1; --yc-warn:#976017; --yc-warn-bg:#fff7e9; --yc-shadow:0 16px 48px #0000000a; --yc-node-shadow:0 5px 18px #00000009; --yc-line:#b6b6b6; --yc-radius:14px; }
[data-theme="dark"] { color-scheme:dark; --yc-bg:#111112; --yc-surface:#1c1c1e; --yc-panel:#242426; --yc-hover:#303032; --yc-text:#eeeeef; --yc-muted:#a0a0a4; --yc-border:#363639; --yc-strong:#f5f5f5; --yc-on-strong:#191919; --yc-success:#82c9a0; --yc-success-bg:#203129; --yc-warn:#e1b974; --yc-warn-bg:#362e20; --yc-shadow:0 16px 48px #0004; --yc-node-shadow:0 5px 18px #0003; --yc-line:#626268; }
* { box-sizing:border-box; } body { margin:0; background:var(--yc-bg); color:var(--yc-text); font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; }
button,input,textarea,select { font:inherit; } button { cursor:pointer; } button,input,textarea,select { color:inherit; } button:disabled { opacity:.38; cursor:not-allowed; } button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible { outline:2px solid var(--yc-strong); outline-offset:3px; } button { border:0; background:transparent; }
.review-bar { min-height:54px; padding:9px 22px; display:flex; align-items:center; justify-content:space-between; gap:15px; background:var(--yc-strong); color:var(--yc-on-strong); font-size:12px; }
.review-label { display:flex; align-items:center; gap:10px; flex-wrap:wrap; } .review-label b { font-size:12px; letter-spacing:.05em; } .review-label span { opacity:.76; }
.review-nav { display:flex; align-items:center; gap:5px; flex-wrap:wrap; } .review-nav select { max-width:155px; border:1px solid #8887; border-radius:7px; padding:4px 8px; background:transparent; color:inherit; } .review-nav option { color:var(--yc-text); background:var(--yc-surface); } .review-nav button { color:inherit; padding:4px 8px; border-radius:6px; }
.review-nav button:hover { background:#8884; } .app-shell { display:flex; padding:7px; gap:7px; height:calc(100dvh - 54px); min-height:640px; }
.app-sidebar { width:210px; flex-shrink:0; background:var(--yc-panel); border-radius:var(--yc-radius); padding:21px 13px 14px; display:flex; flex-direction:column; } .brand { display:flex; align-items:center; gap:10px; padding:0 10px 26px; font-size:19px; font-weight:650; letter-spacing:1px; } .brand-mark { font-size:33px; line-height:30px; letter-spacing:-3px; font-weight:500; } .brand small { font-size:10px; display:block; font-weight:400; color:var(--yc-muted); letter-spacing:.06em; margin-top:-3px; }
.side-action { width:100%; display:flex; align-items:center; gap:10px; padding:10px 12px; text-align:left; border-radius:9px; color:var(--yc-muted); margin-bottom:4px; } .side-action.active { color:var(--yc-text); background:var(--yc-surface); box-shadow:0 2px 6px #00000004; } .side-action:hover { background:var(--yc-hover); color:var(--yc-text); } .side-glyph { width:18px; text-align:center; font-size:17px; } .side-group { font-size:11px; color:var(--yc-muted); padding:25px 12px 8px; } .side-foot { margin-top:auto; padding:15px 10px 0; display:flex; align-items:center; gap:9px; } .avatar { display:grid; place-items:center; width:29px; height:29px; border-radius:50%; background:var(--yc-hover); font-size:11px; font-weight:600; } .side-foot small { display:block; color:var(--yc-muted); font-size:10px; }
.app-stage { position:relative; flex:1; min-width:0; display:flex; flex-direction:column; background:var(--yc-surface); border-radius:var(--yc-radius); overflow:hidden; box-shadow:var(--yc-shadow); } .app-topbar { height:57px; flex-shrink:0; display:flex; justify-content:space-between; align-items:center; padding:0 25px; gap:10px; } .breadcrumb { display:flex; align-items:center; gap:13px; font-size:12px; color:var(--yc-muted); } .breadcrumb strong { color:var(--yc-text); font-weight:500; } .top-actions { display:flex; align-items:center; gap:8px; } .mini-btn { display:inline-flex; justify-content:center; align-items:center; gap:6px; padding:6px 10px; border-radius:7px; font-size:12px; } .mini-btn:hover { background:var(--yc-hover); } .top-status { font-size:11px; color:var(--yc-muted); }
.content { flex:1; min-height:0; overflow:hidden; position:relative; } .home-page { overflow:auto; height:100%; padding:58px 30px 30px; } .home-inner { width:min(780px,100%); margin:0 auto; } .eyebrow { color:var(--yc-muted); font-size:11px; letter-spacing:.14em; } h1 { font-size:32px; font-weight:550; letter-spacing:-1px; margin:12px 0 8px; } .home-subtitle { font-size:14px; color:var(--yc-muted); margin:0 0 27px; } .scene-switch { display:flex; gap:7px; margin:26px 0 18px; flex-wrap:wrap; } .scene-switch button { padding:7px 16px; border-radius:22px; font-size:13px; color:var(--yc-muted); } .scene-switch button[aria-pressed="true"] { background:var(--yc-hover); color:var(--yc-text); font-weight:550; } .scene-switch button:hover { background:var(--yc-panel); }
.home-composer { border:1px solid var(--yc-border); border-radius:19px; background:var(--yc-surface); padding:18px; box-shadow:0 6px 24px #00000006; } .home-composer textarea { width:100%; height:86px; resize:none; border:0; outline:none; background:transparent; font-size:15px; line-height:1.8; } .composer-tools { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-top:9px; } .composer-tool-left { display:flex; align-items:center; gap:6px; flex-wrap:wrap; } .soft-pill { padding:4px 9px; border-radius:6px; background:var(--yc-panel); color:var(--yc-muted); font-size:11px; } .primary { border-radius:9px; background:var(--yc-strong); color:var(--yc-on-strong); padding:9px 15px; font-size:13px; font-weight:500; } .primary:hover { opacity:.88; } .secondary { padding:8px 13px; border:1px solid var(--yc-border); background:var(--yc-surface); border-radius:8px; font-size:12px; } .secondary:hover { background:var(--yc-hover); } .text-button { padding:4px 2px; font-size:12px; color:var(--yc-muted); text-decoration:underline; text-underline-offset:4px; }
.home-examples { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin:36px 0; } .example { text-align:left; padding:15px 17px; min-height:118px; border:1px solid var(--yc-border); border-radius:12px; background:var(--yc-panel); } .example:hover { border-color:var(--yc-muted); } .example span { font-size:11px; color:var(--yc-muted); } .example strong { display:block; font-size:13px; font-weight:500; margin:9px 0 5px; } .example p { margin:0; color:var(--yc-muted); font-size:11px; } .home-note { color:var(--yc-muted); font-size:11px; text-align:center; }
.conversation-only { height:100%; display:flex; flex-direction:column; max-width:780px; margin:0 auto; padding:0 20px; } .chat-area { display:flex; flex-direction:column; flex:1; min-height:0; position:relative; } .chat-scroll { overflow-y:auto; flex:1; min-height:0; padding:25px 3px 18px; scrollbar-width:thin; } .message { margin-bottom:24px; } .message.user { display:flex; justify-content:flex-end; } .user-bubble { max-width:87%; background:var(--yc-panel); padding:12px 16px; border-radius:13px 13px 3px 13px; font-size:13px; white-space:pre-wrap; } .assistant-label { font-weight:550; font-size:12px; display:flex; gap:7px; align-items:center; margin-bottom:9px; } .assistant-dot { width:20px; height:20px; display:grid; place-items:center; border-radius:6px; background:var(--yc-strong); color:var(--yc-on-strong); font-size:15px; line-height:1; } .assistant-text { font-size:13px; margin:0 0 13px; line-height:1.85; } .facts { display:flex; gap:6px; flex-wrap:wrap; margin:12px 0 16px; } .fact { font-size:10px; border-radius:5px; padding:3px 7px; background:var(--yc-panel); color:var(--yc-muted); } .fact::before { content:"✓ "; color:var(--yc-success); }
.question-card,.plan-card,.quote-card,.receipt-card { border:1px solid var(--yc-border); border-radius:12px; padding:18px; margin-top:11px; background:var(--yc-surface); } .card-overline { display:flex; align-items:center; justify-content:space-between; font-size:10px; color:var(--yc-muted); margin-bottom:12px; gap:10px; } .step-dots { display:flex; gap:4px; } .step-dots i { width:17px; height:3px; background:var(--yc-border); border-radius:2px; } .step-dots i.current { background:var(--yc-strong); } .question-title { font-size:16px; line-height:1.5; margin:5px 0 5px; font-weight:550; } .question-help { color:var(--yc-muted); font-size:11px; margin:0 0 14px; } .options { display:grid; gap:7px; } .option { display:flex; text-align:left; gap:10px; align-items:flex-start; width:100%; border:1px solid var(--yc-border); border-radius:8px; padding:10px 12px; } .option[aria-pressed="true"] { background:var(--yc-panel); border-color:var(--yc-strong); } .option-radio { flex-shrink:0; margin-top:4px; height:12px; width:12px; border-radius:50%; border:1px solid var(--yc-muted); } .option[aria-pressed="true"] .option-radio { border:4px solid var(--yc-strong); } .option strong { display:block; font-size:12px; font-weight:500; } .option small { display:block; color:var(--yc-muted); font-size:10px; margin-top:2px; } .free-label { display:block; font-size:11px; color:var(--yc-muted); margin:14px 0 6px; } .field { width:100%; border:1px solid var(--yc-border); border-radius:8px; padding:9px 11px; background:var(--yc-surface); font-size:12px; resize:vertical; min-height:40px; } .card-footer { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:15px; } .card-footer .left,.card-footer .right { display:flex; gap:7px; align-items:center; } .quiet { color:var(--yc-muted); font-size:10px; } .submitted { display:flex; justify-content:space-between; align-items:center; gap:8px; background:var(--yc-panel); border-radius:8px; padding:11px 13px; font-size:12px; } .submitted span { color:var(--yc-success); } .plan-card h3 { font-size:20px; font-weight:550; margin:5px 0 10px; } .plan-card .logline { font-size:13px; line-height:1.85; margin:0 0 15px; } .plan-summary { display:grid; grid-template-columns:1fr 1fr; gap:10px 18px; margin:14px 0 17px; font-size:11px; } .plan-summary strong { display:block; font-weight:500; color:var(--yc-text); font-size:12px; } .plan-summary span { color:var(--yc-muted); } .scope-note { padding:10px 12px; background:var(--yc-panel); border-radius:7px; font-size:11px; line-height:1.75; }
.composer-wrap { position:relative; flex-shrink:0; padding:6px 0 15px; background:var(--yc-surface); } .chat-composer { border:1px solid var(--yc-border); border-radius:12px; padding:10px 12px; background:var(--yc-surface); } .chat-composer textarea { width:100%; border:0; outline:none; background:transparent; height:47px; resize:none; font-size:12px; padding:0; } .chat-composer .composer-tools { margin:1px 0 0; } .chat-composer .primary { padding:5px 11px; font-size:12px; } .progress-trigger { width:100%; display:flex; justify-content:space-between; align-items:center; gap:10px; padding:11px 4px; text-align:left; font-size:11px; } .progress-trigger .progress-info { display:flex; align-items:center; gap:7px; } .progress-track { height:3px; width:64px; background:var(--yc-border); border-radius:4px; overflow:hidden; } .progress-track span { display:block; height:100%; background:var(--yc-strong); border-radius:4px; transition:width .25s; } .progress-count { font-variant-numeric:tabular-nums; color:var(--yc-muted); font-size:10px; } .plan-popover { position:absolute; z-index:20; left:0; right:0; bottom:calc(100% - 4px); border:1px solid var(--yc-border); background:var(--yc-surface); border-radius:12px; padding:17px; box-shadow:var(--yc-shadow); } .plan-popover h3 { font-size:13px; margin:0 0 14px; display:flex; justify-content:space-between; align-items:center; } .plan-popover ol { padding:0; margin:0; list-style:none; } .plan-popover li { display:flex; gap:10px; align-items:flex-start; padding:8px 0; font-size:11px; } .plan-popover li strong { display:block; font-size:12px; font-weight:500; } .plan-popover li small { color:var(--yc-muted); font-size:10px; } .plan-indicator { width:20px; height:20px; border-radius:50%; background:var(--yc-panel); color:var(--yc-muted); display:grid; place-items:center; font-size:10px; margin-top:1px; } .plan-indicator.done { color:var(--yc-success); background:var(--yc-success-bg); } .plan-indicator.active { background:var(--yc-strong); color:var(--yc-on-strong); }
.canvas-layout { display:flex; height:100%; } .canvas-pane { flex:1; min-width:0; position:relative; overflow:hidden; background-color:var(--yc-panel); background-image:radial-gradient(var(--yc-line) .65px,transparent .65px); background-size:19px 19px; } .canvas-heading { position:absolute; left:22px; top:17px; z-index:3; max-width:calc(100% - 44px); } .canvas-heading strong { font-size:13px; font-weight:550; } .canvas-heading span { display:block; color:var(--yc-muted); font-size:10px; margin-top:2px; } .canvas-scroll { overflow:auto; height:100%; padding-top:65px; padding-bottom:70px; scrollbar-width:thin; } .canvas-scaled { width:840px; height:675px; position:relative; transform-origin:top left; } .canvas-size { position:relative; margin:0 22px; } .edges { position:absolute; inset:0; width:840px; height:675px; pointer-events:none; overflow:visible; } .edges path { stroke:var(--yc-line); stroke-width:1.4; fill:none; } .canvas-node { position:absolute; width:210px; background:var(--yc-surface); border:1px solid var(--yc-border); border-radius:10px; box-shadow:var(--yc-node-shadow); } .canvas-node.selected { outline:1.5px solid var(--yc-strong); outline-offset:3px; } .node-header { display:flex; align-items:center; gap:6px; padding:9px 10px; font-size:10px; min-height:35px; } .node-type { color:var(--yc-muted); font-size:11px; } .node-title { flex:1; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .node-edit { padding:0 2px; font-size:12px; color:var(--yc-muted); } .node-edit:hover { color:var(--yc-text); } .node-port { position:absolute; border:2px solid var(--yc-surface); background:var(--yc-line); width:8px; height:8px; border-radius:50%; top:50%; left:-5px; } .node-port.out { right:-5px; left:auto; } .node-text { padding:0 12px 13px; font-size:11px; line-height:1.85; } .node-text h4 { font-size:14px; margin:1px 0 9px; font-weight:550; } .node-foot { padding:7px 10px; display:flex; justify-content:space-between; align-items:center; font-size:9px; color:var(--yc-muted); } .node-foot button { font-size:9px; color:var(--yc-text); } .node-empty { height:127px; margin:0 8px; border-radius:5px; background:var(--yc-panel); color:var(--yc-muted); display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:10px; gap:5px; } .node-empty strong { font-size:17px; font-weight:400; color:var(--yc-line); } .node-empty.running { background:linear-gradient(110deg,var(--yc-panel),var(--yc-hover)); } .chat-pane { width:440px; flex-shrink:0; background:var(--yc-surface); padding:0 19px; border-left:1px solid var(--yc-border); display:flex; flex-direction:column; } .chat-pane-header { display:flex; justify-content:space-between; align-items:center; padding:16px 0 8px; font-size:12px; flex-shrink:0; } .chat-pane-header strong { font-weight:550; } .chat-pane .chat-scroll { padding-top:15px; } .chat-pane .question-card,.chat-pane .plan-card,.chat-pane .quote-card { padding:14px; } .chat-pane .message { margin-bottom:21px; } .canvas-tools { position:absolute; bottom:19px; left:20px; display:flex; gap:4px; padding:5px; border:1px solid var(--yc-border); border-radius:9px; background:var(--yc-surface); z-index:3; box-shadow:var(--yc-node-shadow); } .canvas-tools button { padding:5px 8px; font-size:11px; border-radius:5px; } .canvas-tools button:hover { background:var(--yc-hover); } .canvas-tools .divider { background:var(--yc-border); width:1px; margin:3px 5px; } .canvas-legend { position:absolute; bottom:21px; right:19px; font-size:10px; color:var(--yc-muted); padding:7px 9px; background:var(--yc-surface); border-radius:6px; } .mobile-toggle { display:none; }
.scene-art { height:127px; margin:0 8px; border-radius:5px; overflow:hidden; background:#24292b; position:relative; color:#d8dbd4; } .scene-art::before { content:""; position:absolute; left:58%; top:8%; width:26%; height:85%; background:linear-gradient(100deg,#526364,#b7bc9d); transform:skewY(-6deg); box-shadow:-4px 0 20px #a4baa32b; } .scene-art::after { content:""; position:absolute; width:43px; height:67px; background:#141b1f; left:35%; bottom:-5px; border-radius:42% 42% 0 0; box-shadow:2px 0 12px #0b1418; } .scene-art .head { position:absolute; z-index:1; height:20px; width:18px; border-radius:50%; left:calc(35% + 12px); bottom:57px; background:#131a1d; } .scene-art .clock-face { position:absolute; top:18%; left:14%; width:21px; height:21px; border:1px solid #a7b1a5; border-radius:50%; opacity:.7; } .clock-face::before { content:""; position:absolute; width:1px; height:7px; background:#a7b1a5; left:9px; top:3px; transform:rotate(35deg); transform-origin:bottom; } .scene-art .art-grain { position:absolute; inset:0; background:repeating-linear-gradient(0deg,#ffffff04 0,#ffffff04 1px,transparent 1px,transparent 3px); z-index:2; pointer-events:none; } .scene-art .art-label { position:absolute; z-index:3; left:7px; bottom:5px; font-size:8px; color:#e6e6df; background:#1118; padding:1px 5px; border-radius:3px; } .scene-art .play { position:absolute; z-index:4; left:calc(50% - 14px); top:calc(50% - 14px); width:28px; height:28px; border-radius:50%; background:#0007; color:#fff; display:grid; place-items:center; font-size:10px; } .scene-art.scene-2 { background:#353029; } .scene-art.scene-2::before { left:19%; top:48%; height:25%; width:60%; background:linear-gradient(10deg,#5e5943,#b5ae8d); transform:rotate(-8deg); } .scene-art.scene-2::after { left:59%; width:55px; height:85px; bottom:-10px; transform:rotate(-25deg); } .scene-art.scene-2 .head { display:none; } .scene-art.scene-2 .clock-face { width:32px; height:22px; left:38%; top:35%; border-radius:1px; transform:rotate(-9deg); border-color:#ddd7b8; } .scene-art.scene-3 { background:#232b2d; } .scene-art.scene-3::before { left:37%; width:31%; background:#b4bbb0; } .scene-art.scene-3::after { left:40%; bottom:-3px; height:68px; } .scene-art.scene-3 .head { left:calc(40% + 12px); bottom:61px; } .scene-art.scene-3 .clock-face { display:none; }
.quote-title { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:14px; gap:10px; } .quote-title strong { font-size:15px; font-weight:550; } .quote-title b { font-size:20px; letter-spacing:-.5px; } .quote-title small { font-size:10px; font-weight:400; color:var(--yc-muted); } .quote-row { display:flex; justify-content:space-between; gap:10px; font-size:11px; margin:8px 0; } .quote-row span { color:var(--yc-muted); } .quote-total { padding-top:12px; margin-top:12px; border-top:1px solid var(--yc-border); } .quote-note { font-size:10px; color:var(--yc-muted); margin:11px 0; line-height:1.8; } .quote-check { display:flex; align-items:flex-start; gap:7px; font-size:11px; margin:14px 0; } .quote-check input { accent-color:var(--yc-strong); margin-top:4px; } .success-note { color:var(--yc-success); font-size:11px; } .warning { color:var(--yc-warn); background:var(--yc-warn-bg); padding:11px 12px; border-radius:8px; font-size:11px; margin:10px 0; } .result-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:7px; margin:12px 0; } .result-grid button { min-width:0; padding:0; text-align:left; } .result-grid .scene-art { margin:0; height:114px; } .result-grid small { display:block; font-size:9px; margin-top:5px; color:var(--yc-muted); } .receipt-card { font-size:11px; } .receipt-line { display:flex; justify-content:space-between; align-items:center; gap:10px; margin:8px 0; } .receipt-line span { color:var(--yc-muted); } .receipt-card .primary { font-size:11px; } .chat-more { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
.dialog { width:min(820px,calc(100% - 32px)); max-height:calc(100dvh - 44px); border:1px solid var(--yc-border); border-radius:16px; padding:0; background:var(--yc-surface); color:var(--yc-text); box-shadow:0 30px 120px #0003; } .dialog::backdrop { background:#1117; backdrop-filter:blur(3px); } .dialog-head { padding:19px 24px; display:flex; align-items:center; justify-content:space-between; gap:10px; border-bottom:1px solid var(--yc-border); } .dialog-head h2 { margin:0; font-size:15px; font-weight:550; } .dialog-body { overflow:auto; padding:23px 28px; max-height:calc(100dvh - 190px); } .dialog-body h3 { font-size:14px; font-weight:550; margin:23px 0 9px; } .dialog-body h3:first-child { margin-top:0; } .dialog-body p { font-size:12px; line-height:1.9; color:var(--yc-muted); } .dialog-foot { padding:16px 24px; display:flex; justify-content:space-between; gap:12px; align-items:center; border-top:1px solid var(--yc-border); } .dialog-foot div { display:flex; gap:8px; } .detail-meta { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; } .detail-meta span { font-size:10px; background:var(--yc-panel); padding:4px 8px; border-radius:5px; } .detail-assets { display:grid; grid-template-columns:repeat(3,1fr); gap:9px; } .detail-asset { background:var(--yc-panel); border-radius:8px; padding:12px; font-size:11px; } .detail-asset strong { display:block; font-size:12px; font-weight:500; margin-bottom:5px; } .detail-asset span { color:var(--yc-muted); } .shot-table { width:100%; border-collapse:collapse; font-size:11px; line-height:1.8; } .shot-table th { text-align:left; font-weight:500; color:var(--yc-muted); background:var(--yc-panel); padding:8px; } .shot-table td { padding:10px 8px; border-bottom:1px solid var(--yc-border); vertical-align:top; } .shot-table th:first-child { width:65px; } .shot-table td b { display:block; font-weight:500; } .error-text { color:var(--yc-warn); font-size:11px; min-height:17px; } .large-art { max-width:270px; margin:auto; } .large-art .scene-art { height:420px; margin:0; } .large-art .scene-art::after { width:85px; height:170px; } .large-art .scene-art .head { width:36px; height:44px; bottom:160px; left:calc(35% + 24px); } .large-art .scene-art .art-label { font-size:11px; padding:4px 8px; } .toast { position:fixed; z-index:100; bottom:25px; left:50%; transform:translateX(-50%); background:var(--yc-strong); color:var(--yc-on-strong); padding:11px 17px; border-radius:9px; font-size:12px; max-width:90%; box-shadow:var(--yc-shadow); }
[hidden] { display:none!important; } .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
@media(min-width:1700px) { .chat-pane { width:470px; padding:0 25px; } .home-page { padding-top:85px; } }
@media(max-width:1250px) { .app-sidebar { width:172px; } .chat-pane { width:420px; padding:0 16px; } .canvas-legend { display:none; } .home-page { padding-top:37px; } }
@media(max-width:1040px) { .app-sidebar { width:66px; padding:19px 7px 12px; } .brand { padding:0 7px 24px; } .brand-word,.side-action .nav-text,.side-group,.side-foot .account-text { display:none; } .side-action { justify-content:center; padding:10px; } .side-foot { padding-left:10px; } .chat-pane { width:405px; } .app-topbar { padding:0 18px; } }
@media(max-width:820px) { .review-bar { align-items:flex-start; padding:9px 13px; gap:7px; } .review-label { gap:4px; flex-direction:column; align-items:flex-start; } .review-label span { font-size:10px; } .review-nav { justify-content:flex-end; } .review-nav label { display:none; } .review-nav select { width:123px; font-size:11px; } .app-shell { height:calc(100dvh - 62px); min-height:590px; } .app-sidebar { display:none; } .app-topbar { height:50px; padding:0 13px; } .top-status { display:none; } .home-page { padding:35px 19px 24px; } h1 { font-size:27px; } .home-examples { gap:8px; } .example { padding:12px; min-height:133px; } .canvas-layout .chat-pane { width:100%; border:0; padding:0 20px; } .canvas-layout .canvas-pane { display:none; } .canvas-layout.show-canvas .canvas-pane { display:block; } .canvas-layout.show-canvas .chat-pane { display:none; } .mobile-toggle { display:inline-flex; } .conversation-only { padding:0 18px; } .canvas-heading { top:15px; } .dialog-body { padding:19px; } .detail-assets { grid-template-columns:1fr; } }
@media(max-width:480px) { .review-nav button { padding:4px 5px; font-size:11px; } .review-label b { font-size:10px; } .review-nav { gap:1px; } .home-page { padding-top:28px; } .home-examples { grid-template-columns:1fr; margin-top:22px; } .example { min-height:auto; padding:12px 14px; } .example strong { margin:3px 0; } .home-subtitle { font-size:12px; } .scene-switch { gap:3px; margin-top:19px; } .scene-switch button { padding:6px 12px; font-size:12px; } .composer-tool-left { gap:3px; } .home-composer { padding:14px; } .home-composer textarea { height:100px; font-size:13px; } .home-note { display:none; } .question-card,.plan-card { padding:14px; } .plan-summary { gap:8px; } .breadcrumb { gap:8px; } .breadcrumb .optional { display:none; } .card-footer { align-items:flex-start; } .dialog-foot { align-items:flex-start; padding:14px 18px; } .dialog-foot .quiet { max-width:42%; } .dialog-foot div { flex-wrap:wrap; justify-content:flex-end; } .shot-table { font-size:10px; } }
@media(prefers-reduced-motion:reduce) { *,*::before,*::after { transition:none!important; scroll-behavior:auto!important; } }
</style>
</head>
<body>
<div class="review-bar" aria-label="设计评审控制,与产品界面分开">
<div class="review-label"><b>交互原型 · 示例数据</b><span>不会调用模型或扣费</span></div>
<div class="review-nav"><label for="review-state">评审状态</label><select id="review-state" aria-label="跳转评审状态"><option value="home">从首页体验</option><option value="questions">需求问答</option><option value="proposal">方案待确认</option><option value="quote">画布与费用确认</option><option value="running">生成中</option><option value="complete">结果与局部调整</option><option value="failure">结果回写失败</option><option value="casual">普通聊天</option></select><button data-action="theme" aria-label="切换明暗主题">浅 / 深</button><button data-action="reset">重置</button></div>
</div>
<div id="app" class="app-shell"></div>
<dialog id="detail-dialog" class="dialog" aria-labelledby="dialog-title"></dialog>
<div id="toast" class="toast" role="status" hidden></div>
<script>
(() => {
'use strict';
const root = document.getElementById('app');
const dialog = document.getElementById('detail-dialog');
const defaultPrompt = '帮我做一集 15 秒、9:16 的悬疑推理短片。雨夜,一位年轻侦探发现一封写着自己名字的信。';
const questions = [
{id:'hook',title:'故事的悬念,你更喜欢哪种方向?',hint:'已知是悬疑推理短片,只补充影响故事走向的选择。',options:[['时间循环','那封信来自未来的自己,结尾留下循环线索。'],['身份谜局','寄信人与侦探身份交错,制造一次认知反转。'],['无声追踪','门外脚步声逐渐逼近,用环境制造压迫感。']]},
{id:'style',title:'画面希望呈现什么质感?',hint:'这会影响角色设定、镜头提示词和后续画面生成。',options:[['写实电影感','冷色雨夜、克制灯光、真实人物,突出悬疑氛围。'],['黑白侦探片','强烈明暗对比与胶片颗粒,经典黑色电影风格。'],['悬疑动画','风格化人物与场景,更适合系列化内容。']]},
{id:'ending',title:'这 15 秒,你希望如何收尾?',hint:'可以选一个方向,也可以在下面直接写你的想法。',options:[['留一个钩子','门外出现与侦探完全相同的身影,故事戛然而止。'],['揭示一次反转','侦探打开信,发现每句话都在描述眼前的动作。'],['交给你决定','根据已有设定,选择最适合短片节奏的结尾。']]}
];
const initial = () => ({stage:'home',scene:'通用',prompt:defaultPrompt,answers:['','',''],extras:['','',''],question:0,version:1,planOpen:false,quoteAgreed:false,textApproved:false,canvasCreated:false,mobileCanvas:false,phase:0,selectedNode:'',nodeNames:{},messages:[],editNote:'',retry:false,restored:false,zoom:0.8});
let s = initial(); let timer = null; let toastTimer = null; let previousFocus = null;
const esc = v => String(v ?? '').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const btn = (text,action,cls='secondary',extra='') => `<button class="${cls}" data-action="${action}" ${extra}>${text}</button>`;
const referenceNames = ['角色参考 · 年轻侦探','场景参考 · 雨夜办公室','道具参考 · 神秘信件'];
const nodeName = id => s.nodeNames[id] || (id==='script'?'故事与视频方案':id.startsWith('video')?'雨夜来信 · 15 秒短片':referenceNames[Number(id.split('-')[1])-1]);
const art = (index=1,video=false) => `<div class="scene-art scene-${index}" role="img" aria-label="${video?'15 秒短片':referenceNames[index-1]}的抽象示意,并非真实生成素材"><i class="head"></i><i class="clock-face"></i><i class="art-grain"></i><span class="art-label">${video?'短片封面':'参考图片'}示意</span>${video?'<span class="play" aria-hidden="true">▶</span>':''}</div>`;
const toast = message => { clearTimeout(toastTimer); const el=document.getElementById('toast'); el.textContent=message; el.hidden=false; toastTimer=setTimeout(()=>el.hidden=true,2800); };
const stopTimer = () => {if(timer)clearInterval(timer);timer=null;};
function progress() {
if(s.stage==='questions')return {done:0,active:0,label:'完善创作需求'};
if(s.stage==='proposal')return {done:1,active:1,label:'等待确认创意方案'};
if(s.stage==='quote')return {done:s.retry?4:3,active:s.retry?4:3,label:s.retry?'等待视频重做授权':'等待确认生成费用'};
if(s.stage==='running')return {done:s.phase<2?3:s.phase<4?4:5,active:s.phase<2?3:s.phase<4?4:5,label:s.retry?'正在重做 15 秒短片':s.phase<2?'正在生成 3 张参考图片':s.phase<4?'正在生成 1 段 15 秒短片':'正在回写结果'};
if(s.stage==='failure')return {done:5,active:5,label:'产物已就绪 · 等待恢复回写'};
if(s.stage==='complete')return {done:6,active:-1,label:'创作已完成'};
return {done:0,active:-1,label:''};
}
function shell() {
const onCanvas=s.canvasCreated;
return `<aside class="app-sidebar"><div class="brand"><span class="brand-mark">∞</span><span class="brand-word">影策<small>创作工作台</small></span></div>
<button class="side-action ${!onCanvas?'active':''}" data-action="home"><span class="side-glyph">⌂</span><span class="nav-text">创作首页</span></button>
<button class="side-action" data-action="short-drama"><span class="side-glyph">▤</span><span class="nav-text">短剧工作台</span></button>
<button class="side-action ${onCanvas?'active':''}" data-action="canvas"><span class="side-glyph">▦</span><span class="nav-text">画布</span></button>
<button class="side-action" data-action="assets"><span class="side-glyph">▧</span><span class="nav-text">素材库</span></button>
<div class="side-group">最近创作</div><button class="side-action" data-action="resume"><span class="side-glyph">◷</span><span class="nav-text">雨夜来信</span></button>
<div class="side-foot"><span class="avatar">YC</span><span class="account-text">我的工作空间<small>个人创作</small></span></div></aside>
<main class="app-stage"><header class="app-topbar"><div class="breadcrumb"><span class="optional">影策</span><span class="optional">/</span><strong>${onCanvas?'雨夜来信':s.stage==='home'?'创作首页':'新创作'}</strong>${s.stage!=='home'?'<span class="soft-pill">悬疑短片</span>':''}</div><div class="top-actions">${onCanvas?'<span class="top-status">同一会话 · 5 个节点</span>':s.stage!=='home'?'<span class="top-status">画布节点 0</span>':''}${onCanvas?btn(s.mobileCanvas?'返回会话':'查看画布','mobile-canvas','mini-btn mobile-toggle'):''}${btn('新创作','new','mini-btn')}</div></header><div class="content">${s.stage==='home'?home():onCanvas?canvas():`<section class="conversation-only">${chat()}</section>`}</div></main>`;
}
function home() {
return `<section class="home-page"><div class="home-inner"><div class="eyebrow">从一个想法,到一份作品</div><h1>今天,想创作点什么?</h1><p class="home-subtitle">告诉我你的想法,我们一起把它变成画面。</p>
<div class="scene-switch" aria-label="创作场景">${['通用','短剧','营销','电商'].map(x=>`<button data-action="scene" data-scene="${x}" aria-pressed="${s.scene===x}">${x}</button>`).join('')}</div>
<form id="home-form" class="home-composer"><label for="home-prompt" class="sr-only">描述你的创作需求</label><textarea id="home-prompt" placeholder="描述你想创作的内容,也可以添加参考素材…">${esc(s.prompt)}</textarea><div class="composer-tools"><div class="composer-tool-left">${btn('+ 素材','attach','mini-btn')}<span class="soft-pill">Agent 自动</span><span class="soft-pill">技能 · 短片创作</span></div><button type="submit" class="primary">开始创作 ↑</button></div></form>
<div class="home-examples"><button class="example" data-action="example" data-example="short"><span>短剧 / 影视</span><strong>15 秒,讲一个悬疑故事</strong><p>从创意、参考素材到完整短片</p></button><button class="example" data-action="example" data-example="marketing"><span>营销创作</span><strong>把一个卖点讲得更动人</strong><p>从受众、文案到视觉方案</p></button><button class="example" data-action="example" data-example="commerce"><span>电商设计</span><strong>让商品成为画面的主角</strong><p>从商品素材到场景图组</p></button></div><p class="home-note">本次完整演示:悬疑短片。营销、电商入口展示后续复用位置。</p></div></section>`;
}
function userMessage(text) {return `<div class="message user"><div class="user-bubble">${esc(text)}</div></div>`;}
function assistant(body) {return `<div class="message assistant"><div class="assistant-label"><span class="assistant-dot">∞</span>影策</div>${body}</div>`;}
function questionCard() {
const q=questions[s.question];
return `<div class="question-card"><div class="card-overline"><span>补充创作需求 · ${s.question+1} / ${questions.length}</span><span class="step-dots">${questions.map((_,i)=>`<i class="${i<=s.question?'current':''}"></i>`).join('')}</span></div><h3 class="question-title">${q.title}</h3><p class="question-help">${q.hint}</p><div class="options">${q.options.map(([title,desc])=>`<button class="option" data-action="answer" data-value="${esc(title)}" aria-pressed="${s.answers[s.question]===title}"><i class="option-radio"></i><span><strong>${title}</strong><small>${desc}</small></span></button>`).join('')}</div><label class="free-label" for="answer-extra">自定义或补充要求</label><textarea class="field" id="answer-extra" rows="2" placeholder="也可以直接写你的想法…">${esc(s.extras[s.question])}</textarea><div id="question-error" class="error-text" aria-live="polite"></div><div class="card-footer"><div class="left">${btn('上一题','prev-question','mini-btn',s.question===0?'disabled':'')}</div><div class="right">${btn(s.question===2?'提交需求':'下一题 →',s.question===2?'submit-answers':'next-question','primary')}</div></div></div>`;
}
function submitted() {return `<div class="submitted"><div><span>✓ 已提交</span> · ${esc(s.answers[0]||'自定义悬念')} / ${esc(s.answers[1]||'自定义风格')}</div>${btn('查看 / 修改','edit-answers','text-button')}</div>`;}
function titleText(){ return s.answers[0]==='身份谜局'?'另一位侦探':s.answers[0]==='无声追踪'?'门外的脚步':'雨夜来信'; }
function story(){ return s.answers[0]==='身份谜局'?'雨夜,侦探收到一封署着自己名字的信。信中照片上的人正坐在同一张书桌前。门开了,对方称这里是自己的办公室。':s.answers[0]==='无声追踪'?'雨夜,侦探收到一封写着自己名字的信。拆信时,门外的脚步与他的心跳同步。信纸上只有一句:不要让门外的人知道你在这里。':'雨夜,年轻侦探收到一封写着自己名字的信。信里警告:不要开门。敲门声响起,他透过门缝看见的,却是另一个自己。'; }
function proposalCard(compact=false) {
return `<div class="plan-card"><div class="card-overline"><span>创意方案 · v${s.version}</span><span>${s.canvasCreated?'已确认版本可回看':'等待你的确认'}</span></div><h3>《${titleText()}》</h3><p class="logline">${story()}</p>${compact?'':`<div class="plan-summary"><div><strong>1 集 · 15 秒 · 9:16</strong><span>一段视频,分为 3 个叙事节奏</span></div><div><strong>${esc(s.answers[1]||'写实电影感')}</strong><span>冷色雨夜 / 克制表演</span></div><div><strong>角色、场景与道具</strong><span>年轻侦探 / 旧办公室 / 神秘信件</span></div><div><strong>预期产物</strong><span>3 张参考图 + 1 段 15 秒短片</span></div></div>`}${s.editNote?`<p class="quiet">本版调整:${esc(s.editNote)}</p>`:''}<div class="scope-note">${s.canvasCreated?'这是已确认的创意方向。修改后会生成新版本,再确认受影响的节点。':'确认后创建 1 个方案文本节点、3 个参考图片节点、1 个视频节点及连线。图片和视频生成费用另行确认。'}</div><div class="card-footer"><div class="left">${btn('查看完整方案','view-plan','text-button')}</div><div class="right">${!s.canvasCreated?btn('修改','edit-plan','secondary')+btn('确认方案','confirm-plan','primary'):''}</div></div></div>`;
}
function quote() {
const retry=s.retry;
return `<div class="quote-card"><div class="card-overline"><span>${retry?'视频重做授权':'生成授权'}</span><span>示例报价 · 非真实计价</span></div><div class="quote-title"><strong>${retry?'仅重做 15 秒短片':'生成参考图与 15 秒短片'}</strong><div><b>${retry?'72':'84'}</b><small> 示例积分</small></div></div>${retry?'<div class="quote-row"><span>复用 3 张已完成参考图</span><b>0 新增积分</b></div>':'<div class="quote-row"><span>角色 / 场景 / 道具参考图 · 示例图像模型</span><b>3 × 4 = 12</b></div>'}<div class="quote-row"><span>15 秒短片 · 示例视频模型</span><b>1 × 72 = 72</b></div><div class="quote-row quote-total"><span>本次授权上限</span><b>${retry?'72':'84'} 示例积分</b></div><p class="quote-note">${retry?'仅重新生成视频;原视频保留为上一版,3 张参考图保持原样。':'先生成角色、场景、道具参考图,再按视频模型已验证的参考输入能力,生成一段 15 秒视频。'} 不包含自动重试、额外变体或后续批次。模型、数量或费用变化后需要重新确认。</p><label class="quote-check"><input type="checkbox" id="quote-agree" ${s.quoteAgreed?'checked':''}>同意上述生成范围和积分上限</label><div class="card-footer"><span class="quiet">未授权前不提交生成任务</span>${btn('确认并生成','approve','primary',s.quoteAgreed?'':'disabled')}</div></div>`;
}
function result() {
return `<p class="assistant-text">${s.restored?'15 秒短片已恢复到原视频节点。没有重新请求模型,也没有新增生成费用。':s.retry?'15 秒短片的新版本已完成,原视频和 3 张参考图保留。':'3 张参考图和 1 段 15 秒短片已完成,结果已回写到对应节点。'}</p><button data-action="preview" data-kind="video" data-shot="1" style="display:block;width:100%;padding:0">${art(1,true)}</button><p class="quiet">《${titleText()}》· 15 秒 · 9:16${s.retry?' · v2':''} · 视频封面示意</p><div class="result-grid">${[1,2,3].map(i=>`<button data-action="preview" data-kind="image" data-shot="${i}">${art(i,false)}<small>${referenceNames[i-1]}</small></button>`).join('')}</div><div class="receipt-card"><div class="receipt-line"><b>本轮结果</b><span>示例数据</span></div><div class="receipt-line"><span>画布结果</span><strong>已回写 4 项产物</strong></div><div class="receipt-line"><span>本次示例积分</span><strong>${s.retry?'72':'84'}</strong></div><div class="receipt-line"><span>视频交付</span><strong>1 段可预览的 15 秒短片</strong></div></div><div class="chat-more">${btn('只重做视频','redo','secondary')}${btn('预览短片','preview','secondary','data-kind="video" data-shot="1"')}${btn('继续调整节点','edit-node-prompt','secondary')}</div>`;
}
function chat() {
let body='';
if(s.stage==='casual') {
body=userMessage('悬疑短片和恐怖短片有什么区别?')+assistant('<p class="assistant-text">悬疑短片主要让人想知道“真相是什么”,靠线索、信息差和反转推动;恐怖短片更强调威胁感与恐惧体验。</p><p class="assistant-text">你这类 15 秒短片可以先给一个异常线索,再用最后一个镜头改变观众的理解。比如,一封来自未来的信。</p>');
} else {
body=userMessage(s.prompt)+assistant(`<p class="assistant-text">我已经记下这几个条件,接下来只补充会影响创作方向的信息。</p><div class="facts"><span class="fact">悬疑推理</span><span class="fact">1 集 · 15 秒</span><span class="fact">9:16 竖屏</span><span class="fact">雨夜 · 年轻侦探</span></div>${s.stage==='questions'?questionCard():submitted()}`);
if(s.stage!=='questions') body+=assistant(`<p class="assistant-text">根据你的选择,我整理了一个适合 15 秒表达的故事。先看看方向是否符合预期。</p>${proposalCard(s.canvasCreated)}`);
if(s.canvasCreated)body+=assistant('<p class="assistant-text">方案已确认,画布已创建 5 个节点及 6 条连线:方案文本、角色参考、场景参考、道具参考,以及 15 秒短片。节点中的提示词可以继续调整。</p>');
if(s.stage==='quote')body+=assistant(quote());
if(s.stage==='running')body+=assistant(`<p class="assistant-text">已获得${s.retry?'仅重做视频':'本批次'}授权。${s.phase<2?'正在生成角色、场景、道具参考图。':s.phase<4?'参考图已就绪,正在生成一段 15 秒短片。':'媒体已生成,正在回写画布结果。'}</p><div class="receipt-card"><div class="receipt-line"><span>当前步骤</span><strong>${s.phase<2?'生成参考图片':s.phase<4?'生成 15 秒短片':'回写结果'}</strong></div><div class="receipt-line"><span>后续操作</span><strong>${s.phase<2?'参考图片成功后才开始视频':s.phase<4?'视频完成后检查并回写':'绑定已有产物到原节点'}</strong></div><p class="quiet">评审原型正在按预设状态演示;没有真实任务。</p>${btn('暂停演示','pause','secondary')}</div>`);
if(s.stage==='failure')body+=assistant(`<p class="assistant-text">3 张参考图和 1 段 15 秒短片已经生成,但视频产物未能回写到画布。</p><div class="warning">回写中断 · 已有产物和任务记录保留。恢复时只绑定已有产物,不重新生成。</div><div class="receipt-card"><div class="receipt-line"><span>生成任务</span><strong>4 / 4 已完成</strong></div><div class="receipt-line"><span>画布回写</span><strong>3 / 4 已完成</strong></div><div class="receipt-line"><span>本次恢复新增生成费</span><strong>0 示例积分</strong></div>${btn('恢复结果回写','restore','primary')}</div>`);
if(s.stage==='complete')body+=assistant(result());
}
body+=s.messages.map(m=>m.role==='user'?userMessage(m.text):assistant(`<p class="assistant-text">${esc(m.text)}</p>`)).join('');
return `<div class="chat-area"><div class="chat-scroll" id="chat-scroll">${body}</div>${composer()}</div>`;
}
function composer() {
const p=progress(),hasTask=s.stage!=='casual'&&s.stage!=='home';
return `<div class="composer-wrap">${hasTask?`${s.planOpen?planPopover():''}<button class="progress-trigger" data-action="toggle-plan" aria-expanded="${s.planOpen}" aria-controls="execution-plan"><span class="progress-info"><span>${p.done===6?'✓':'◷'}</span>${p.label}</span><span class="progress-info"><span class="progress-track"><span style="width:${p.done/6*100}%"></span></span><span class="progress-count">${p.done}/6</span><span>${s.planOpen?'⌄':'⌃'}</span></span></button>`:''}<form class="chat-composer" id="chat-form"><label class="sr-only" for="chat-input">继续补充或调整创作需求</label><textarea id="chat-input" placeholder="${s.stage==='questions'?'也可以直接补充你的想法…':s.stage==='complete'?'继续调整,例如:把场景节点改名为「雨夜旧办公室」':'继续补充想法,或调整已有方案…'}"></textarea><div class="composer-tools"><div class="composer-tool-left">${btn('+','attach','mini-btn','aria-label="添加素材"')}<span class="soft-pill">Agent · 自动</span></div><button class="primary" type="submit" aria-label="发送消息">↑</button></div></form></div>`;
}
function planPopover() {
const p=progress(); const labels=[['完善创作需求','从上下文提取已有条件,只补问缺失项'],['确认创意方案','故事、设定、叙事节奏与创建范围'],['创建画布节点','5 个节点及 6 条连线'],['生成参考图片','角色、场景、道具各 1 张'],['生成 15 秒短片','按视频能力引用参考素材,直接生成一段视频'],['检查并回写结果','按任务回执和节点结果确认完成']];
return `<section id="execution-plan" class="plan-popover" aria-label="创作执行计划"><h3>创作计划 <span class="quiet">${p.done}/6</span>${btn('收起','toggle-plan','mini-btn')}</h3><ol>${labels.map(([name,desc],i)=>`<li><span class="plan-indicator ${i<p.done?'done':i===p.active?'active':''}">${i<p.done?'✓':i+1}</span><span><strong>${name}${i===p.active&&s.stage==='failure'?' · 等待恢复':''}</strong><small>${desc}</small></span></li>`).join('')}</ol></section>`;
}
function nodeStatus(index,video) {
if(s.stage==='complete')return 'done';
if(s.stage==='failure')return video?'failure':'done';
if(s.stage==='running') {
if(s.retry&&!video)return 'done';
if(!video)return s.phase>=2?'done':'running';
return s.phase>=4?'done':s.phase>=2?'running':'empty';
}
if(s.retry)return 'done';
return 'empty';
}
function mediaNode(i,video) {
const id=`${video?'video':'image'}-${i}`,status=nodeStatus(i,video); const name=nodeName(id); const y=video?234:23+(i-1)*211;
return `<article class="canvas-node ${s.selectedNode===id?'selected':''}" data-node="${id}" style="left:${video?567:278}px;top:${y}px;width:210px"><span class="node-port"></span>${video?'':'<span class="node-port out"></span>'}<header class="node-header"><span class="node-type">${video?'▷':'▧'}</span><span class="node-title">${esc(name)}</span><button class="node-edit" data-action="rename" data-id="${id}" aria-label="修改${esc(name)}名称">✎</button></header>${status==='done'?`<button data-action="preview" data-kind="${video?'video':'image'}" data-shot="${i}" style="display:block;padding:0;width:100%">${art(i,video)}</button>`:`<div class="node-empty ${status==='running'?'running':''}"><strong>${status==='failure'?'!':status==='running'?'◷':video?'▷':'▧'}</strong><span>${status==='failure'?'已有结果,等待恢复回写':status==='running'?'生成中 · 演示状态':'等待授权生成'}</span><span class="quiet">${video?'15 秒 · 9:16 · 完整短片':'9:16 · 参考图片'}</span></div>`}<footer class="node-foot"><span>${status==='done'?'✓ 示例产物已回写':status==='failure'?'回写待恢复':status==='running'?'任务已接受':'提示词已填充'}</span><button data-action="node-detail" data-id="${id}">查看参数</button></footer></article>`;
}
function canvas() {
return `<section class="canvas-layout ${s.mobileCanvas?'show-canvas':''}"><div class="canvas-pane"><div class="canvas-heading"><strong>《${titleText()}》</strong><span>方案 v${s.version} · 方案文本 → 角色 / 场景 / 道具参考 → 15 秒短片</span></div><div class="canvas-scroll" id="canvas-scroll"><div class="canvas-size" id="canvas-size"><div class="canvas-scaled" id="canvas-scaled"><svg class="edges" viewBox="0 0 840 675" role="img" aria-label="方案文本连接角色、场景、道具参考图片;三个已支持的参考输入汇入同一个 15 秒视频节点"><path d="M220 332 C247 332 248 118 278 118"/><path d="M220 332 C247 332 248 329 278 329"/><path d="M220 332 C247 332 248 540 278 540"/>${[118,329,540].map(y=>`<path d="M488 ${y} C525 ${y} 530 329 567 329"/>`).join('')}</svg><article class="canvas-node" style="left:20px;top:223px;width:200px"><span class="node-port out"></span><header class="node-header"><span class="node-type">T</span><span class="node-title">${esc(nodeName('script'))}</span><button class="node-edit" data-action="rename" data-id="script" aria-label="修改方案节点名称">✎</button></header><div class="node-text"><h4>${titleText()}</h4>${story()}<div class="facts"><span class="fact">15 秒</span><span class="fact">9:16</span></div><span class="quiet">片内节奏:信件出现 → 信中警告 → 门外的自己</span></div><footer class="node-foot"><span>方案 v${s.version}</span><button data-action="view-plan">查看方案</button></footer></article>${[1,2,3].map(i=>mediaNode(i,false)).join('')}${mediaNode(1,true)}</div></div></div><div class="canvas-tools">${btn('-','zoom-out','')}${btn(`${Math.round(s.zoom*100)}%`,'fit','', 'id="zoom-label"')}${btn('+','zoom-in','')}<span class="divider"></span>${btn('适应画布','fit','')}${btn('整理节点','arrange','')}</div><span class="canvas-legend">图片、视频均为示意</span></div><aside class="chat-pane"><div class="chat-pane-header"><strong>创作助手</strong><span class="quiet">接续首页会话</span></div>${chat()}</aside></section>`;
}
function resizeCanvas(fit=false) {
const pane=root.querySelector('.canvas-pane'); const scaled=document.getElementById('canvas-scaled'); const size=document.getElementById('canvas-size');
if(!pane||!scaled||!size)return;
if(fit&&pane.clientWidth>0)s.zoom=Math.min(1,Math.max(.35,(pane.clientWidth-46)/840));
scaled.style.transform=`scale(${s.zoom})`;size.style.width=`${840*s.zoom}px`;size.style.height=`${675*s.zoom}px`;
const zoomLabel=document.getElementById('zoom-label');if(zoomLabel)zoomLabel.textContent=`${Math.round(s.zoom*100)}%`;
}
function render({bottom=false,fit=false}={}) {
const oldScroll=document.getElementById('chat-scroll'); const y=oldScroll?.scrollTop||0;root.innerHTML=shell();document.getElementById('review-state').value=s.stage;resizeCanvas(fit);
const scroll=document.getElementById('chat-scroll');if(scroll)scroll.scrollTop=bottom?scroll.scrollHeight:y;
}
function showDialog(title,body,foot='') {
previousFocus=document.activeElement;dialog.innerHTML=`<div class="dialog-head"><h2 id="dialog-title">${title}</h2>${btn('关闭 ×','close-dialog','mini-btn','aria-label="关闭弹窗"')}</div><div class="dialog-body">${body}</div>${foot?`<div class="dialog-foot">${foot}</div>`:''}`;if(!dialog.open)dialog.showModal();
}
function closeDialog(){dialog.close();previousFocus?.focus?.();}
function planDetail() {
showDialog(`《${titleText()}》· 创意方案 v${s.version}`,`<div class="detail-meta"><span>1 段视频 · 15 秒</span><span>9:16 竖屏</span><span>${esc(s.answers[1]||'写实电影感')}</span><span>3 个片内叙事节奏</span></div><h3>故事大纲</h3><p>${story()} 通过一封信和一次开门动作,在 15 秒内完成“发现异常—危险逼近—身份反转”。</p><h3>角色、场景与道具参考</h3><div class="detail-assets"><div class="detail-asset"><strong>角色 · 年轻侦探</strong><span>约 28 岁,短发、深色风衣,神情克制。全片沿用同一人物参考。</span></div><div class="detail-asset"><strong>场景 · 旧办公室</strong><span>雨夜、窄窗、木桌、走廊门,冷色环境中保留一盏暖灯。</span></div><div class="detail-asset"><strong>道具 · 神秘信件</strong><span>米白信封、黑色手写字迹。文字与声音的精确表现需以模型实际能力为准。</span></div></div><h3>一段 15 秒视频内部的节奏</h3><p>以下时间段共同写入一次视频生成的创作描述,属于同一段视频内部的叙事安排。</p><table class="shot-table"><thead><tr><th>时间</th><th>画面与动作</th><th>镜头语言 / 声音意图</th></tr></thead><tbody><tr><td>00–05s</td><td><b>01 · 信件出现</b>侦探在雨夜办公室发现写着自己名字的信封。</td><td>中景缓慢推进。雨声与室内钟声。</td></tr><tr><td>05–10s</td><td><b>02 · 信中警告</b>手部拆信,读到警告,敲门声突然响起。</td><td>信纸特写,焦点切到门口。</td></tr><tr><td>10–15s</td><td><b>03 · 门外的自己</b>门缓缓打开,同样衣着的身影站在门外,停在揭示瞬间。</td><td>越肩视角,保持两人外观一致。</td></tr></tbody></table><h3>本轮产物与确认范围</h3><p>创建 1 个方案文本节点、3 个参考图片节点、1 个视频节点及 6 条连线。先生成角色、场景、道具参考图,再引用已确认方案与模型支持的参考输入,直接生成 1 段可预览的 15 秒短片。</p><div class="scope-note">本例以已验证支持 15 秒和所需参考输入的视频模型为前提;具体参考数量、声音及文字效果以所选模型能力为准。未满足时先调整方案和报价,不提交不支持的任务。查看方案不会创建或更改画布节点。</div>${s.extras.some(Boolean)?`<h3>你的补充要求</h3>${s.extras.filter(Boolean).map(x=>`<p>${esc(x)}</p>`).join('')}`:''}${s.editNote?`<h3>本版调整</h3><p>${esc(s.editNote)}</p>`:''}`,`<span class="quiet">${s.canvasCreated?'节点已按已确认版本创建':'画布节点:0 · 等待确认'}</span><div>${btn('修改方案','edit-plan','secondary')}${!s.canvasCreated?btn('确认方案,进入画布','confirm-plan','primary'):btn('返回画布','close-dialog','primary')}</div>`);
}
function editPlan() { showDialog('调整创意方案',`<p>说明你想修改的部分。更新后会展示新版本,重新确认受影响的范围。</p><label class="free-label" for="plan-edit">修改要求</label><textarea id="plan-edit" class="field" rows="5" placeholder="例如:第二镜头突出信纸细节,结尾只露出门外人的轮廓。">${esc(s.editNote)}</textarea><p class="quiet">原型会记录调整说明并生成新版本;不会调用模型改写文本。</p>`,`<span class="quiet">暂不更改画布</span><div>${btn('取消','close-dialog','secondary')}${btn('更新方案','save-plan','primary')}</div>`); }
function seed(stage) {
stopTimer();s=initial();s.stage=stage;
if(['proposal','quote','running','complete','failure'].includes(stage)){s.answers=['时间循环','写实电影感','留一个钩子'];s.canvasCreated=['quote','running','complete','failure'].includes(stage);}
if(stage==='running')s.phase=2;
render({bottom:stage!=='home'&&stage!=='questions',fit:true});
}
function startRun() {stopTimer();s.stage='running';s.phase=s.retry?2:0;s.quoteAgreed=false;render({bottom:true});timer=setInterval(()=>{if(s.stage!=='running'){stopTimer();return;}s.phase+=1;if(s.phase>=5){stopTimer();s.stage='complete';s.planOpen=false;}render({bottom:true});},1900);}
function answerSave() {const el=document.getElementById('answer-extra');if(el)s.extras[s.question]=el.value.trim();}
function answerValid() {answerSave();if(s.answers[s.question]||s.extras[s.question])return true;document.getElementById('question-error').textContent='选择一个方向,或输入你的想法后继续。';return false;}
function sendChat(text) {
if(!text.trim())return;s.messages.push({role:'user',text:text.trim()});
if(s.stage==='questions') {s.extras[s.question]=text.trim();s.messages.push({role:'assistant',text:'已记录到当前问题的补充要求。你可以直接继续下一题。'});}
else if(s.canvasCreated&&/改名|命名/.test(text)){s.nodeNames['image-2']='场景参考 · 雨夜旧办公室';s.selectedNode='image-2';s.messages.push({role:'assistant',text:'已将场景参考节点改名为“场景参考 · 雨夜旧办公室”。这是节点文字调整,没有触发重新生成。'});}
else if(s.stage==='proposal') {s.editNote=text.trim();s.version+=1;s.messages.push({role:'assistant',text:`已记录方案调整,更新为 v${s.version},请查看并确认。原型仅演示版本与确认状态。`});}
else {s.messages.push({role:'assistant',text:s.canvasCreated?'已收到。这个原型可以演示节点改名、查看参数、局部重做和回写恢复;自由对话理解将在接入现有模型工具循环后实现。':'悬疑通常用线索和反转建立吸引力。普通聊天不创建任务,所以不会出现执行计划。'});}
render({bottom:true});
}
document.addEventListener('click',event=>{
const button=event.target.closest('[data-action]');if(!button)return;const a=button.dataset.action;
if(a==='theme'){const dark=document.documentElement.dataset.theme==='dark';document.documentElement.dataset.theme=dark?'light':'dark';return;}
if(a==='reset'||a==='new'){if(dialog.open)closeDialog();seed('home');return;}
if(a==='home'){stopTimer();s.stage='home';s.canvasCreated=false;s.messages=[];render();return;}
if(a==='scene'){s.prompt=document.getElementById('home-prompt')?.value||s.prompt;s.scene=button.dataset.scene;render();return;}
if(a==='example'){if(button.dataset.example==='short'){s.prompt=defaultPrompt;s.scene='短剧';render();document.getElementById('home-prompt').focus();}else toast('本次聚焦短片完整链路;该场景沿用同一输入和问答框架。');return;}
if(a==='short-drama'){toast('产品中沿用已有短剧工作台;本原型聚焦首页创作链路。');return;}
if(a==='assets'||a==='attach'){showDialog('添加参考素材',`<p>产品中复用现有素材库与上传选择器。此评审原型不上传文件。</p><div class="scope-note">素材会进入同一会话上下文。模型已经知道的角色、场景、比例和类型,应优先从素材与用户描述中提取。</div>`,`<span class="quiet">本例暂无附件</span><div>${btn('返回创作','close-dialog','primary')}</div>`);return;}
if(a==='canvas'){if(!s.canvasCreated)toast('方案尚未确认,画布保持 0 个节点。');else{s.mobileCanvas=true;render({fit:true});}return;}
if(a==='resume'){if(s.stage==='home')seed('questions');else toast('当前就是这段创作会话。');return;}
if(a==='answer'){answerSave();s.answers[s.question]=button.dataset.value;render();return;}
if(a==='prev-question'){answerSave();s.question=Math.max(0,s.question-1);render();return;}
if(a==='next-question'){if(answerValid()){s.question=Math.min(2,s.question+1);render({bottom:true});}return;}
if(a==='submit-answers'){if(answerValid()){s.stage='proposal';s.question=0;render({bottom:true});}return;}
if(a==='edit-answers'){if(s.canvasCreated){showDialog('已提交的创作需求',questions.map((q,i)=>`<h3>${q.title}</h3><p>${esc(s.answers[i]||'自定义')} ${esc(s.extras[i])}</p>`),`<span class="quiet">调整后生成新方案版本</span><div>${btn('修改方案','edit-plan','primary')}</div>`);}else{s.stage='questions';s.question=0;s.version+=1;render({bottom:true});}return;}
if(a==='view-plan'){planDetail();return;}
if(a==='edit-plan'){editPlan();return;}
if(a==='save-plan'){const note=document.getElementById('plan-edit').value.trim();if(!note){toast('请填写调整要求。');return;}s.editNote=note;s.version+=1;closeDialog();if(s.canvasCreated){s.messages.push({role:'assistant',text:`已记录方案 v${s.version} 的调整。现有节点暂不改变;原型本期以首次方案确认及节点局部编辑为主要评审路径。`});}else s.stage='proposal';render({bottom:true});return;}
if(a==='confirm-plan'){if(dialog.open)closeDialog();s.canvasCreated=true;s.stage='quote';s.planOpen=false;s.quoteAgreed=false;render({bottom:true,fit:true});return;}
if(a==='approve-text'){s.textApproved=true;s.stage='questions';closeDialog();render({bottom:true});return;}
if(a==='approve'){if(s.quoteAgreed)startRun();return;}
if(a==='toggle-plan'){s.planOpen=!s.planOpen;render();return;}
if(a==='pause'){stopTimer();toast('演示已暂停,可在顶部选择状态继续评审。');return;}
if(a==='restore'){s.restored=true;s.stage='complete';s.planOpen=false;render({bottom:true});toast('已仅恢复结果回写,新增生成费用为 0。');return;}
if(a==='redo'){s.retry=true;s.restored=false;s.stage='quote';s.quoteAgreed=false;s.selectedNode='video-1';render({bottom:true});return;}
if(a==='edit-node-prompt'){document.getElementById('chat-input').value='把场景参考节点改名为“场景参考 · 雨夜旧办公室”';document.getElementById('chat-input').focus();return;}
if(a==='rename'){const id=button.dataset.id;showDialog('修改节点名称',`<label class="free-label" for="node-name">节点名称</label><input id="node-name" class="field" value="${esc(nodeName(id))}"><p class="quiet">仅修改画布节点名称,不触发生成。</p>`,`<span></span><div>${btn('保存名称','save-name','primary',`data-id="${id}"`)}</div>`);return;}
if(a==='save-name'){const name=document.getElementById('node-name').value.trim();if(!name){toast('节点名称不能为空。');return;}s.nodeNames[button.dataset.id]=name;s.selectedNode=button.dataset.id;closeDialog();render();toast('节点名称已更新。');return;}
if(a==='node-detail'){const id=button.dataset.id;const video=id.startsWith('video');showDialog('节点参数',`<div class="detail-meta"><span>${video?'视频节点':'图片节点'}</span><span>9:16</span><span>${video?'15 秒 · 1 段完整短片':'单张参考图'}</span></div><h3>提示词</h3><p>${video?`${story()} 一段 15 秒视频依次表现信件出现、信中警告、门外身影三个叙事节奏,使用已确认的角色、场景、道具参考。`:id.endsWith('1')?'年轻侦探,约 28 岁,短发、深色风衣、克制神情。写实电影质感,清晰体现人物外观,用作全片人物参考。':id.endsWith('2')?'雨夜旧办公室,窄窗、木桌、走廊门。冷色窗光与桌面暖灯对比,用作全片场景参考。':'米白信封、黑色手写字迹及展开的信纸。道具细节清晰,用作视频中神秘信件的外观参考。'}</p><h3>参考关系</h3><p>${video?'本例选择已验证支持 15 秒及三类参考输入的视频模型。三个参考节点成功后才提交一次视频任务。实际接入时按模型能力校验数量与输入角色。':'引用已确认的创意方案中对应设定,生成后供同一个视频节点使用。'}</p><div class="scope-note">参数调整与重新生成分开。产生新生成费用时,需要新的批次授权。</div>`,`<span class="quiet">当前展示既有语义节点</span><div>${btn('关闭','close-dialog','primary')}</div>`);return;}
if(a==='preview'){const shot=Number(button.dataset.shot||1),video=button.dataset.kind==='video';showDialog(video?`《${titleText()}》· 15 秒短片示意`:`${referenceNames[shot-1]} · 图片示意`,`<div class="large-art">${art(shot,video)}</div><p style="text-align:center">${video?'示例封面 · 没有真实视频文件,播放按钮表示产品中的完整短片预览入口。':'抽象参考图片示意,不是模型真实生成素材。'}</p>`,`<span class="quiet">${video?'15 秒 · 1 段视频':'1 张参考图'} · 9:16</span><div>${btn('返回','close-dialog','primary')}</div>`);return;}
if(a==='zoom-in'||a==='zoom-out'){s.zoom=Math.min(1.5,Math.max(.35,s.zoom+(a==='zoom-in'?.1:-.1)));resizeCanvas();return;}
if(a==='fit'){resizeCanvas(true);return;}
if(a==='arrange'){s.selectedNode='';resizeCanvas(true);toast('已按“方案 → 角色 / 场景 / 道具参考 → 15 秒短片”整理并适应画布。');return;}
if(a==='mobile-canvas'){s.mobileCanvas=!s.mobileCanvas;render({fit:true});return;}
if(a==='close-dialog'){closeDialog();return;}
});
document.addEventListener('submit',event=>{if(event.target.id==='home-form'){event.preventDefault();const input=document.getElementById('home-prompt').value.trim();if(!input){toast('先写下你的创作想法。');return;}s.prompt=input;if(!/15|悬疑|侦探|雨夜/.test(input)){toast('本原型完整演示悬疑短片,请使用下方短片示例。');return;}s.canvasCreated=false;if(!s.textApproved){showDialog('确认本次创作规划预算',`<p>理解需求、补充问答和生成创意方案也可能产生文本模型费用。</p><div class="receipt-card"><div class="receipt-line"><span>授权范围</span><strong>本次短片需求梳理与方案生成</strong></div><div class="receipt-line"><span>文本请求上限</span><strong>6 次示例请求</strong></div><div class="receipt-line"><span>总费用上限</span><strong>2 示例积分</strong></div></div><p>范围内继续补问与规划,达到请求次数或费用上限后再次确认。图片和视频任务稍后单独报价。</p><div class="scope-note">这里的模型次数和积分仅用于设计评审,不是真实价格;点击确认不会请求模型或扣费。</div>`,`<span class="quiet">示例预算 · 本次会话有效</span><div>${btn('取消','close-dialog','secondary')}${btn('确认,开始梳理需求','approve-text','primary')}</div>`);}else{s.stage='questions';render({bottom:true});}}if(event.target.id==='chat-form'){event.preventDefault();sendChat(document.getElementById('chat-input').value);}});
document.addEventListener('change',event=>{if(event.target.id==='review-state')seed(event.target.value);if(event.target.id==='quote-agree'){s.quoteAgreed=event.target.checked;const approve=root.querySelector('[data-action="approve"]');if(approve)approve.disabled=!s.quoteAgreed;}});
dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)closeDialog();}});
window.addEventListener('resize',()=>resizeCanvas(true));
document.documentElement.dataset.theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';
render();
})();
</script>
</body>
</html>