99 Info ,
1010 Loader2 ,
1111 Trash2 ,
12+ Video ,
1213 X ,
1314 ZoomIn ,
1415 ZoomOut ,
@@ -17,6 +18,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
1718import { toast } from 'sonner'
1819import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card'
1920import { ImageComparison } from '@/components/ui/ImageComparison'
21+ import { useVideoGenerator } from '@/hooks/useVideoGenerator'
2022import { upscaleImage } from '@/lib/api'
2123
2224interface ImageResultCardProps {
@@ -27,6 +29,7 @@ interface ImageResultCardProps {
2729 isBlurred : boolean
2830 isUpscaled : boolean
2931 isUpscaling : boolean
32+ giteeToken ?: string
3033 setShowInfo : ( v : boolean ) => void
3134 setIsBlurred : ( v : boolean ) => void
3235 handleUpscale : ( ) => void
@@ -42,6 +45,7 @@ export function ImageResultCard({
4245 isBlurred,
4346 isUpscaled,
4447 isUpscaling : externalIsUpscaling ,
48+ giteeToken,
4549 setShowInfo,
4650 setIsBlurred,
4751 handleUpscale : _externalHandleUpscale ,
@@ -63,6 +67,10 @@ export function ImageResultCard({
6367 const dragStartRef = useRef ( { x : 0 , y : 0 , posX : 0 , posY : 0 } )
6468 const imageContainerRef = useRef < HTMLDivElement > ( null )
6569
70+ // Video generation state
71+ const { videoState, generateVideo } = useVideoGenerator ( )
72+ const [ showVideo , setShowVideo ] = useState ( false )
73+
6674 // Use display URL if set (after applying upscale), otherwise original
6775 const currentImageUrl = displayUrl || imageDetails ?. url
6876
@@ -119,6 +127,32 @@ export function ImageResultCard({
119127 setIsComparing ( false )
120128 } , [ ] )
121129
130+ // Handle video generation
131+ const handleGenerateVideo = useCallback ( async ( ) => {
132+ if ( ! currentImageUrl || ! imageDetails ) return
133+
134+ if ( ! giteeToken ) {
135+ toast . error ( 'Please configure Gitee AI token first' )
136+ return
137+ }
138+
139+ const width = Number . parseInt ( imageDetails . dimensions . split ( 'x' ) [ 0 ] , 10 ) || 1024
140+ const height = Number . parseInt ( imageDetails . dimensions . split ( 'x' ) [ 1 ] , 10 ) || 1024
141+
142+ toast . info ( 'Starting video generation...' )
143+ await generateVideo ( currentImageUrl , imageDetails . prompt , width , height , 'gitee' , giteeToken )
144+ } , [ currentImageUrl , imageDetails , generateVideo , giteeToken ] )
145+
146+ // Show video when generation succeeds
147+ useEffect ( ( ) => {
148+ if ( videoState . status === 'success' ) {
149+ setShowVideo ( true )
150+ toast . success ( 'Video generated successfully!' )
151+ } else if ( videoState . status === 'failed' ) {
152+ toast . error ( videoState . error || 'Video generation failed' )
153+ }
154+ } , [ videoState . status , videoState . error ] )
155+
122156 // Zoom controls
123157 const handleZoomIn = useCallback ( ( ) => {
124158 setScale ( ( s ) => Math . min ( s * 1.5 , 8 ) )
@@ -231,14 +265,25 @@ export function ImageResultCard({
231265 < div className = "relative rounded-lg overflow-hidden bg-zinc-900 border border-zinc-800 group" >
232266 { imageDetails ? (
233267 < >
234- { /* Normal image display with double-click to fullscreen */ }
235- < img
236- src = { currentImageUrl || '' }
237- alt = "Generated"
238- className = { `w-full transition-all duration-300 cursor-pointer ${ isBlurred ? 'blur-xl' : '' } ` }
239- onDoubleClick = { handleDoubleClick }
240- title = "Double-click to view fullscreen"
241- />
268+ { /* Video or Image display */ }
269+ { showVideo && videoState . videoUrl ? (
270+ < video
271+ src = { videoState . videoUrl }
272+ controls
273+ autoPlay
274+ loop
275+ muted
276+ className = "w-full"
277+ />
278+ ) : (
279+ < img
280+ src = { currentImageUrl || '' }
281+ alt = "Generated"
282+ className = { `w-full transition-all duration-300 cursor-pointer ${ isBlurred ? 'blur-xl' : '' } ` }
283+ onDoubleClick = { handleDoubleClick }
284+ title = "Double-click to view fullscreen"
285+ />
286+ ) }
242287
243288 { /* Floating Toolbar */ }
244289 < div className = "absolute bottom-3 left-1/2 -translate-x-1/2 pointer-events-none" >
@@ -271,6 +316,39 @@ export function ImageResultCard({
271316 { isBlurred ? < EyeOff className = "w-5 h-5" /> : < Eye className = "w-5 h-5" /> }
272317 </ button >
273318 < div className = "w-px h-5 bg-white/10" />
319+ { /* Generate Video */ }
320+ < button
321+ type = "button"
322+ onClick = {
323+ videoState . status === 'success'
324+ ? ( ) => setShowVideo ( ! showVideo )
325+ : handleGenerateVideo
326+ }
327+ disabled = {
328+ videoState . status === 'generating' || videoState . status === 'polling'
329+ }
330+ title = {
331+ videoState . status === 'generating' || videoState . status === 'polling'
332+ ? 'Generating video...'
333+ : videoState . status === 'success'
334+ ? showVideo
335+ ? 'Show image'
336+ : 'Show video'
337+ : 'Generate video'
338+ }
339+ className = { `flex items-center justify-center w-10 h-10 rounded-xl transition-all ${
340+ videoState . status === 'success'
341+ ? 'text-green-400 bg-green-500/10'
342+ : 'text-white/70 hover:text-white hover:bg-white/10'
343+ } disabled:cursor-not-allowed disabled:opacity-50`}
344+ >
345+ { videoState . status === 'generating' || videoState . status === 'polling' ? (
346+ < Loader2 className = "w-5 h-5 animate-spin text-orange-400" />
347+ ) : (
348+ < Video className = "w-5 h-5" />
349+ ) }
350+ </ button >
351+ < div className = "w-px h-5 bg-white/10" />
274352 { /* Download */ }
275353 < button
276354 type = "button"
0 commit comments