mirror of
https://github.com/Wan-Video/Wan2.1.git
synced 2025-11-02 21:42:16 +00:00
This commit implements Phase 3 of the Wan2.1 PWA, closing all critical integration gaps between frontend, backend, database, and Replicate API. ## Backend Integration ✅ ### Database Writes - Create generation records BEFORE calling Replicate - Store job_id for tracking Replicate predictions - Track progress, status, and completion timestamps - Save video URLs and error messages ### Credit System - Atomic credit deduction using database function deduct_credits() - Automatic refunds on generation failures via refund_credits() - Complete audit trail in credit_transactions table - Transaction logging for all credit operations ### Webhook Handler - Created /api/webhooks/replicate endpoint - HMAC signature verification for security - Automatic status updates from Replicate push notifications - Maps Replicate statuses to application statuses - Triggers refunds for failed generations ### Updated Generation Flow 1. Check user credits before starting 2. Create generation record (status: queued) 3. Start Replicate job and get job_id 4. Update record with job_id (status: processing) 5. Deduct credits atomically 6. Webhook updates status when complete 7. Polling fallback if webhook fails ## Frontend Enhancements ✅ ### Error Handling - Added sonner for beautiful toast notifications - Success/error/loading states with retry actions - User-friendly error messages - Providers component wraps app with Toaster ### Form Validation - Zod schemas for T2V and I2V inputs - Prompt length validation (10-500 chars) - Model and resolution validation - Credit cost calculator ### Credit Management - useCredits hook for real-time credit fetching - Optimistic updates on generation start - Credit refresh functionality - Loading and error states ### Image Upload - Drag-and-drop ImageUpload component - Client-side validation (file type, size) - Image preview functionality - Max 10MB size limit with user feedback - Ready for I2V integration ### Settings Page - Basic settings page structure - Placeholders for Profile, Billing, API Keys - Ready for Phase 4 enhancements ## Database Changes ✅ ### New Migration: 002_credit_system.sql - credit_transactions table with audit trail - deduct_credits() function for atomic operations - add_credits() function for purchases/bonuses - refund_credits() function for failed generations - Added job_id, progress, error_message columns to generations ## Documentation ✅ ### PHASE_3_IMPLEMENTATION.md - Complete implementation guide - Testing checklist (backend, frontend, E2E) - Deployment steps with webhook registration - Known issues and limitations - Metrics to monitor - Phase 4 roadmap ## Files Changed ### Backend (4 files) - apps/api/main.py - Added webhooks router - apps/api/routes/generation.py - Complete rewrite with DB integration - apps/api/routes/webhooks.py - NEW webhook handler - packages/db/migrations/002_credit_system.sql - NEW credit system ### Frontend (7 files) - apps/web/package.json - Added sonner - apps/web/src/app/layout.tsx - Added Providers wrapper - apps/web/src/app/dashboard/settings/page.tsx - NEW settings page - apps/web/src/components/providers.tsx - NEW toast provider - apps/web/src/components/generation/image-upload.tsx - NEW upload component - apps/web/src/lib/hooks/use-credits.ts - NEW credit management hook - apps/web/src/lib/validation/generation.ts - NEW Zod schemas ### Documentation (1 file) - PHASE_3_IMPLEMENTATION.md - NEW comprehensive guide ## Testing Required ### Backend - [ ] Database writes on generation start - [ ] Credit deduction accuracy - [ ] Webhook updates from Replicate - [ ] Refunds on failures ### Frontend - [ ] Toast notifications - [ ] Form validation - [ ] Credit display and warnings - [ ] Image upload ### Integration - [ ] End-to-end generation flow - [ ] Credit deduction → generation → completion - [ ] Webhook vs polling updates ## Next Steps (Phase 4) 1. Payment integration with Stripe 2. Retry logic for failed generations 3. Cancel in-progress generations 4. In-app video player 5. Batch operations 6. Admin panel ## Environment Variables ### New Required Variables - REPLICATE_WEBHOOK_SECRET - For webhook signature verification See PHASE_3_IMPLEMENTATION.md for complete setup instructions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useCallback } from "react"
|
|
import { Upload, X } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { toast } from "sonner"
|
|
|
|
interface ImageUploadProps {
|
|
onImageSelect: (file: File) => void
|
|
onImageRemove: () => void
|
|
maxSizeMB?: number
|
|
}
|
|
|
|
export function ImageUpload({ onImageSelect, onImageRemove, maxSizeMB = 10 }: ImageUploadProps) {
|
|
const [preview, setPreview] = useState<string | null>(null)
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
|
|
const validateAndProcessFile = useCallback(
|
|
(file: File) => {
|
|
// Validate file type
|
|
if (!file.type.startsWith("image/")) {
|
|
toast.error("Invalid file type", {
|
|
description: "Please upload an image file (PNG, JPG, WEBP)",
|
|
})
|
|
return false
|
|
}
|
|
|
|
// Validate file size
|
|
const maxSizeBytes = maxSizeMB * 1024 * 1024
|
|
if (file.size > maxSizeBytes) {
|
|
toast.error("File too large", {
|
|
description: `Image must be under ${maxSizeMB}MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`,
|
|
})
|
|
return false
|
|
}
|
|
|
|
// Create preview
|
|
const reader = new FileReader()
|
|
reader.onload = () => setPreview(reader.result as string)
|
|
reader.readAsDataURL(file)
|
|
|
|
onImageSelect(file)
|
|
return true
|
|
},
|
|
[maxSizeMB, onImageSelect]
|
|
)
|
|
|
|
const handleDrop = useCallback(
|
|
(e: React.DragEvent<HTMLDivElement>) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
|
|
const file = e.dataTransfer.files[0]
|
|
if (file) {
|
|
validateAndProcessFile(file)
|
|
}
|
|
},
|
|
[validateAndProcessFile]
|
|
)
|
|
|
|
const handleFileInput = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) {
|
|
validateAndProcessFile(file)
|
|
}
|
|
},
|
|
[validateAndProcessFile]
|
|
)
|
|
|
|
const handleRemove = () => {
|
|
setPreview(null)
|
|
onImageRemove()
|
|
}
|
|
|
|
if (preview) {
|
|
return (
|
|
<div className="relative">
|
|
<img src={preview} alt="Upload preview" className="w-full rounded-lg object-cover" />
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="icon"
|
|
className="absolute right-2 top-2"
|
|
onClick={handleRemove}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
onDrop={handleDrop}
|
|
onDragOver={(e) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
className={`rounded-lg border-2 border-dashed p-8 text-center transition cursor-pointer ${
|
|
isDragging
|
|
? "border-primary bg-primary/5"
|
|
: "border-muted hover:border-primary/50 hover:bg-muted/50"
|
|
}`}
|
|
>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleFileInput}
|
|
className="hidden"
|
|
id="image-upload"
|
|
/>
|
|
<label htmlFor="image-upload" className="cursor-pointer">
|
|
<Upload className="mx-auto mb-4 h-8 w-8 text-muted-foreground" />
|
|
<p className="font-medium">Drop an image here</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
or click to browse (PNG, JPG, WEBP • Max {maxSizeMB}MB)
|
|
</p>
|
|
</label>
|
|
</div>
|
|
)
|
|
}
|