feat(video): add video status tracking and thumbnail handling

- Implement useVideo hook for single video retrieval
- Make thumbnail optional in VideoCompleted interface
- Add prompt parameter to addOpenAIVideo and handle progress updates
- Add auto-refresh for in-progress videos and update progress
This commit is contained in:
icarus 2025-10-12 17:37:56 +08:00
parent eba370210f
commit c3c125f3a3
6 changed files with 56 additions and 14 deletions

View File

@ -7,14 +7,15 @@ export const useAddOpenAIVideo = (providerId: string) => {
const { addVideo } = useVideos(providerId) const { addVideo } = useVideos(providerId)
const addOpenAIVideo = useCallback( const addOpenAIVideo = useCallback(
(video: OpenAI.Videos.Video) => { (video: OpenAI.Videos.Video, prompt: string) => {
switch (video.status) { switch (video.status) {
case 'queued': case 'queued':
addVideo({ addVideo({
id: video.id, id: video.id,
status: video.status, status: video.status,
type: 'openai', type: 'openai',
metadata: video metadata: video,
prompt
}) })
break break
case 'in_progress': case 'in_progress':
@ -23,7 +24,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
status: 'in_progress', status: 'in_progress',
type: 'openai', type: 'openai',
progress: video.progress, progress: video.progress,
metadata: video metadata: video,
prompt
}) })
break break
case 'completed': case 'completed':
@ -31,7 +33,9 @@ export const useAddOpenAIVideo = (providerId: string) => {
id: video.id, id: video.id,
status: 'completed', status: 'completed',
type: 'openai', type: 'openai',
metadata: video metadata: video,
prompt,
thumbnail: null
}) })
break break
case 'failed': case 'failed':
@ -40,7 +44,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
status: 'failed', status: 'failed',
type: 'openai', type: 'openai',
error: video.error, error: video.error,
metadata: video metadata: video,
prompt
}) })
break break
} }

View File

@ -1,11 +1,16 @@
import { retrieveVideo } from '@renderer/services/ApiService' import { retrieveVideo } from '@renderer/services/ApiService'
import { SystemProviderIds } from '@renderer/types' import { SystemProviderIds } from '@renderer/types'
import useSWR, { useSWRConfig } from 'swr' import { useEffect } from 'react'
import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'
import { useProvider } from '../useProvider' import { useProvider } from '../useProvider'
import { useAddOpenAIVideo } from './useAddOpenAIVideo'
import { useVideo } from './useVideo'
import { useVideos } from './useVideos'
export const useOpenAIVideo = (id: string) => { export const useOpenAIVideo = (id: string) => {
const { provider: openai } = useProvider(SystemProviderIds.openai) const providerId = SystemProviderIds.openai
const { provider: openai } = useProvider(providerId)
const fetcher = async () => { const fetcher = async () => {
return retrieveVideo({ return retrieveVideo({
type: 'openai', type: 'openai',
@ -13,12 +18,37 @@ export const useOpenAIVideo = (id: string) => {
provider: openai provider: openai
}) })
} }
const { data, isLoading, error } = useSWR(`video/openai/${id}`, fetcher, { const video = useVideo(providerId, id)
revalidateOnFocus: false, const { updateVideo } = useVideos(providerId)
revalidateOnMount: true const addOpenAIVideo = useAddOpenAIVideo(providerId)
}) let options: SWRConfiguration = {}
switch (video?.status) {
case 'in_progress':
options = {
refreshInterval: 3000
}
break
default:
options = {
revalidateOnFocus: false,
revalidateOnMount: true
}
}
const { data, isLoading, error } = useSWR(`video/openai/${id}`, fetcher, options)
const { mutate } = useSWRConfig() const { mutate } = useSWRConfig()
const revalidate = () => mutate(`video/openai/${id}`) const revalidate = () => mutate(`video/openai/${id}`)
useEffect(() => {
// update progress
if (data && data.video.status === 'in_progress' && data.video.progress) {
if (video) {
updateVideo({ id: video.id, progress: data.video.progress })
} else {
addOpenAIVideo(data.video, 'Prompt lost')
}
}
}, [addOpenAIVideo, data, updateVideo, video])
return { return {
video: data, video: data,
isLoading, isLoading,

View File

@ -0,0 +1,7 @@
import { useVideos } from './useVideos'
export const useVideo = (providerId: string, id: string) => {
const { videos } = useVideos(providerId)
const video = videos.find((v) => v.id === id)
return video
}

View File

@ -89,7 +89,7 @@ const VideoListItem = ({ video, isActive, onClick }: { video: Video; isActive: b
{/* Thumbnail placeholder */} {/* Thumbnail placeholder */}
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-default-100 to-default-200"> <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-default-100 to-default-200">
{showThumbnail ? ( {showThumbnail ? (
<img src={video.thumbnail} alt="Video thumbnail" className="h-full w-full object-cover" /> <img src={video.thumbnail ?? ''} alt="Video thumbnail" className="h-full w-full object-cover" />
) : ( ) : (
<div className="flex flex-col items-center gap-2 text-default-400"> <div className="flex flex-col items-center gap-2 text-default-400">
<div className="text-2xl">🎬</div> <div className="text-2xl">🎬</div>

View File

@ -53,7 +53,7 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
const video = result.video const video = result.video
switch (result.type) { switch (result.type) {
case 'openai': case 'openai':
addOpenAIVideo(video) addOpenAIVideo(video, params.params.prompt)
break break
default: default:
logger.error(`Invalid video type ${result.type}.`) logger.error(`Invalid video type ${result.type}.`)

View File

@ -30,7 +30,7 @@ export interface VideoInProgress extends VideoBase {
export interface VideoCompleted extends VideoBase { export interface VideoCompleted extends VideoBase {
status: 'completed' status: 'completed'
/** When generation completed, firstly try to retrieve thumbnail. */ /** When generation completed, firstly try to retrieve thumbnail. */
thumbnail: string thumbnail: string | null
} }
export interface VideoDownloading extends VideoBase { export interface VideoDownloading extends VideoBase {