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 { addSpan, endSpan } from '@renderer/services/SpanManagerService'
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 {
CreateVideoParams,
@ -524,7 +524,7 @@ export default class ModernAiProvider {
/**
* 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)
}

View File

@ -5,7 +5,7 @@ import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@render
import { getProviderByModel } from '@renderer/services/AssistantService'
import { withSpanResult } from '@renderer/services/SpanManagerService'
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 {
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') {
const response = await this.apiClient.retrieveVideoContent(params)
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 { 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 'notion-helper'
import { useCallback, useEffect } from 'react'
import useSWR from 'swr'
const logger = loggerService.withContext('useVideo')
export const useVideos = (providerId: string) => {
const videos = useAppSelector((state) => state.video.videoMap[providerId])
@ -17,12 +31,19 @@ export const useVideos = (providerId: string) => {
)
const updateVideo = useCallback(
(update: Partial<Video> & { id: string }) => {
(update: Partial<Omit<Video, 'status'>> & { id: string }) => {
dispatch(updateVideoAction({ providerId, update }))
},
[dispatch, providerId]
)
const setVideo = useCallback(
(video: Video) => {
dispatch(setVideoAction({ providerId, video }))
},
[dispatch, providerId]
)
const setVideos = useCallback(
(newVideos: Video[]) => {
dispatch(setVideosAction({ providerId, videos: newVideos }))
@ -43,6 +64,74 @@ export const useVideos = (providerId: string) => {
}
}, [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 {
videos: videos ?? [],
addVideo,

View File

@ -5,16 +5,12 @@ import { CheckCircleIcon, CircleXIcon, ClockIcon, DownloadIcon, PlusIcon } from
import { useTranslation } from 'react-i18next'
export type VideoListProps = {
providerId: string
videos: Video[]
activeVideoId?: string
setActiveVideoId: (id: string | undefined) => void
}
export const VideoList = ({ providerId, activeVideoId, setActiveVideoId }: VideoListProps) => {
const { videos } = useVideos(providerId)
// const displayVideos = mockVideos
const displayVideos = videos
export const VideoList = ({ videos, activeVideoId, setActiveVideoId }: VideoListProps) => {
return (
<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)}>
<PlusIcon size={24} />
</div>
{displayVideos.map((video) => (
{videos.map((video) => (
<VideoListItem
key={video.id}
video={video}

View File

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

View File

@ -10,7 +10,7 @@ import { isDedicatedImageGenerationModel, isEmbeddingModel } from '@renderer/con
import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
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 type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { type Chunk, ChunkType } from '@renderer/types/chunk'
@ -414,7 +414,7 @@ export async function retrieveVideo(params: RetrieveVideoParams): Promise<Retrie
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)
return ai.retrieveVideoContent(params)
}

View File

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