video-flow-b/components/pages/work-flow/ai-editing-button.tsx
2025-10-27 15:48:42 +08:00

359 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* AI剪辑按钮组件 - video-flow-b集成OpenCut AI剪辑功能
* 文件路径: video-flow-b/components/pages/work-flow/ai-editing-button.tsx
*/
"use client";
import React, { useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Zap,
Loader2,
CheckCircle,
AlertCircle,
Film,
Sparkles,
Play,
Download
} from 'lucide-react';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { createAIEditingAdapter, VideoFlowTaskObject, AIEditingAdapter } from './ai-editing-adapter';
interface AIEditingButtonProps {
/** 项目ID */
projectId: string;
/** video-flow-b任务对象 */
taskObject: VideoFlowTaskObject;
/** 是否禁用按钮 */
disabled?: boolean;
/** 按钮大小 */
size?: 'sm' | 'md' | 'lg';
/** 完成回调 */
onComplete?: (finalVideoUrl: string) => void;
/** 错误回调 */
onError?: (error: string) => void;
}
interface ProgressState {
progress: number;
message: string;
stage: 'idle' | 'processing' | 'completed' | 'error';
}
/**
* AI剪辑按钮组件
* 提供一键AI剪辑功能集成OpenCut的智能剪辑算法
*/
export const AIEditingButton: React.FC<AIEditingButtonProps> = ({
projectId,
taskObject,
disabled = false,
size = 'md',
onComplete,
onError
}) => {
const [progressState, setProgressState] = useState<ProgressState>({
progress: 0,
message: '',
stage: 'idle'
});
const [isProcessing, setIsProcessing] = useState(false);
const [finalVideoUrl, setFinalVideoUrl] = useState<string | null>(null);
// 检查是否可以执行AI剪辑
const canExecute = AIEditingAdapter.canExecuteAIEditing(taskObject);
const availableVideoCount = AIEditingAdapter.getAvailableVideoCount(taskObject);
/**
* 执行AI剪辑的主要逻辑
*/
const handleAIEditing = useCallback(async () => {
if (!canExecute || isProcessing) {
return;
}
setIsProcessing(true);
setProgressState({
progress: 0,
message: '准备开始AI剪辑...',
stage: 'processing'
});
try {
// 创建AI剪辑适配器
const adapter = createAIEditingAdapter(projectId, taskObject, {
onProgress: (progress, message) => {
setProgressState({
progress,
message,
stage: 'processing'
});
},
onComplete: (url) => {
setFinalVideoUrl(url);
setProgressState({
progress: 100,
message: 'AI剪辑完成',
stage: 'completed'
});
onComplete?.(url);
},
onError: (error) => {
setProgressState({
progress: 0,
message: error,
stage: 'error'
});
onError?.(error);
}
});
// 执行自动化AI剪辑
const resultUrl = await adapter.executeAutoAIEditing();
console.log('✅ AI剪辑完成视频URL:', resultUrl);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'AI剪辑过程中发生未知错误';
console.error('❌ AI剪辑失败:', error);
setProgressState({
progress: 0,
message: errorMessage,
stage: 'error'
});
onError?.(errorMessage);
} finally {
setIsProcessing(false);
// 3秒后重置状态
setTimeout(() => {
if (progressState.stage !== 'processing') {
setProgressState({
progress: 0,
message: '',
stage: 'idle'
});
}
}, 3000);
}
}, [projectId, taskObject, canExecute, isProcessing, onComplete, onError, progressState.stage]);
/**
* 获取按钮显示内容
*/
const getButtonContent = () => {
switch (progressState.stage) {
case 'processing':
return {
icon: Loader2,
text: '剪辑中...',
className: 'animate-spin'
};
case 'completed':
return {
icon: CheckCircle,
text: '完成',
className: 'text-green-500'
};
case 'error':
return {
icon: AlertCircle,
text: '失败',
className: 'text-red-500'
};
default:
return {
icon: Zap,
text: 'AI一键剪辑',
className: 'text-blue-500'
};
}
};
const buttonContent = getButtonContent();
const Icon = buttonContent.icon;
return (
<div className="relative">
{/* 主按钮 */}
<motion.button
onClick={handleAIEditing}
disabled={disabled || !canExecute || isProcessing}
className={`
relative flex items-center gap-2 px-4 py-2 rounded-lg
backdrop-blur-lg bg-gradient-to-r from-blue-500/20 to-purple-500/20
border border-white/20 shadow-xl
text-white font-medium text-sm
hover:from-blue-500/30 hover:to-purple-500/30
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-300
${size === 'sm' ? 'px-3 py-1.5 text-xs' : ''}
${size === 'lg' ? 'px-6 py-3 text-base' : ''}
`}
whileHover={{ scale: disabled || !canExecute ? 1 : 1.05 }}
whileTap={{ scale: disabled || !canExecute ? 1 : 0.95 }}
>
<Icon
className={`w-4 h-4 ${buttonContent.className}`}
size={size === 'sm' ? 14 : size === 'lg' ? 18 : 16}
/>
<span>{buttonContent.text}</span>
{/* 闪烁效果 */}
{progressState.stage === 'idle' && canExecute && (
<motion.div
className="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg opacity-20"
animate={{
opacity: [0.2, 0.5, 0.2],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
</motion.button>
{/* 进度条 */}
<AnimatePresence>
{progressState.stage === 'processing' && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute top-full left-0 right-0 mt-2 p-3 rounded-lg backdrop-blur-lg bg-black/30 border border-white/20"
>
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-4 h-4 text-blue-400" />
<span className="text-xs text-white font-medium">AI剪辑进行中</span>
<span className="text-xs text-blue-400 ml-auto">
{Math.round(progressState.progress)}%
</span>
</div>
{/* 进度条 */}
<div className="w-full bg-white/10 rounded-full h-2 mb-2">
<motion.div
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progressState.progress}%` }}
transition={{ duration: 0.3 }}
/>
</div>
{/* 状态消息 */}
<p className="text-xs text-white/80">
{progressState.message}
</p>
</motion.div>
)}
</AnimatePresence>
{/* 不可用提示 */}
{!canExecute && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute -top-12 left-1/2 transform -translate-x-1/2 px-3 py-1 rounded-md bg-yellow-500/90 text-white text-xs whitespace-nowrap"
>
1
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-yellow-500/90" />
</motion.div>
)}
{/* 视频信息提示 */}
{canExecute && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute -bottom-8 left-0 right-0 text-center"
>
<span className="text-xs text-white/60">
<Film className="w-3 h-3 inline mr-1" />
{availableVideoCount}
</span>
</motion.div>
)}
{/* 完成后的下载按钮 */}
<AnimatePresence>
{finalVideoUrl && progressState.stage === 'completed' && (
<motion.a
href={finalVideoUrl}
download
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="absolute -right-12 top-0 p-2 rounded-full backdrop-blur-lg bg-green-500/20 border border-green-500/30 text-green-400 hover:bg-green-500/30 transition-colors"
title="下载视频"
>
<Download className="w-4 h-4" />
</motion.a>
)}
</AnimatePresence>
</div>
);
};
/**
* 简化版AI剪辑图标按钮
* 用于空间受限的场景
*/
export const AIEditingIconButton: React.FC<Omit<AIEditingButtonProps, 'size'> & {
size?: 'sm' | 'md' | 'lg'
}> = ({
projectId,
taskObject,
disabled = false,
size = 'md',
onComplete,
onError
}) => {
const [isProcessing, setIsProcessing] = useState(false);
const canExecute = AIEditingAdapter.canExecuteAIEditing(taskObject);
const handleClick = useCallback(async () => {
if (!canExecute || isProcessing) return;
setIsProcessing(true);
try {
const adapter = createAIEditingAdapter(projectId, taskObject, {
onComplete: (url) => {
onComplete?.(url);
setIsProcessing(false);
},
onError: (error) => {
onError?.(error);
setIsProcessing(false);
}
});
await adapter.executeAutoAIEditing();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'AI剪辑失败';
onError?.(errorMessage);
setIsProcessing(false);
}
}, [projectId, taskObject, canExecute, isProcessing, onComplete, onError]);
return (
<GlassIconButton
icon={isProcessing ? Loader2 : Zap}
size={size}
tooltip={canExecute ? "AI一键剪辑" : "需要完成的视频片段"}
onClick={handleClick}
disabled={disabled || !canExecute || isProcessing}
className={isProcessing ? "animate-pulse" : ""}
/>
);
};
export default AIEditingButton;