mxivideo/src/components/AICreationChat.tsx

357 lines
13 KiB
TypeScript
Raw 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.

import React, { useState, useRef, useEffect } from 'react'
import { Sparkles, Send, User, Bot, Wand2, Play, Download, Loader } from 'lucide-react'
import { Project } from '../services/projectService'
import { Model } from '../services/modelService'
interface AICreationChatProps {
project: Project
models: Model[]
onMaterialCreated: () => void
}
interface ChatMessage {
id: string
type: 'user' | 'assistant' | 'system'
content: string
timestamp: Date
creationTask?: CreationTask
}
interface CreationTask {
id: string
model: Model
prompt: string
status: 'pending' | 'processing' | 'completed' | 'failed'
progress: number
result?: {
videoPath: string
thumbnailPath: string
duration: number
}
error?: string
}
const AICreationChat: React.FC<AICreationChatProps> = ({
project,
models,
onMaterialCreated
}) => {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
type: 'system',
content: `欢迎使用AI创作助手我可以帮您为项目"${project.product_name}"创作视频素材。请告诉我您想要什么样的内容。`,
timestamp: new Date()
}
])
const [inputText, setInputText] = useState('')
const [selectedModel, setSelectedModel] = useState<Model | null>(models[0] || null)
const [isCreating, setIsCreating] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
scrollToBottom()
}, [messages])
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
const handleSendMessage = async () => {
if (!inputText.trim() || !selectedModel) return
const userMessage: ChatMessage = {
id: Date.now().toString(),
type: 'user',
content: inputText.trim(),
timestamp: new Date()
}
setMessages(prev => [...prev, userMessage])
setInputText('')
setIsCreating(true)
// 模拟AI响应
setTimeout(() => {
const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
type: 'assistant',
content: `好的!我将使用模特"${selectedModel.model_number}"为您创作关于"${project.product_name}"的视频素材。正在开始创作...`,
timestamp: new Date()
}
setMessages(prev => [...prev, assistantMessage])
// 开始创作任务
startCreationTask(inputText.trim(), selectedModel)
}, 1000)
}
const startCreationTask = async (prompt: string, model: Model) => {
const taskId = Date.now().toString()
const creationTask: CreationTask = {
id: taskId,
model,
prompt,
status: 'pending',
progress: 0
}
const taskMessage: ChatMessage = {
id: taskId + '_task',
type: 'assistant',
content: '正在创作中...',
timestamp: new Date(),
creationTask
}
setMessages(prev => [...prev, taskMessage])
// 模拟创作过程
const steps = [
{ progress: 10, status: '初始化AI模型...' },
{ progress: 30, status: '分析模特特征...' },
{ progress: 50, status: '生成视频内容...' },
{ progress: 70, status: '渲染视频帧...' },
{ progress: 90, status: '后处理优化...' },
{ progress: 100, status: '创作完成!' }
]
for (const step of steps) {
await new Promise(resolve => setTimeout(resolve, 1500))
const updatedTask = {
...creationTask,
status: step.progress === 100 ? 'completed' as const : 'processing' as const,
progress: step.progress,
result: step.progress === 100 ? {
videoPath: `${project.local_directory}/ai_generated_${taskId}.mp4`,
thumbnailPath: `${project.local_directory}/ai_generated_${taskId}_thumb.jpg`,
duration: 15
} : undefined
}
setMessages(prev => prev.map(msg =>
msg.id === taskId + '_task'
? { ...msg, creationTask: updatedTask }
: msg
))
}
// 添加完成消息
setTimeout(() => {
const completionMessage: ChatMessage = {
id: (Date.now() + 2).toString(),
type: 'assistant',
content: '✨ 视频创作完成!您可以预览或下载生成的素材。需要调整什么吗?',
timestamp: new Date()
}
setMessages(prev => [...prev, completionMessage])
setIsCreating(false)
onMaterialCreated()
}, 500)
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
const getStatusColor = (status: CreationTask['status']) => {
switch (status) {
case 'pending':
return 'text-yellow-600'
case 'processing':
return 'text-blue-600'
case 'completed':
return 'text-green-600'
case 'failed':
return 'text-red-600'
default:
return 'text-gray-600'
}
}
return (
<div className="h-full flex flex-col">
{/* 头部 */}
<div className="p-4 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-blue-50">
<div className="flex items-center mb-3">
<Sparkles className="text-purple-600 mr-2" size={20} />
<h3 className="font-semibold text-gray-900">AI创作助手</h3>
</div>
{/* 模特选择 */}
<div className="mb-2">
<label className="text-xs text-gray-600 mb-1 block">:</label>
<select
value={selectedModel?.id || ''}
onChange={(e) => {
const model = models.find(m => m.id === e.target.value)
setSelectedModel(model || null)
}}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{models.length === 0 ? (
<option value=""></option>
) : (
models.map((model) => (
<option key={model.id} value={model.id}>
{model.model_number}
</option>
))
)}
</select>
</div>
<p className="text-xs text-gray-600">
"{project.product_name}"
</p>
</div>
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[80%] ${message.type === 'user' ? 'order-2' : 'order-1'}`}>
{/* 消息头部 */}
<div className={`flex items-center mb-1 ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`flex items-center ${message.type === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${
message.type === 'user'
? 'bg-blue-600 text-white ml-2'
: message.type === 'system'
? 'bg-purple-600 text-white mr-2'
: 'bg-gray-600 text-white mr-2'
}`}>
{message.type === 'user' ? (
<User size={12} />
) : message.type === 'system' ? (
<Sparkles size={12} />
) : (
<Bot size={12} />
)}
</div>
<span className="text-xs text-gray-500">
{message.timestamp.toLocaleTimeString()}
</span>
</div>
</div>
{/* 消息内容 */}
<div className={`rounded-lg px-3 py-2 ${
message.type === 'user'
? 'bg-blue-600 text-white'
: message.type === 'system'
? 'bg-purple-100 text-purple-800 border border-purple-200'
: 'bg-gray-100 text-gray-800'
}`}>
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
{/* 创作任务卡片 */}
{message.creationTask && (
<div className="mt-3 p-3 bg-white rounded-lg border border-gray-200">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Wand2 size={14} className="text-purple-600 mr-1" />
<span className="text-sm font-medium text-gray-900">
{message.creationTask.model.model_number}
</span>
</div>
<span className={`text-xs ${getStatusColor(message.creationTask.status)}`}>
{message.creationTask.status === 'pending' && '等待中'}
{message.creationTask.status === 'processing' && '创作中'}
{message.creationTask.status === 'completed' && '已完成'}
{message.creationTask.status === 'failed' && '失败'}
</span>
</div>
<p className="text-xs text-gray-600 mb-2">
{message.creationTask.prompt}
</p>
{/* 进度条 */}
{message.creationTask.status === 'processing' && (
<div className="mb-2">
<div className="flex items-center justify-between text-xs text-gray-600 mb-1">
<span></span>
<span>{message.creationTask.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className="bg-purple-600 h-1 rounded-full transition-all duration-300"
style={{ width: `${message.creationTask.progress}%` }}
></div>
</div>
</div>
)}
{/* 结果操作 */}
{message.creationTask.status === 'completed' && message.creationTask.result && (
<div className="flex items-center space-x-2 mt-2">
<button className="flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors">
<Play size={12} className="mr-1" />
</button>
<button className="flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors">
<Download size={12} className="mr-1" />
</button>
<span className="text-xs text-gray-500">
{message.creationTask.result.duration}s
</span>
</div>
)}
</div>
)}
</div>
</div>
</div>
))}
{isCreating && (
<div className="flex justify-start">
<div className="flex items-center space-x-2 text-gray-500">
<Loader className="animate-spin" size={16} />
<span className="text-sm">AI正在思考...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 输入区域 */}
<div className="p-4 border-t border-gray-200 bg-gray-50">
<div className="flex space-x-2">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyPress}
placeholder={selectedModel ? `告诉我您想要什么样的${project.product_name}视频...` : '请先选择一个模特'}
disabled={!selectedModel || isCreating}
rows={2}
className="flex-1 resize-none border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-sm"
/>
<button
onClick={handleSendMessage}
disabled={!inputText.trim() || !selectedModel || isCreating}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center"
>
<Send size={16} />
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Enter Shift + Enter
</p>
</div>
</div>
)
}
export default AICreationChat