kv-app/app/upload/page.tsx

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>
)
}