feat(video): add video deletion functionality

implement video deletion for openai provider
add i18n strings for deletion states and errors
update video list ui to support deletion
handle pending states during deletion
This commit is contained in:
icarus 2025-10-13 21:52:48 +08:00
parent 2fab33de41
commit 85daceb417
12 changed files with 263 additions and 95 deletions

View File

@ -12,7 +12,15 @@ 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, RetrieveVideoContentParams } from '@renderer/types'
import type {
Assistant,
DeleteVideoParams,
DeleteVideoResult,
GenerateImageParams,
Model,
Provider,
RetrieveVideoContentParams
} from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import {
CreateVideoParams,
@ -528,6 +536,13 @@ export default class ModernAiProvider {
return this.legacyProvider.retrieveVideoContent(params)
}
/**
* We manually implement this method before aisdk supports it well
*/
public async deleteVideo(params: DeleteVideoParams): Promise<DeleteVideoResult> {
return this.legacyProvider.deleteVideo(params)
}
public getBaseURL(): string {
return this.legacyProvider.getBaseURL()
}

View File

@ -36,7 +36,12 @@ import {
OpenAIResponseSdkTool,
OpenAIResponseSdkToolCall
} from '@renderer/types/sdk'
import { CreateVideoParams, RetrieveVideoContentParams, RetrieveVideoParams } from '@renderer/types/video'
import {
CreateVideoParams,
DeleteVideoParams,
RetrieveVideoContentParams,
RetrieveVideoParams
} from '@renderer/types/video'
import { addImageFileToContents } from '@renderer/utils/formats'
import {
isSupportedToolUse,
@ -168,6 +173,11 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
return sdk.videos.downloadContent(params.videoId, params.query, params.options)
}
public async deleteVideo(params: DeleteVideoParams): Promise<OpenAI.Videos.VideoDeleteResponse> {
const sdk = await this.getSdkInstance()
return sdk.videos.delete(params.videoId, params.options)
}
private async handlePdfFile(file: FileMetadata): Promise<OpenAI.Responses.ResponseInputFile | undefined> {
if (file.size > 32 * MB) return undefined
try {

View File

@ -5,7 +5,14 @@ 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, RetrieveVideoContentParams } from '@renderer/types'
import type {
DeleteVideoParams,
DeleteVideoResult,
GenerateImageParams,
Model,
Provider,
RetrieveVideoContentParams
} from '@renderer/types'
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
import {
CreateVideoParams,
@ -222,6 +229,18 @@ export default class AiProvider {
}
}
public async deleteVideo(params: DeleteVideoParams): Promise<DeleteVideoResult> {
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
const result = await this.apiClient.deleteVideo(params)
return {
type: 'openai',
result
}
} else {
throw new Error('Video deletion is not supported by this provider')
}
}
public getBaseURL(): string {
return this.apiClient.getBaseURL()
}

View File

@ -1,10 +1,10 @@
import OpenAI from '@cherrystudio/openai'
import { useCallback } from 'react'
import { useVideos } from './useVideos'
import { useProviderVideos } from './useProviderVideos'
export const useAddOpenAIVideo = (providerId: string) => {
const { addVideo } = useVideos(providerId)
const { addVideo } = useProviderVideos(providerId)
const addOpenAIVideo = useCallback(
(video: OpenAI.Videos.Video, prompt: string) => {

View File

@ -1,19 +1,23 @@
import { retrieveVideo } from '@renderer/services/ApiService'
import { SystemProviderIds } from '@renderer/types'
import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'
import { useProvider } from '../useProvider'
import { useVideo } from './useVideo'
export const useOpenAIVideo = (id: string) => {
const providerId = SystemProviderIds.openai
const { provider: openai } = useProvider(providerId)
export const useOpenAIVideo = (providerId: string, id: string) => {
const { provider } = useProvider(providerId)
const fetcher = async () => {
return retrieveVideo({
type: 'openai',
videoId: id,
provider: openai
})
switch (provider.type) {
case 'openai-response':
return retrieveVideo({
type: 'openai',
videoId: id,
provider
})
default:
throw new Error(`Unsupported provider type: ${provider.type}`)
}
}
const video = useVideo(providerId, id)
let options: SWRConfiguration = {}

View File

@ -970,6 +970,7 @@
"delete_confirm": "Are you sure you want to delete?",
"delete_failed": "Failed to delete",
"delete_success": "Deleted successfully",
"deleting": "Deleting...",
"description": "Description",
"detail": "Detail",
"disabled": "Disabled",
@ -1087,6 +1088,9 @@
},
"content": "Content",
"data": "Data",
"delete": {
"failed": "Failed to delete."
},
"detail": "Error Details",
"details": "Details",
"errors": "Errors",
@ -4650,6 +4654,14 @@
"title": "Update"
},
"video": {
"delete": {
"error": {
"not_found": {
"description": "The video was not found remotely. It will only be deleted locally.",
"title": "Video not found."
}
}
},
"error": {
"create": "Failed to create video",
"download": "Failed to download video.",

View File

@ -7,13 +7,14 @@ export type VideoListProps = {
videos: Video[]
activeVideoId?: string
setActiveVideoId: (id: string | undefined) => void
onDelete: (id: string) => void
}
export const VideoList = ({ videos, activeVideoId, setActiveVideoId }: VideoListProps) => {
export const VideoList = ({ videos, activeVideoId, setActiveVideoId, onDelete }: VideoListProps) => {
return (
<div className="w-40 space-y-3 overflow-auto p-2">
<div className="flex w-40 flex-col gap-1 space-y-3 overflow-auto p-2">
<div
className="group relative flex aspect-square cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg"
className="group relative flex aspect-square cursor-pointer items-center justify-center rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg"
onClick={() => setActiveVideoId(undefined)}>
<PlusIcon size={24} />
</div>
@ -24,6 +25,7 @@ export const VideoList = ({ videos, activeVideoId, setActiveVideoId }: VideoList
video={video}
isActive={activeVideoId === video.id}
onClick={() => setActiveVideoId(video.id)}
onDelete={() => onDelete(video.id)}
/>
))}
</div>

View File

@ -1,16 +1,20 @@
import { cn, Progress, Spinner } from '@heroui/react'
import { DeleteIcon } from '@renderer/components/Icons'
import { Video } from '@renderer/types/video'
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
import { CheckCircleIcon, CircleXIcon, ClockIcon, DownloadIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export const VideoListItem = ({
video,
isActive,
onClick
onClick,
onDelete
}: {
video: Video
isActive: boolean
onClick: () => void
onDelete: () => void
}) => {
const { t } = useTranslation()
@ -77,60 +81,70 @@ export const VideoListItem = ({
video.thumbnail !== null
return (
<div
className={cn(
`group relative aspect-square cursor-pointer overflow-hidden rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg ${getStatusColor()}`,
isActive ? 'border-primary' : undefined
)}
onClick={onClick}>
{/* Thumbnail placeholder */}
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-default-100 to-default-200">
{showThumbnail ? (
<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="text-2xl">🎬</div>
<ContextMenu>
<ContextMenuTrigger>
<div
className={cn(
`group relative aspect-square cursor-pointer overflow-hidden rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg ${getStatusColor()}`,
isActive ? 'border-primary' : undefined
)}
onClick={onClick}>
{/* Thumbnail placeholder */}
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-default-100 to-default-200">
{showThumbnail ? (
<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="text-2xl">🎬</div>
</div>
)}
</div>
)}
</div>
{/* Status overlay */}
<div className="absolute inset-0 bg-black/20 opacity-0 transition-opacity group-hover:opacity-100" />
{/* Status overlay */}
<div className="absolute inset-0 bg-black/20 opacity-0 transition-opacity group-hover:opacity-100" />
{/* Status indicator */}
{getStatusIcon() && (
<div className="absolute top-2 right-2 flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 backdrop-blur-sm">
{getStatusIcon()}
<span className="font-medium text-black text-xs">{getStatusLabel()}</span>
{/* Status indicator */}
{getStatusIcon() && (
<div className="absolute top-2 right-2 flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 backdrop-blur-sm">
{getStatusIcon()}
<span className="font-medium text-black text-xs">{getStatusLabel()}</span>
</div>
)}
{/* Progress bar for in_progress and downloading states */}
{showProgress && (
<div className="absolute right-0 bottom-0 left-0 p-2">
<Progress
aria-label="progress bar"
size="sm"
value={video.progress}
color={video.status === 'downloading' ? 'primary' : 'primary'}
className="w-full"
showValueLabel={false}
/>
</div>
)}
{/* Video info overlay */}
<div className="absolute right-0 bottom-0 left-0 bg-gradient-to-t from-black/60 to-transparent p-3 pt-6 opacity-0 transition-opacity group-hover:opacity-100">
<div className="text-white">
<p className="truncate font-medium text-sm">{video.metadata.id}</p>
{video.prompt && <p className="mt-1 line-clamp-2 text-xs opacity-80">{video.prompt}</p>}
</div>
</div>
{/* Failed state overlay */}
{video.status === 'failed' && (
<div className="absolute inset-0 flex items-center justify-center bg-danger/10"></div>
)}
</div>
)}
{/* Progress bar for in_progress and downloading states */}
{showProgress && (
<div className="absolute right-0 bottom-0 left-0 p-2">
<Progress
aria-label="progress bar"
size="sm"
value={video.progress}
color={video.status === 'downloading' ? 'primary' : 'primary'}
className="w-full"
showValueLabel={false}
/>
</div>
)}
{/* Video info overlay */}
<div className="absolute right-0 bottom-0 left-0 bg-gradient-to-t from-black/60 to-transparent p-3 pt-6 opacity-0 transition-opacity group-hover:opacity-100">
<div className="text-white">
<p className="truncate font-medium text-sm">{video.metadata.id}</p>
{video.prompt && <p className="mt-1 line-clamp-2 text-xs opacity-80">{video.prompt}</p>}
</div>
</div>
{/* Failed state overlay */}
{video.status === 'failed' && (
<div className="absolute inset-0 flex items-center justify-center bg-danger/10"></div>
)}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={onDelete}>
<DeleteIcon className="text-danger" />
<span className="text-danger">{t('common.delete')}</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}

View File

@ -2,10 +2,14 @@
import { Divider } from '@heroui/react'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { usePending } from '@renderer/hooks/usePending'
import { useProvider } from '@renderer/hooks/useProvider'
import { useVideos } from '@renderer/hooks/video/useVideos'
import { useProviderVideos } from '@renderer/hooks/video/useProviderVideos'
import { useVideoThumbnail } from '@renderer/hooks/video/useVideoThumbnail'
import { deleteVideo } from '@renderer/services/ApiService'
import { SystemProviderIds } from '@renderer/types'
import { CreateVideoParams } from '@renderer/types/video'
import { getErrorMessage } from '@renderer/utils'
import { deepUpdate } from '@renderer/utils/deepUpdate'
import { isVideoModel } from '@renderer/utils/model/video'
import { DeepPartial } from 'ai'
@ -32,7 +36,12 @@ export const VideoPage = () => {
},
options: {}
})
const { videos, removeVideo } = useProviderVideos(providerId)
// const activeVideo = useMemo(() => mockVideos.find((v) => v.id === activeVideoId), [activeVideoId])
const [activeVideoId, setActiveVideoId] = useState<string>()
const activeVideo = useMemo(() => videos.find((v) => v.id === activeVideoId), [activeVideoId, videos])
const { setPending } = usePending()
const { removeThumbnail } = useVideoThumbnail()
const updateParams = useCallback((update: DeepPartial<Omit<CreateVideoParams, 'type'>>) => {
setParams((prev) => deepUpdate<CreateVideoParams>(prev, update))
@ -47,9 +56,55 @@ export const VideoPage = () => {
[updateParams]
)
const { videos } = useVideos(providerId)
// const activeVideo = useMemo(() => mockVideos.find((v) => v.id === activeVideoId), [activeVideoId])
const activeVideo = useMemo(() => videos.find((v) => v.id === activeVideoId), [activeVideoId, videos])
const afterDeleteVideo = useCallback(
(id: string) => {
removeVideo(id)
removeThumbnail(id)
},
[removeThumbnail, removeVideo]
)
const handleDeleteVideo = useCallback(
async (id: string) => {
switch (provider.type) {
case 'openai-response':
try {
setPending(id, true)
const promise = deleteVideo({
type: 'openai',
videoId: id,
provider
})
window.toast.loading({
title: t('common.deleting'),
promise
})
const result = await promise
if (result.result.deleted) {
afterDeleteVideo(id)
} else {
window.toast.error(t('error.delete.failed'))
}
} catch (e) {
if (e instanceof Error && e.message.includes('404')) {
window.toast.warning({
title: t('video.delete.error.not_found.title'),
description: t('video.delete.error.not_found.description')
})
afterDeleteVideo(id)
} else {
window.toast.error({ title: t('error.delete.failed'), description: getErrorMessage(e) })
}
} finally {
setPending(id, undefined)
}
break
default:
throw new Error(`Provider type "${provider.type}" is not supported for video deletion`)
}
},
[afterDeleteVideo, provider, setPending, t]
)
return (
<div className="flex flex-1 flex-col">
@ -73,7 +128,12 @@ export const VideoPage = () => {
<VideoPanel provider={provider} params={params} updateParams={updateParams} video={activeVideo} />
<Divider orientation="vertical" />
{/* Video list */}
<VideoList videos={videos} activeVideoId={activeVideoId} setActiveVideoId={setActiveVideoId} />
<VideoList
videos={videos}
activeVideoId={activeVideoId}
setActiveVideoId={setActiveVideoId}
onDelete={handleDeleteVideo}
/>
</div>
</div>
)

View File

@ -1,7 +1,8 @@
import { Button, cn, Image, Skeleton, Textarea, Tooltip } from '@heroui/react'
import { loggerService } from '@logger'
import { usePending } from '@renderer/hooks/usePending'
import { useAddOpenAIVideo } from '@renderer/hooks/video/useAddOpenAIVideo'
import { useVideos } from '@renderer/hooks/video/useVideos'
import { useProviderVideos } from '@renderer/hooks/video/useProviderVideos'
import { createVideo, retrieveVideoContent } from '@renderer/services/ApiService'
import FileManager from '@renderer/services/FileManager'
import { FileTypes, Provider, VideoFileMetadata } from '@renderer/types'
@ -13,7 +14,7 @@ import dayjs from 'dayjs'
import { isEmpty } from 'lodash'
import { ArrowUp, CircleXIcon, ImageIcon } from 'lucide-react'
import mime from 'mime-types'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { VideoViewer } from './VideoViewer'
@ -30,20 +31,20 @@ const logger = loggerService.withContext('VideoPanel')
export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanelProps) => {
const { t } = useTranslation()
const addOpenAIVideo = useAddOpenAIVideo(provider.id)
const { setVideo } = useVideos(provider.id)
const { setVideo } = useProviderVideos(provider.id)
const [isProcessing, setIsProcessing] = useState(false)
const { pendingMap, setPending: setPendingById } = usePending()
const fileInputRef = useRef<HTMLInputElement>(null)
const inputReference = params.params.input_reference
const couldCreateVideo = useMemo(
() =>
!isProcessing &&
!isEmpty(params.params.prompt) &&
video?.status !== 'queued' &&
video?.status !== 'downloading' &&
video?.status !== 'in_progress',
[isProcessing, params.params.prompt, video?.status]
video?.status !== 'in_progress' &&
(video === undefined || pendingMap[video.id] !== true),
[params.params.prompt, pendingMap, video]
)
useEffect(() => {
@ -54,9 +55,19 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
}
}, [updateParams, video])
const isPending = video ? pendingMap[video.id] : false
const setPending = useCallback(
(value: boolean) => {
if (video) {
setPendingById(video.id, value ? value : undefined)
}
},
[setPendingById, video]
)
const handleCreateVideo = useCallback(async () => {
if (!couldCreateVideo) return
setIsProcessing(true)
setPending(true)
try {
if (video === undefined) {
const result = await createVideo(params)
@ -75,9 +86,9 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
} catch (e) {
window.toast.error({ title: t('video.error.create'), description: getErrorMessage(e), timeout: 5000 })
} finally {
setIsProcessing(false)
setPending(false)
}
}, [addOpenAIVideo, couldCreateVideo, params, t, video])
}, [addOpenAIVideo, couldCreateVideo, params, setPending, t, video])
const handleRegenerateVideo = useCallback(() => {
window.toast.info('Not implemented')
@ -208,13 +219,13 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
startContent={<ImageIcon size={16} className={cn(inputReference ? 'text-primary' : undefined)} />}
isIconOnly
className="h-6 w-6 min-w-0"
isDisabled={isProcessing}
isDisabled={isPending}
onPress={handleUploadFile}
/>
</Tooltip>
</>
)
}, [handleUploadFile, isProcessing, inputReference, t, updateParams])
}, [handleUploadFile, inputReference, isPending, t, updateParams])
return (
<div className="flex flex-1 flex-col p-2">
@ -231,7 +242,7 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
value={params.params.prompt}
onValueChange={setPrompt}
isClearable
isDisabled={isProcessing}
isDisabled={isPending}
classNames={{ inputWrapper: 'pb-8' }}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
@ -274,7 +285,7 @@ export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanel
radius="full"
isIconOnly
isDisabled={!couldCreateVideo}
isLoading={isProcessing}
isLoading={isPending}
className="h-6 w-6 min-w-0"
onPress={handleCreateVideo}>
<ArrowUp size={16} className="text-primary-foreground" />

View File

@ -10,6 +10,7 @@ import {
Spinner,
useDisclosure
} from '@heroui/react'
import { usePending } from '@renderer/hooks/usePending'
import FileManager from '@renderer/services/FileManager'
import { Video, VideoDownloaded, VideoFailed } from '@renderer/types/video'
import dayjs from 'dayjs'
@ -33,6 +34,8 @@ export type VideoViewerProps =
export const VideoViewer = ({ video, onDownload, onRegenerate }: VideoViewerProps) => {
const { t } = useTranslation()
const [loadSuccess, setLoadSuccess] = useState<boolean | undefined>(undefined)
const { pendingMap } = usePending()
const isPending = video ? pendingMap[video.id] : false
useEffect(() => {
setLoadSuccess(undefined)
}, [video?.id])
@ -43,7 +46,7 @@ export const VideoViewer = ({ video, onDownload, onRegenerate }: VideoViewerProp
{video && video.status === 'queued' && <QueuedVideo />}
{video && video.status === 'in_progress' && <InProgressVideo progress={video.progress} />}
{video && video.status === 'completed' && (
<CompletedVideo video={video} onDownload={onDownload} onRegenerate={onRegenerate} />
<CompletedVideo video={video} isDisabled={isPending} onDownload={onDownload} onRegenerate={onRegenerate} />
)}
{video && video.status === 'downloading' && <DownloadingVideo progress={video.progress} />}
{video && video.status === 'downloaded' && loadSuccess !== false && (
@ -51,7 +54,7 @@ export const VideoViewer = ({ video, onDownload, onRegenerate }: VideoViewerProp
)}
{video && video.status === 'failed' && <FailedVideo error={video.error} />}
{video && video.status === 'downloaded' && loadSuccess === false && (
<LoadFailedVideo onRedownload={onDownload} />
<LoadFailedVideo isDisabled={isPending} onRedownload={onDownload} />
)}
</div>
</>
@ -87,10 +90,12 @@ const InProgressVideo = ({ progress }: { progress: number }) => {
const CompletedVideo = ({
video,
isDisabled,
onDownload,
onRegenerate
}: {
video: Video
isDisabled?: boolean
onDownload: () => void
onRegenerate: () => void
}) => {
@ -101,7 +106,9 @@ const CompletedVideo = ({
<div className="flex h-full w-full flex-col items-center justify-center gap-2 rounded-2xl bg-warning-200">
<Clock9Icon size={64} className="text-warning" />
<span className="font-bold text-2xl">{t('video.expired')}</span>
<Button onPress={onRegenerate}>{t('common.regenerate')}</Button>
<Button onPress={onRegenerate} isDisabled={isDisabled}>
{t('common.regenerate')}
</Button>
</div>
)
}
@ -109,7 +116,9 @@ const CompletedVideo = ({
<div className="flex h-full w-full flex-col items-center justify-center gap-2 rounded-2xl bg-success-200">
<CheckCircleIcon size={64} className="text-success" />
<span className="font-bold text-2xl">{t('video.status.completed')}</span>
<Button onPress={onDownload}>{t('common.download')}</Button>
<Button onPress={onDownload} isDisabled={isDisabled}>
{t('common.download')}
</Button>
</div>
)
}
@ -163,7 +172,7 @@ const FailedVideo = ({ error }: { error: VideoFailed['error'] }) => {
)
}
const LoadFailedVideo = ({ onRedownload }: { onRedownload: () => void }) => {
const LoadFailedVideo = ({ isDisabled, onRedownload }: { isDisabled?: boolean; onRedownload: () => void }) => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
@ -171,7 +180,9 @@ const LoadFailedVideo = ({ onRedownload }: { onRedownload: () => void }) => {
<span className="font-bold text-2xl">{t('video.error.load.message')}</span>
<span>{t('video.error.load.reason')}</span>
<div className="my-2 flex justify-between gap-2">
<Button onPress={onRedownload}>{t('common.redownload')}</Button>
<Button onPress={onRedownload} isDisabled={isDisabled}>
{t('common.redownload')}
</Button>
</div>
</div>
)

View File

@ -10,7 +10,12 @@ 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, RetrieveVideoContentParams } from '@renderer/types'
import type {
DeleteVideoParams,
DeleteVideoResult,
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'
@ -419,6 +424,11 @@ export async function retrieveVideoContent(params: RetrieveVideoContentParams):
return ai.retrieveVideoContent(params)
}
export async function deleteVideo(params: DeleteVideoParams): Promise<DeleteVideoResult> {
const ai = new AiProviderNew(params.provider)
return ai.deleteVideo(params)
}
export function hasApiKey(provider: Provider) {
if (!provider) return false
if (['ollama', 'lmstudio', 'vertexai', 'cherryai'].includes(provider.id)) return true