kv-app/components/tabs/CreateTab.tsx

345 lines
No EOL
11 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Sparkles, Play, Download, RefreshCw, Loader2, CheckCircle, AlertCircle } from 'lucide-react'
import { useSettings } from '@/lib/context/SettingsContext'
import { createWorkflowWithAI } from '@/lib/api/opencode'
import { generateImage, generateVideo, waitForTaskCompletion } from '@/lib/api/api34ai'
import { WorkflowNode } from '@/lib/types'
const nodeIcons: Record<string, string> = {
idea: '💡',
script: '📝',
image: '🖼️',
video: '🎬',
}
const nodeLabels: Record<string, string> = {
idea: 'Idea',
script: 'Script',
image: 'Image',
video: 'Video',
}
export default function CreateTab() {
const { settings } = useSettings()
const [idea, setIdea] = useState('')
const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false)
const [isExecuting, setIsExecuting] = useState(false)
const [workflow, setWorkflow] = useState<WorkflowNode[]>([])
const [currentStep, setCurrentStep] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const handleCreateWorkflow = async () => {
if (!idea.trim()) {
setError('Please enter a video idea')
return
}
if (!settings.opencodeApiKey) {
setError('Please configure OpenCode API key in Settings')
return
}
setIsCreatingWorkflow(true)
setError(null)
try {
const result = await createWorkflowWithAI(
settings.opencodeApiKey,
settings.opencodeModel,
idea
)
const nodes: WorkflowNode[] = result.nodes.map((node, index) => ({
id: `node-${index}`,
type: node.type,
data: {
prompt: node.prompt,
status: 'idle',
model: node.type === 'image'
? settings.imageModel
: node.type === 'video'
? settings.videoModel
: undefined,
duration: settings.defaultDuration,
aspectRatio: settings.defaultAspectRatio,
},
position: { x: 100, y: 100 + index * 150 },
}))
setWorkflow(nodes)
} catch (e: any) {
setError(e.message || 'Failed to create workflow')
} finally {
setIsCreatingWorkflow(false)
}
}
const handleExecuteWorkflow = async () => {
if (!settings.api34aiKey) {
setError('Please configure 34ai API key in Settings')
return
}
setIsExecuting(true)
setError(null)
try {
for (const node of workflow) {
setCurrentStep(nodeLabels[node.type])
const updatedNodes = [...workflow]
const nodeIndex = updatedNodes.findIndex(n => n.id === node.id)
updatedNodes[nodeIndex] = {
...node,
data: { ...node.data, status: 'running' },
}
setWorkflow(updatedNodes)
try {
if (node.type === 'image') {
const result = await generateImage(
settings.api34aiKey,
{
prompt: node.data.prompt,
model: node.data.model || 'banana_pro_2',
ratio: node.data.aspectRatio || '1:1',
resolution: '1K'
}
)
const imageUrl = await waitForTaskCompletion(
settings.api34aiKey,
result.task_id
)
updatedNodes[nodeIndex] = {
...updatedNodes[nodeIndex],
data: {
...updatedNodes[nodeIndex].data,
status: 'completed',
result: imageUrl,
},
}
setWorkflow([...updatedNodes])
}
else if (node.type === 'video') {
const videoResult = await generateVideo(
settings.api34aiKey,
{
prompt: node.data.prompt,
model: node.data.model || 'veo3.1',
duration: node.data.duration || 4,
ratio: node.data.aspectRatio || '16:9',
resolution: '720p',
quality: 'fast'
}
)
const videoUrl = await waitForTaskCompletion(
settings.api34aiKey,
videoResult.task_id
)
updatedNodes[nodeIndex] = {
...updatedNodes[nodeIndex],
data: {
...updatedNodes[nodeIndex].data,
status: 'completed',
result: videoUrl,
},
}
setWorkflow([...updatedNodes])
}
else {
updatedNodes[nodeIndex] = {
...updatedNodes[nodeIndex],
data: {
...updatedNodes[nodeIndex].data,
status: 'completed',
},
}
setWorkflow([...updatedNodes])
}
} catch (nodeError: any) {
updatedNodes[nodeIndex] = {
...updatedNodes[nodeIndex],
data: {
...updatedNodes[nodeIndex].data,
status: 'error',
error: nodeError.message,
},
}
setWorkflow([...updatedNodes])
throw nodeError
}
}
} catch (e: any) {
setError(e.message || 'Workflow execution failed')
} finally {
setIsExecuting(false)
setCurrentStep(null)
}
}
const downloadMedia = async (url: string, filename: string) => {
try {
const response = await fetch(url)
const blob = await response.blob()
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename
link.click()
window.URL.revokeObjectURL(downloadUrl)
} catch (e) {
console.error('Download failed:', e)
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <Loader2 className="animate-spin text-primary" size={16} />
case 'completed':
return <CheckCircle className="text-green-500" size={16} />
case 'error':
return <AlertCircle className="text-red-500" size={16} />
default:
return <div className="w-4 h-4 rounded-full bg-slate-600" />
}
}
return (
<div className="space-y-6">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Create Video from Idea</h2>
<p className="text-slate-400">Enter your video idea and let AI create the workflow</p>
</div>
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<label className="block text-sm font-medium text-slate-300 mb-2">
Your Video Idea
</label>
<textarea
value={idea}
onChange={(e) => setIdea(e.target.value)}
placeholder="E.g., A robot learning to dance at a disco, with neon lights and upbeat music"
className="w-full h-32 bg-slate-800 border border-slate-600 rounded-lg px-4 py-3 text-white placeholder-slate-500 focus:outline-none focus:border-primary resize-none"
/>
<div className="mt-4 flex gap-3">
<button
onClick={handleCreateWorkflow}
disabled={isCreatingWorkflow}
className="flex items-center gap-2 px-6 py-2.5 bg-primary hover:bg-primary-dark disabled:bg-slate-600 rounded-lg text-white font-medium transition-colors"
>
{isCreatingWorkflow ? (
<Loader2 className="animate-spin" size={18} />
) : (
<Sparkles size={18} />
)}
Create Workflow
</button>
{workflow.length > 0 && (
<button
onClick={() => setWorkflow([])}
className="flex items-center gap-2 px-6 py-2.5 bg-slate-700 hover:bg-slate-600 rounded-lg text-white font-medium transition-colors"
>
<RefreshCw size={18} />
Reset
</button>
)}
</div>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500 rounded-lg p-4 text-red-400">
{error}
</div>
)}
{currentStep && (
<div className="bg-primary/10 border border-primary rounded-lg p-4 text-primary flex items-center gap-2">
<Loader2 className="animate-spin" size={18} />
Executing: {currentStep}...
</div>
)}
{workflow.length > 0 && (
<div className="bg-surface rounded-xl p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Workflow</h3>
<button
onClick={handleExecuteWorkflow}
disabled={isExecuting}
className="flex items-center gap-2 px-6 py-2.5 bg-green-600 hover:bg-green-700 disabled:bg-slate-600 rounded-lg text-white font-medium transition-colors"
>
{isExecuting ? (
<Loader2 className="animate-spin" size={18} />
) : (
<Play size={18} />
)}
{isExecuting ? 'Running...' : 'Run Pipeline'}
</button>
</div>
<div className="space-y-4">
{workflow.map((node, index) => (
<div
key={node.id}
className="flex items-start gap-4 p-4 bg-slate-800 rounded-lg border border-slate-600"
>
<div className="flex items-center gap-3 min-w-[120px]">
<span className="text-2xl">{nodeIcons[node.type]}</span>
<span className="font-medium text-white">{nodeLabels[node.type]}</span>
</div>
<div className="flex-1">
<p className="text-slate-300 text-sm">{node.data.prompt}</p>
{node.data.result && (
<div className="mt-3">
{node.type === 'image' && (
<img
src={node.data.result}
alt="Generated"
className="max-h-40 rounded-lg border border-slate-600"
/>
)}
{node.type === 'video' && (
<video
src={node.data.result}
controls
className="max-h-40 rounded-lg border border-slate-600"
/>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">
{getStatusIcon(node.data.status)}
{node.data.status === 'completed' && node.data.result && (
<button
onClick={() => downloadMedia(
node.data.result!,
`${node.type}-${Date.now()}.${node.type === 'video' ? 'mp4' : 'jpg'}`
)}
className="p-2 hover:bg-slate-700 rounded-lg transition-colors"
title="Download"
>
<Download size={18} className="text-slate-400 hover:text-white" />
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}