refactor(video): update video handling and type definitions

- Rename RetrieveVideoParams to RetrieveVideoContentParams for consistency
- Move video list management to parent component
- Add setVideo action and improve video state updates
- Implement video status polling and thumbnail fetching
This commit is contained in:
icarus 2025-10-13 13:22:49 +08:00
parent d8363b5591
commit 8a45fe70d0
7 changed files with 128 additions and 23 deletions

View File

@ -12,7 +12,7 @@ import { loggerService } from '@logger'
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings' import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
import { addSpan, endSpan } from '@renderer/services/SpanManagerService' import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types' import type { Assistant, GenerateImageParams, Model, Provider, RetrieveVideoContentParams } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes' import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { import {
CreateVideoParams, CreateVideoParams,
@ -524,7 +524,7 @@ export default class ModernAiProvider {
/** /**
* We manually implement this method before aisdk supports it well * We manually implement this method before aisdk supports it well
*/ */
public async retrieveVideoContent(params: RetrieveVideoParams): Promise<RetrieveVideoContentResult> { public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
return this.legacyProvider.retrieveVideoContent(params) return this.legacyProvider.retrieveVideoContent(params)
} }

View File

@ -5,7 +5,7 @@ import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@render
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { withSpanResult } from '@renderer/services/SpanManagerService' import { withSpanResult } from '@renderer/services/SpanManagerService'
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { GenerateImageParams, Model, Provider } from '@renderer/types' import type { GenerateImageParams, Model, Provider, RetrieveVideoContentParams } from '@renderer/types'
import type { RequestOptions, SdkModel } from '@renderer/types/sdk' import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
import { import {
CreateVideoParams, CreateVideoParams,
@ -210,7 +210,7 @@ export default class AiProvider {
} }
} }
public async retrieveVideoContent(params: RetrieveVideoParams): Promise<RetrieveVideoContentResult> { public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') { if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
const response = await this.apiClient.retrieveVideoContent(params) const response = await this.apiClient.retrieveVideoContent(params)
return { return {

View File

@ -1,7 +1,21 @@
import { loggerService } from '@logger'
import { retrieveVideo, retrieveVideoContent } from '@renderer/services/ApiService'
import { getProviderById } from '@renderer/services/ProviderService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { addVideoAction, removeVideoAction, setVideosAction, updateVideoAction } from '@renderer/store/video' import {
addVideoAction,
removeVideoAction,
setVideoAction,
setVideosAction,
updateVideoAction
} from '@renderer/store/video'
import { SystemProviderIds } from '@renderer/types'
import { Video } from '@renderer/types/video' import { Video } from '@renderer/types/video'
import { video } from 'notion-helper'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import useSWR from 'swr'
const logger = loggerService.withContext('useVideo')
export const useVideos = (providerId: string) => { export const useVideos = (providerId: string) => {
const videos = useAppSelector((state) => state.video.videoMap[providerId]) const videos = useAppSelector((state) => state.video.videoMap[providerId])
@ -17,12 +31,19 @@ export const useVideos = (providerId: string) => {
) )
const updateVideo = useCallback( const updateVideo = useCallback(
(update: Partial<Video> & { id: string }) => { (update: Partial<Omit<Video, 'status'>> & { id: string }) => {
dispatch(updateVideoAction({ providerId, update })) dispatch(updateVideoAction({ providerId, update }))
}, },
[dispatch, providerId] [dispatch, providerId]
) )
const setVideo = useCallback(
(video: Video) => {
dispatch(setVideoAction({ providerId, video }))
},
[dispatch, providerId]
)
const setVideos = useCallback( const setVideos = useCallback(
(newVideos: Video[]) => { (newVideos: Video[]) => {
dispatch(setVideosAction({ providerId, videos: newVideos })) dispatch(setVideosAction({ providerId, videos: newVideos }))
@ -43,6 +64,74 @@ export const useVideos = (providerId: string) => {
} }
}, [setVideos, videos]) }, [setVideos, videos])
// update videos from api
const openai = getProviderById(SystemProviderIds.openai)
const fetcher = async () => {
if (!videos || !openai) return []
const openaiVideos = videos.filter((v) => v.type === 'openai')
const jobs = openaiVideos.map((v) => retrieveVideo({ type: 'openai', videoId: v.id, provider: openai }))
const result = await Promise.allSettled(jobs)
return result.filter((p) => p.status === 'fulfilled').map((p) => p.value)
}
const { data, isLoading, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
useEffect(() => {
logger.debug('effect', { data, videos })
if (!data || !videos) return
data.forEach((v) => {
const retrievedVideo = v.video
const storeVideo = videos.find((v) => v.id === retrievedVideo.id)
if (!storeVideo) {
logger.warn(`Try to update video ${retrievedVideo.id}, but it's not in the store.`)
return
}
switch (retrievedVideo.status) {
case 'in_progress':
if (storeVideo.status === 'queued' || storeVideo.status === 'in_progress') {
setVideo({
...storeVideo,
status: 'in_progress',
progress: retrievedVideo.progress,
metadata: retrievedVideo
})
}
break
case 'completed':
// Only update it when in_progress/queued -> completed
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
setVideo({ ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo })
// try to request thumbnail here.
if (!openai) {
logger.warn(
`Try to fetch thumbnail for video ${retrievedVideo.id}, but provider ${providerId} not found.`
)
return
}
retrieveVideoContent({
type: 'openai',
provider: openai,
videoId: retrievedVideo.id,
query: { variant: 'thumbnail' }
})
.then((v) => {
// TODO: this is a iamge/webp type response. save it somewhere.
logger.debug('thumbnail resposne', v.response)
})
.catch((e) => {
logger.error(`Failed to get thumbnail for video ${retrievedVideo.id}`, e as Error)
})
}
break
case 'failed':
setVideo({
...storeVideo,
status: 'failed',
error: retrievedVideo.error,
metadata: retrievedVideo
})
}
})
}, [data])
return { return {
videos: videos ?? [], videos: videos ?? [],
addVideo, addVideo,

View File

@ -5,16 +5,12 @@ import { CheckCircleIcon, CircleXIcon, ClockIcon, DownloadIcon, PlusIcon } from
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export type VideoListProps = { export type VideoListProps = {
providerId: string videos: Video[]
activeVideoId?: string activeVideoId?: string
setActiveVideoId: (id: string | undefined) => void setActiveVideoId: (id: string | undefined) => void
} }
export const VideoList = ({ providerId, activeVideoId, setActiveVideoId }: VideoListProps) => { export const VideoList = ({ videos, activeVideoId, setActiveVideoId }: VideoListProps) => {
const { videos } = useVideos(providerId)
// const displayVideos = mockVideos
const displayVideos = videos
return ( return (
<div className="w-40 space-y-3 overflow-auto p-2"> <div className="w-40 space-y-3 overflow-auto p-2">
@ -23,7 +19,7 @@ export const VideoList = ({ providerId, activeVideoId, setActiveVideoId }: Video
onClick={() => setActiveVideoId(undefined)}> onClick={() => setActiveVideoId(undefined)}>
<PlusIcon size={24} /> <PlusIcon size={24} />
</div> </div>
{displayVideos.map((video) => ( {videos.map((video) => (
<VideoListItem <VideoListItem
key={video.id} key={video.id}
video={video} video={video}

View File

@ -18,6 +18,7 @@ import { ProviderSetting } from './settings/ProviderSetting'
import { SettingsGroup } from './settings/shared' import { SettingsGroup } from './settings/shared'
import { VideoList } from './VideoList' import { VideoList } from './VideoList'
import { VideoPanel } from './VideoPanel' import { VideoPanel } from './VideoPanel'
import { useVideos } from '@renderer/hooks/video/useVideos'
export const VideoPage = () => { export const VideoPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -47,6 +48,7 @@ export const VideoPage = () => {
[updateParams] [updateParams]
) )
const { videos } = useVideos(providerId)
const activeVideo = useMemo(() => mockVideos.find((v) => v.id === activeVideoId), [activeVideoId]) const activeVideo = useMemo(() => mockVideos.find((v) => v.id === activeVideoId), [activeVideoId])
return ( return (
@ -71,7 +73,7 @@ export const VideoPage = () => {
<VideoPanel provider={provider} params={params} updateParams={updateParams} video={activeVideo} /> <VideoPanel provider={provider} params={params} updateParams={updateParams} video={activeVideo} />
<Divider orientation="vertical" /> <Divider orientation="vertical" />
{/* Video list */} {/* Video list */}
<VideoList providerId={providerId} activeVideoId={activeVideoId} setActiveVideoId={setActiveVideoId} /> <VideoList videos={videos} activeVideoId={activeVideoId} setActiveVideoId={setActiveVideoId} />
</div> </div>
</div> </div>
) )

View File

@ -10,7 +10,7 @@ import { isDedicatedImageGenerationModel, isEmbeddingModel } from '@renderer/con
import { getStoreSetting } from '@renderer/hooks/useSettings' import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import type { FetchChatCompletionParams } from '@renderer/types' import type { FetchChatCompletionParams, RetrieveVideoContentParams } from '@renderer/types'
import { Assistant, MCPServer, MCPTool, Model, Provider } from '@renderer/types' import { Assistant, MCPServer, MCPTool, Model, Provider } from '@renderer/types'
import type { StreamTextParams } from '@renderer/types/aiCoreTypes' import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { type Chunk, ChunkType } from '@renderer/types/chunk' import { type Chunk, ChunkType } from '@renderer/types/chunk'
@ -414,7 +414,7 @@ export async function retrieveVideo(params: RetrieveVideoParams): Promise<Retrie
return ai.retrieveVideo(params) return ai.retrieveVideo(params)
} }
export async function retrieveVideoContent(params: RetrieveVideoParams): Promise<RetrieveVideoContentResult> { export async function retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
const ai = new AiProviderNew(params.provider) const ai = new AiProviderNew(params.provider)
return ai.retrieveVideoContent(params) return ai.retrieveVideoContent(params)
} }

View File

@ -2,7 +2,7 @@ import { loggerService } from '@logger'
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Video } from '@renderer/types/video' import { Video } from '@renderer/types/video'
const logger = loggerService.withContext('Store:paintings') const logger = loggerService.withContext('Store:video')
export interface VideoState { export interface VideoState {
/** Provider ID to videos */ /** Provider ID to videos */
@ -32,15 +32,32 @@ const videoSlice = createSlice({
}, },
updateVideo: ( updateVideo: (
state: VideoState, state: VideoState,
action: PayloadAction<{ providerId: string; update: Partial<Video> & { id: string } }> action: PayloadAction<{ providerId: string; update: Partial<Omit<Video, 'status'>> & { id: string } }>
) => { ) => {
const { providerId, update } = action.payload const { providerId, update } = action.payload
const videos = state.videoMap[providerId]
const existingIndex = state.videoMap[providerId]?.findIndex((c) => c.id === update.id) if (videos) {
if (existingIndex && existingIndex !== -1) { let video = videos.find((v) => v.id === update.id)
state.videoMap[providerId] = state.videoMap[providerId]?.map((c) => (c.id === update.id ? { ...c, update } : c)) if (video) {
video = { ...video, ...update }
} else {
logger.error(`Video with id ${update.id} not found in ${providerId}`)
}
} else { } else {
logger.error(`Video with id ${update.id} not found in ${providerId}`) logger.error(`Videos with Provider ${providerId} is undefined.`)
}
},
setVideo: (state: VideoState, action: PayloadAction<{ providerId: string; video: Video }>) => {
const { providerId, video } = action.payload
if (state.videoMap[providerId]) {
const index = state.videoMap[providerId].findIndex((v) => v.id === video.id)
if (index !== -1) {
state.videoMap[providerId][index] = video
} else {
state.videoMap[providerId].push(video)
}
} else {
state.videoMap[providerId] = [video]
} }
}, },
setVideos: (state: VideoState, action: PayloadAction<{ providerId: string; videos: Video[] }>) => { setVideos: (state: VideoState, action: PayloadAction<{ providerId: string; videos: Video[] }>) => {
@ -54,6 +71,7 @@ export const {
addVideo: addVideoAction, addVideo: addVideoAction,
removeVideo: removeVideoAction, removeVideo: removeVideoAction,
updateVideo: updateVideoAction, updateVideo: updateVideoAction,
setVideo: setVideoAction,
setVideos: setVideosAction setVideos: setVideosAction
} = videoSlice.actions } = videoSlice.actions