Spaces:
Runtime error
Runtime error
File size: 8,449 Bytes
5752a28 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | import { useState } from 'react';
import { Upload, Image as ImageIcon, Loader2, Shield } from 'lucide-react';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { API_BASE_URL } from '../config';
interface UploadSectionProps {
onAnalyze: (imageUrl: string) => void;
isAnalyzing: boolean;
}
const SAMPLE_IMAGES = [
{
id: '1',
name: 'Urgency & Scarcity Checkout',
url: `${API_BASE_URL}/api/samples/checkout_urgency.png`,
description: 'Fintech checkout with a countdown timer, fake spot scarcity, live-viewer social proof and a confirmshaming opt-out.'
},
{
id: '2',
name: 'Confirmshaming Cancellation',
url: `${API_BASE_URL}/api/samples/cancel_confirmshaming.png`,
description: 'Subscription cancellation modal using guilt-inducing copy, loss framing and a buried 6-step cancel flow.'
},
{
id: '3',
name: 'Pre-Selected Fees Signup',
url: `${API_BASE_URL}/api/samples/signup_sneaking.png`,
description: 'Loan account signup with pre-checked paid add-ons, a trial that silently converts, and sneaked data sharing.'
}
];
export function UploadSection({ onAnalyze, isAnalyzing }: UploadSectionProps) {
const [selectedSample, setSelectedSample] = useState<string | null>(null);
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result as string;
setUploadedImage(result);
setSelectedSample(null);
};
reader.readAsDataURL(file);
}
};
const handleSampleSelect = (url: string) => {
setSelectedSample(url);
setUploadedImage(null);
};
const handleAnalyze = () => {
const imageUrl = uploadedImage || selectedSample;
if (imageUrl) {
onAnalyze(imageUrl);
}
};
const activeImage = uploadedImage || selectedSample;
return (
<div className="space-y-6">
<Card className="p-8 bg-card/40 backdrop-blur-md border border-border shadow-xl rounded-2xl relative overflow-hidden transition-colors duration-300">
{/* Glow overlay */}
<div className="absolute top-0 right-0 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl -z-10 pointer-events-none" />
<div className="absolute bottom-0 left-0 w-80 h-80 bg-purple-500/5 rounded-full blur-3xl -z-10 pointer-events-none" />
<h2 className="text-xl font-bold text-foreground mb-6 flex items-center gap-2">
<Upload className="w-5 h-5 text-indigo-500 dark:text-indigo-400" />
Audit New UI Interface
</h2>
<div className="grid md:grid-cols-2 gap-8">
{/* Upload Area (Fits target image directly inside) */}
<div className="space-y-4">
<label
htmlFor="file-upload"
className={`relative flex flex-col items-center justify-center w-full h-72 border-2 border-dashed rounded-xl cursor-pointer overflow-hidden transition-all duration-300 ${
isAnalyzing
? 'border-border bg-muted/10 cursor-not-allowed'
: activeImage
? 'border-indigo-500/50 bg-slate-900/10 dark:bg-slate-950/40 shadow-inner'
: 'border-border bg-muted/20 hover:border-indigo-500/50 hover:bg-muted/40'
}`}
>
{activeImage ? (
<>
<img
src={activeImage}
alt="Uploaded UI Target"
className="w-full h-full object-contain p-2"
/>
{/* Hover Overlay */}
<div className="absolute inset-0 bg-slate-950/50 opacity-0 hover:opacity-100 transition-opacity duration-300 flex flex-col items-center justify-center text-center p-4">
<Upload className="w-8 h-8 text-indigo-400 mb-2" />
<span className="text-xs font-extrabold text-white">Click or Drop to Replace Screenshot</span>
</div>
{/* Bottom Indicator badge */}
<div className="absolute bottom-2 left-2">
<span className="text-[9px] font-mono bg-indigo-600 text-white px-2.5 py-1 rounded-md uppercase tracking-wider font-extrabold shadow-lg border border-indigo-500/20">
Target Interface Loaded
</span>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center pt-5 pb-6 px-4 text-center">
<div className="p-4 bg-indigo-500/5 rounded-2xl mb-4 border border-indigo-500/10">
<Upload className="w-10 h-10 text-indigo-500 dark:text-indigo-400 animate-pulse" />
</div>
<p className="mb-2 text-sm text-foreground/80">
<span className="font-semibold text-indigo-500 dark:text-indigo-400">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-muted-foreground">PNG, JPG or GIF (MAX. 10MB)</p>
</div>
)}
<input
id="file-upload"
type="file"
className="hidden"
accept="image/*"
onChange={handleFileUpload}
disabled={isAnalyzing}
/>
</label>
</div>
{/* Sample Images */}
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<ImageIcon className="w-5 h-5 text-indigo-500 dark:text-indigo-400" />
<h3 className="font-semibold text-foreground">Select Fintech Sample Screen</h3>
</div>
<div className="space-y-3">
{SAMPLE_IMAGES.map((sample) => (
<button
key={sample.id}
onClick={() => handleSampleSelect(sample.url)}
disabled={isAnalyzing}
className={`w-full text-left p-4 rounded-xl border transition-all duration-300 relative overflow-hidden group ${
selectedSample === sample.url
? 'border-indigo-500 bg-indigo-500/5 shadow-md shadow-indigo-500/5'
: 'border-border bg-muted/10 hover:border-slate-400 dark:hover:border-slate-700 hover:bg-muted/20'
} ${isAnalyzing ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div className="font-bold text-sm text-foreground mb-1 group-hover:text-indigo-500 dark:group-hover:text-indigo-400 transition-colors">
{sample.name}
</div>
<div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
{sample.description}
</div>
</button>
))}
</div>
</div>
</div>
{/* Analyze Button */}
<div className="mt-8 flex justify-center border-t border-border pt-6">
<Button
onClick={handleAnalyze}
disabled={!activeImage || isAnalyzing}
size="lg"
className={`min-w-[240px] h-12 rounded-xl text-xs font-bold uppercase tracking-wider transition-all duration-300 ${
!activeImage
? 'bg-muted text-muted-foreground cursor-not-allowed border border-border'
: 'bg-gradient-to-r from-indigo-600 via-indigo-500 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-[0_0_20px_rgba(99,102,241,0.3)] hover:scale-[1.03]'
}`}
>
{isAnalyzing ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin text-white" />
Processing AI Auditing...
</>
) : (
<>
<Shield className="w-5 h-5 mr-2 text-white" />
Analyze Deceptive Copywriting
</>
)}
</Button>
</div>
</Card>
</div>
);
} |