306 lines
No EOL
11 KiB
TypeScript
306 lines
No EOL
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useRef } from 'react'
|
|
import Header from '@/components/Header'
|
|
import { Upload, Loader2, Sparkles, Download, Play } from 'lucide-react'
|
|
import { useSettings } from '@/lib/context/SettingsContext'
|
|
import { analyzeMediaWithAI } from '@/lib/api/opencode'
|
|
import { generateVideo, waitForTaskCompletion } from '@/lib/api/api34ai'
|
|
|
|
interface AnalysisResult {
|
|
contentSummary: string
|
|
style: { lighting: string; colorPalette: string; mood: string }
|
|
motion: { type: string; transitions: string[]; duration: string }
|
|
script: string
|
|
keyframes: { time: string; description: string }[]
|
|
}
|
|
|
|
export default function UploadPage() {
|
|
const { settings, isMounted } = useSettings()
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const [file, setFile] = useState<File | null>(null)
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
|
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|
const [analysis, setAnalysis] = useState<AnalysisResult | null>(null)
|
|
const [generatedVideo, setGeneratedVideo] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
if (!isMounted) {
|
|
return (
|
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
<div className="text-white">Loading...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}
|
|
|
|
const handleDragLeave = () => {
|
|
setIsDragging(false)
|
|
}
|
|
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
|
|
const droppedFile = e.dataTransfer.files[0]
|
|
if (droppedFile) {
|
|
handleFileSelect(droppedFile)
|
|
}
|
|
}
|
|
|
|
const handleFileSelect = (selectedFile: File) => {
|
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/quicktime']
|
|
if (!validTypes.includes(selectedFile.type)) {
|
|
setError('Invalid file type. Please upload JPG, PNG, GIF, or MP4.')
|
|
return
|
|
}
|
|
|
|
setFile(selectedFile)
|
|
setPreviewUrl(URL.createObjectURL(selectedFile))
|
|
setAnalysis(null)
|
|
setGeneratedVideo(null)
|
|
setError(null)
|
|
}
|
|
|
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFile = e.target.files?.[0]
|
|
if (selectedFile) {
|
|
handleFileSelect(selectedFile)
|
|
}
|
|
}
|
|
|
|
const handleAnalyze = async () => {
|
|
if (!file || !previewUrl) return
|
|
|
|
if (!settings.opencodeApiKey) {
|
|
setError('Please configure OpenCode API key in Settings')
|
|
return
|
|
}
|
|
|
|
setIsAnalyzing(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const mediaType = file.type.startsWith('video') ? 'video' : 'image'
|
|
const result = await analyzeMediaWithAI(
|
|
settings.opencodeApiKey,
|
|
settings.opencodeModel,
|
|
mediaType,
|
|
previewUrl
|
|
)
|
|
setAnalysis(result)
|
|
} catch (e: any) {
|
|
setError(e.message || 'Failed to analyze media')
|
|
} finally {
|
|
setIsAnalyzing(false)
|
|
}
|
|
}
|
|
|
|
const handleRecreate = async () => {
|
|
if (!analysis || !settings.api34aiKey) return
|
|
|
|
setIsGenerating(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const videoResult = await generateVideo(
|
|
settings.api34aiKey,
|
|
{
|
|
prompt: analysis.script,
|
|
model: settings.videoModel || 'veo3.1',
|
|
duration: settings.defaultDuration,
|
|
ratio: settings.defaultAspectRatio,
|
|
resolution: '720p',
|
|
quality: 'fast'
|
|
}
|
|
)
|
|
|
|
const videoUrl = await waitForTaskCompletion(
|
|
settings.api34aiKey,
|
|
videoResult.task_id
|
|
)
|
|
|
|
setGeneratedVideo(videoUrl)
|
|
} catch (e: any) {
|
|
setError(e.message || 'Failed to generate video')
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
const downloadVideo = async () => {
|
|
if (!generatedVideo) return
|
|
|
|
try {
|
|
const response = await fetch(generatedVideo)
|
|
const blob = await response.blob()
|
|
const url = window.URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `recreated-${Date.now()}.mp4`
|
|
link.click()
|
|
window.URL.revokeObjectURL(url)
|
|
} catch (e) {
|
|
console.error('Download failed:', e)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
<main className="pt-20 pb-8 px-4">
|
|
<div className="max-w-4xl mx-auto space-y-6">
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-2xl font-bold text-white mb-2">Upload & Analyze Media</h2>
|
|
<p className="text-slate-400">Upload an image or video for AI to analyze and recreate</p>
|
|
</div>
|
|
|
|
{!file ? (
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className={`border-2 border-dashed rounded-xl p-12 text-center cursor-pointer transition-colors ${
|
|
isDragging
|
|
? 'border-primary bg-primary/10'
|
|
: 'border-slate-600 hover:border-slate-500'
|
|
}`}
|
|
>
|
|
<Upload className="mx-auto mb-4 text-slate-400" size={48} />
|
|
<p className="text-slate-400 mb-2">Drag & drop files here or click to upload</p>
|
|
<p className="text-slate-500 text-sm">Supports: JPG, PNG, GIF, MP4, MOV</p>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/gif,video/mp4,video/quicktime"
|
|
onChange={handleFileInputChange}
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="bg-surface rounded-xl p-6 border border-slate-700">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<p className="text-white font-medium">{file.name}</p>
|
|
<p className="text-slate-400 text-sm">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setFile(null)
|
|
setPreviewUrl(null)
|
|
setAnalysis(null)
|
|
setGeneratedVideo(null)
|
|
}}
|
|
className="text-slate-400 hover:text-white"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{file.type.startsWith('video') ? (
|
|
<video src={previewUrl!} controls className="w-full rounded-lg max-h-64" />
|
|
) : (
|
|
<img src={previewUrl!} alt="Preview" className="w-full rounded-lg max-h-64 object-contain" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleAnalyze}
|
|
disabled={isAnalyzing}
|
|
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"
|
|
>
|
|
{isAnalyzing ? <Loader2 className="animate-spin" size={18} /> : <Sparkles size={18} />}
|
|
{isAnalyzing ? 'Analyzing...' : 'Analyze with AI'}
|
|
</button>
|
|
|
|
{analysis && (
|
|
<button
|
|
onClick={handleRecreate}
|
|
disabled={isGenerating}
|
|
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"
|
|
>
|
|
{isGenerating ? <Loader2 className="animate-spin" size={18} /> : <Play size={18} />}
|
|
{isGenerating ? 'Generating...' : 'Recreate with 34ai'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-500/10 border border-red-500 rounded-lg p-4 text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{analysis && (
|
|
<div className="bg-surface rounded-xl p-6 border border-slate-700 space-y-4">
|
|
<h3 className="text-lg font-semibold text-white">AI Analysis</h3>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-1">Content Summary</h4>
|
|
<p className="text-slate-300">{analysis.contentSummary}</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-1">Lighting</h4>
|
|
<p className="text-slate-300 text-sm">{analysis.style.lighting}</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-1">Color Palette</h4>
|
|
<p className="text-slate-300 text-sm">{analysis.style.colorPalette}</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-1">Mood</h4>
|
|
<p className="text-slate-300 text-sm">{analysis.style.mood}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-1">Script (AI Generated)</h4>
|
|
<p className="text-slate-300 whitespace-pre-wrap">{analysis.script}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-400 mb-2">Animation Breakdown</h4>
|
|
<div className="space-y-2">
|
|
{analysis.keyframes.map((kf, i) => (
|
|
<div key={i} className="flex gap-3 text-sm">
|
|
<span className="text-primary font-mono">{kf.time}</span>
|
|
<span className="text-slate-300">{kf.description}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{generatedVideo && (
|
|
<div className="bg-surface rounded-xl p-6 border border-slate-700">
|
|
<h3 className="text-lg font-semibold text-white mb-4">Generated Video</h3>
|
|
<video src={generatedVideo} controls className="w-full rounded-lg mb-4" />
|
|
<button
|
|
onClick={downloadVideo}
|
|
className="flex items-center gap-2 px-6 py-2.5 bg-green-600 hover:bg-green-700 rounded-lg text-white font-medium"
|
|
>
|
|
<Download size={18} />
|
|
Download Video
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
} |