video-shuoshan/prototype/video-generation.html
zyc ffe92f7b15 Initial commit: 即梦视频生成平台
- web/: React + Vite + TypeScript 前端
- backend/: Django + DRF + SimpleJWT 后端
- prototype/: HTML 设计原型
- docs/: PRD 和设计评审文档
- test: 单元测试 + E2E 极限测试
2026-03-13 09:59:33 +08:00

894 lines
36 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.0">
<title>即梦 — AI 视频生成</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'page-bg': '#0a0a0f',
'bar-bg': '#16161e',
'bar-border': '#2a2a38',
'primary': '#00b8e6',
'txt-primary': '#ffffff',
'txt-secondary': '#8a8a9a',
'txt-disabled': '#4a4a5a',
'hover-bg': 'rgba(255,255,255,0.06)',
'dropdown-bg': '#1e1e2a',
'upload-bg': 'rgba(255,255,255,0.04)',
'upload-border': '#2a2a38',
'send-disabled': '#3a3a4a',
'send-active': '#00b8e6',
'sidebar-bg': '#0e0e14',
},
fontFamily: {
'sans': ['Noto Sans SC', 'system-ui', 'sans-serif'],
},
borderRadius: {
'bar': '20px',
'btn': '8px',
'thumb': '8px',
'drop': '12px',
}
}
}
}
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body { font-family: 'Noto Sans SC', system-ui, sans-serif; background: #0a0a0f; color: #fff; }
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a2a38; border-radius: 4px; }
/* Textarea */
.prompt-textarea {
background: transparent;
border: none;
outline: none;
resize: none;
color: #fff;
font-size: 14px;
line-height: 1.6;
width: 100%;
min-height: 24px;
max-height: 144px;
font-family: 'Noto Sans SC', system-ui, sans-serif;
}
.prompt-textarea::placeholder {
color: #5a5a6a;
}
/* Upload area dashed border */
.upload-trigger {
border: 1.5px dashed #3a3a48;
background: rgba(255,255,255,0.03);
transition: all 0.2s;
cursor: pointer;
}
.upload-trigger:hover {
border-color: #5a5a6a;
background: rgba(255,255,255,0.06);
}
/* Toolbar button base */
.toolbar-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 12px;
height: 32px;
border-radius: 8px;
font-size: 13px;
color: #8a8a9a;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
position: relative;
}
.toolbar-btn:hover {
background: rgba(255,255,255,0.06);
color: #b0b0c0;
}
.toolbar-btn.active-primary {
color: #00b8e6;
}
.toolbar-btn.active-primary:hover {
color: #33ccf0;
}
/* Dropdown menu */
.dropdown-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
background: #1e1e2a;
border: 1px solid #2a2a38;
border-radius: 12px;
padding: 6px;
min-width: 160px;
z-index: 100;
opacity: 0;
transform: translateY(8px);
pointer-events: none;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.dropdown-menu.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
color: #b0b0c0;
cursor: pointer;
transition: all 0.12s;
}
.dropdown-item:hover {
background: rgba(255,255,255,0.06);
color: #fff;
}
.dropdown-item.selected {
color: #00b8e6;
}
.dropdown-item .check-icon {
margin-left: auto;
opacity: 0;
}
.dropdown-item.selected .check-icon {
opacity: 1;
}
/* Send button */
.send-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.send-btn.disabled {
background: #3a3a4a;
cursor: not-allowed;
}
.send-btn.enabled {
background: #00b8e6;
box-shadow: 0 2px 12px rgba(0,184,230,0.3);
}
.send-btn.enabled:hover {
background: #00ccff;
box-shadow: 0 4px 20px rgba(0,184,230,0.5);
}
/* Thumbnail */
.thumb-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
background: #1a1a24;
flex-shrink: 0;
}
.thumb-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-close {
position: absolute;
top: 4px;
right: 4px;
width: 18px;
height: 18px;
background: rgba(0,0,0,0.7);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.thumb-item:hover .thumb-close { opacity: 1; }
.thumb-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2px 0;
text-align: center;
font-size: 10px;
color: #fff;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
}
/* Sidebar icon */
.sidebar-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 0;
color: #5a5a6a;
cursor: pointer;
transition: color 0.15s;
font-size: 11px;
}
.sidebar-item:hover, .sidebar-item.active {
color: #b0b0c0;
}
.sidebar-item.active {
color: #00b8e6;
}
/* Keyframe arrow animation */
@keyframes arrow-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.arrow-animate { animation: arrow-pulse 2s ease-in-out infinite; }
/* Toast */
.toast {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: #1e1e2a;
border: 1px solid #2a2a38;
color: #fff;
padding: 10px 24px;
border-radius: 10px;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 999;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Responsive mobile */
@media (max-width: 767px) {
.toolbar-label { display: none; }
.sidebar { display: none; }
}
</style>
</head>
<body>
<!-- Toast notification -->
<div id="toast" class="toast">已发送生成请求</div>
<!-- Layout: Sidebar + Main -->
<div class="flex h-full">
<!-- Sidebar -->
<aside class="sidebar w-[60px] h-full bg-[#0e0e14] border-r border-[#1a1a24] flex flex-col items-center py-4 flex-shrink-0 z-50">
<!-- Logo -->
<div class="mb-6 cursor-pointer">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<path d="M4 8L14 2L24 8V20L14 26L4 20V8Z" fill="#00b8e6" opacity="0.9"/>
<path d="M14 2L24 8L14 14L4 8L14 2Z" fill="#33ccf0"/>
<path d="M10 10L18 6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
</svg>
</div>
<div class="sidebar-item" title="灵感">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1"/></svg>
<span>灵感</span>
</div>
<div class="sidebar-item active" title="生成">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<span>生成</span>
</div>
<div class="sidebar-item" title="资产">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg>
<span>资产</span>
</div>
<div class="sidebar-item" title="画布">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
<span>画布</span>
</div>
<!-- Bottom items -->
<div class="mt-auto flex flex-col items-center gap-1">
<div class="sidebar-item" title="API">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
<span class="text-[10px]">API</span>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col relative overflow-hidden">
<!-- Empty content area (feed would go here) -->
<div class="flex-1 flex items-center justify-center">
<p class="text-txt-disabled text-sm opacity-40 select-none">在下方输入提示词,开始创作 AI 视频</p>
</div>
<!-- InputBar — Fixed at bottom -->
<div class="w-full px-4 pb-5 pt-2" id="inputbar-wrapper">
<div class="mx-auto" style="max-width: 900px;">
<div class="bg-bar-bg border border-bar-border rounded-bar overflow-hidden" id="inputbar">
<!-- Upper area: Upload + Prompt -->
<div class="p-4 pb-2 flex gap-3" id="input-area">
<!-- Upload Section — Universal Mode -->
<div id="upload-universal" class="flex-shrink-0 flex gap-2 items-start">
<!-- Empty state: upload trigger -->
<div id="upload-trigger-btn" class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span class="text-[11px] text-txt-disabled">参考内容</span>
</div>
<!-- Thumbnail container (hidden initially) -->
<div id="thumb-container" class="flex gap-2 items-start" style="display:none;"></div>
<!-- Add more button (hidden initially) -->
<div id="add-more-btn" class="upload-trigger rounded-btn w-[80px] h-[80px] flex items-center justify-center" style="display:none;" onclick="triggerUpload()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
</div>
<!-- Upload Section — Keyframe Mode (hidden) -->
<div id="upload-keyframe" class="flex-shrink-0 flex gap-3 items-center" style="display:none;">
<div class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload('first')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>
<span class="text-[11px] text-txt-disabled">首帧</span>
</div>
<div class="arrow-animate text-txt-disabled">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>
</div>
<div class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload('last')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span class="text-[11px] text-txt-disabled">尾帧</span>
</div>
</div>
<!-- Prompt Input -->
<div class="flex-1 py-1">
<textarea id="prompt-input" class="prompt-textarea" rows="1"
placeholder="上传1-5张参考图或视频输入文字自由组合图、文、音、视频多元素定义精彩互动。"
oninput="autoResize(this); updateSendBtn()"
></textarea>
</div>
</div>
<!-- Divider -->
<div class="h-px bg-bar-border mx-4 opacity-50"></div>
<!-- Toolbar -->
<div class="flex items-center px-3 py-2 gap-1" id="toolbar">
<!-- 视频生成 dropdown -->
<div class="relative">
<button class="toolbar-btn active-primary" onclick="toggleDropdown('gen-dropdown')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
<span class="toolbar-label">视频生成</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div id="gen-dropdown" class="dropdown-menu" style="min-width:150px;">
<div class="dropdown-item selected" onclick="selectDropdown('gen-dropdown','视频生成',this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
视频生成
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="dropdown-item" onclick="selectDropdown('gen-dropdown','图片生成',this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
图片生成
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
</div>
<!-- Model selector -->
<div class="relative">
<button class="toolbar-btn" onclick="toggleDropdown('model-dropdown')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
<span class="toolbar-label" id="model-label">Seedance 2.0</span>
</button>
<div id="model-dropdown" class="dropdown-menu" style="min-width:190px;">
<div class="dropdown-item selected" onclick="selectModel('Seedance 2.0', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/></svg>
Seedance 2.0
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="dropdown-item" onclick="selectModel('Seedance 2.0 Fast', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
Seedance 2.0 Fast
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
</div>
<!-- Mode selector -->
<div class="relative">
<button class="toolbar-btn" onclick="toggleDropdown('mode-dropdown')" id="mode-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="mode-icon"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
<span class="toolbar-label" id="mode-label">全能参考</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div id="mode-dropdown" class="dropdown-menu" style="min-width:150px;">
<div class="dropdown-item selected" id="mode-universal-item" onclick="switchMode('universal')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
全能参考
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="dropdown-item" id="mode-keyframe-item" onclick="switchMode('keyframe')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>
首尾帧
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
</div>
<!-- Aspect ratio selector -->
<div class="relative">
<button class="toolbar-btn" id="ratio-btn" onclick="toggleDropdown('ratio-dropdown')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<span class="toolbar-label" id="ratio-label">21:9</span>
</button>
<div id="ratio-dropdown" class="dropdown-menu" style="min-width:120px;">
<div class="dropdown-item" onclick="selectRatio('16:9',this)">16:9<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item" onclick="selectRatio('9:16',this)">9:16<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item" onclick="selectRatio('1:1',this)">1:1<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item selected" onclick="selectRatio('21:9',this)">21:9<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item" onclick="selectRatio('4:3',this)">4:3<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item" onclick="selectRatio('3:4',this)">3:4<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
</div>
</div>
<!-- Duration selector -->
<div class="relative">
<button class="toolbar-btn" id="duration-btn" onclick="toggleDropdown('duration-dropdown')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span class="toolbar-label" id="duration-label">15s</span>
</button>
<div id="duration-dropdown" class="dropdown-menu" style="min-width:100px;">
<div class="dropdown-item" onclick="selectDuration('5s',this)">5s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item" onclick="selectDuration('10s',this)">10s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="dropdown-item selected" onclick="selectDuration('15s',this)">15s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
</div>
</div>
<!-- @ button (universal mode only) -->
<button class="toolbar-btn" id="at-btn" onclick="insertAt()">
<span style="font-size:15px;font-weight:600;">@</span>
</button>
<!-- Spacer -->
<div class="flex-1"></div>
<!-- Credits indicator -->
<div class="flex items-center gap-1 text-txt-secondary text-xs mr-2 opacity-70">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span>30</span>
</div>
<!-- Send button -->
<button class="send-btn disabled" id="send-btn" onclick="handleSend()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
</button>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Hidden file input -->
<input type="file" id="file-input" accept="image/*,video/*" multiple style="display:none;" onchange="handleFiles(event)">
<script>
// ======== State ========
let state = {
mode: 'universal', // 'universal' | 'keyframe'
model: 'seedance_2.0',
ratio: '21:9',
duration: '15s',
prevRatio: '21:9',
prevDuration: '15s',
references: [], // [{id, file, previewUrl, label}]
firstFrame: null,
lastFrame: null,
uploadTarget: null, // 'first' | 'last' | null
};
let fileCounter = 0;
let openDropdownId = null;
// ======== Textarea auto-resize ========
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 144) + 'px';
}
// ======== Send button state ========
function updateSendBtn() {
const btn = document.getElementById('send-btn');
const hasText = document.getElementById('prompt-input').value.trim().length > 0;
const hasFiles = state.mode === 'universal' ? state.references.length > 0 : (state.firstFrame || state.lastFrame);
if (hasText || hasFiles) {
btn.classList.remove('disabled');
btn.classList.add('enabled');
} else {
btn.classList.remove('enabled');
btn.classList.add('disabled');
}
}
// ======== File upload ========
function triggerUpload(target) {
state.uploadTarget = target || null;
const input = document.getElementById('file-input');
input.multiple = state.mode === 'universal';
input.click();
}
function handleFiles(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
if (state.mode === 'universal') {
const remaining = 5 - state.references.length;
if (remaining <= 0) {
showToast('最多上传5张参考内容');
return;
}
const toAdd = files.slice(0, remaining);
toAdd.forEach(file => {
fileCounter++;
const previewUrl = URL.createObjectURL(file);
const type = file.type.startsWith('video') ? '视频' : '图片';
state.references.push({
id: 'ref_' + fileCounter,
file,
previewUrl,
label: type + fileCounter,
});
});
if (files.length > remaining) {
showToast('最多上传5张参考内容');
}
renderUniversalThumbs();
} else {
// Keyframe mode
const file = files[0];
const previewUrl = URL.createObjectURL(file);
if (state.uploadTarget === 'first') {
state.firstFrame = { file, previewUrl };
} else {
state.lastFrame = { file, previewUrl };
}
renderKeyframeThumbs();
}
updateSendBtn();
event.target.value = '';
}
function renderUniversalThumbs() {
const trigger = document.getElementById('upload-trigger-btn');
const container = document.getElementById('thumb-container');
const addMore = document.getElementById('add-more-btn');
if (state.references.length === 0) {
trigger.style.display = 'flex';
container.style.display = 'none';
addMore.style.display = 'none';
return;
}
trigger.style.display = 'none';
container.style.display = 'flex';
addMore.style.display = state.references.length < 5 ? 'flex' : 'none';
container.innerHTML = state.references.map((ref, i) => `
<div class="thumb-item" id="${ref.id}">
<img src="${ref.previewUrl}" alt="${ref.label}">
<div class="thumb-close" onclick="removeRef('${ref.id}')">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</div>
<div class="thumb-label">${ref.label}</div>
</div>
`).join('');
}
function removeRef(id) {
const ref = state.references.find(r => r.id === id);
if (ref) URL.revokeObjectURL(ref.previewUrl);
state.references = state.references.filter(r => r.id !== id);
renderUniversalThumbs();
updateSendBtn();
}
function renderKeyframeThumbs() {
const section = document.getElementById('upload-keyframe');
const frames = section.querySelectorAll('.upload-trigger, .thumb-item-kf');
// Re-render the keyframe section
section.innerHTML = '';
// First frame
if (state.firstFrame) {
const div = document.createElement('div');
div.className = 'thumb-item';
div.style.width = '80px';
div.style.height = '80px';
div.innerHTML = `<img src="${state.firstFrame.previewUrl}" alt="首帧"><div class="thumb-close" onclick="removeKeyframe('first')"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div><div class="thumb-label">首帧</div>`;
section.appendChild(div);
} else {
const div = document.createElement('div');
div.className = 'upload-trigger rounded-btn';
div.style.cssText = 'width:80px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;';
div.onclick = () => triggerUpload('first');
div.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg><span class="text-[11px] text-txt-disabled">首帧</span>`;
section.appendChild(div);
}
// Arrow
const arrow = document.createElement('div');
arrow.className = 'arrow-animate text-txt-disabled';
arrow.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>`;
section.appendChild(arrow);
// Last frame
if (state.lastFrame) {
const div = document.createElement('div');
div.className = 'thumb-item';
div.style.width = '80px';
div.style.height = '80px';
div.innerHTML = `<img src="${state.lastFrame.previewUrl}" alt="尾帧"><div class="thumb-close" onclick="removeKeyframe('last')"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div><div class="thumb-label">尾帧</div>`;
section.appendChild(div);
} else {
const div = document.createElement('div');
div.className = 'upload-trigger rounded-btn';
div.style.cssText = 'width:80px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;';
div.onclick = () => triggerUpload('last');
div.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span class="text-[11px] text-txt-disabled">尾帧</span>`;
section.appendChild(div);
}
}
function removeKeyframe(which) {
if (which === 'first') {
if (state.firstFrame) URL.revokeObjectURL(state.firstFrame.previewUrl);
state.firstFrame = null;
} else {
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
state.lastFrame = null;
}
renderKeyframeThumbs();
updateSendBtn();
}
// ======== Dropdowns ========
function toggleDropdown(id) {
const el = document.getElementById(id);
if (openDropdownId && openDropdownId !== id) {
document.getElementById(openDropdownId).classList.remove('open');
}
el.classList.toggle('open');
openDropdownId = el.classList.contains('open') ? id : null;
}
function closeAllDropdowns() {
document.querySelectorAll('.dropdown-menu.open').forEach(d => d.classList.remove('open'));
openDropdownId = null;
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.toolbar-btn') && !e.target.closest('.dropdown-menu')) {
closeAllDropdowns();
}
});
function selectDropdown(dropdownId, label, item) {
const menu = document.getElementById(dropdownId);
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
closeAllDropdowns();
}
function selectModel(name, item) {
document.getElementById('model-label').textContent = name;
const menu = document.getElementById('model-dropdown');
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
closeAllDropdowns();
}
function selectRatio(ratio, item) {
state.ratio = ratio;
state.prevRatio = ratio;
document.getElementById('ratio-label').textContent = ratio;
const menu = document.getElementById('ratio-dropdown');
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
closeAllDropdowns();
}
function selectDuration(duration, item) {
state.duration = duration;
if (state.mode === 'universal') state.prevDuration = duration;
document.getElementById('duration-label').textContent = duration;
const menu = document.getElementById('duration-dropdown');
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
closeAllDropdowns();
}
// ======== Mode switching ========
function switchMode(mode) {
if (state.mode === mode) { closeAllDropdowns(); return; }
state.mode = mode;
// Update mode dropdown selection
document.getElementById('mode-universal-item').classList.toggle('selected', mode === 'universal');
document.getElementById('mode-keyframe-item').classList.toggle('selected', mode === 'keyframe');
closeAllDropdowns();
const modeLabel = document.getElementById('mode-label');
const modeIcon = document.getElementById('mode-icon');
const uploadUniv = document.getElementById('upload-universal');
const uploadKf = document.getElementById('upload-keyframe');
const ratioBtn = document.getElementById('ratio-btn');
const ratioLabel = document.getElementById('ratio-label');
const durationLabel = document.getElementById('duration-label');
const atBtn = document.getElementById('at-btn');
const promptInput = document.getElementById('prompt-input');
if (mode === 'keyframe') {
modeLabel.textContent = '首尾帧';
modeIcon.innerHTML = '<path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/>';
uploadUniv.style.display = 'none';
uploadKf.style.display = 'flex';
renderKeyframeThumbs();
// Ratio: auto match, disabled
ratioLabel.textContent = '自动匹配';
ratioBtn.style.opacity = '0.5';
ratioBtn.style.pointerEvents = 'none';
// Duration: 5s
durationLabel.textContent = '5s';
state.duration = '5s';
document.querySelectorAll('#duration-dropdown .dropdown-item').forEach(i => {
i.classList.toggle('selected', i.textContent.trim() === '5s');
});
// Hide @ button
atBtn.style.display = 'none';
// Update placeholder
promptInput.placeholder = '输入描述,定义首帧到尾帧的运动过程';
// Clear universal references
state.references.forEach(r => URL.revokeObjectURL(r.previewUrl));
state.references = [];
renderUniversalThumbs();
} else {
modeLabel.textContent = '全能参考';
modeIcon.innerHTML = '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>';
uploadUniv.style.display = 'flex';
uploadKf.style.display = 'none';
// Restore ratio
ratioLabel.textContent = state.prevRatio;
state.ratio = state.prevRatio;
ratioBtn.style.opacity = '1';
ratioBtn.style.pointerEvents = 'auto';
// Restore duration
durationLabel.textContent = state.prevDuration;
state.duration = state.prevDuration;
document.querySelectorAll('#duration-dropdown .dropdown-item').forEach(i => {
i.classList.toggle('selected', i.textContent.trim() === state.prevDuration);
});
// Show @ button
atBtn.style.display = 'inline-flex';
// Update placeholder
promptInput.placeholder = '上传1-5张参考图或视频输入文字自由组合图、文、音、视频多元素定义精彩互动。';
// Clear keyframe
if (state.firstFrame) URL.revokeObjectURL(state.firstFrame.previewUrl);
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
state.firstFrame = null;
state.lastFrame = null;
}
updateSendBtn();
}
// ======== @ Insert ========
function insertAt() {
const input = document.getElementById('prompt-input');
const start = input.selectionStart;
const end = input.selectionEnd;
const text = input.value;
input.value = text.substring(0, start) + '@' + text.substring(end);
input.selectionStart = input.selectionEnd = start + 1;
input.focus();
updateSendBtn();
}
// ======== Send ========
function handleSend() {
const btn = document.getElementById('send-btn');
if (btn.classList.contains('disabled')) return;
showToast('已发送生成请求');
}
// ======== Toast ========
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
// ======== Keyboard shortcut ========
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
handleSend();
}
});
// ======== Drag and drop ========
const inputbar = document.getElementById('inputbar');
inputbar.addEventListener('dragover', (e) => {
e.preventDefault();
inputbar.style.borderColor = '#00b8e6';
});
inputbar.addEventListener('dragleave', () => {
inputbar.style.borderColor = '#2a2a38';
});
inputbar.addEventListener('drop', (e) => {
e.preventDefault();
inputbar.style.borderColor = '#2a2a38';
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/') || f.type.startsWith('video/'));
if (!files.length) return;
// Simulate file input
const dt = new DataTransfer();
files.forEach(f => dt.items.add(f));
const input = document.getElementById('file-input');
input.files = dt.files;
input.dispatchEvent(new Event('change'));
});
// ======== Auto-focus ========
window.addEventListener('load', () => {
document.getElementById('prompt-input').focus();
});
</script>
</body>
</html>