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