forked from 77media/video-flow
更新 使用新模板流程
This commit is contained in:
parent
f2c4844e05
commit
f44bee5009
@ -354,7 +354,7 @@ export interface CreateMovieProjectV4Request {
|
|||||||
/** 视频时长 */
|
/** 视频时长 */
|
||||||
video_duration: string;
|
video_duration: string;
|
||||||
/** 是否是图生 */
|
/** 是否是图生 */
|
||||||
is_image_to_video: boolean;
|
use_img2video: boolean;
|
||||||
/** pcode编码 */
|
/** pcode编码 */
|
||||||
pcode: string;
|
pcode: string;
|
||||||
/** 角色简介数组 */
|
/** 角色简介数组 */
|
||||||
|
|||||||
@ -146,6 +146,7 @@ export interface StoryTemplateEntity {
|
|||||||
template_id: string;
|
template_id: string;
|
||||||
/** 故事模板视频URL */
|
/** 故事模板视频URL */
|
||||||
show_url: string;
|
show_url: string;
|
||||||
|
pcode: string;
|
||||||
/** 故事角色 */
|
/** 故事角色 */
|
||||||
storyRole: {
|
storyRole: {
|
||||||
/** 角色名 */
|
/** 角色名 */
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
Clapperboard,
|
Clapperboard,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
LayoutTemplate,
|
LayoutTemplate,
|
||||||
|
ArrowRight
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@ -22,25 +23,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useUploadFile } from "@/app/service/domain/service";
|
import { useUploadFile } from "@/app/service/domain/service";
|
||||||
import { ActionButton } from "../common/ActionButton";
|
import { ActionButton } from "../common/ActionButton";
|
||||||
import GlobalLoad from "../common/GlobalLoad";
|
import GlobalLoad from "../common/GlobalLoad";
|
||||||
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 防抖函数
|
|
||||||
* @param {Function} func - 需要防抖的函数
|
|
||||||
* @param {number} wait - 等待时间(ms)
|
|
||||||
* @returns {Function} - 防抖后的函数
|
|
||||||
*/
|
|
||||||
const debounce = (func: Function, wait: number) => {
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
|
||||||
return function executedFunction(...args: any[]) {
|
|
||||||
const later = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
func(...args);
|
|
||||||
};
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PcTemplateModalProps {
|
interface PcTemplateModalProps {
|
||||||
isTemplateCreating: boolean;
|
isTemplateCreating: boolean;
|
||||||
@ -108,7 +90,6 @@ export const PcTemplateModal = ({
|
|||||||
// 自由输入框布局
|
// 自由输入框布局
|
||||||
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
|
||||||
const leftListRef = useRef<HTMLDivElement | null>(null);
|
const leftListRef = useRef<HTMLDivElement | null>(null);
|
||||||
// 组件挂载时获取模板列表
|
// 组件挂载时获取模板列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -228,7 +209,6 @@ export const PcTemplateModal = ({
|
|||||||
configOptions.mode,
|
configOptions.mode,
|
||||||
configOptions.resolution,
|
configOptions.resolution,
|
||||||
configOptions.language,
|
configOptions.language,
|
||||||
aspectUI as AspectRatioValue
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
@ -755,16 +735,10 @@ export const PcTemplateModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 横/竖屏选择 */}
|
|
||||||
<AspectRatioSelector
|
|
||||||
value={aspectUI}
|
|
||||||
onChange={setAspectUI}
|
|
||||||
placement="top"
|
|
||||||
/>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isCreating={isTemplateCreating || localLoading > 0}
|
isCreating={isTemplateCreating || localLoading > 0}
|
||||||
handleCreateVideo={handleConfirm}
|
handleCreateVideo={handleConfirm}
|
||||||
icon={<Clapperboard className="w-5 h-5" />}
|
icon={<ArrowRight className="w-5 h-5" />}
|
||||||
disabled={isTemplateCreating || localLoading > 0}
|
disabled={isTemplateCreating || localLoading > 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
import TemplatePreviewModal from "@/components/common/TemplatePreviewModal"
|
||||||
import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal"
|
import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal"
|
||||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"
|
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ const FamousTemplate: React.FC = () => {
|
|||||||
const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({})
|
const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({})
|
||||||
const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null)
|
const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null)
|
||||||
const [isPreviewReady, setIsPreviewReady] = useState(false)
|
const [isPreviewReady, setIsPreviewReady] = useState(false)
|
||||||
const [activeTab, setActiveTab] = useState<"all" | "music" | "animation" | "thriller">("all")
|
const [activeTab, setActiveTab] = useState<"all" | "music" | "animation" | "fantasy">("all")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void getTemplateStoryList()
|
void getTemplateStoryList()
|
||||||
@ -42,15 +43,15 @@ const FamousTemplate: React.FC = () => {
|
|||||||
Hot Templates
|
Hot Templates
|
||||||
</h2>
|
</h2>
|
||||||
<div data-alt="template-tabs" className="flex items-center gap-2 ml-4">
|
<div data-alt="template-tabs" className="flex items-center gap-2 ml-4">
|
||||||
{(["all", "music", "animation", "thriller"] as const).map((tab) => (
|
{(["all", "music", "animation", "fantasy"] as const).map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
type="button"
|
type="button"
|
||||||
data-alt={`template-tab-${tab}`}
|
data-alt={`template-tab-${tab}`}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`px-3 py-1 italic rounded-none text-sm transition-colors border ${
|
className={`px-3 py-1 rounded-none text-sm transition-colors border ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "border-white/60 bg-white/60 text-slate-900"
|
? "border-white/60 bg-white/80 text-slate-900"
|
||||||
: "border-white/20 text-white/80 hover:border-white/40 hover:bg-white/10"
|
: "border-white/20 text-white/80 hover:border-white/40 hover:bg-white/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -119,78 +120,19 @@ const FamousTemplate: React.FC = () => {
|
|||||||
const active = topTemplates.find((x) => (x.id || x.template_id) === activeTemplateId)
|
const active = topTemplates.find((x) => (x.id || x.template_id) === activeTemplateId)
|
||||||
if (!active || !active.show_url) return null
|
if (!active || !active.show_url) return null
|
||||||
return (
|
return (
|
||||||
<div
|
<TemplatePreviewModal
|
||||||
data-alt="template-preview-modal"
|
open={!!activeTemplateId}
|
||||||
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"
|
videoUrl={active.show_url}
|
||||||
onClick={() => setActiveTemplateId(null)}
|
onClose={() => setActiveTemplateId(null)}
|
||||||
>
|
title={active.name}
|
||||||
<div
|
description={active.generateText || active.name}
|
||||||
data-alt="template-preview-modal-content"
|
onPrimaryAction={() => {
|
||||||
className="relative w-[80vw] min-h-[40vw] rounded-lg overflow-hidden border border-white/30 bg-black shadow-2xl"
|
setInitialTemplateId(active.id || active.template_id)
|
||||||
onClick={(e) => e.stopPropagation()}
|
setIsModalOpen(true)
|
||||||
>
|
setActiveTemplateId(null)
|
||||||
<>
|
}}
|
||||||
<video
|
primaryLabel="Try it Free"
|
||||||
src={active.show_url}
|
/>
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
onCanPlay={() => setIsPreviewReady(true)}
|
|
||||||
className={`w-full h-auto transition-opacity duration-200 ${isPreviewReady ? "opacity-100" : "opacity-0"}`}
|
|
||||||
/>
|
|
||||||
{!isPreviewReady && (
|
|
||||||
<div
|
|
||||||
data-alt="template-preview-loading"
|
|
||||||
className="absolute inset-0 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<div className="h-10 w-10 rounded-full border-2 border-white/30 border-t-white/80 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* template-preview-header */}
|
|
||||||
{isPreviewReady && (
|
|
||||||
<div
|
|
||||||
data-alt="template-preview-header"
|
|
||||||
className="absolute top-0 left-0 right-0 bg-gradient-to-b from-black/90 via-black/80 to-transparent px-6 py-8 text-center"
|
|
||||||
>
|
|
||||||
<div className="text-bold text-2xl text-white/90 line-clamp-2 text-left mb-4">
|
|
||||||
{active.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isPreviewReady && (
|
|
||||||
<div
|
|
||||||
data-alt="template-preview-footer"
|
|
||||||
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/80 to-transparent px-6 py-8 text-center"
|
|
||||||
>
|
|
||||||
<div className="text-base text-white/90 line-clamp-2 text-left mb-4">
|
|
||||||
{active.generateText || active.name}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="items-center text-bold justify-center rounded-full border border-white/30 bg-white/10 px-6 py-2 text-sm text-xl text-white transition hover:border-white hover:bg-white hover:text-slate-900"
|
|
||||||
onClick={() => {
|
|
||||||
setInitialTemplateId(active.id || active.template_id)
|
|
||||||
setIsModalOpen(true)
|
|
||||||
setActiveTemplateId(null)
|
|
||||||
}}
|
|
||||||
data-alt="template-preview-try-it"
|
|
||||||
>
|
|
||||||
Try it Free
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-alt="template-preview-modal-close"
|
|
||||||
className="absolute top-3 right-3 inline-flex items-center justify-center rounded-full border border-white/30 bg-white/10 p-2 text-white hover:border-white hover:bg-white hover:text-slate-900"
|
|
||||||
onClick={() => setActiveTemplateId(null)}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { fetchSettingByCode } from "@/api/serversetting";
|
import { fetchSettingByCode } from "@/api/serversetting";
|
||||||
import { X, Eclipse } from "lucide-react";
|
import { X, ChevronUp, ChevronDown } from "lucide-react";
|
||||||
import { ChatInputBox } from "@/components/ChatInputBox/ChatInputBox";
|
import { ChatInputBox } from "@/components/ChatInputBox/ChatInputBox";
|
||||||
|
import { VideoCreationForm } from '@/components/pages/create-video/CreateInput';
|
||||||
|
|
||||||
export const HOME_BANNER_CODE = "homeBanner";
|
export const HOME_BANNER_CODE = "homeBanner";
|
||||||
|
const HOME_BANNER_COLLAPSE_KEY = "homeBannerCollapsedDate";
|
||||||
|
|
||||||
/** CTA config for banner */
|
/** CTA config for banner */
|
||||||
export interface HomeBannerCTAConfig {
|
export interface HomeBannerCTAConfig {
|
||||||
@ -38,9 +40,22 @@ export default function HomeBanner() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isFlying, setIsFlying] = useState<boolean>(false);
|
const [isFlying, setIsFlying] = useState<boolean>(false);
|
||||||
const autoCollapseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const autoCollapseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const skipAutoCollapseRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
/** Returns YYYY-MM-DD for user's local timezone */
|
||||||
|
const getLocalDateKey = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(now.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleDismiss = () => {
|
const handleDismiss = () => {
|
||||||
setIsFlying(true);
|
setIsFlying(true);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(HOME_BANNER_COLLAPSE_KEY, getLocalDateKey());
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBannerClick = () => {
|
const handleBannerClick = () => {
|
||||||
@ -49,12 +64,27 @@ export default function HomeBanner() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto collapse after mount (3s)
|
// Initialize from persisted state: keep collapsed for the rest of the day
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(HOME_BANNER_COLLAPSE_KEY);
|
||||||
|
if (saved && saved === getLocalDateKey()) {
|
||||||
|
setIsFlying(true);
|
||||||
|
skipAutoCollapseRef.current = true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto collapse after mount (2s) unless already collapsed today
|
||||||
|
useEffect(() => {
|
||||||
|
if (skipAutoCollapseRef.current) return;
|
||||||
if (autoCollapseTimerRef.current) return;
|
if (autoCollapseTimerRef.current) return;
|
||||||
autoCollapseTimerRef.current = setTimeout(() => {
|
autoCollapseTimerRef.current = setTimeout(() => {
|
||||||
setIsFlying(true);
|
setIsFlying(true);
|
||||||
}, 3000);
|
try {
|
||||||
|
localStorage.setItem(HOME_BANNER_COLLAPSE_KEY, getLocalDateKey());
|
||||||
|
} catch {}
|
||||||
|
}, 2000);
|
||||||
return () => {
|
return () => {
|
||||||
if (autoCollapseTimerRef.current) {
|
if (autoCollapseTimerRef.current) {
|
||||||
clearTimeout(autoCollapseTimerRef.current);
|
clearTimeout(autoCollapseTimerRef.current);
|
||||||
@ -130,9 +160,9 @@ export default function HomeBanner() {
|
|||||||
{/* Banner overlay - stacked above */}
|
{/* Banner overlay - stacked above */}
|
||||||
<section
|
<section
|
||||||
data-alt="home-banner"
|
data-alt="home-banner"
|
||||||
className={`absolute inset-0 z-10 isolate overflow-hidden rounded-3xl px-6 py-6 text-white border-2 border-transparent hover:border-custom-blue/50 transition-all duration-500 ease-in-out ${
|
className={`absolute inset-0 z-10 isolate overflow-hidden rounded-3xl px-6 py-6 text-white border-2 border-transparent hover:border-custom-blue/50 transition-all duration-400 ease-in-out ${
|
||||||
isFlying
|
isFlying
|
||||||
? "cursor-pointer translate-x-[90%] -translate-y-[70%] scale-[0.85] opacity-95 rotate-3"
|
? "translate-x-[90%] -translate-y-[70%] scale-[0.85] opacity-95 rotate-3"
|
||||||
: "translate-x-0 translate-y-0 scale-100 opacity-100 rotate-0"
|
: "translate-x-0 translate-y-0 scale-100 opacity-100 rotate-0"
|
||||||
}`}
|
}`}
|
||||||
aria-label="Home banner"
|
aria-label="Home banner"
|
||||||
@ -161,12 +191,12 @@ export default function HomeBanner() {
|
|||||||
className="text-white hover:bg-white/20 rounded-full p-2"
|
className="text-white hover:bg-white/20 rounded-full p-2"
|
||||||
aria-label="Dismiss banner"
|
aria-label="Dismiss banner"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<ChevronUp className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isFlying ? (
|
{isFlying ? (
|
||||||
<button type="button" onClick={handleBannerClick} className="absolute left-[-15px] bottom-[-20px] h-5 w-5">
|
<button type="button" onClick={handleBannerClick} className="bg-white/50 rounded-full absolute left-[-15px] bottom-[-20px] h-5 w-5">
|
||||||
<Eclipse className="inset-0 h-5 w-5" />
|
<ChevronDown className="inset-0 h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{eyebrow ? (
|
{eyebrow ? (
|
||||||
@ -208,8 +238,8 @@ export default function HomeBanner() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Base content - always present under the banner */}
|
{/* Base content - always present under the banner */}
|
||||||
<div data-alt="home-banner-base" className="relative bg-transparent py-6 min-h-[300px]">
|
<div data-alt="home-banner-base" className="relative p-6 px-12 min-h-[300px] flex items-center justify-center bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)]">
|
||||||
<ChatInputBox noData={false} />
|
<VideoCreationForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const MyMovies: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<section data-alt="my-movies" className="w-full">
|
<section data-alt="my-movies" className="w-full">
|
||||||
<div data-alt="my-movies-header" className="mb-4 flex items-center justify-between">
|
<div data-alt="my-movies-header" className="mb-4 flex items-center justify-between">
|
||||||
<h2 data-alt="my-movies-title" className="text-xl py-4 font-semibold text-white">My Movies</h2>
|
<h2 data-alt="my-movies-title" className="text-xl py-4 font-semibold text-white">My Projects</h2>
|
||||||
<Link data-alt="all-movies-link" href="/movies" className="text-sm px-2 border rounded-full text-blue-400 hover:text-blue-300">All movies →</Link>
|
<Link data-alt="all-movies-link" href="/movies" className="text-sm px-2 border rounded-full text-blue-400 hover:text-blue-300">All movies →</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
121
components/common/TemplatePreviewModal.tsx
Normal file
121
components/common/TemplatePreviewModal.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface TemplatePreviewModalProps {
|
||||||
|
/** Control visibility */
|
||||||
|
open: boolean;
|
||||||
|
/** Video URL to preview */
|
||||||
|
videoUrl: string | null;
|
||||||
|
/** Close callback */
|
||||||
|
onClose: () => void;
|
||||||
|
/** Optional title shown over the video */
|
||||||
|
title?: string;
|
||||||
|
/** Optional description shown over the video */
|
||||||
|
description?: string;
|
||||||
|
/** Optional primary action (e.g., Try it) */
|
||||||
|
onPrimaryAction?: () => void;
|
||||||
|
/** Optional primary action label */
|
||||||
|
primaryLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fullscreen, Tailwind-based template preview modal with video content.
|
||||||
|
* Overlays title/description if provided and supports an optional primary action.
|
||||||
|
*/
|
||||||
|
export function TemplatePreviewModal({
|
||||||
|
open,
|
||||||
|
videoUrl,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onPrimaryAction,
|
||||||
|
primaryLabel = 'Try it',
|
||||||
|
}: TemplatePreviewModalProps) {
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setIsReady(false);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open || !videoUrl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-alt="template-preview-modal"
|
||||||
|
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-alt="template-preview-modal-content"
|
||||||
|
className="relative w-[70vw] min-h-[40vw] rounded-lg overflow-hidden border border-white/30 bg-black shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={videoUrl}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
onCanPlay={() => setIsReady(true)}
|
||||||
|
className={`w-full h-auto transition-opacity duration-200 ${isReady ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isReady && (
|
||||||
|
<div
|
||||||
|
data-alt="template-preview-loading"
|
||||||
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="h-10 w-10 rounded-full border-2 border-white/30 border-t-white/80 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isReady && (title || description) && (
|
||||||
|
<div
|
||||||
|
data-alt="template-preview-header"
|
||||||
|
className="absolute top-0 left-0 right-0 bg-gradient-to-b from-black/90 via-black/80 to-transparent px-6 py-8 text-left"
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<div className="text-bold text-2xl text-white/90 line-clamp-2 mb-2">{title}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isReady && onPrimaryAction && (
|
||||||
|
<div
|
||||||
|
data-alt="template-preview-footer"
|
||||||
|
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/80 to-transparent px-8 py-6 text-center"
|
||||||
|
>
|
||||||
|
{description && (
|
||||||
|
<div className="text-base text-white/80 line-clamp-2 text-left">{description}</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-full border border-white/30 bg-white/10 mt-3 px-6 py-2 text-white hover:border-white hover:bg-white hover:text-slate-900"
|
||||||
|
onClick={onPrimaryAction}
|
||||||
|
data-alt="template-preview-primary"
|
||||||
|
>
|
||||||
|
{primaryLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-alt="template-preview-modal-close"
|
||||||
|
className="absolute top-3 right-3 inline-flex items-center justify-center rounded-full border border-white/30 bg-white/10 p-2 text-white hover:border-white hover:bg-white hover:text-slate-900"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplatePreviewModal;
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export function PortraitAnimeSelector({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
}: PortraitAnimeSelectorProps) {
|
}: PortraitAnimeSelectorProps) {
|
||||||
const [lastAnimeChoice, setLastAnimeChoice] = useState<string>('STANDARD_V1_734684_116483');
|
const [lastAnimeChoice, setLastAnimeChoice] = useState<string>('STANDARD_V1_734684_116483');
|
||||||
const [animeOptions, setAnimeOptions] = useState<Array<{ name: string; pcode: string }>>([])
|
const [animeOptions, setAnimeOptions] = useState<Array<{ name: string; pcode: string }>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { PhotoPreviewSection } from '../PhotoPreview';
|
import { PhotoPreviewSection } from '../PhotoPreview';
|
||||||
import type { PhotoItem, PhotoType } from '../PhotoPreview/types';
|
import type { PhotoItem, PhotoType } from '../PhotoPreview/types';
|
||||||
import {
|
import {
|
||||||
@ -11,17 +11,24 @@ import {
|
|||||||
ArrowRightOutlined,
|
ArrowRightOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown, Popover } from 'antd';
|
||||||
import { ConfigPanel } from './ConfigPanel';
|
import { ConfigPanel } from './ConfigPanel';
|
||||||
import { MobileConfigModal } from './MobileConfigModal';
|
import { MobileConfigModal } from './MobileConfigModal';
|
||||||
import { AddItemModal } from './AddItemModal';
|
import { AddItemModal } from './AddItemModal';
|
||||||
import { defaultConfig } from './config-options';
|
import { defaultConfig } from './config-options';
|
||||||
import type { ConfigOptions } from './config-options';
|
import type { ConfigOptions } from './config-options';
|
||||||
|
import { Eye, Check, ArrowRight, X } from 'lucide-react';
|
||||||
|
import TemplatePreviewModal from '@/components/common/TemplatePreviewModal';
|
||||||
|
import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal"
|
||||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||||
import { MovieProjectService, MovieProjectMode } from '@/app/service/Interaction/MovieProjectService';
|
import { MovieProjectService, MovieProjectMode } from '@/app/service/Interaction/MovieProjectService';
|
||||||
import type { CreateMovieProjectV4Request } from '@/api/DTO/movie_start_dto';
|
import type { CreateMovieProjectV4Request } from '@/api/DTO/movie_start_dto';
|
||||||
import { getCurrentUser } from '@/lib/auth';
|
import { getCurrentUser } from '@/lib/auth';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useTemplateStoryServiceHook } from '@/app/service/Interaction/templateStoryService';
|
||||||
|
import { StoryTemplateEntity } from '@/app/service/domain/Entities';
|
||||||
|
import { useUploadFile } from '@/app/service/domain/service';
|
||||||
|
|
||||||
export default function VideoCreationForm() {
|
export default function VideoCreationForm() {
|
||||||
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
@ -32,12 +39,58 @@ export default function VideoCreationForm() {
|
|||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [replacingIndex, setReplacingIndex] = useState<number | null>(null);
|
const [replacingIndex, setReplacingIndex] = useState<number | null>(null);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
// Template modal states (align with FamousTemplate usage)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined);
|
||||||
|
const [isTemplateCreating, setIsTemplateCreating] = useState(false);
|
||||||
|
const [isRoleGenerating, setIsRoleGenerating] = useState<{ [key: string]: boolean }>({});
|
||||||
|
const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({});
|
||||||
|
const [currentTemplate, setCurrentTemplate] = useState<StoryTemplateEntity | null>(null);
|
||||||
|
const [inputPlaceholder, setInputPlaceholder] = useState<string>('');
|
||||||
|
const [templateTitle, setTemplateTitle] = useState<string>('');
|
||||||
|
const [isMentionOpen, setIsMentionOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const { isMobile, isDesktop } = useDeviceType();
|
const { isMobile, isDesktop } = useDeviceType();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Template list for mention popover */
|
||||||
|
const { templateStoryList, getTemplateStoryList } = useTemplateStoryServiceHook();
|
||||||
|
const isTemplateSelected = useMemo(() => {
|
||||||
|
return templateStoryList.find(i => i.pcode === configOptions.pcode);
|
||||||
|
}, [configOptions.pcode, templateStoryList]);
|
||||||
|
useEffect(() => {
|
||||||
|
void getTemplateStoryList();
|
||||||
|
}, [getTemplateStoryList]);
|
||||||
|
|
||||||
const characterInputRef = useRef<HTMLInputElement>(null);
|
const characterInputRef = useRef<HTMLInputElement>(null);
|
||||||
const sceneInputRef = useRef<HTMLInputElement>(null);
|
const sceneInputRef = useRef<HTMLInputElement>(null);
|
||||||
const propInputRef = useRef<HTMLInputElement>(null);
|
const propInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { uploadFile } = useUploadFile();
|
||||||
|
|
||||||
|
/** Clear current template related states */
|
||||||
|
const clearTemplateSelection = () => {
|
||||||
|
handleConfigChange('pcode', '');
|
||||||
|
setInputPlaceholder('');
|
||||||
|
setTemplateTitle('');
|
||||||
|
setPhotos([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Apply selected template to current form state */
|
||||||
|
const applyTemplateSelection = (template: StoryTemplateEntity) => {
|
||||||
|
const characterPhotos = (template.storyRole || []).map((role, index) => ({
|
||||||
|
url: role.photo_url,
|
||||||
|
type: 'character' as const,
|
||||||
|
id: `character-${Date.now()}-${index}`,
|
||||||
|
name: role.role_name,
|
||||||
|
description: role.role_description,
|
||||||
|
})).filter(p => p.url);
|
||||||
|
|
||||||
|
setPhotos(prev => [...prev, ...characterPhotos]);
|
||||||
|
setInputPlaceholder(template.generateText || template.name);
|
||||||
|
setTemplateTitle(template.name);
|
||||||
|
handleConfigChange('pcode', template.pcode || '');
|
||||||
|
};
|
||||||
|
|
||||||
/** Handle file upload */
|
/** Handle file upload */
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: PhotoType) => {
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: PhotoType) => {
|
||||||
@ -47,24 +100,33 @@ export default function VideoCreationForm() {
|
|||||||
// Check if we're replacing an existing photo
|
// Check if we're replacing an existing photo
|
||||||
if (replacingIndex !== null) {
|
if (replacingIndex !== null) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
setPhotos(prevPhotos => {
|
void (async () => {
|
||||||
const updatedPhotos = [...prevPhotos];
|
try {
|
||||||
updatedPhotos[replacingIndex] = {
|
const uploadedUrl = await uploadFile(file);
|
||||||
...updatedPhotos[replacingIndex],
|
setPhotos(prevPhotos => {
|
||||||
url: URL.createObjectURL(file),
|
const updatedPhotos = [...prevPhotos];
|
||||||
};
|
updatedPhotos[replacingIndex] = {
|
||||||
return updatedPhotos;
|
...updatedPhotos[replacingIndex],
|
||||||
});
|
url: uploadedUrl,
|
||||||
setReplacingIndex(null);
|
};
|
||||||
|
return updatedPhotos;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setReplacingIndex(null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
} else {
|
} else {
|
||||||
// Add new photos
|
// Add new photos
|
||||||
const newPhotos: PhotoItem[] = Array.from(files).map((file, index) => ({
|
void (async () => {
|
||||||
url: URL.createObjectURL(file),
|
const fileArray = Array.from(files);
|
||||||
type,
|
const uploadedUrls = await Promise.all(fileArray.map((file) => uploadFile(file)));
|
||||||
id: `${type}-${Date.now()}-${index}`,
|
const newPhotos: PhotoItem[] = uploadedUrls.map((url, index) => ({
|
||||||
}));
|
url,
|
||||||
|
type,
|
||||||
setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]);
|
id: `${type}-${Date.now()}-${index}`,
|
||||||
|
}));
|
||||||
|
setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
@ -131,33 +193,36 @@ export default function VideoCreationForm() {
|
|||||||
// Check if we're editing or adding
|
// Check if we're editing or adding
|
||||||
if (typeof data.index === 'number' && editingIndex !== null) {
|
if (typeof data.index === 'number' && editingIndex !== null) {
|
||||||
// Edit mode - update existing photo
|
// Edit mode - update existing photo
|
||||||
setPhotos(prevPhotos => {
|
void (async () => {
|
||||||
const updatedPhotos = [...prevPhotos];
|
let uploadedUrl: string | undefined;
|
||||||
const existingPhoto = updatedPhotos[editingIndex];
|
if (data.file.size > 0) {
|
||||||
|
uploadedUrl = await uploadFile(data.file);
|
||||||
updatedPhotos[editingIndex] = {
|
}
|
||||||
...existingPhoto,
|
setPhotos(prevPhotos => {
|
||||||
// Only update URL if a new file was selected (not the dummy file)
|
const updatedPhotos = [...prevPhotos];
|
||||||
...(data.file.size > 0 && { url: URL.createObjectURL(data.file) }),
|
const existingPhoto = updatedPhotos[editingIndex];
|
||||||
|
updatedPhotos[editingIndex] = {
|
||||||
|
...existingPhoto,
|
||||||
|
...(uploadedUrl ? { url: uploadedUrl } : {}),
|
||||||
|
...(data.name && { name: data.name }),
|
||||||
|
...(data.description && { description: data.description }),
|
||||||
|
};
|
||||||
|
return updatedPhotos;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
// Add mode - create new photo
|
||||||
|
void (async () => {
|
||||||
|
const uploadedUrl = await uploadFile(data.file);
|
||||||
|
const newPhoto: PhotoItem = {
|
||||||
|
url: uploadedUrl,
|
||||||
|
type: currentItemType,
|
||||||
|
id: `${currentItemType}-${Date.now()}`,
|
||||||
...(data.name && { name: data.name }),
|
...(data.name && { name: data.name }),
|
||||||
...(data.description && { description: data.description }),
|
...(data.description && { description: data.description }),
|
||||||
};
|
};
|
||||||
|
setPhotos(prevPhotos => [...prevPhotos, newPhoto]);
|
||||||
return updatedPhotos;
|
})();
|
||||||
});
|
|
||||||
console.log('Updated item:', { ...data, type: currentItemType, index: editingIndex });
|
|
||||||
} else {
|
|
||||||
// Add mode - create new photo
|
|
||||||
const newPhoto: PhotoItem = {
|
|
||||||
url: URL.createObjectURL(data.file),
|
|
||||||
type: currentItemType,
|
|
||||||
id: `${currentItemType}-${Date.now()}`,
|
|
||||||
...(data.name && { name: data.name }),
|
|
||||||
...(data.description && { description: data.description }),
|
|
||||||
};
|
|
||||||
|
|
||||||
setPhotos(prevPhotos => [...prevPhotos, newPhoto]);
|
|
||||||
console.log('Added item:', { ...data, type: currentItemType });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,7 +258,7 @@ export default function VideoCreationForm() {
|
|||||||
aspect_ratio: configOptions.aspect_ratio,
|
aspect_ratio: configOptions.aspect_ratio,
|
||||||
expansion_mode: configOptions.expansion_mode,
|
expansion_mode: configOptions.expansion_mode,
|
||||||
video_duration: configOptions.videoDuration,
|
video_duration: configOptions.videoDuration,
|
||||||
is_image_to_video: photos.length > 0,
|
use_img2video: photos.length > 0,
|
||||||
pcode: configOptions.pcode === 'portrait' ? '' : configOptions.pcode,
|
pcode: configOptions.pcode === 'portrait' ? '' : configOptions.pcode,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -257,9 +322,25 @@ export default function VideoCreationForm() {
|
|||||||
data-alt="content-container"
|
data-alt="content-container"
|
||||||
className="flex-1 border border-white/10 rounded-3xl bg-gradient-to-br from-[#1a1a1a]/50 to-[#0a0a0a]/50 backdrop-blur-sm overflow-hidden flex flex-col"
|
className="flex-1 border border-white/10 rounded-3xl bg-gradient-to-br from-[#1a1a1a]/50 to-[#0a0a0a]/50 backdrop-blur-sm overflow-hidden flex flex-col"
|
||||||
>
|
>
|
||||||
|
{templateTitle && (
|
||||||
|
<div data-alt="template-title" className="px-4 pt-2 flex items-center">
|
||||||
|
<div data-alt="template-title-text" className="text-white text-xs font-bold truncate">
|
||||||
|
{templateTitle}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
data-alt="clear-template"
|
||||||
|
className="ml-2 p-1 rounded hover:bg-white/10 text-gray-400 hover:text-white"
|
||||||
|
onClick={() => {
|
||||||
|
clearTemplateSelection();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Photo Preview Section - Top */}
|
{/* Photo Preview Section - Top */}
|
||||||
{photos.length > 0 && (
|
{photos.length > 0 && (
|
||||||
<div data-alt="photo-preview-wrapper" className="p-4 pb-0">
|
<div data-alt="photo-preview-wrapper" className="pt-2 pl-4 pb-0">
|
||||||
<PhotoPreviewSection
|
<PhotoPreviewSection
|
||||||
photos={photos}
|
photos={photos}
|
||||||
onEdit={handleEditPhoto}
|
onEdit={handleEditPhoto}
|
||||||
@ -273,7 +354,7 @@ export default function VideoCreationForm() {
|
|||||||
<textarea
|
<textarea
|
||||||
data-alt="main-text-input"
|
data-alt="main-text-input"
|
||||||
className="w-full h-full bg-transparent text-gray-300 text-base placeholder-gray-500 resize-none outline-none border-none"
|
className="w-full h-full bg-transparent text-gray-300 text-base placeholder-gray-500 resize-none outline-none border-none"
|
||||||
placeholder="Share a topic, idea, or instructions with Video Agent to produce a full avatar video"
|
placeholder={inputPlaceholder || "Share a topic, idea, or instructions with Video Agent to produce a full avatar video"}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -344,13 +425,75 @@ export default function VideoCreationForm() {
|
|||||||
onChange={(e) => handleFileUpload(e, 'prop')}
|
onChange={(e) => handleFileUpload(e, 'prop')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mention Button */}
|
{/* Mention Button with Popover (template list) */}
|
||||||
<button
|
<Popover
|
||||||
data-alt="mention-button"
|
placement="topLeft"
|
||||||
className="w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400"
|
trigger={["click"]}
|
||||||
|
arrow={false}
|
||||||
|
open={isMentionOpen}
|
||||||
|
onOpenChange={(open) => setIsMentionOpen(open)}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
data-alt="mention-popover-content"
|
||||||
|
className="w-64 max-h-64 overflow-y-auto"
|
||||||
|
>
|
||||||
|
{templateStoryList && templateStoryList.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1">
|
||||||
|
{templateStoryList.map((tpl) => {
|
||||||
|
const tplKey = (tpl as any).id || (tpl as any).template_id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tplKey}
|
||||||
|
data-alt={`mention-template-item-${tplKey}`}
|
||||||
|
className="group relative px-3 py-2 rounded-lg text-gray-200 hover:bg-white/10 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<div className={`flex items-center gap-3 cursor-pointer ${tpl.pcode === configOptions.pcode ? 'text-custom-blue' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsMentionOpen(false);
|
||||||
|
const url = (tpl as any).show_url as string | undefined;
|
||||||
|
if (url) {
|
||||||
|
setCurrentTemplate(tpl as StoryTemplateEntity);
|
||||||
|
} else {
|
||||||
|
setInitialTemplateId(tplKey);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="truncate">{tpl.name}</span>
|
||||||
|
{/* Floating actions on hover, right-aligned and overlaying the name */}
|
||||||
|
<div className="absolute px-2 right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity z-10">
|
||||||
|
<button
|
||||||
|
data-alt={`mention-template-use-${tplKey}`}
|
||||||
|
className="px-0 py-1 text-xs rounded-full hover:text-custom-blue/80"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsMentionOpen(false);
|
||||||
|
clearTemplateSelection();
|
||||||
|
const selected = tpl as StoryTemplateEntity;
|
||||||
|
applyTemplateSelection(selected);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div data-alt="mention-no-templates" className="text-gray-400 text-sm">No Avaliable Templates</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="text-base font-bold">@</span>
|
<button
|
||||||
</button>
|
data-alt="mention-button"
|
||||||
|
className={`w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400 ${isTemplateSelected ? 'text-yellow-500' : ''}`}
|
||||||
|
>
|
||||||
|
<span className="text-base font-bold">@</span>
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}
|
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
@ -416,6 +559,32 @@ export default function VideoCreationForm() {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<PcTemplateModal
|
||||||
|
isTemplateCreating={isTemplateCreating}
|
||||||
|
setIsTemplateCreating={setIsTemplateCreating}
|
||||||
|
isRoleGenerating={isRoleGenerating}
|
||||||
|
setIsRoleGenerating={setIsRoleGenerating}
|
||||||
|
isItemGenerating={isItemGenerating}
|
||||||
|
setIsItemGenerating={setIsItemGenerating}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
initialTemplateId={initialTemplateId}
|
||||||
|
configOptions={{ mode: "auto", resolution: "720p", language: "english", videoDuration: "auto" }}
|
||||||
|
/>
|
||||||
|
<TemplatePreviewModal
|
||||||
|
open={!!currentTemplate && !!currentTemplate.show_url}
|
||||||
|
videoUrl={currentTemplate?.show_url || ''}
|
||||||
|
onClose={() => setCurrentTemplate(null)}
|
||||||
|
title={currentTemplate?.name || ''}
|
||||||
|
description={currentTemplate?.generateText || currentTemplate?.name}
|
||||||
|
onPrimaryAction={() => {
|
||||||
|
if (!currentTemplate) return;
|
||||||
|
clearTemplateSelection();
|
||||||
|
applyTemplateSelection(currentTemplate);
|
||||||
|
setCurrentTemplate(null)
|
||||||
|
}}
|
||||||
|
primaryLabel="Try it Free"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,11 +93,10 @@ export default function PhotoPreviewSection({
|
|||||||
{/* Photo List Container with Horizontal Scroll */}
|
{/* Photo List Container with Horizontal Scroll */}
|
||||||
<div
|
<div
|
||||||
data-alt="photo-list-container"
|
data-alt="photo-list-container"
|
||||||
className="flex gap-2 overflow-x-auto overflow-y-hidden photo-list-scrollbar pb-1"
|
className="flex gap-2 overflow-x-auto overflow-y-hidden photo-list-scrollbar"
|
||||||
>
|
>
|
||||||
{photos.map((photo, index) => {
|
{photos.map((photo, index) => {
|
||||||
const photoName = photo.name || `${getTypeLabel(photo.type)} ${index + 1}`;
|
const photoName = photo.name || `${getTypeLabel(photo.type)} ${index + 1}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={photo.id || `photo-${index}`}
|
key={photo.id || `photo-${index}`}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user