diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image0.png b/frontend/apps/editor3d/app/public/resources/images/generate/image0.png new file mode 100644 index 0000000000..d69369ae9f Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image0.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image1.png b/frontend/apps/editor3d/app/public/resources/images/generate/image1.png new file mode 100644 index 0000000000..b7e944d140 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image1.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image10.png b/frontend/apps/editor3d/app/public/resources/images/generate/image10.png new file mode 100644 index 0000000000..e6e74a9828 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image10.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image11.png b/frontend/apps/editor3d/app/public/resources/images/generate/image11.png new file mode 100644 index 0000000000..77ea5e4b56 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image11.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image12.png b/frontend/apps/editor3d/app/public/resources/images/generate/image12.png new file mode 100644 index 0000000000..f226cc3429 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image12.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image13.png b/frontend/apps/editor3d/app/public/resources/images/generate/image13.png new file mode 100644 index 0000000000..d3aa3add12 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image13.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image14.png b/frontend/apps/editor3d/app/public/resources/images/generate/image14.png new file mode 100644 index 0000000000..49563c366d Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image14.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image15.png b/frontend/apps/editor3d/app/public/resources/images/generate/image15.png new file mode 100644 index 0000000000..ddc1f5060f Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image15.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image16.png b/frontend/apps/editor3d/app/public/resources/images/generate/image16.png new file mode 100644 index 0000000000..b2374e90e9 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image16.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image17.png b/frontend/apps/editor3d/app/public/resources/images/generate/image17.png new file mode 100644 index 0000000000..1520a72bc1 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image17.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image18.png b/frontend/apps/editor3d/app/public/resources/images/generate/image18.png new file mode 100644 index 0000000000..411ac1e4f2 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image18.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image19.png b/frontend/apps/editor3d/app/public/resources/images/generate/image19.png new file mode 100644 index 0000000000..3afb4ed2bb Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image19.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image2.png b/frontend/apps/editor3d/app/public/resources/images/generate/image2.png new file mode 100644 index 0000000000..652a008e4c Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image2.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image20.png b/frontend/apps/editor3d/app/public/resources/images/generate/image20.png new file mode 100644 index 0000000000..45a335385c Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image20.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image3.png b/frontend/apps/editor3d/app/public/resources/images/generate/image3.png new file mode 100644 index 0000000000..7efbe26eed Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image3.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image4.png b/frontend/apps/editor3d/app/public/resources/images/generate/image4.png new file mode 100644 index 0000000000..102b2fa652 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image4.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image5.png b/frontend/apps/editor3d/app/public/resources/images/generate/image5.png new file mode 100644 index 0000000000..fdac17b380 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image5.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image6.png b/frontend/apps/editor3d/app/public/resources/images/generate/image6.png new file mode 100644 index 0000000000..2d8e34a0eb Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image6.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image7.png b/frontend/apps/editor3d/app/public/resources/images/generate/image7.png new file mode 100644 index 0000000000..c457d2279d Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image7.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image8.png b/frontend/apps/editor3d/app/public/resources/images/generate/image8.png new file mode 100644 index 0000000000..aac964e0c6 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image8.png differ diff --git a/frontend/apps/editor3d/app/public/resources/images/generate/image9.png b/frontend/apps/editor3d/app/public/resources/images/generate/image9.png new file mode 100644 index 0000000000..d5b34b0b44 Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/generate/image9.png differ diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx index 3693beead0..073351fe97 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx @@ -140,7 +140,11 @@ export const TopBar = ({
- + {currentAppTabId === "3D" ? ( + + ) : ( +

{currentAppTabId === "2D" ? "Canvas" : "Generate"}

+ )}
diff --git a/frontend/apps/editor3d/app/src/pages/PageVideo/BackgroundGallery.tsx b/frontend/apps/editor3d/app/src/pages/PageVideo/BackgroundGallery.tsx new file mode 100644 index 0000000000..42dcb18c17 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageVideo/BackgroundGallery.tsx @@ -0,0 +1,171 @@ +import { useEffect, useState, useRef } from "react"; + +function getRandomImages(count: number) { + const images = Array.from({ length: count }, (_, i) => ({ + id: i + 1, + src: `/resources/images/generate/image${i + 1}.png`, + alt: `Generated image ${i + 1}`, + })); + + // Shuffle the array + return [...images].sort(() => Math.random() - 0.5); +} + +interface ImageColumnProps { + images: Array<{ id: number; src: string; alt: string }>; + delay: number; + reverse?: boolean; + isLoaded: boolean; + duration?: number; +} + +function ImageColumn({ + images, + delay, + reverse = false, + duration = 100, + isLoaded, +}: ImageColumnProps) { + const columnRef = useRef(null); + + useEffect(() => { + if (!columnRef.current) return; + + const column = columnRef.current; + const animation = column.animate( + [ + { transform: reverse ? "translateY(-50%)" : "translateY(0)" }, + { transform: reverse ? "translateY(0)" : "translateY(-50%)" }, + ], + { + duration: duration * 1000, + iterations: Infinity, + easing: "linear", + delay: delay + 1000, + fill: "both", + }, + ); + + if (!isLoaded) { + animation.pause(); + } + + return () => { + animation.cancel(); + }; + }, [delay, duration, isLoaded, reverse]); + + return ( +
+
+ {[...images, ...images].map((image, i) => ( +
+ {image.alt} +
+ ))} +
+
+ ); +} + +export default function GalleryBackground() { + const [images, _setImages] = useState(() => getRandomImages(20)); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + // delay for smooth animation + const timer = setTimeout(() => { + setIsLoaded(true); + }, 100); + + return () => clearTimeout(timer); + }, []); + + // Columns for masonry layout + const columns = { + col1: images.filter((_, i) => i % 5 === 0), + col2: images.filter((_, i) => i % 5 === 1), + col3: images.filter((_, i) => i % 5 === 2), + col4: images.filter((_, i) => i % 5 === 3), + col5: images.filter((_, i) => i % 5 === 4), + }; + + return ( + <> +
+
+
+
+ + + + + +
+
+
+ + ); +} diff --git a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx index ae1e4cda39..eda059c933 100644 --- a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx @@ -1,28 +1,27 @@ import { useState, useRef } from "react"; import { JobContextType } from "libs/common/src/lib/types"; import { PromptBoxVideo } from "@storyteller/ui-promptbox"; +import BackgroundGallery from "./BackgroundGallery"; +import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover"; +import { faFilm, faClock } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const ImageToVideo = () => { const containerRef = useRef(null); - const [isDialogOpen, setIsDialogOpen] = useState(false); + const [selectedModel, setSelectedModel] = useState("Kling 2.0"); + const [useSystemPrompt, setUseSystemPrompt] = useState(false); + const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(""); + const [isDialogOpen, setIsDialogOpen] = useState(false); - const handleClose = () => { - setIsDialogOpen(false); - }; - + const handleClose = () => setIsDialogOpen(false); const handleAddImage = (image: File) => { setSelectedImage(image); setIsImageBeingProcessed(true); setImagePreviewUrl(URL.createObjectURL(image)); - - // TODO: Implement actual image processing here - // For now, we'll just simulate processing - setTimeout(() => { - setIsImageBeingProcessed(false); - }, 3000); + setTimeout(() => setIsImageBeingProcessed(false), 3000); }; const jobContext: JobContextType = { @@ -32,13 +31,41 @@ const ImageToVideo = () => { clearJobTokens: () => {}, }; + const modelList: PopoverItem[] = [ + { + label: "Kling 2.0", + icon: , + selected: selectedModel === "Kling 2.0", + description: "High quality model", + badges: [{ label: "2 min.", icon: }], + }, + { + label: "Runway", + icon: , + selected: selectedModel === "Runway", + description: "Fast and high-quality model", + badges: [{ label: "2 min.", icon: }], + }, + ]; + + const handleModelSelect = (item: PopoverItem) => { + setSelectedModel(item.label); + }; + return (
-
+
+
+ Generate Video + + Add an image, prompt, then generate + +
+ { // TODO: Implement job context logic @@ -50,6 +77,21 @@ const ImageToVideo = () => { console.log("Enqueue pressed"); }} /> + + + +
+ +
diff --git a/frontend/libs/components/popover/src/lib/popover.tsx b/frontend/libs/components/popover/src/lib/popover.tsx index 61d49b5f32..56b8a4c6ff 100644 --- a/frontend/libs/components/popover/src/lib/popover.tsx +++ b/frontend/libs/components/popover/src/lib/popover.tsx @@ -1,7 +1,14 @@ -import { ReactNode } from "react"; -import { Popover, Transition } from "@headlessui/react"; +import { ReactNode, useRef, useState, useEffect } from "react"; +import { + Popover, + Transition, + PopoverButton, + PopoverPanel, +} from "@headlessui/react"; import { twMerge } from "tailwind-merge"; import { Button } from "@storyteller/ui-button"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronUp, faCircleCheck } from "@fortawesome/pro-solid-svg-icons"; export interface PopoverItem { label: string; @@ -9,6 +16,11 @@ export interface PopoverItem { icon?: ReactNode; action?: string; disabled?: boolean; + description?: string; + badges?: Array<{ + label: string; + icon?: ReactNode; + }>; } interface PopoverMenuProps { @@ -19,8 +31,8 @@ interface PopoverMenuProps { showAddButton?: boolean; disableAddButton?: boolean; showIconsInList?: boolean; - mode?: "default" | "toggle" | "button"; - triggerLabel?: string; + mode?: "default" | "toggle" | "button" | "hoverSelect"; + triggerLabel?: string | ReactNode; children?: ReactNode; buttonClassName?: string; panelClassName?: string; @@ -81,12 +93,97 @@ export const PopoverMenu = ({ end: "right-0", }; + // Track hover state and refs + const [isHovering, setIsHovering] = useState(false); + const hoverTimeoutRef = useRef(null); + const popoverButtonRef = useRef(null); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + }; + }, []); + + const handleButtonMouseEnter = (open: boolean, openFn: () => void) => { + if (mode !== "hoverSelect") return; + + setIsHovering(true); + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + + if (!open) { + hoverTimeoutRef.current = setTimeout(() => { + openFn(); + }, 100); + } + }; + + const handleButtonMouseLeave = (closeFn: () => void) => { + if (mode !== "hoverSelect") return; + + setIsHovering(false); + + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + + hoverTimeoutRef.current = setTimeout(() => { + if (!isHovering) { + closeFn(); + } + }, 300); + }; + + const handlePanelMouseEnter = () => { + if (mode !== "hoverSelect") return; + setIsHovering(true); + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + }; + + const handlePanelMouseLeave = (closeFn: () => void) => { + if (mode !== "hoverSelect") return; + setIsHovering(false); + + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + + hoverTimeoutRef.current = setTimeout(() => { + closeFn(); + }, 300); + }; + return ( - - {({ close }) => ( - <> - - <> +
+ + {({ open, close }) => ( + <> + + handleButtonMouseEnter(open, () => { + if (popoverButtonRef.current && !open) { + popoverButtonRef.current.click(); + } + }) + } + onMouseLeave={() => handleButtonMouseLeave(close)} + onClick={(e) => { + if (mode === "hoverSelect" && open) { + e.preventDefault(); + e.stopPropagation(); + } + }} + ref={popoverButtonRef} + > {triggerIcon} {mode === "toggle" && selectedItem ? ( {selectedItem.label} @@ -94,17 +191,19 @@ export const PopoverMenu = ({ {mode === "default" && triggerLabel ? ( {triggerLabel} ) : null} - - + {mode === "hoverSelect" && selectedItem ? ( +
+ {triggerLabel} +
+ {selectedItem.label} + +
+
+ ) : null} +
-
-
handlePanelMouseLeave(close)} > {panelTitle && (
@@ -151,6 +255,91 @@ export const PopoverMenu = ({ )} {mode === "default" && children ? (
{children}
+ ) : mode === "hoverSelect" ? ( +
+ {items.map((item, index) => ( +
{ + if (!item.disabled) { + handleItemClick(item, close); + } + }} + className={twMerge( + "group flex cursor-pointer items-start gap-2 rounded-lg px-2 py-2 transition-all", + item.selected + ? "bg-black/40 border-l-4 border-primary" + : "hover:bg-black/20", + item.disabled + ? "!cursor-not-allowed opacity-50" + : "" + )} + style={{ minHeight: 48 }} + > +
+
+ {showIconsInList && ( + + {item.icon} + + )} +
+
+ + {item.label} + +
+ + {item.description && ( +
+ {item.description} +
+ )} + +
+ {item.badges && + Array.isArray(item.badges) && + item.badges.map((badge, i) => ( +
+ + {badge?.icon && ( + {badge.icon} + )} + {badge?.label || ""} + +
+ ))} +
+
+
+ + {item.selected && ( + + + + )} +
+
+ ))} + {showAddButton && onAdd && ( + + )} +
) : (
{items.map((item, index) => ( @@ -203,11 +392,11 @@ export const PopoverMenu = ({
)}
- + -
- - )} - + + )} + +
); }; diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index 88d6a705c9..ac26935236 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -445,7 +445,7 @@ export const PromptBoxVideo = ({ > {content} -
+
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
{referenceImages.map((image) => ( @@ -523,7 +523,7 @@ export const PromptBoxVideo = ({