Lance / index.html
akhaliq's picture
akhaliq HF Staff
Enlarge input & example media previews, add bottom-row preview wrappers, and switch examples fold to a light theme
7aef049
Raw
History Blame Contribute Delete
115 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lance 3.0: Unified Multimodal Modeling</title>
<!-- SEO Meta Tags -->
<meta name="description" content="Lance is a state-of-the-art unified multimodal model by multi-task synergy, capable of video and image generation, editing, and video/image understanding.">
<meta name="keywords" content="multimodal, video generation, image generation, video understanding, text-to-video, image editing, AI model">
<!-- Premium Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Obsidian Dark Glassmorphic Design System */
:root {
--bg-base: #070a13;
--bg-obsidian: #0b0f19;
--glass-bg: rgba(11, 15, 25, 0.42);
--glass-bg-hover: rgba(22, 32, 53, 0.55);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-border-focus: rgba(249, 115, 22, 0.4);
--text-primary: #f8fafc;
--text-secondary: #e2e8f0;
--text-muted: #94a3b8;
--accent-orange: #f97316;
--accent-pink: #ec4899;
--accent-gradient: linear-gradient(135deg, #f97316 0%, #ec4899 100%);
--accent-gradient-hover: linear-gradient(135deg, #ea580c 0%, #db2777 100%);
--shadow-premium: 0 16px 48px -15px rgba(0, 0, 0, 0.7);
--shadow-glow: 0 0 25px rgba(249, 115, 22, 0.25);
--font-outfit: 'Outfit', sans-serif;
--font-inter: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--transition-smooth: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
--transition-fast: all 0.15s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg-base);
color: var(--text-primary);
font-family: var(--font-inter);
min-height: 100vh;
overflow-x: hidden;
line-height: 1.5;
}
/* --- IMMERSIVE FULL-SCREEN BACKGROUND CANVAS --- */
.viewport-bg-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1;
pointer-events: none;
overflow: hidden;
}
.fs-bg-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1), transform 15s ease;
}
.fs-bg-element.active {
opacity: 1;
transform: scale(1.02);
}
.fs-bg-video {
pointer-events: auto;
}
/* Ambient glowing overlay protecting text readability */
.fs-bg-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(7, 10, 19, 0.35) 0%, rgba(7, 10, 19, 0.2) 60%, rgba(7, 10, 19, 0.8) 100%);
z-index: 2;
pointer-events: none;
}
/* --- FOLD 1: FLOATING CINEMATIC VIEWPORT LAYER --- */
.cinematic-viewport {
height: 100vh;
width: 100%;
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 30px 40px;
overflow: hidden;
}
@media (max-width: 768px) {
.cinematic-viewport {
padding: 20px;
}
}
/* Top Bar Header */
header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.brand-link {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.brand-logo {
height: 30px;
width: auto;
object-fit: contain;
}
.brand-divider {
width: 1px;
height: 16px;
background: rgba(255, 255, 255, 0.3);
}
.brand-name {
font-family: var(--font-outfit);
font-size: 19px;
font-weight: 700;
color: #fff;
letter-spacing: -0.5px;
}
/* Center Nav Links Pill */
.nav-links-pill {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 100px;
padding: 4px 8px;
display: flex;
gap: 6px;
}
@media (max-width: 768px) {
.nav-links-pill {
display: none;
}
}
.nav-link-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.75);
font-family: var(--font-outfit);
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
border-radius: 100px;
cursor: pointer;
transition: var(--transition-fast);
text-decoration: none;
}
.nav-link-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.05);
}
.lang-toggle {
display: flex;
align-items: center;
gap: 12px;
}
.lang-btn-active {
background: #fff;
color: #070a13;
border: none;
font-family: var(--font-outfit);
font-size: 11px;
font-weight: 700;
padding: 6px 12px;
border-radius: 100px;
cursor: pointer;
}
.lang-btn-inactive {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.6);
font-family: var(--font-outfit);
font-size: 11px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: var(--transition-fast);
}
.lang-btn-inactive:hover {
color: #fff;
}
/* Immersive Center Prompt Console CSS */
.viewport-center-trigger {
position: absolute;
top: 48%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 15;
text-align: center;
width: 90%;
max-width: 680px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
/* --- Output Mode: compact re-prompt bar at bottom-left --- */
.viewport-center-trigger.output-mode {
top: auto;
bottom: 24px;
left: 24px;
transform: none;
width: calc(100% - 48px);
max-width: 560px;
gap: 0px;
}
.viewport-center-trigger.output-mode .center-prompt-card {
border-radius: 14px;
padding: 8px 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
background: rgba(10, 15, 30, 0.55);
}
.viewport-center-trigger.output-mode .center-prompt-header {
display: none;
}
.viewport-center-trigger.output-mode .center-prompt-body textarea.prompt-textarea {
height: 24px;
font-size: 13px;
}
.viewport-center-trigger.output-mode .center-prompt-footer {
border-top: none;
padding-top: 4px;
margin-top: 0;
}
.viewport-center-trigger.output-mode .center-options-btn {
padding: 6px 12px;
font-size: 12px;
}
.viewport-center-trigger.output-mode .center-generate-btn {
padding: 6px 16px;
font-size: 12px;
}
.viewport-center-trigger.output-mode .quick-examples-container {
display: none;
}
/* Hide branding when output is showing */
.viewport-bottom.output-hidden {
opacity: 0;
pointer-events: none;
transform: translateY(30px);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.viewport-bottom {
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.center-prompt-card {
width: 100%;
background: rgba(10, 15, 30, 0.28);
border: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-radius: 16px;
padding: 12px 14px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
gap: 8px;
transition: border-color var(--transition-smooth), box-shadow var(--transition-smooth);
}
.center-prompt-card:focus-within {
border-color: rgba(249, 115, 22, 0.4);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 15px rgba(249, 115, 22, 0.15);
}
.center-prompt-header {
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 4px;
}
.active-task-pill {
background: rgba(249, 115, 22, 0.15);
border: 1px solid rgba(249, 115, 22, 0.3);
color: #fdba74;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 4px 10px;
border-radius: 6px;
font-family: var(--font-outfit);
}
.center-prompt-body {
width: 100%;
}
.center-prompt-body textarea.prompt-textarea {
width: 100%;
height: 44px;
background: transparent;
border: none;
outline: none;
resize: none;
color: #fff;
font-size: 14px;
line-height: 1.6;
font-family: var(--font-inter);
padding: 4px;
}
.center-prompt-body textarea.prompt-textarea::placeholder {
color: rgba(255, 255, 255, 0.35);
}
.center-prompt-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 8px;
margin-top: 2px;
}
.center-options-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
padding: 8px 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
font-family: var(--font-outfit);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition-fast);
}
.center-options-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
.center-generate-btn {
background: linear-gradient(135deg, #f97316 0%, #f43f5e 100%);
border: none;
color: #fff;
padding: 8px 20px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
font-family: var(--font-outfit);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(244, 63, 94, 0.3);
transition: var(--transition-smooth);
}
.center-generate-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(244, 63, 94, 0.45);
filter: brightness(1.15);
}
/* Quick Example Prompt Pills CSS */
.quick-examples-container {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 0 8px;
animation: fadeIn 0.8s ease-in-out;
}
.quick-examples-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
font-family: var(--font-outfit);
white-space: nowrap;
}
.quick-examples-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.quick-example-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: rgba(255, 255, 255, 0.65);
padding: 4px 12px 4px 6px;
border-radius: 100px;
font-size: 12px;
font-family: var(--font-inter);
cursor: pointer;
transition: var(--transition-fast);
max-width: 260px;
white-space: nowrap;
overflow: hidden;
}
.quick-example-pill:hover {
background: rgba(249, 115, 22, 0.12);
border-color: rgba(249, 115, 22, 0.3);
color: #fdba74;
transform: translateY(-1px);
}
.quick-example-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #000;
flex-shrink: 0;
}
.quick-example-thumb-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(249, 115, 22, 0.1);
border: 1px solid rgba(249, 115, 22, 0.2);
color: var(--accent-orange);
font-size: 9px;
flex-shrink: 0;
}
.quick-example-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* Bottom Section elements */
.viewport-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
padding-bottom: 10px;
gap: 40px;
position: relative;
z-index: 15;
}
@media (max-width: 900px) {
.viewport-bottom {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
}
/* Bottom Left Caption */
.bottom-caption {
max-width: 580px;
text-align: left;
}
.caption-title {
font-family: var(--font-outfit);
font-size: clamp(36px, 5.5vw, 68px);
font-weight: 800;
color: #fff;
line-height: 1.05;
margin-bottom: 12px;
letter-spacing: -2px;
}
.caption-desc {
font-size: 13.5px;
color: rgba(255, 255, 255, 0.85);
line-height: 1.55;
font-family: var(--font-inter);
}
/* Bottom Right Actions */
.bottom-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.action-pill {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #fff;
padding: 10px 20px;
border-radius: 100px;
font-family: var(--font-outfit);
font-weight: 600;
font-size: 13px;
text-decoration: none;
cursor: pointer;
transition: var(--transition-fast);
display: inline-flex;
align-items: center;
gap: 6px;
}
.action-pill:hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.action-pill.primary {
background: #fff;
color: #070a13;
border-color: #fff;
}
.action-pill.primary:hover {
background: rgba(255, 255, 255, 0.9);
color: #000;
}
.circle-action-btn {
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition-fast);
}
.circle-action-btn:hover {
background: rgba(255, 255, 255, 0.18);
transform: scale(1.05);
}
.circle-action-btn i {
width: 16px;
height: 16px;
}
/* --- SLIDE-OUT MODEL CONTROLS SIDEBAR --- */
.controls-sidebar {
position: fixed;
top: 90px;
left: -460px; /* Hidden by default */
width: 420px;
max-height: calc(100vh - 140px);
overflow-y: auto;
z-index: 100;
background: rgba(10, 15, 30, 0.28);
border: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border-radius: 24px;
padding: 24px;
box-shadow: var(--shadow-premium);
transition: left 0.5s cubic-bezier(0.16, 1, 0.3, 1);
scrollbar-width: none;
}
.controls-sidebar::-webkit-scrollbar {
display: none;
}
.controls-sidebar.open {
left: 30px; /* Slide in overlay! */
}
/* Sidebar Title and Close */
.panel-title {
font-family: var(--font-outfit);
font-size: 17px;
font-weight: 700;
margin-bottom: 18px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 10px;
}
.panel-title-left {
display: flex;
align-items: center;
gap: 8px;
}
.panel-title i {
color: var(--accent-orange);
width: 18px;
height: 18px;
}
.close-sidebar-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.close-sidebar-btn:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.close-sidebar-btn i {
width: 16px;
height: 16px;
}
/* --- Controls Form Elements --- */
.form-group {
margin-bottom: 18px;
}
.form-label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.prompt-textarea {
width: 100%;
min-height: 110px;
background: rgba(15, 23, 42, 0.45);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 14px;
color: var(--text-primary);
font-family: var(--font-inter);
font-size: 14px;
line-height: 1.5;
resize: vertical;
outline: none;
transition: var(--transition-fast);
}
.prompt-textarea:focus {
border-color: var(--glass-border-focus);
box-shadow: 0 0 15px rgba(249, 115, 22, 0.15);
}
.prompt-textarea::placeholder {
color: rgba(255, 255, 255, 0.35);
}
/* Selection Dropdowns Grid */
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: 12px;
margin-bottom: 18px;
}
.select-wrapper {
position: relative;
}
.select-control {
width: 100%;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 8px 12px;
color: var(--text-primary);
font-family: var(--font-outfit);
font-size: 13px;
font-weight: 600;
outline: none;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
transition: var(--transition-fast);
}
.select-control:focus {
border-color: var(--glass-border-focus);
}
.select-wrapper::after {
content: '▼';
font-size: 8px;
color: var(--text-secondary);
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
/* Drag and Drop File Input */
.upload-zone {
width: 100%;
border: 1.5px dashed rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 24px 16px;
text-align: center;
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: var(--transition-smooth);
display: none;
}
.upload-zone.dragover {
border-color: var(--accent-orange);
background: rgba(249, 115, 22, 0.05);
}
.upload-zone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.upload-icon {
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--glass-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: var(--transition-fast);
}
.upload-title {
font-family: var(--font-outfit);
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.upload-desc {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
}
.file-input {
display: none;
}
/* File Upload Preview Card */
.preview-zone {
display: none;
width: 100%;
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 12px;
background: rgba(15, 23, 42, 0.35);
flex-direction: column;
gap: 10px;
}
.preview-media-container {
width: 100%;
height: 200px;
border-radius: 10px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.preview-media-container img,
.preview-media-container video {
width: 100%;
height: 100%;
object-fit: contain;
}
.preview-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.preview-info {
flex: 1;
min-width: 0;
}
.preview-name {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preview-size {
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
margin-top: 1px;
}
.clear-file-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 6px;
border-radius: 50%;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.clear-file-btn:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/* Collapsible Accordion (Advanced parameters) */
.accordion {
margin-top: 18px;
border: 1px solid var(--glass-border);
border-radius: 14px;
background: rgba(15, 23, 42, 0.2);
overflow: hidden;
transition: var(--transition-smooth);
}
.accordion-header {
width: 100%;
padding: 10px 16px;
background: transparent;
border: none;
color: var(--text-secondary);
font-family: var(--font-outfit);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.accordion-header:hover {
color: var(--text-primary);
}
.accordion-header i {
width: 14px;
height: 14px;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.accordion.open .accordion-header i {
transform: rotate(180deg);
}
.accordion-content {
padding: 0 16px;
max-height: 0;
overflow: hidden;
transition: max-height 0.35s cubic-bezier(0.16, 1, 0.3, 1), padding 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.accordion.open .accordion-content {
padding: 8px 16px 16px;
max-height: 400px;
}
.number-control {
width: 100%;
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 8px 10px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
}
.slider-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.slider-control {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
outline: none;
-webkit-appearance: none;
accent-color: var(--accent-orange);
cursor: pointer;
}
.slider-val {
min-width: 28px;
text-align: right;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
color: var(--accent-orange);
}
/* Generate Primary Trigger button */
.gen-btn {
width: 100%;
margin-top: 18px;
padding: 12px 24px;
background: var(--accent-gradient);
border: none;
border-radius: 14px;
color: #0f172a;
font-family: var(--font-outfit);
font-size: 15px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: var(--shadow-glow);
transition: var(--transition-smooth);
}
.gen-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 0 35px rgba(249, 115, 22, 0.35);
background: var(--accent-gradient-hover);
}
.gen-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
/* --- FLOATING STATUS & TERMINAL OVERLAYS (RIGHT SIDE) --- */
.floating-right-container {
position: fixed;
top: 90px;
right: 30px;
z-index: 100;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 20px;
pointer-events: none; /* Let events fall through to elements below */
}
@media (max-width: 1024px) {
.floating-right-container {
right: 20px;
left: 20px;
align-items: stretch;
pointer-events: auto;
}
}
/* Queue Status Card overlay */
.status-panel {
background: rgba(11, 15, 25, 0.55);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 14px 18px;
display: none;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 380px;
box-shadow: var(--shadow-premium);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
animation: slide-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
}
@keyframes slide-in {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.status-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(249, 115, 22, 0.1);
border-radius: 50%;
border-top-color: var(--accent-orange);
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status-details {
min-width: 0;
}
.status-title {
font-family: var(--font-outfit);
font-size: 13px;
font-weight: 700;
color: #fff;
}
.status-desc {
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 1px;
}
.status-timer {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
color: var(--accent-orange);
background: rgba(249, 115, 22, 0.08);
border: 1px solid rgba(249, 115, 22, 0.15);
padding: 3px 8px;
border-radius: 5px;
flex-shrink: 0;
}
/* Text console / Terminal for VQA/VQA answers */
.terminal-container {
width: 380px;
padding: 24px;
background: rgba(7, 10, 19, 0.65);
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.6;
overflow-y: auto;
border-radius: 20px;
border: 1px solid var(--glass-border);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow: var(--shadow-premium);
display: none;
position: relative;
max-height: 350px;
pointer-events: auto;
}
.terminal-container::-webkit-scrollbar {
width: 6px;
}
.terminal-container::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 3px;
}
.terminal-header {
color: var(--accent-orange);
font-family: var(--font-outfit);
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.copy-btn {
position: absolute;
top: 14px;
right: 14px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 6px;
border-radius: 6px;
cursor: pointer;
transition: var(--transition-fast);
}
.copy-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.copy-btn i {
width: 12px;
height: 12px;
}
/* Floating media controls bar (mute/download/lightbox) */
.output-actions-bar {
display: none; /* Controlled dynamically */
background: rgba(15, 23, 42, 0.45);
border: 1px solid var(--glass-border);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-radius: 100px;
padding: 6px 12px;
gap: 12px;
align-items: center;
box-shadow: var(--shadow-premium);
animation: slide-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
}
.action-icon-btn {
background: transparent;
border: none;
color: var(--text-secondary);
width: 34px;
height: 34px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
}
.action-icon-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.action-icon-btn i {
width: 16px;
height: 16px;
}
.action-pill-btn {
background: var(--accent-gradient);
border: none;
color: #0f172a;
font-family: var(--font-outfit);
font-weight: 700;
font-size: 12px;
padding: 6px 14px;
border-radius: 100px;
cursor: pointer;
transition: var(--transition-fast);
display: inline-flex;
align-items: center;
gap: 4px;
}
.action-pill-btn:hover {
background: var(--accent-gradient-hover);
}
/* Highlight pulse on slide-out controls sidebar */
@keyframes pulse-highlight {
0%, 100% { border-color: var(--glass-border); }
50% { border-color: var(--accent-orange); box-shadow: 0 0 25px rgba(249, 115, 22, 0.2); }
}
.highlight-pulse {
animation: pulse-highlight 1.2s ease 2;
}
/* --- FOLD 2: RECOMMENDED EXAMPLES GRID SECTION (BELOW THE FOLD) --- */
.examples-section-fold {
background: linear-gradient(180deg, #f8f9fb 0%, #ffffff 100%);
position: relative;
z-index: 10;
margin-top: 0px;
padding: 60px 40px 40px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
display: none; /* Controlled dynamically */
}
@media (max-width: 768px) {
.examples-section-fold {
padding: 40px 20px 20px;
}
}
.examples-container {
max-width: 1200px;
margin: 0 auto;
}
.section-header {
font-family: var(--font-outfit);
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 6px;
color: #1e293b;
}
.section-header i {
color: var(--accent-orange);
width: 18px;
height: 18px;
}
.examples-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
gap: 20px;
}
.example-card {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 18px;
padding: 16px;
cursor: pointer;
transition: var(--transition-smooth);
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.example-card:hover {
transform: translateY(-3px);
border-color: rgba(249, 115, 22, 0.35);
box-shadow: 0 8px 24px rgba(249, 115, 22, 0.1);
}
.example-media-preview {
width: 100%;
height: 200px;
border-radius: 10px;
overflow: hidden;
background: #f1f5f9;
}
.example-media-preview img,
.example-media-preview video {
width: 100%;
height: 100%;
object-fit: contain;
}
.example-prompt {
font-size: 12.5px;
color: #475569;
line-height: 1.45;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 500;
}
.example-card:hover .example-prompt {
color: #1e293b;
}
.example-badge {
position: absolute;
top: 12px;
right: 12px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(0, 0, 0, 0.08);
color: var(--accent-orange);
font-size: 8px;
font-family: var(--font-outfit);
font-weight: 700;
padding: 2px 6px;
border-radius: 99px;
text-transform: uppercase;
}
/* --- Full Screen Lightbox Modal --- */
.lightbox-modal {
display: none;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(5, 7, 13, 0.96);
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.lightbox-modal.open {
display: flex;
opacity: 1;
}
.lightbox-content {
max-width: 92%;
max-height: 88%;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255, 255, 255, 0.08);
transform: scale(0.95);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.lightbox-modal.open .lightbox-content {
transform: scale(1);
}
.lightbox-img {
width: auto;
height: auto;
max-width: 100%;
max-height: 88vh;
display: block;
object-fit: contain;
}
.lightbox-close {
position: absolute;
top: 24px;
right: 24px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #fff;
padding: 10px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox-close:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.lightbox-close i {
width: 18px;
height: 18px;
}
/* --- Footer Info --- */
footer {
margin-top: 50px;
text-align: center;
border-top: 1px solid var(--glass-border);
padding-top: 20px;
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
}
footer a {
color: var(--text-secondary);
text-decoration: none;
}
footer a:hover {
color: var(--accent-orange);
}
/* Light theme footer inside examples section */
.examples-section-fold footer {
color: rgba(0, 0, 0, 0.35);
border-top-color: rgba(0, 0, 0, 0.08);
}
.examples-section-fold footer a {
color: #64748b;
}
.examples-section-fold footer a:hover {
color: var(--accent-orange);
}
/* --- NEW PREMIUM UI CUSTOM STYLES --- */
/* Top Task Segmented Navigation Bar */
.top-task-selector-container {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
max-width: 90vw;
}
.top-task-nav-bar {
background: rgba(10, 16, 32, 0.45);
border: 1px solid var(--glass-border);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 100px;
padding: 4px;
display: flex;
gap: 4px;
box-shadow: var(--shadow-premium);
}
.task-nav-btn {
background: transparent;
border: none;
color: var(--text-secondary);
font-family: var(--font-outfit);
font-weight: 600;
font-size: 12.5px;
padding: 7px 15px;
border-radius: 100px;
cursor: pointer;
transition: var(--transition-fast);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.task-nav-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.05);
}
.task-nav-btn.active {
background: var(--accent-gradient);
color: #0f172a;
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.2);
}
@media (max-width: 900px) {
.top-task-selector-container {
position: relative;
top: 0;
left: 0;
transform: none;
margin: 20px auto 10px;
width: calc(100% - 40px);
}
.top-task-nav-bar {
width: 100%;
overflow-x: auto;
justify-content: flex-start;
padding: 6px;
border-radius: 14px;
-webkit-overflow-scrolling: touch;
}
.top-task-nav-bar::-webkit-scrollbar {
display: none;
}
}
/* Input Media Preview inside Center Prompt Card */
.input-media-preview {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 8px;
margin-top: 10px;
gap: 8px;
animation: slide-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.input-preview-thumbnail {
width: 100%;
height: 200px;
border-radius: 8px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.input-preview-thumbnail img,
.input-preview-thumbnail video {
width: 100%;
height: 100%;
object-fit: contain;
}
.input-preview-bottom-row {
display: flex;
align-items: center;
gap: 10px;
}
.input-preview-info {
display: flex;
flex-direction: column;
min-width: 0;
flex-grow: 1;
}
.input-preview-name {
font-size: 13px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.input-preview-size {
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
}
.input-preview-clear {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--text-secondary);
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
flex-shrink: 0;
}
.input-preview-clear:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.input-preview-clear i {
width: 14px;
height: 14px;
}
/* Ambient Blur Backdrop Effect */
.fs-bg-element.ambient-blur {
filter: blur(50px) brightness(0.35);
transform: scale(1.15);
object-fit: cover;
transition: opacity 0.8s ease-in-out;
}
/* Centered Output Viewport Container */
.centered-output-container {
position: fixed;
top: 48%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
display: none;
justify-content: center;
align-items: center;
width: 80vw;
height: 65vh;
pointer-events: none;
}
.centered-output-container.active {
display: flex;
animation: zoom-in-premium 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes zoom-in-premium {
from { transform: translate(-50%, -48%) scale(0.95); opacity: 0; }
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
.output-media-card {
background: rgba(15, 23, 42, 0.3);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 10px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
max-width: 100%;
max-height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
position: relative;
overflow: hidden;
}
.centered-media-element {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
border-radius: 12px;
display: none;
}
.centered-media-element.active {
display: block;
}
/* Large Centered Text Terminal styling */
.terminal-container.centered-large {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
max-height: 60vh;
margin: 0;
z-index: 100;
background: rgba(7, 10, 19, 0.85);
border: 1px solid rgba(249, 115, 22, 0.3);
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8), 0 0 30px rgba(249, 115, 22, 0.15);
font-size: 15px;
line-height: 1.7;
padding: 28px;
display: none;
pointer-events: auto;
}
.close-terminal-btn {
position: absolute;
top: 14px;
right: 48px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 6px;
border-radius: 6px;
cursor: pointer;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.close-terminal-btn:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.close-terminal-btn i {
width: 14px;
height: 14px;
}
</style>
</head>
<body>
<!-- IMMERSIVE FULL-SCREEN BACKGROUND CANVAS -->
<div class="viewport-bg-container">
<!-- Background elements -->
<!-- Fallback standard beautiful background loop image (Vibrant Falcon flight) -->
<img id="bg-fallback" class="fs-bg-element active" src="assets/default_background.jpg" alt="Lance Immersive Backdrop">
<!-- Interactive Full Screen Image output -->
<img id="bg-image-output" class="fs-bg-element" alt="Lance Output Canvas">
<!-- Interactive Full Screen Video output -->
<video id="bg-video-output" class="fs-bg-element fs-bg-video" loop playsinline></video>
<!-- Text protecting overlay -->
<div class="fs-bg-overlay"></div>
</div>
<!-- Centered Visual Output Viewport -->
<div class="centered-output-container" id="centered-media-viewport">
<div class="output-media-card">
<img id="centered-image-output" class="centered-media-element" alt="Lance Generated Image Output">
<video id="centered-video-output" class="centered-media-element" loop playsinline controls></video>
</div>
</div>
<!-- FOLD 1: FLOATING CINEMATIC VIEWPORT LAYER -->
<div class="cinematic-viewport">
<!-- Top Bar Header -->
<header>
<a href="#" class="brand-link">
<!-- transparent PNG brand logo -->
<img src="https://huggingface.co/spaces/bytedance-research/Lance/resolve/main/assets/logo/lance-logo.png" class="brand-logo" alt="ByteDance Logo">
<div class="brand-divider"></div>
<span class="brand-name">ByteDance</span>
</a>
<!-- Dynamic Top task switcher navigation -->
<div class="top-task-selector-container">
<div class="top-task-nav-bar" id="top-task-navbar">
<!-- populated dynamically -->
</div>
</div>
</header>
<!-- Immersive Center Prompt Console -->
<div class="viewport-center-trigger">
<div class="center-prompt-card">
<div class="center-prompt-header">
<span class="active-task-pill" id="center-task-badge">Text to Video</span>
</div>
<div class="center-prompt-body">
<textarea class="prompt-textarea" id="prompt-input" placeholder="Describe what you want to generate..."></textarea>
<!-- Media preview inside the input box -->
<div class="input-media-preview" id="center-media-preview" style="display: none;">
<div class="input-preview-thumbnail" id="center-preview-thumbnail"></div>
<div class="input-preview-bottom-row">
<div class="input-preview-info">
<span class="input-preview-name" id="center-preview-name">file_name.mp4</span>
<span class="input-preview-size" id="center-preview-size">0.0 MB</span>
</div>
<button class="input-preview-clear" id="center-preview-clear" title="Remove attachment">
<i data-lucide="x"></i>
</button>
</div>
</div>
</div>
<div class="center-prompt-footer">
<button class="center-options-btn" onclick="toggleSidebar()" title="Configure settings and inputs">
<i data-lucide="sliders"></i>
<span>Model Controls</span>
</button>
<!-- Visual Input attach button for multimodal tasks -->
<button class="center-options-btn" id="center-attach-btn" title="Attach input media (image or video)" style="display: none;">
<i data-lucide="paperclip"></i>
<span>Attach Media</span>
</button>
<button class="center-generate-btn" id="generate-trigger">
<i data-lucide="sparkles"></i>
<span>Generate</span>
</button>
</div>
</div>
<!-- Quick Example Pills below the input card -->
<div class="quick-examples-container" id="quick-examples-box">
<span class="quick-examples-label">Try these:</span>
<div class="quick-examples-list" id="quick-examples-wrapper">
<!-- Dynamic example pills go here -->
</div>
</div>
</div>
<!-- Bottom Layout Elements -->
<div class="viewport-bottom">
<div class="bottom-caption">
<h2 class="caption-title">Lance</h2>
<p class="caption-desc" style="max-width: 600px;">
Lance is a 3B native unified multimodal model for image and video understanding, generation, and editing, trained from scratch within a training budget of no more than 128 GPUs using a staged multi-task recipe.
</p>
</div>
<footer class="main-footer" style="font-size: 11px; color: rgba(255, 255, 255, 0.35); font-family: var(--font-inter);">
<p>Powered by <a href="https://gradio.app" target="_blank" rel="noopener noreferrer" style="color: rgba(255, 255, 255, 0.5); text-decoration: none;">Gradio.Server</a></p>
</footer>
</div>
</div>
<!-- SLIDE-OUT CONTROLS SIDEBAR -->
<div class="controls-sidebar" id="sidebar-controls-pane">
<div class="panel-title">
<div class="panel-title-left">
<i data-lucide="sliders"></i>
<span>Model Controls</span>
</div>
<button class="close-sidebar-btn" onclick="toggleSidebar()" title="Close controls">
<i data-lucide="x"></i>
</button>
</div>
<!-- Segmented Tab task selectors in sidebar (hidden, synced behind-the-scenes) -->
<div class="form-group" style="display: none;">
<label class="form-label">Task family</label>
<div class="select-wrapper">
<select class="select-control" id="task-select-dropdown">
<!-- populated dynamically -->
</select>
</div>
</div>
<!-- Media Upload Zone (Drag-and-drop) -->
<div class="form-group" id="media-upload-container">
<label class="form-label" id="media-field-label" for="file-selector">Media Input</label>
<div class="upload-zone" id="drag-drop-zone">
<div class="upload-zone-content">
<div class="upload-icon">
<i data-lucide="upload-cloud" id="upload-icon-element"></i>
</div>
<span class="upload-title" id="upload-title-element">Upload video file</span>
<span class="upload-desc" id="upload-desc-element">Drag & drop your file here, or click to browse</span>
</div>
<input type="file" id="file-selector" class="file-input">
</div>
<!-- Selected File Preview -->
<div class="preview-zone" id="file-preview-card">
<div class="preview-media-container" id="preview-media-wrapper">
<!-- JS will inject img/video elements dynamically -->
</div>
<div class="preview-bottom-row">
<div class="preview-info">
<div class="preview-name" id="preview-filename">file_name.mp4</div>
<div class="preview-size" id="preview-filesize">0.0 MB</div>
</div>
<button class="clear-file-btn" id="clear-file-trigger" title="Remove file">
<i data-lucide="trash-2"></i>
</button>
</div>
</div>
</div>
<!-- Parameters Configurations -->
<div class="controls-grid">
<div class="form-group" id="res-group">
<label class="form-label" for="res-control">Resolution</label>
<div class="select-wrapper">
<select class="select-control" id="res-control">
<!-- Dynamic resolutions -->
</select>
</div>
</div>
<div class="form-group" id="ar-group">
<label class="form-label" for="ar-control">Aspect Ratio</label>
<div class="select-wrapper">
<select class="select-control" id="ar-control">
<!-- Dynamic ratios -->
</select>
</div>
</div>
<div class="form-group" id="duration-group">
<label class="form-label" for="duration-control">Duration</label>
<div class="select-wrapper">
<select class="select-control" id="duration-control">
<!-- Dynamic durations -->
</select>
</div>
</div>
<div class="form-group" id="rife-group">
<label class="form-label" for="rife-control">Interpolation</label>
<div class="select-wrapper">
<select class="select-control" id="rife-control">
<option value="Yes" selected>Yes</option>
<option value="No">No</option>
</select>
</div>
</div>
</div>
<!-- Collapsible Advanced Parameters -->
<div class="accordion" id="adv-accordion">
<button class="accordion-header" id="adv-accordion-toggle">
<span>Advanced Parameters</span>
<i data-lucide="chevron-down"></i>
</button>
<div class="accordion-content">
<div class="form-group">
<label class="form-label" for="seed-control">Seed (-1 for random)</label>
<input type="number" class="number-control" id="seed-control" value="42" min="-1" step="1">
</div>
<div class="form-group">
<label class="form-label" for="steps-control">Inference Timesteps</label>
<div class="slider-wrapper">
<input type="range" class="slider-control" id="steps-control" min="1" max="50" step="1" value="30">
<span class="slider-val" id="steps-val">30</span>
</div>
</div>
<div class="form-group">
<label class="form-label" for="shift-control">Timestep Shift</label>
<div class="slider-wrapper">
<input type="range" class="slider-control" id="shift-control" min="0.0" max="10.0" step="0.5" value="3.5">
<span class="slider-val" id="shift-val">3.5</span>
</div>
</div>
<div class="form-group">
<label class="form-label" for="cfg-control">CFG Text Scale</label>
<div class="slider-wrapper">
<input type="range" class="slider-control" id="cfg-control" min="1.0" max="12.0" step="0.5" value="4.0">
<span class="slider-val" id="cfg-val">4.0</span>
</div>
</div>
</div>
</div>
</div>
<!-- FLOATING STATUS & TERMINAL OVERLAYS (RIGHT SIDE) -->
<div class="floating-right-container">
<!-- Queue Status Card -->
<div class="status-panel" id="status-card">
<div class="status-left">
<div class="spinner"></div>
<div class="status-details">
<div class="status-title" id="status-title-text">Processing request...</div>
<div class="status-desc" id="status-desc-text">In Gradio server queue</div>
</div>
</div>
<div class="status-timer" id="status-elapsed-timer">0.0s</div>
</div>
<!-- Text Terminal console for VQA answers -->
<div class="terminal-container" id="output-text-container">
<div class="terminal-header">
<i data-lucide="terminal"></i> Response Output
</div>
<button class="close-terminal-btn" id="close-terminal-trigger" title="Close response console">
<i data-lucide="x"></i>
</button>
<button class="copy-btn" id="copy-clipboard-btn" title="Copy response to clipboard">
<i data-lucide="copy"></i>
</button>
<div id="output-text-content">Response will print here...</div>
</div>
<!-- Overlay floating media control actions bar -->
<div class="output-actions-bar" id="output-actions-card">
<!-- Volume Sound Toggle (unmute fullscreen background video) -->
<button class="action-icon-btn" id="bg-volume-toggle" title="Unmute video sound">
<i data-lucide="volume-x"></i>
</button>
<!-- Fullscreen Image zoom lightbox trigger -->
<button class="action-icon-btn" id="bg-lightbox-trigger" title="Open fullscreen zoom">
<i data-lucide="maximize-2"></i>
</button>
<!-- Direct Download link button -->
<a id="bg-download-link" class="action-pill-btn" download="lance_generation" target="_blank" rel="noopener noreferrer">
<i data-lucide="download"></i>
<span>Download</span>
</a>
</div>
</div>
<!-- Image Lightbox Modal -->
<div class="lightbox-modal" id="lightbox-modal-element">
<button class="lightbox-close" id="lightbox-close-trigger" title="Close zoom">
<i data-lucide="x"></i>
</button>
<div class="lightbox-content">
<img class="lightbox-img" id="lightbox-full-img" src="" alt="Lance High-Res Output">
</div>
</div>
<!-- Global page scroll helpers -->
<script>
function scrollToMain() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function toggleSidebar() {
const pane = document.getElementById("sidebar-controls-pane");
pane.classList.toggle("open");
}
</script>
<!-- Script imports and Client bindings -->
<script type="module">
import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
// Global State variables
let appConfig = null;
let activeTask = null; // internal name, e.g. "t2v"
let selectedFile = null; // raw File object
let uploadedFileReference = null; // Gradio serialized file reference
let gradioClient = null;
let activeJob = null;
let timerInterval = null;
let timerStart = 0;
let videoMuted = true;
// Background canvas caches
const bgFallback = document.getElementById("bg-fallback");
const bgImageOutput = document.getElementById("bg-image-output");
const bgVideoOutput = document.getElementById("bg-video-output");
const outputActionsCard = document.getElementById("output-actions-card");
const bgVolumeToggle = document.getElementById("bg-volume-toggle");
const bgLightboxTrigger = document.getElementById("bg-lightbox-trigger");
const bgDownloadLink = document.getElementById("bg-download-link");
// Element caches
const taskSelectDropdown = document.getElementById("task-select-dropdown");
const promptInput = document.getElementById("prompt-input");
const promptFieldLabel = document.getElementById("prompt-field-label");
const sidebarControlsPane = document.getElementById("sidebar-controls-pane");
// Media upload caches
const mediaUploadContainer = document.getElementById("media-upload-container");
const mediaFieldLabel = document.getElementById("media-field-label");
const dragDropZone = document.getElementById("drag-drop-zone");
const fileSelector = document.getElementById("file-selector");
const filePreviewCard = document.getElementById("file-preview-card");
const previewMediaWrapper = document.getElementById("preview-media-wrapper");
const previewFilename = document.getElementById("preview-filename");
const previewFilesize = document.getElementById("preview-filesize");
const clearFileTrigger = document.getElementById("clear-file-trigger");
const uploadIconElement = document.getElementById("upload-icon-element");
const uploadTitleElement = document.getElementById("upload-title-element");
const uploadDescElement = document.getElementById("upload-desc-element");
// Controls caches
const resControl = document.getElementById("res-control");
const arControl = document.getElementById("ar-control");
const durationControl = document.getElementById("duration-control");
const resGroup = document.getElementById("res-group");
const arGroup = document.getElementById("ar-group");
const durationGroup = document.getElementById("duration-group");
const rifeControl = document.getElementById("rife-control");
const rifeGroup = document.getElementById("rife-group");
// Advanced controls
const advAccordion = document.getElementById("adv-accordion");
const advAccordionToggle = document.getElementById("adv-accordion-toggle");
const seedControl = document.getElementById("seed-control");
const stepsControl = document.getElementById("steps-control");
const stepsVal = document.getElementById("steps-val");
const shiftControl = document.getElementById("shift-control");
const shiftVal = document.getElementById("shift-val");
const cfgControl = document.getElementById("cfg-control");
const cfgVal = document.getElementById("cfg-val");
// New Visual and Task Switcher Element Caches
const topTaskNavbar = document.getElementById("top-task-navbar");
const centerAttachBtn = document.getElementById("center-attach-btn");
const centerMediaPreview = document.getElementById("center-media-preview");
const centerPreviewThumbnail = document.getElementById("center-preview-thumbnail");
const centerPreviewName = document.getElementById("center-preview-name");
const centerPreviewSize = document.getElementById("center-preview-size");
const centerPreviewClear = document.getElementById("center-preview-clear");
const centeredMediaViewport = document.getElementById("centered-media-viewport");
const centeredImageOutput = document.getElementById("centered-image-output");
const centeredVideoOutput = document.getElementById("centered-video-output");
const closeTerminalTrigger = document.getElementById("close-terminal-trigger");
// Trigger and status panel
const generateTrigger = document.getElementById("generate-trigger");
const statusCard = document.getElementById("status-card");
const statusTitleText = document.getElementById("status-title-text");
const statusDescText = document.getElementById("status-desc-text");
const statusElapsedTimer = document.getElementById("status-elapsed-timer");
// Viewer panel (Text consoles)
const outputTextContainer = document.getElementById("output-text-container");
const outputTextContent = document.getElementById("output-text-content");
const copyClipboardBtn = document.getElementById("copy-clipboard-btn");
const lightboxModalElement = document.getElementById("lightbox-modal-element");
const lightboxCloseTrigger = document.getElementById("lightbox-close-trigger");
const lightboxFullImg = document.getElementById("lightbox-full-img");
// --- Core Startup initialization ---
window.addEventListener("DOMContentLoaded", async () => {
lucide.createIcons();
// Connect to Gradio Client with status events enabled
try {
gradioClient = await Client.connect(window.location.origin, {
events: ["status", "data"]
});
console.log("Connected to Gradio Server Client engine successfully");
} catch (err) {
console.error("Gradio Client connection failed:", err);
}
// Fetch dynamic system settings and cases
try {
const response = await fetch("/api/config");
appConfig = await response.json();
console.log("Fetched app config successfully:", appConfig);
initializeUI();
} catch (err) {
console.error("Config fetch failed:", err);
}
});
// --- UI Setup & Bindings ---
function initializeUI() {
if (!appConfig) return;
// Generate Task dropdown items and Top Segmented Nav Switcher
taskSelectDropdown.innerHTML = "";
topTaskNavbar.innerHTML = "";
appConfig.TASK_CHOICES.forEach((taskLabel, idx) => {
const internalTask = appConfig.TASK_LABEL_TO_INTERNAL[taskLabel];
// Dropdown setup
const opt = document.createElement("option");
opt.value = internalTask;
opt.textContent = taskLabel;
if (idx === 0) opt.selected = true;
taskSelectDropdown.appendChild(opt);
// Top Segmented Pill Button setup
const btn = document.createElement("button");
btn.className = "task-nav-btn";
btn.setAttribute("data-task", internalTask);
// Cohesive Lucide icons
let iconName = "sparkles";
if (internalTask === "t2v") iconName = "video";
else if (internalTask === "t2i") iconName = "image";
else if (internalTask === "video_edit") iconName = "film";
else if (internalTask === "image_edit") iconName = "scissors";
else if (internalTask === "x2t_video") iconName = "help-circle";
else if (internalTask === "x2t_image") iconName = "help-circle";
btn.innerHTML = `<i data-lucide="${iconName}"></i><span>${taskLabel}</span>`;
if (idx === 0) btn.classList.add("active");
btn.addEventListener("click", () => {
document.querySelectorAll(".task-nav-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
switchTask(internalTask, taskLabel);
});
topTaskNavbar.appendChild(btn);
});
// Bind click to task dropdown change
taskSelectDropdown.addEventListener("change", (e) => {
const internalTaskName = e.target.value;
const taskLabel = e.target.options[e.target.selectedIndex].text;
switchTask(internalTaskName, taskLabel);
});
// Bind Advanced Accordion toggles
advAccordionToggle.addEventListener("click", () => {
advAccordion.classList.toggle("open");
});
// Bind slider parameter displays
stepsControl.addEventListener("input", (e) => stepsVal.textContent = e.target.value);
shiftControl.addEventListener("input", (e) => shiftVal.textContent = parseFloat(e.target.value).toFixed(1));
cfgControl.addEventListener("input", (e) => cfgVal.textContent = parseFloat(e.target.value).toFixed(1));
// Bind Drag & Drop Media uploads
dragDropZone.addEventListener("click", () => fileSelector.click());
dragDropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dragDropZone.classList.add("dragover");
});
dragDropZone.addEventListener("dragleave", () => dragDropZone.classList.remove("dragover"));
dragDropZone.addEventListener("drop", (e) => {
e.preventDefault();
dragDropZone.classList.remove("dragover");
if (e.dataTransfer.files.length > 0) {
handleSelectedFile(e.dataTransfer.files[0]);
}
});
fileSelector.addEventListener("change", (e) => {
if (e.target.files.length > 0) {
handleSelectedFile(e.target.files[0]);
}
});
clearFileTrigger.addEventListener("click", () => clearSelectedFile());
// Bind Generate Trigger
generateTrigger.addEventListener("click", () => triggerGeneration());
// Bind Fullscreen Image zoom lightbox trigger
bgLightboxTrigger.addEventListener("click", () => {
if (bgImageOutput.src && bgImageOutput.classList.contains("active")) {
lightboxFullImg.src = bgImageOutput.src;
lightboxModalElement.classList.add("open");
}
});
lightboxCloseTrigger.addEventListener("click", () => lightboxModalElement.classList.remove("open"));
lightboxModalElement.addEventListener("click", (e) => {
if (e.target === lightboxModalElement) {
lightboxModalElement.classList.remove("open");
}
});
// Bind Audio Volume toggles (mute/unmute generated videos)
bgVolumeToggle.addEventListener("click", () => {
videoMuted = !videoMuted;
bgVideoOutput.muted = videoMuted;
centeredVideoOutput.muted = videoMuted;
if (videoMuted) {
bgVolumeToggle.innerHTML = '<i data-lucide="volume-x"></i>';
bgVolumeToggle.title = "Unmute video sound";
} else {
bgVolumeToggle.innerHTML = '<i data-lucide="volume-2"></i>';
bgVolumeToggle.title = "Mute video sound";
}
lucide.createIcons();
});
// Bind Text Copy button
copyClipboardBtn.addEventListener("click", () => {
const text = outputTextContent.innerText;
navigator.clipboard.writeText(text).then(() => {
const originalHTML = copyClipboardBtn.innerHTML;
copyClipboardBtn.innerHTML = '<i data-lucide="check" style="color:#22c55e"></i>';
lucide.createIcons();
setTimeout(() => {
copyClipboardBtn.innerHTML = originalHTML;
lucide.createIcons();
}, 2000);
});
});
// Bind Visual attach button inside prompt box to target hidden file selector
centerAttachBtn.addEventListener("click", () => {
fileSelector.click();
});
// Bind Clear button inside prompt box preview
centerPreviewClear.addEventListener("click", (e) => {
e.stopPropagation();
clearSelectedFile();
});
// Bind Close button inside centered large output terminal
closeTerminalTrigger.addEventListener("click", () => {
resetOutputViewer();
});
// Load initial task
if (taskSelectDropdown.options.length > 0) {
const opt = taskSelectDropdown.options[0];
switchTask(opt.value, opt.text);
}
}
// --- Task switching logic ---
function switchTask(internalTask, taskLabel) {
activeTask = internalTask;
console.log("Switching task page to:", activeTask);
// Sync dropdown just in case it was triggered programmatically via populateExample
if (taskSelectDropdown.value !== activeTask) {
taskSelectDropdown.value = activeTask;
}
// Sync top segmented navbar active button
document.querySelectorAll(".task-nav-btn").forEach(btn => {
if (btn.getAttribute("data-task") === activeTask) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
// Pulse highlight sidebar panel for feedback
sidebarControlsPane.classList.add("highlight-pulse");
setTimeout(() => sidebarControlsPane.classList.remove("highlight-pulse"), 1200);
// Clear current file inputs
clearSelectedFile();
// Toggle Text Prompt Field Properties
const centerTaskBadge = document.getElementById("center-task-badge");
if (centerTaskBadge) {
centerTaskBadge.textContent = taskLabel;
}
if (activeTask === "t2v" || activeTask === "t2i") {
if (promptFieldLabel) promptFieldLabel.textContent = "Prompt";
promptInput.placeholder = "Describe what you want to generate...";
promptInput.value = "";
} else if (activeTask === "image_edit" || activeTask === "video_edit") {
if (promptFieldLabel) promptFieldLabel.textContent = "Instruction";
promptInput.placeholder = "Describe the edit or changes you want...";
promptInput.value = "";
} else {
if (promptFieldLabel) promptFieldLabel.textContent = "Question";
promptInput.placeholder = "Ask a question about the uploaded media...";
promptInput.value = "";
}
// Toggle Input Media upload visibility and Center Attach Button
const isEdit = activeTask === "image_edit" || activeTask === "video_edit";
const isUnderstanding = activeTask === "x2t_video" || activeTask === "x2t_image";
const isVideoTask = activeTask === "video_edit" || activeTask === "x2t_video";
if (isEdit || isUnderstanding) {
mediaUploadContainer.style.display = "block";
centerAttachBtn.style.display = "flex";
mediaFieldLabel.textContent = isVideoTask ? "Input Video" : "Input Image";
// Customize drag-drop zone instructions
uploadIconElement.setAttribute("data-lucide", isVideoTask ? "video" : "image");
uploadTitleElement.textContent = isVideoTask ? "Upload Input Video" : "Upload Input Image";
uploadDescElement.textContent = isVideoTask ? "MP4 video formats supported" : "PNG, JPG or JPEG supported";
lucide.createIcons();
dragDropZone.style.display = "block";
} else {
mediaUploadContainer.style.display = "none";
centerAttachBtn.style.display = "none";
dragDropZone.style.display = "none";
}
// Populate options dropdown controls dynamically
populateOptions(activeTask);
// Populate examples
renderQuickExamples(activeTask);
}
// --- Populate Resolutions, ratios, durations dynamically ---
function populateOptions(task) {
const isVideo = task === "t2v" || task === "video_edit" || task === "x2t_video";
const isImage = task === "t2i" || task === "image_edit" || task === "x2t_image";
const isTextToVisual = task === "t2v" || task === "t2i";
const isT2V = task === "t2v";
// Toggle settings visibility
resGroup.style.display = (isVideo && isT2V) ? "block" : "none";
arGroup.style.display = isTextToVisual ? "block" : "none";
durationGroup.style.display = isT2V ? "block" : "none";
rifeGroup.style.display = (task === "t2v" || task === "video_edit") ? "block" : "none";
// Resolution controls
resControl.innerHTML = "";
appConfig.VIDEO_RESOLUTION_DISPLAY_CHOICES.forEach(res => {
const opt = document.createElement("option");
opt.value = res[1];
opt.textContent = res[0];
if (res[1] === appConfig.DEFAULT_RESOLUTION) opt.selected = true;
resControl.appendChild(opt);
});
// Aspect Ratio controls
arControl.innerHTML = "";
appConfig.ASPECT_RATIO_CHOICES.forEach(ratio => {
const opt = document.createElement("option");
opt.value = ratio;
opt.textContent = ratio;
const defaultRatio = isImage ? appConfig.DEFAULT_IMAGE_ASPECT_RATIO : appConfig.DEFAULT_VIDEO_ASPECT_RATIO;
if (ratio === defaultRatio) opt.selected = true;
arControl.appendChild(opt);
});
// Duration controls
durationControl.innerHTML = "";
for (let s = 1; s <= appConfig.MAX_VIDEO_DURATION_SECONDS; s++) {
const opt = document.createElement("option");
opt.value = s;
opt.textContent = `${s}s`;
if (s === appConfig.DEFAULT_VIDEO_DURATION_SECONDS) opt.selected = true;
durationControl.appendChild(opt);
}
}
// Helper to render thumbnails for either File objects or URL paths
function renderThumbnail(wrapperElement, fileOrUrl, isVideoType) {
wrapperElement.innerHTML = "";
if (isVideoType) {
const video = document.createElement("video");
video.src = typeof fileOrUrl === "string" ? fileOrUrl : URL.createObjectURL(fileOrUrl);
video.muted = true;
video.playsInline = true;
wrapperElement.appendChild(video);
video.addEventListener("loadeddata", () => {
video.currentTime = 0.5;
});
} else {
const img = document.createElement("img");
img.src = typeof fileOrUrl === "string" ? fileOrUrl : URL.createObjectURL(fileOrUrl);
wrapperElement.appendChild(img);
}
}
// --- Handle Uploaded Files locally ---
function handleSelectedFile(file) {
if (!file) return;
selectedFile = file;
const sizeText = `${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`;
const isVideoType = selectedFile.type.startsWith("video/");
// 1. Update Controls Sidebar file details & thumbnail
previewFilename.textContent = selectedFile.name;
previewFilesize.textContent = sizeText;
renderThumbnail(previewMediaWrapper, selectedFile, isVideoType);
// 2. Update Center Prompt Card file details & thumbnail
centerPreviewName.textContent = selectedFile.name;
centerPreviewSize.textContent = sizeText;
renderThumbnail(centerPreviewThumbnail, selectedFile, isVideoType);
// Toggle element visibilities
dragDropZone.style.display = "none";
filePreviewCard.style.display = "flex";
centerMediaPreview.style.display = "flex";
uploadedFileReference = null;
}
function clearSelectedFile() {
selectedFile = null;
uploadedFileReference = null;
fileSelector.value = "";
filePreviewCard.style.display = "none";
centerMediaPreview.style.display = "none";
previewMediaWrapper.innerHTML = "";
centerPreviewThumbnail.innerHTML = "";
const isEdit = activeTask === "image_edit" || activeTask === "video_edit";
const isUnderstanding = activeTask === "x2t_video" || activeTask === "x2t_image";
if (isEdit || isUnderstanding) {
dragDropZone.style.display = "block";
}
}
// --- Load Case Example onto UI ---
function populateExample(exampleRow) {
console.log("Loading recommended case example:", exampleRow);
// Slide in Controls Sidebar automatically on selection
if (!sidebarControlsPane.classList.contains("open")) {
sidebarControlsPane.classList.add("open");
}
// Sync with dropdown and tasks
let newInternalTask = activeTask;
if (exampleRow[5] && exampleRow[5].startsWith("t2v_")) newInternalTask = "t2v";
else if (exampleRow[5] && exampleRow[5].startsWith("t2i_")) newInternalTask = "t2i";
else if (exampleRow[5] && exampleRow[5].startsWith("video_edit_")) newInternalTask = "video_edit";
else if (exampleRow[5] && exampleRow[5].startsWith("image_edit_")) newInternalTask = "image_edit";
else if (exampleRow[5] && exampleRow[5].startsWith("x2t_video_")) newInternalTask = "x2t_video";
else if (exampleRow[5] && exampleRow[5].startsWith("x2t_image_")) newInternalTask = "x2t_image";
if (newInternalTask !== activeTask) {
const opt = Array.from(taskSelectDropdown.options).find(o => o.value === newInternalTask);
const taskLabel = opt ? opt.text : newInternalTask;
switchTask(newInternalTask, taskLabel);
} else {
sidebarControlsPane.classList.add("highlight-pulse");
setTimeout(() => sidebarControlsPane.classList.remove("highlight-pulse"), 1200);
}
// Prompt / Instruction
promptInput.value = exampleRow[0] || "";
// Dynamic video/image files
clearSelectedFile();
const hasVideo = exampleRow[2];
const hasImage = exampleRow[4];
if (hasVideo && (activeTask === "video_edit" || activeTask === "x2t_video")) {
const filename = hasVideo.split("/").pop();
// 1. Sidebar Preview
previewFilename.textContent = filename;
previewFilesize.textContent = "Example Asset";
renderThumbnail(previewMediaWrapper, hasVideo, true);
// 2. Center Card Preview
centerPreviewName.textContent = filename;
centerPreviewSize.textContent = "Example Asset";
renderThumbnail(centerPreviewThumbnail, hasVideo, true);
dragDropZone.style.display = "none";
filePreviewCard.style.display = "flex";
centerMediaPreview.style.display = "flex";
uploadedFileReference = hasVideo;
} else if (hasImage && (activeTask === "image_edit" || activeTask === "x2t_image")) {
const filename = hasImage.split("/").pop();
// 1. Sidebar Preview
previewFilename.textContent = filename;
previewFilesize.textContent = "Example Asset";
renderThumbnail(previewMediaWrapper, hasImage, false);
// 2. Center Card Preview
centerPreviewName.textContent = filename;
centerPreviewSize.textContent = "Example Asset";
renderThumbnail(centerPreviewThumbnail, hasImage, false);
dragDropZone.style.display = "none";
filePreviewCard.style.display = "flex";
centerMediaPreview.style.display = "flex";
uploadedFileReference = hasImage;
}
// Trigger reset of viewer output
// resetOutputViewer();
// Scroll to the main fold view
scrollToMain();
}
// --- Render Quick Examples below the center prompt input ---
function renderQuickExamples(task) {
const quickExamplesWrapper = document.getElementById("quick-examples-wrapper");
const quickExamplesBox = document.getElementById("quick-examples-box");
if (!quickExamplesWrapper) return;
let cases = [];
if (task === "t2v") {
cases = appConfig.VIDEO_GENERATION_EXAMPLES;
} else if (task === "video_edit") {
cases = appConfig.VIDEO_EDIT_EXAMPLES;
} else if (task === "x2t_video") {
cases = appConfig.VIDEO_UNDERSTANDING_EXAMPLES;
} else if (task === "t2i") {
cases = appConfig.IMAGE_GENERATION_EXAMPLES;
} else if (task === "image_edit") {
cases = appConfig.IMAGE_EDIT_EXAMPLES;
} else if (task === "x2t_image") {
cases = appConfig.IMAGE_UNDERSTANDING_EXAMPLES;
}
if (cases && cases.length > 0) {
quickExamplesBox.style.display = "flex";
quickExamplesWrapper.innerHTML = "";
// Show up to 4 examples max
const maxExamples = Math.min(4, cases.length);
for (let i = 0; i < maxExamples; i++) {
const row = cases[i];
const promptText = row[0];
if (!promptText) continue;
const pill = document.createElement("button");
pill.className = "quick-example-pill";
// Resolve visual preview URL (Index 1/2 for video, Index 3/4 for image)
const isVideoType = (task === "t2v" || task === "video_edit" || task === "x2t_video");
let mediaUrl = isVideoType ? resolveMediaUrl(row[1] || row[2]) : resolveMediaUrl(row[3] || row[4]);
if (mediaUrl) {
if (isVideoType) {
const thumbVideo = document.createElement("video");
thumbVideo.src = mediaUrl;
thumbVideo.className = "quick-example-thumb";
thumbVideo.muted = true;
thumbVideo.playsInline = true;
// Micro-animation: Hover to play tiny video thumbnail
pill.addEventListener("mouseenter", () => {
thumbVideo.play().catch(() => {});
});
pill.addEventListener("mouseleave", () => {
thumbVideo.pause();
thumbVideo.currentTime = 0.5;
});
pill.appendChild(thumbVideo);
thumbVideo.addEventListener("loadeddata", () => {
thumbVideo.currentTime = 0.5;
});
} else {
const thumbImg = document.createElement("img");
thumbImg.src = mediaUrl;
thumbImg.className = "quick-example-thumb";
pill.appendChild(thumbImg);
}
} else {
const fallbackIcon = document.createElement("div");
fallbackIcon.className = "quick-example-thumb-icon";
fallbackIcon.innerHTML = '<i data-lucide="sparkles" style="width:10px;height:10px;"></i>';
pill.appendChild(fallbackIcon);
}
// Add text element
const textSpan = document.createElement("span");
textSpan.className = "quick-example-text";
textSpan.textContent = promptText;
textSpan.title = promptText;
pill.appendChild(textSpan);
pill.addEventListener("click", () => {
populateExample(row);
});
quickExamplesWrapper.appendChild(pill);
}
lucide.createIcons();
} else {
quickExamplesBox.style.display = "none";
}
}
// Global Helper: resolve a file path to a servable URL
function resolveMediaUrl(filePath) {
if (!filePath || filePath.trim() === "") return null;
if (filePath.startsWith("http")) return filePath;
return "/api/file?path=" + encodeURIComponent(filePath);
}
// --- Helper to calculate height/width based on Aspect Ratio ---
function calculateDimensions(aspectRatio, isImage) {
let width = 640;
let height = 352;
if (isImage) {
const map = {
"21:9": [1168, 496],
"16:9": [1024, 576],
"3:2": [944, 624],
"4:3": [880, 672],
"1:1": [768, 768],
"3:4": [672, 880],
"2:3": [624, 944],
"9:16": [576, 1024]
};
if (map[aspectRatio]) {
width = map[aspectRatio][0];
height = map[aspectRatio][1];
}
} else {
const res = resControl.value || "video_360p";
const map360p = {
"21:9": [672, 288],
"16:9": [640, 352],
"3:2": [528, 352],
"4:3": [560, 416],
"1:1": [480, 480],
"3:4": [416, 560],
"2:3": [352, 528],
"9:16": [352, 640]
};
const map480p = {
"21:9": [976, 416],
"16:9": [848, 480],
"3:2": [784, 528],
"4:3": [736, 560],
"1:1": [640, 640],
"3:4": [560, 736],
"2:3": [528, 784],
"9:16": [480, 848]
};
const map = res === "video_480p" ? map480p : map360p;
if (map[aspectRatio]) {
width = map[aspectRatio][0];
height = map[aspectRatio][1];
}
}
return { width, height };
}
// --- Start Active Status Timer ---
function startTimer() {
clearInterval(timerInterval);
timerStart = Date.now();
statusElapsedTimer.textContent = "0.0s";
timerInterval = setInterval(() => {
const elapsed = (Date.now() - timerStart) / 1000;
statusElapsedTimer.textContent = `${elapsed.toFixed(1)}s`;
}, 100);
}
function stopTimer() {
clearInterval(timerInterval);
}
// --- Main Generation execution ---
async function triggerGeneration() {
if (!gradioClient) {
alert("Gradio Client is not fully initialized. Please reload the page.");
return;
}
const prompt = promptInput.value.trim();
if (!prompt) {
alert("Please enter a prompt, instruction or question.");
promptInput.focus();
return;
}
const isImage = activeTask === "t2i" || activeTask === "image_edit" || activeTask === "x2t_image";
const isVideo = activeTask === "t2v" || activeTask === "video_edit" || activeTask === "x2t_video";
const isUnderstanding = activeTask === "x2t_video" || activeTask === "x2t_image";
const isEdit = activeTask === "image_edit" || activeTask === "video_edit";
const isT2V = activeTask === "t2v";
// Validate media uploads
if ((isEdit || isUnderstanding) && !selectedFile && !uploadedFileReference) {
alert(isVideo ? "Please upload an input video." : "Please upload an input image.");
return;
}
// Close controls drawer during processing to keep screen clean
if (sidebarControlsPane.classList.contains("open")) {
sidebarControlsPane.classList.remove("open");
}
// Enable loading state on trigger button
generateTrigger.disabled = true;
generateTrigger.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:8px;"></span><span>Processing...</span>';
// Show status Overlay
statusCard.style.display = "flex";
statusTitleText.textContent = "Connecting...";
statusDescText.textContent = "Initializing connection to Gradio's queuing engine";
startTimer();
// Reset current viewer displays
// resetOutputViewer();
try {
// Handle local file uploads first
let finalVideoInput = null;
let finalImageInput = null;
if (selectedFile) {
statusTitleText.textContent = "Uploading file...";
statusDescText.textContent = `Uploading ${selectedFile.name} to server`;
const fileRef = await handle_file(selectedFile);
if (isVideo) finalVideoInput = fileRef;
else finalImageInput = fileRef;
} else if (uploadedFileReference) {
const fileRef = { path: uploadedFileReference };
if (isVideo) finalVideoInput = fileRef;
else finalImageInput = fileRef;
}
// Resolve dimensions
const aspectRatio = arControl.value || "16:9";
const dims = calculateDimensions(aspectRatio, isImage);
// Default system prompts if empty
let systemPrompt = null;
if (activeTask === "x2t_video") systemPrompt = appConfig.V2T_QA_SYSTEM_PROMPT;
if (activeTask === "x2t_image") systemPrompt = appConfig.I2T_QA_SYSTEM_PROMPT;
// Frame parameters count
let frames = 61; // default
if (isT2V) {
const secs = parseInt(durationControl.value) || 5;
frames = 12 * secs + 1;
}
// Build flat positional payload array for run_generation API endpoint
const payload = [
activeTask,
prompt,
systemPrompt,
finalVideoInput,
finalImageInput,
dims.height,
dims.width,
frames,
parseInt(seedControl.value) || 42,
resControl.value || "video_480p",
parseInt(stepsControl.value) || 30,
parseFloat(shiftControl.value) || 3.5,
parseFloat(cfgControl.value) || 4.0,
rifeControl.value === "Yes"
];
console.log("Submitting payload to /run_generation API:", payload);
statusTitleText.textContent = "Queued Request...";
statusDescText.textContent = "Waiting for concurrent GPU allocation slot";
// Connect to job submit listener for queue status
activeJob = gradioClient.submit("/run_generation", payload);
let result = null;
for await (const message of activeJob) {
if (message.type === "status") {
console.log("Queue Job update received:", message);
const stage = message.status; // "pending", "generating", "complete", "error"
if (stage === "pending") {
const pos = message.position !== undefined ? message.position + 1 : 1;
const size = message.size || pos;
statusTitleText.textContent = `In Queue (Position ${pos}/${size})`;
statusDescText.textContent = "Request is locked in Gradio's concurrency manager";
} else if (stage === "iterating" || stage === "generating") {
statusTitleText.textContent = "Generating outputs...";
statusDescText.textContent = "Lance is executing generation in ZeroGPU environment";
}
} else if (message.type === "data") {
result = message;
}
}
stopTimer();
statusCard.style.display = "none";
if (result) {
console.log("Generation finished successfully. Result:", result);
displayResult(result);
} else {
throw new Error("No data returned from job");
}
} catch (err) {
stopTimer();
statusCard.style.display = "none";
console.error("Generation failed:", err);
alert(`Error during generation: ${err.message || err}`);
} finally {
// Restore button trigger state
generateTrigger.disabled = false;
generateTrigger.innerHTML = '<i data-lucide="rocket"></i><span>Generate Task</span>';
lucide.createIcons();
activeJob = null;
}
}
// --- Render final generated assets onto full screen background ---
function displayResult(result) {
console.log("displayResult called with:", JSON.stringify(result));
if (!result || !result.data) {
console.warn("displayResult: No result or result.data, resetting viewer");
resetOutputViewer();
return;
}
// The API returns a single JSON string. result.data may be:
// - An array with one element: ["json_string"]
// - A direct string: "json_string"
let rawData = result.data;
if (Array.isArray(rawData)) rawData = rawData[0];
console.log("rawData after unwrap:", rawData);
let parsed;
try {
if (typeof rawData === "string") {
parsed = JSON.parse(rawData);
} else if (typeof rawData === "object" && rawData !== null) {
parsed = rawData;
} else {
console.warn("displayResult: Unexpected rawData type:", typeof rawData);
resetOutputViewer();
return;
}
} catch (e) {
console.error("displayResult: Failed to parse JSON:", e, rawData);
resetOutputViewer();
return;
}
console.log("displayResult parsed:", parsed);
const videoPath = parsed.video || "";
const imagePath = parsed.image || "";
const textData = parsed.text || "";
const statusMsg = parsed.status || "";
const videoUrl = resolveMediaUrl(videoPath);
const imageUrl = resolveMediaUrl(imagePath);
console.log("Resolved URLs:", { videoUrl, imageUrl, textData, statusMsg });
// If there's only a status message with no actual content
if (statusMsg && !videoUrl && !imageUrl && !textData) {
alert(`Inference Status: ${statusMsg}`);
return;
}
// Output Video generated: Takes up full-screen background as ambient blur + centered sharp preview
if (videoUrl) {
// 1. Ambient Blur Backdrop
bgFallback.classList.remove("active");
bgImageOutput.classList.remove("active");
bgImageOutput.src = "";
bgLightboxTrigger.style.display = "none";
bgVideoOutput.src = videoUrl;
bgVideoOutput.className = "fs-bg-element fs-bg-video ambient-blur active";
bgVideoOutput.muted = true; // ambient blur backdrop is always silent
bgVideoOutput.play();
// 2. Centered Sharp Foreground
centeredMediaViewport.classList.add("active");
centeredImageOutput.classList.remove("active");
centeredImageOutput.src = "";
centeredVideoOutput.src = videoUrl;
centeredVideoOutput.classList.add("active");
centeredVideoOutput.muted = videoMuted;
centeredVideoOutput.play();
bgVolumeToggle.style.display = "flex";
bgDownloadLink.href = videoUrl;
outputActionsCard.style.display = "flex";
outputTextContainer.style.display = "none";
outputTextContainer.classList.remove("centered-large");
enterOutputMode();
console.log("Video output displayed:", videoUrl);
}
// Output Image generated: Takes up full-screen background as ambient blur + centered sharp preview
else if (imageUrl) {
// 1. Ambient Blur Backdrop
bgFallback.classList.remove("active");
bgVideoOutput.classList.remove("active");
bgVideoOutput.removeAttribute("src");
try { bgVideoOutput.load(); } catch (e) {}
bgVolumeToggle.style.display = "none";
bgImageOutput.src = imageUrl;
bgImageOutput.className = "fs-bg-element ambient-blur active";
bgLightboxTrigger.style.display = "flex";
// 2. Centered Sharp Foreground
centeredMediaViewport.classList.add("active");
centeredVideoOutput.classList.remove("active");
centeredVideoOutput.removeAttribute("src");
try { centeredVideoOutput.load(); } catch (e) {}
centeredImageOutput.src = imageUrl;
centeredImageOutput.classList.add("active");
bgDownloadLink.href = imageUrl;
outputActionsCard.style.display = "flex";
outputTextContainer.style.display = "none";
outputTextContainer.classList.remove("centered-large");
enterOutputMode();
console.log("Image output displayed:", imageUrl);
}
// Output Text / Question answers: Rendered in enlarged centered panel
else if (textData) {
// Reset visual backgrounds
bgFallback.classList.add("active");
bgImageOutput.classList.remove("active");
bgImageOutput.src = "";
bgVideoOutput.classList.remove("active");
bgVideoOutput.removeAttribute("src");
try { bgVideoOutput.load(); } catch (e) {}
centeredMediaViewport.classList.remove("active");
centeredImageOutput.classList.remove("active");
centeredImageOutput.src = "";
centeredVideoOutput.classList.remove("active");
centeredVideoOutput.removeAttribute("src");
outputTextContent.textContent = textData;
outputTextContainer.classList.add("centered-large");
outputTextContainer.style.display = "block";
outputActionsCard.style.display = "none";
bgLightboxTrigger.style.display = "none";
bgVolumeToggle.style.display = "none";
enterOutputMode();
console.log("Text output displayed centered:", textData);
} else {
console.warn("displayResult: No usable output found, resetting");
resetOutputViewer();
}
}
// --- Helper to hide viewer outputs back to empty state ---
function resetOutputViewer() {
// Reset full-screen background elements back to fallback
bgFallback.classList.add("active");
bgImageOutput.classList.remove("active");
bgImageOutput.src = "";
bgImageOutput.className = "fs-bg-element";
bgLightboxTrigger.style.display = "none";
bgVideoOutput.classList.remove("active");
bgVideoOutput.removeAttribute("src");
bgVideoOutput.className = "fs-bg-element fs-bg-video";
try { bgVideoOutput.load(); } catch (e) {}
bgVolumeToggle.style.display = "none";
// Reset centered outputs
centeredMediaViewport.classList.remove("active");
centeredImageOutput.classList.remove("active");
centeredImageOutput.src = "";
centeredVideoOutput.classList.remove("active");
centeredVideoOutput.removeAttribute("src");
try { centeredVideoOutput.load(); } catch (e) {}
// Reset actions card and terminal
outputActionsCard.style.display = "none";
bgDownloadLink.href = "";
outputTextContainer.style.display = "none";
outputTextContainer.classList.remove("centered-large");
outputTextContent.textContent = "";
exitOutputMode();
lucide.createIcons();
}
// --- Output mode: collapse prompt to bottom, reveal full output ---
function enterOutputMode() {
const centerPanel = document.querySelector(".viewport-center-trigger");
const bottomPanel = document.querySelector(".viewport-bottom");
if (centerPanel) centerPanel.classList.add("output-mode");
if (bottomPanel) bottomPanel.classList.add("output-hidden");
}
function exitOutputMode() {
const centerPanel = document.querySelector(".viewport-center-trigger");
const bottomPanel = document.querySelector(".viewport-bottom");
if (centerPanel) centerPanel.classList.remove("output-mode");
if (bottomPanel) bottomPanel.classList.remove("output-hidden");
}
</script>
</body>
</html>