@@ -1,235 +0,0 @@
<!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 16 px 48 px #000000 0 a ; --yc-node-shadow : 0 5 px 18 px #000000 09 ; --yc-line : #b6b6b6 ; --yc-radius : 14 px ; }
[ 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 16 px 48 px #0004 ; --yc-node-shadow : 0 5 px 18 px #0003 ; --yc-line : #626268 ; }
* { box-sizing : border-box ; } body { margin : 0 ; background : var ( - - yc - bg ) ; color : var ( - - yc - text ) ; font : 14 px / 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 : 2 px solid var ( - - yc - strong ) ; outline-offset : 3 px ; } button { border : 0 ; background : transparent ; }
. review-bar { min-height : 54 px ; padding : 9 px 22 px ; display : flex ; align-items : center ; justify-content : space-between ; gap : 15 px ; background : var ( - - yc - strong ) ; color : var ( - - yc - on - strong ) ; font-size : 12 px ; }
. review-label { display : flex ; align-items : center ; gap : 10 px ; flex-wrap : wrap ; } . review-label b { font-size : 12 px ; letter-spacing : .05 em ; } . review-label span { opacity : .76 ; }
. review-nav { display : flex ; align-items : center ; gap : 5 px ; flex-wrap : wrap ; } . review-nav select { max-width : 155 px ; border : 1 px solid #8887 ; border-radius : 7 px ; padding : 4 px 8 px ; background : transparent ; color : inherit ; } . review-nav option { color : var ( - - yc - text ) ; background : var ( - - yc - surface ) ; } . review-nav button { color : inherit ; padding : 4 px 8 px ; border-radius : 6 px ; }
. review-nav button : hover { background : #8884 ; } . app-shell { display : flex ; padding : 7 px ; gap : 7 px ; height : calc ( 100 dvh - 54 px ) ; min-height : 640 px ; }
. app-sidebar { width : 210 px ; flex-shrink : 0 ; background : var ( - - yc - panel ) ; border-radius : var ( - - yc - radius ) ; padding : 21 px 13 px 14 px ; display : flex ; flex-direction : column ; } . brand { display : flex ; align-items : center ; gap : 10 px ; padding : 0 10 px 26 px ; font-size : 19 px ; font-weight : 650 ; letter-spacing : 1 px ; } . brand-mark { font-size : 33 px ; line-height : 30 px ; letter-spacing : -3 px ; font-weight : 500 ; } . brand small { font-size : 10 px ; display : block ; font-weight : 400 ; color : var ( - - yc - muted ) ; letter-spacing : .06 em ; margin-top : -3 px ; }
. side-action { width : 100 % ; display : flex ; align-items : center ; gap : 10 px ; padding : 10 px 12 px ; text-align : left ; border-radius : 9 px ; color : var ( - - yc - muted ) ; margin-bottom : 4 px ; } . side-action . active { color : var ( - - yc - text ) ; background : var ( - - yc - surface ) ; box-shadow : 0 2 px 6 px #000000 04 ; } . side-action : hover { background : var ( - - yc - hover ) ; color : var ( - - yc - text ) ; } . side-glyph { width : 18 px ; text-align : center ; font-size : 17 px ; } . side-group { font-size : 11 px ; color : var ( - - yc - muted ) ; padding : 25 px 12 px 8 px ; } . side-foot { margin-top : auto ; padding : 15 px 10 px 0 ; display : flex ; align-items : center ; gap : 9 px ; } . avatar { display : grid ; place-items : center ; width : 29 px ; height : 29 px ; border-radius : 50 % ; background : var ( - - yc - hover ) ; font-size : 11 px ; font-weight : 600 ; } . side-foot small { display : block ; color : var ( - - yc - muted ) ; font-size : 10 px ; }
. 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 : 57 px ; flex-shrink : 0 ; display : flex ; justify-content : space-between ; align-items : center ; padding : 0 25 px ; gap : 10 px ; } . breadcrumb { display : flex ; align-items : center ; gap : 13 px ; font-size : 12 px ; color : var ( - - yc - muted ) ; } . breadcrumb strong { color : var ( - - yc - text ) ; font-weight : 500 ; } . top-actions { display : flex ; align-items : center ; gap : 8 px ; } . mini-btn { display : inline-flex ; justify-content : center ; align-items : center ; gap : 6 px ; padding : 6 px 10 px ; border-radius : 7 px ; font-size : 12 px ; } . mini-btn : hover { background : var ( - - yc - hover ) ; } . top-status { font-size : 11 px ; color : var ( - - yc - muted ) ; }
. content { flex : 1 ; min-height : 0 ; overflow : hidden ; position : relative ; } . home-page { overflow : auto ; height : 100 % ; padding : 58 px 30 px 30 px ; } . home-inner { width : min ( 780 px , 100 % ) ; margin : 0 auto ; } . eyebrow { color : var ( - - yc - muted ) ; font-size : 11 px ; letter-spacing : .14 em ; } h1 { font-size : 32 px ; font-weight : 550 ; letter-spacing : -1 px ; margin : 12 px 0 8 px ; } . home-subtitle { font-size : 14 px ; color : var ( - - yc - muted ) ; margin : 0 0 27 px ; } . scene-switch { display : flex ; gap : 7 px ; margin : 26 px 0 18 px ; flex-wrap : wrap ; } . scene-switch button { padding : 7 px 16 px ; border-radius : 22 px ; font-size : 13 px ; 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 : 1 px solid var ( - - yc - border ) ; border-radius : 19 px ; background : var ( - - yc - surface ) ; padding : 18 px ; box-shadow : 0 6 px 24 px #000000 06 ; } . home-composer textarea { width : 100 % ; height : 86 px ; resize : none ; border : 0 ; outline : none ; background : transparent ; font-size : 15 px ; line-height : 1.8 ; } . composer-tools { display : flex ; justify-content : space-between ; align-items : center ; gap : 12 px ; margin-top : 9 px ; } . composer-tool-left { display : flex ; align-items : center ; gap : 6 px ; flex-wrap : wrap ; } . soft-pill { padding : 4 px 9 px ; border-radius : 6 px ; background : var ( - - yc - panel ) ; color : var ( - - yc - muted ) ; font-size : 11 px ; } . primary { border-radius : 9 px ; background : var ( - - yc - strong ) ; color : var ( - - yc - on - strong ) ; padding : 9 px 15 px ; font-size : 13 px ; font-weight : 500 ; } . primary : hover { opacity : .88 ; } . secondary { padding : 8 px 13 px ; border : 1 px solid var ( - - yc - border ) ; background : var ( - - yc - surface ) ; border-radius : 8 px ; font-size : 12 px ; } . secondary : hover { background : var ( - - yc - hover ) ; } . text-button { padding : 4 px 2 px ; font-size : 12 px ; color : var ( - - yc - muted ) ; text-decoration : underline ; text-underline-offset : 4 px ; }
. home-examples { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 12 px ; margin : 36 px 0 ; } . example { text-align : left ; padding : 15 px 17 px ; min-height : 118 px ; border : 1 px solid var ( - - yc - border ) ; border-radius : 12 px ; background : var ( - - yc - panel ) ; } . example : hover { border-color : var ( - - yc - muted ) ; } . example span { font-size : 11 px ; color : var ( - - yc - muted ) ; } . example strong { display : block ; font-size : 13 px ; font-weight : 500 ; margin : 9 px 0 5 px ; } . example p { margin : 0 ; color : var ( - - yc - muted ) ; font-size : 11 px ; } . home-note { color : var ( - - yc - muted ) ; font-size : 11 px ; text-align : center ; }
. conversation-only { height : 100 % ; display : flex ; flex-direction : column ; max-width : 780 px ; margin : 0 auto ; padding : 0 20 px ; } . 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 : 25 px 3 px 18 px ; scrollbar-width : thin ; } . message { margin-bottom : 24 px ; } . message . user { display : flex ; justify-content : flex-end ; } . user-bubble { max-width : 87 % ; background : var ( - - yc - panel ) ; padding : 12 px 16 px ; border-radius : 13 px 13 px 3 px 13 px ; font-size : 13 px ; white-space : pre-wrap ; } . assistant-label { font-weight : 550 ; font-size : 12 px ; display : flex ; gap : 7 px ; align-items : center ; margin-bottom : 9 px ; } . assistant-dot { width : 20 px ; height : 20 px ; display : grid ; place-items : center ; border-radius : 6 px ; background : var ( - - yc - strong ) ; color : var ( - - yc - on - strong ) ; font-size : 15 px ; line-height : 1 ; } . assistant-text { font-size : 13 px ; margin : 0 0 13 px ; line-height : 1.85 ; } . facts { display : flex ; gap : 6 px ; flex-wrap : wrap ; margin : 12 px 0 16 px ; } . fact { font-size : 10 px ; border-radius : 5 px ; padding : 3 px 7 px ; background : var ( - - yc - panel ) ; color : var ( - - yc - muted ) ; } . fact :: before { content : "✓ " ; color : var ( - - yc - success ) ; }
. question-card , . plan-card , . quote-card , . receipt-card { border : 1 px solid var ( - - yc - border ) ; border-radius : 12 px ; padding : 18 px ; margin-top : 11 px ; background : var ( - - yc - surface ) ; } . card-overline { display : flex ; align-items : center ; justify-content : space-between ; font-size : 10 px ; color : var ( - - yc - muted ) ; margin-bottom : 12 px ; gap : 10 px ; } . step-dots { display : flex ; gap : 4 px ; } . step-dots i { width : 17 px ; height : 3 px ; background : var ( - - yc - border ) ; border-radius : 2 px ; } . step-dots i . current { background : var ( - - yc - strong ) ; } . question-title { font-size : 16 px ; line-height : 1.5 ; margin : 5 px 0 5 px ; font-weight : 550 ; } . question-help { color : var ( - - yc - muted ) ; font-size : 11 px ; margin : 0 0 14 px ; } . options { display : grid ; gap : 7 px ; } . option { display : flex ; text-align : left ; gap : 10 px ; align-items : flex-start ; width : 100 % ; border : 1 px solid var ( - - yc - border ) ; border-radius : 8 px ; padding : 10 px 12 px ; } . option [ aria-pressed = "true" ] { background : var ( - - yc - panel ) ; border-color : var ( - - yc - strong ) ; } . option-radio { flex-shrink : 0 ; margin-top : 4 px ; height : 12 px ; width : 12 px ; border-radius : 50 % ; border : 1 px solid var ( - - yc - muted ) ; } . option [ aria-pressed = "true" ] . option-radio { border : 4 px solid var ( - - yc - strong ) ; } . option strong { display : block ; font-size : 12 px ; font-weight : 500 ; } . option small { display : block ; color : var ( - - yc - muted ) ; font-size : 10 px ; margin-top : 2 px ; } . free-label { display : block ; font-size : 11 px ; color : var ( - - yc - muted ) ; margin : 14 px 0 6 px ; } . field { width : 100 % ; border : 1 px solid var ( - - yc - border ) ; border-radius : 8 px ; padding : 9 px 11 px ; background : var ( - - yc - surface ) ; font-size : 12 px ; resize : vertical ; min-height : 40 px ; } . card-footer { display : flex ; align-items : center ; justify-content : space-between ; gap : 8 px ; margin-top : 15 px ; } . card-footer . left , . card-footer . right { display : flex ; gap : 7 px ; align-items : center ; } . quiet { color : var ( - - yc - muted ) ; font-size : 10 px ; } . submitted { display : flex ; justify-content : space-between ; align-items : center ; gap : 8 px ; background : var ( - - yc - panel ) ; border-radius : 8 px ; padding : 11 px 13 px ; font-size : 12 px ; } . submitted span { color : var ( - - yc - success ) ; } . plan-card h3 { font-size : 20 px ; font-weight : 550 ; margin : 5 px 0 10 px ; } . plan-card . logline { font-size : 13 px ; line-height : 1.85 ; margin : 0 0 15 px ; } . plan-summary { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 10 px 18 px ; margin : 14 px 0 17 px ; font-size : 11 px ; } . plan-summary strong { display : block ; font-weight : 500 ; color : var ( - - yc - text ) ; font-size : 12 px ; } . plan-summary span { color : var ( - - yc - muted ) ; } . scope-note { padding : 10 px 12 px ; background : var ( - - yc - panel ) ; border-radius : 7 px ; font-size : 11 px ; line-height : 1.75 ; }
. composer-wrap { position : relative ; flex-shrink : 0 ; padding : 6 px 0 15 px ; background : var ( - - yc - surface ) ; } . chat-composer { border : 1 px solid var ( - - yc - border ) ; border-radius : 12 px ; padding : 10 px 12 px ; background : var ( - - yc - surface ) ; } . chat-composer textarea { width : 100 % ; border : 0 ; outline : none ; background : transparent ; height : 47 px ; resize : none ; font-size : 12 px ; padding : 0 ; } . chat-composer . composer-tools { margin : 1 px 0 0 ; } . chat-composer . primary { padding : 5 px 11 px ; font-size : 12 px ; } . progress-trigger { width : 100 % ; display : flex ; justify-content : space-between ; align-items : center ; gap : 10 px ; padding : 11 px 4 px ; text-align : left ; font-size : 11 px ; } . progress-trigger . progress-info { display : flex ; align-items : center ; gap : 7 px ; } . progress-track { height : 3 px ; width : 64 px ; background : var ( - - yc - border ) ; border-radius : 4 px ; overflow : hidden ; } . progress-track span { display : block ; height : 100 % ; background : var ( - - yc - strong ) ; border-radius : 4 px ; transition : width .25 s ; } . progress-count { font-variant-numeric : tabular-nums ; color : var ( - - yc - muted ) ; font-size : 10 px ; } . plan-popover { position : absolute ; z-index : 20 ; left : 0 ; right : 0 ; bottom : calc ( 100 % - 4 px ) ; border : 1 px solid var ( - - yc - border ) ; background : var ( - - yc - surface ) ; border-radius : 12 px ; padding : 17 px ; box-shadow : var ( - - yc - shadow ) ; } . plan-popover h3 { font-size : 13 px ; margin : 0 0 14 px ; 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 : 10 px ; align-items : flex-start ; padding : 8 px 0 ; font-size : 11 px ; } . plan-popover li strong { display : block ; font-size : 12 px ; font-weight : 500 ; } . plan-popover li small { color : var ( - - yc - muted ) ; font-size : 10 px ; } . plan-indicator { width : 20 px ; height : 20 px ; border-radius : 50 % ; background : var ( - - yc - panel ) ; color : var ( - - yc - muted ) ; display : grid ; place-items : center ; font-size : 10 px ; margin-top : 1 px ; } . 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 ) .65 px , transparent .65 px ) ; background-size : 19 px 19 px ; } . canvas-heading { position : absolute ; left : 22 px ; top : 17 px ; z-index : 3 ; max-width : calc ( 100 % - 44 px ) ; } . canvas-heading strong { font-size : 13 px ; font-weight : 550 ; } . canvas-heading span { display : block ; color : var ( - - yc - muted ) ; font-size : 10 px ; margin-top : 2 px ; } . canvas-scroll { overflow : auto ; height : 100 % ; padding-top : 65 px ; padding-bottom : 70 px ; scrollbar-width : thin ; } . canvas-scaled { width : 840 px ; height : 675 px ; position : relative ; transform-origin : top left ; } . canvas-size { position : relative ; margin : 0 22 px ; } . edges { position : absolute ; inset : 0 ; width : 840 px ; height : 675 px ; pointer-events : none ; overflow : visible ; } . edges path { stroke : var ( - - yc - line ) ; stroke-width : 1.4 ; fill : none ; } . canvas-node { position : absolute ; width : 210 px ; background : var ( - - yc - surface ) ; border : 1 px solid var ( - - yc - border ) ; border-radius : 10 px ; box-shadow : var ( - - yc - node - shadow ) ; } . canvas-node . selected { outline : 1.5 px solid var ( - - yc - strong ) ; outline-offset : 3 px ; } . node-header { display : flex ; align-items : center ; gap : 6 px ; padding : 9 px 10 px ; font-size : 10 px ; min-height : 35 px ; } . node-type { color : var ( - - yc - muted ) ; font-size : 11 px ; } . node-title { flex : 1 ; font-weight : 500 ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; } . node-edit { padding : 0 2 px ; font-size : 12 px ; color : var ( - - yc - muted ) ; } . node-edit : hover { color : var ( - - yc - text ) ; } . node-port { position : absolute ; border : 2 px solid var ( - - yc - surface ) ; background : var ( - - yc - line ) ; width : 8 px ; height : 8 px ; border-radius : 50 % ; top : 50 % ; left : -5 px ; } . node-port . out { right : -5 px ; left : auto ; } . node-text { padding : 0 12 px 13 px ; font-size : 11 px ; line-height : 1.85 ; } . node-text h4 { font-size : 14 px ; margin : 1 px 0 9 px ; font-weight : 550 ; } . node-foot { padding : 7 px 10 px ; display : flex ; justify-content : space-between ; align-items : center ; font-size : 9 px ; color : var ( - - yc - muted ) ; } . node-foot button { font-size : 9 px ; color : var ( - - yc - text ) ; } . node-empty { height : 127 px ; margin : 0 8 px ; border-radius : 5 px ; background : var ( - - yc - panel ) ; color : var ( - - yc - muted ) ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center ; font-size : 10 px ; gap : 5 px ; } . node-empty strong { font-size : 17 px ; font-weight : 400 ; color : var ( - - yc - line ) ; } . node-empty . running { background : linear-gradient ( 110 deg , var ( - - yc - panel ) , var ( - - yc - hover ) ) ; } . chat-pane { width : 440 px ; flex-shrink : 0 ; background : var ( - - yc - surface ) ; padding : 0 19 px ; border-left : 1 px solid var ( - - yc - border ) ; display : flex ; flex-direction : column ; } . chat-pane-header { display : flex ; justify-content : space-between ; align-items : center ; padding : 16 px 0 8 px ; font-size : 12 px ; flex-shrink : 0 ; } . chat-pane-header strong { font-weight : 550 ; } . chat-pane . chat-scroll { padding-top : 15 px ; } . chat-pane . question-card , . chat-pane . plan-card , . chat-pane . quote-card { padding : 14 px ; } . chat-pane . message { margin-bottom : 21 px ; } . canvas-tools { position : absolute ; bottom : 19 px ; left : 20 px ; display : flex ; gap : 4 px ; padding : 5 px ; border : 1 px solid var ( - - yc - border ) ; border-radius : 9 px ; background : var ( - - yc - surface ) ; z-index : 3 ; box-shadow : var ( - - yc - node - shadow ) ; } . canvas-tools button { padding : 5 px 8 px ; font-size : 11 px ; border-radius : 5 px ; } . canvas-tools button : hover { background : var ( - - yc - hover ) ; } . canvas-tools . divider { background : var ( - - yc - border ) ; width : 1 px ; margin : 3 px 5 px ; } . canvas-legend { position : absolute ; bottom : 21 px ; right : 19 px ; font-size : 10 px ; color : var ( - - yc - muted ) ; padding : 7 px 9 px ; background : var ( - - yc - surface ) ; border-radius : 6 px ; } . mobile-toggle { display : none ; }
. scene-art { height : 127 px ; margin : 0 8 px ; border-radius : 5 px ; 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 ( 100 deg , #526364 , #b7bc9d ) ; transform : skewY ( -6 deg ) ; box-shadow : -4 px 0 20 px #a4baa3 2 b ; } . scene-art :: after { content : "" ; position : absolute ; width : 43 px ; height : 67 px ; background : #141b1f ; left : 35 % ; bottom : -5 px ; border-radius : 42 % 42 % 0 0 ; box-shadow : 2 px 0 12 px #0b1418 ; } . scene-art . head { position : absolute ; z-index : 1 ; height : 20 px ; width : 18 px ; border-radius : 50 % ; left : calc ( 35 % + 12 px ) ; bottom : 57 px ; background : #131a1d ; } . scene-art . clock-face { position : absolute ; top : 18 % ; left : 14 % ; width : 21 px ; height : 21 px ; border : 1 px solid #a7b1a5 ; border-radius : 50 % ; opacity : .7 ; } . clock-face :: before { content : "" ; position : absolute ; width : 1 px ; height : 7 px ; background : #a7b1a5 ; left : 9 px ; top : 3 px ; transform : rotate ( 35 deg ) ; transform-origin : bottom ; } . scene-art . art-grain { position : absolute ; inset : 0 ; background : repeating-linear-gradient ( 0 deg , #ffffff 04 0 , #ffffff 04 1 px , transparent 1 px , transparent 3 px ) ; z-index : 2 ; pointer-events : none ; } . scene-art . art-label { position : absolute ; z-index : 3 ; left : 7 px ; bottom : 5 px ; font-size : 8 px ; color : #e6e6df ; background : #1118 ; padding : 1 px 5 px ; border-radius : 3 px ; } . scene-art . play { position : absolute ; z-index : 4 ; left : calc ( 50 % - 14 px ) ; top : calc ( 50 % - 14 px ) ; width : 28 px ; height : 28 px ; border-radius : 50 % ; background : #0007 ; color : #fff ; display : grid ; place-items : center ; font-size : 10 px ; } . scene-art . scene-2 { background : #353029 ; } . scene-art . scene-2 :: before { left : 19 % ; top : 48 % ; height : 25 % ; width : 60 % ; background : linear-gradient ( 10 deg , #5e5943 , #b5ae8d ) ; transform : rotate ( -8 deg ) ; } . scene-art . scene-2 :: after { left : 59 % ; width : 55 px ; height : 85 px ; bottom : -10 px ; transform : rotate ( -25 deg ) ; } . scene-art . scene-2 . head { display : none ; } . scene-art . scene-2 . clock-face { width : 32 px ; height : 22 px ; left : 38 % ; top : 35 % ; border-radius : 1 px ; transform : rotate ( -9 deg ) ; 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 : -3 px ; height : 68 px ; } . scene-art . scene-3 . head { left : calc ( 40 % + 12 px ) ; bottom : 61 px ; } . scene-art . scene-3 . clock-face { display : none ; }
. quote-title { display : flex ; justify-content : space-between ; align-items : flex-start ; margin-bottom : 14 px ; gap : 10 px ; } . quote-title strong { font-size : 15 px ; font-weight : 550 ; } . quote-title b { font-size : 20 px ; letter-spacing : -.5 px ; } . quote-title small { font-size : 10 px ; font-weight : 400 ; color : var ( - - yc - muted ) ; } . quote-row { display : flex ; justify-content : space-between ; gap : 10 px ; font-size : 11 px ; margin : 8 px 0 ; } . quote-row span { color : var ( - - yc - muted ) ; } . quote-total { padding-top : 12 px ; margin-top : 12 px ; border-top : 1 px solid var ( - - yc - border ) ; } . quote-note { font-size : 10 px ; color : var ( - - yc - muted ) ; margin : 11 px 0 ; line-height : 1.8 ; } . quote-check { display : flex ; align-items : flex-start ; gap : 7 px ; font-size : 11 px ; margin : 14 px 0 ; } . quote-check input { accent-color : var ( - - yc - strong ) ; margin-top : 4 px ; } . success-note { color : var ( - - yc - success ) ; font-size : 11 px ; } . warning { color : var ( - - yc - warn ) ; background : var ( - - yc - warn - bg ) ; padding : 11 px 12 px ; border-radius : 8 px ; font-size : 11 px ; margin : 10 px 0 ; } . result-grid { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 7 px ; margin : 12 px 0 ; } . result-grid button { min-width : 0 ; padding : 0 ; text-align : left ; } . result-grid . scene-art { margin : 0 ; height : 114 px ; } . result-grid small { display : block ; font-size : 9 px ; margin-top : 5 px ; color : var ( - - yc - muted ) ; } . receipt-card { font-size : 11 px ; } . receipt-line { display : flex ; justify-content : space-between ; align-items : center ; gap : 10 px ; margin : 8 px 0 ; } . receipt-line span { color : var ( - - yc - muted ) ; } . receipt-card . primary { font-size : 11 px ; } . chat-more { display : flex ; flex-wrap : wrap ; gap : 6 px ; margin-top : 10 px ; }
. dialog { width : min ( 820 px , calc ( 100 % - 32 px ) ) ; max-height : calc ( 100 dvh - 44 px ) ; border : 1 px solid var ( - - yc - border ) ; border-radius : 16 px ; padding : 0 ; background : var ( - - yc - surface ) ; color : var ( - - yc - text ) ; box-shadow : 0 30 px 120 px #0003 ; } . dialog :: backdrop { background : #1117 ; backdrop-filter : blur ( 3 px ) ; } . dialog-head { padding : 19 px 24 px ; display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; border-bottom : 1 px solid var ( - - yc - border ) ; } . dialog-head h2 { margin : 0 ; font-size : 15 px ; font-weight : 550 ; } . dialog-body { overflow : auto ; padding : 23 px 28 px ; max-height : calc ( 100 dvh - 190 px ) ; } . dialog-body h3 { font-size : 14 px ; font-weight : 550 ; margin : 23 px 0 9 px ; } . dialog-body h3 : first-child { margin-top : 0 ; } . dialog-body p { font-size : 12 px ; line-height : 1.9 ; color : var ( - - yc - muted ) ; } . dialog-foot { padding : 16 px 24 px ; display : flex ; justify-content : space-between ; gap : 12 px ; align-items : center ; border-top : 1 px solid var ( - - yc - border ) ; } . dialog-foot div { display : flex ; gap : 8 px ; } . detail-meta { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-bottom : 16 px ; } . detail-meta span { font-size : 10 px ; background : var ( - - yc - panel ) ; padding : 4 px 8 px ; border-radius : 5 px ; } . detail-assets { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 9 px ; } . detail-asset { background : var ( - - yc - panel ) ; border-radius : 8 px ; padding : 12 px ; font-size : 11 px ; } . detail-asset strong { display : block ; font-size : 12 px ; font-weight : 500 ; margin-bottom : 5 px ; } . detail-asset span { color : var ( - - yc - muted ) ; } . shot-table { width : 100 % ; border-collapse : collapse ; font-size : 11 px ; line-height : 1.8 ; } . shot-table th { text-align : left ; font-weight : 500 ; color : var ( - - yc - muted ) ; background : var ( - - yc - panel ) ; padding : 8 px ; } . shot-table td { padding : 10 px 8 px ; border-bottom : 1 px solid var ( - - yc - border ) ; vertical-align : top ; } . shot-table th : first-child { width : 65 px ; } . shot-table td b { display : block ; font-weight : 500 ; } . error-text { color : var ( - - yc - warn ) ; font-size : 11 px ; min-height : 17 px ; } . large-art { max-width : 270 px ; margin : auto ; } . large-art . scene-art { height : 420 px ; margin : 0 ; } . large-art . scene-art :: after { width : 85 px ; height : 170 px ; } . large-art . scene-art . head { width : 36 px ; height : 44 px ; bottom : 160 px ; left : calc ( 35 % + 24 px ) ; } . large-art . scene-art . art-label { font-size : 11 px ; padding : 4 px 8 px ; } . toast { position : fixed ; z-index : 100 ; bottom : 25 px ; left : 50 % ; transform : translateX ( -50 % ) ; background : var ( - - yc - strong ) ; color : var ( - - yc - on - strong ) ; padding : 11 px 17 px ; border-radius : 9 px ; font-size : 12 px ; max-width : 90 % ; box-shadow : var ( - - yc - shadow ) ; }
[ hidden ] { display : none !important ; } . sr-only { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 , 0 , 0 , 0 ) ; white-space : nowrap ; border : 0 ; }
@ media ( min-width : 1700px ) { . chat-pane { width : 470 px ; padding : 0 25 px ; } . home-page { padding-top : 85 px ; } }
@ media ( max-width : 1250px ) { . app-sidebar { width : 172 px ; } . chat-pane { width : 420 px ; padding : 0 16 px ; } . canvas-legend { display : none ; } . home-page { padding-top : 37 px ; } }
@ media ( max-width : 1040px ) { . app-sidebar { width : 66 px ; padding : 19 px 7 px 12 px ; } . brand { padding : 0 7 px 24 px ; } . brand-word , . side-action . nav-text , . side-group , . side-foot . account-text { display : none ; } . side-action { justify-content : center ; padding : 10 px ; } . side-foot { padding-left : 10 px ; } . chat-pane { width : 405 px ; } . app-topbar { padding : 0 18 px ; } }
@ media ( max-width : 820px ) { . review-bar { align-items : flex-start ; padding : 9 px 13 px ; gap : 7 px ; } . review-label { gap : 4 px ; flex-direction : column ; align-items : flex-start ; } . review-label span { font-size : 10 px ; } . review-nav { justify-content : flex-end ; } . review-nav label { display : none ; } . review-nav select { width : 123 px ; font-size : 11 px ; } . app-shell { height : calc ( 100 dvh - 62 px ) ; min-height : 590 px ; } . app-sidebar { display : none ; } . app-topbar { height : 50 px ; padding : 0 13 px ; } . top-status { display : none ; } . home-page { padding : 35 px 19 px 24 px ; } h1 { font-size : 27 px ; } . home-examples { gap : 8 px ; } . example { padding : 12 px ; min-height : 133 px ; } . canvas-layout . chat-pane { width : 100 % ; border : 0 ; padding : 0 20 px ; } . 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 18 px ; } . canvas-heading { top : 15 px ; } . dialog-body { padding : 19 px ; } . detail-assets { grid-template-columns : 1 fr ; } }
@ media ( max-width : 480px ) { . review-nav button { padding : 4 px 5 px ; font-size : 11 px ; } . review-label b { font-size : 10 px ; } . review-nav { gap : 1 px ; } . home-page { padding-top : 28 px ; } . home-examples { grid-template-columns : 1 fr ; margin-top : 22 px ; } . example { min-height : auto ; padding : 12 px 14 px ; } . example strong { margin : 3 px 0 ; } . home-subtitle { font-size : 12 px ; } . scene-switch { gap : 3 px ; margin-top : 19 px ; } . scene-switch button { padding : 6 px 12 px ; font-size : 12 px ; } . composer-tool-left { gap : 3 px ; } . home-composer { padding : 14 px ; } . home-composer textarea { height : 100 px ; font-size : 13 px ; } . home-note { display : none ; } . question-card , . plan-card { padding : 14 px ; } . plan-summary { gap : 8 px ; } . breadcrumb { gap : 8 px ; } . breadcrumb . optional { display : none ; } . card-footer { align-items : flex-start ; } . dialog-foot { align-items : flex-start ; padding : 14 px 18 px ; } . dialog-foot . quiet { max-width : 42 % ; } . dialog-foot div { flex-wrap : wrap ; justify-content : flex-end ; } . shot-table { font-size : 10 px ; } }
@ 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 => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } [ 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 >