Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
Expand Down
236 changes: 133 additions & 103 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from "react";
import { useDropzone } from "react-dropzone";
import { Images } from "./components/Images";
import { processImages, initializeModel, getModelInfo } from "../lib/process";
import { Camera } from "./components/Camera";

interface AppError {
message: string;
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function App() {
const [currentModel, setCurrentModel] = useState<'briaai/RMBG-1.4' | 'Xenova/modnet'>('briaai/RMBG-1.4');
const [isModelSwitching, setIsModelSwitching] = useState(false);
const [images, setImages] = useState<ImageFile[]>([]);
const [showCamera, setShowCamera] = useState(false);

useEffect(() => {
if (isMobileSafari() && !isRedirectPage()) {
Expand Down Expand Up @@ -180,6 +182,13 @@ export default function App() {
},
});

// Add this function to handle camera capture
const handleCameraCapture = async (capturedImage: Blob) => {
const file = new File([capturedImage], `camera-${Date.now()}.jpg`, { type: 'image/jpeg' });
onDrop([file]);
setShowCamera(false);
};

// Remove the full screen error and loading states

return (
Expand Down Expand Up @@ -216,116 +225,137 @@ export default function App() {
</nav>

<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-8 flex-grow">
<div className={`grid ${images.length === 0 ? 'grid-cols-1 lg:grid-cols-2 gap-8' : 'grid-cols-1'}`}>
{images.length === 0 && (
<div className="flex flex-col justify-center space-y-6">
<div className="glass-card p-8">
<h2 className="text-4xl font-bold mb-6">
<span className="gradient-text">Remove Background</span>
<br />
<span className="text-accent">In One Click</span>
</h2>
<p className="text-xl text-gray-300 mb-8">
Transform your images instantly with AI-powered background removal
</p>
<ul className="space-y-4">
{[
'100% Free & Private',
'Browser-based Processing',
'Advanced Background Editing'
].map((feature, index) => (
<li key={index} className="flex items-center gap-3 text-gray-300">
<svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
{feature}
</li>
))}
</ul>
{showCamera ? (
<Camera
onCapture={handleCameraCapture}
onClose={() => setShowCamera(false)}
/>
) : (
<div className={`grid ${images.length === 0 ? 'grid-cols-1 lg:grid-cols-2 gap-8' : 'grid-cols-1'}`}>
{images.length === 0 && (
<div className="flex flex-col justify-center space-y-6">
<div className="glass-card p-8">
<h2 className="text-4xl font-bold mb-6">
<span className="gradient-text">Remove Background</span>
<br />
<span className="text-accent">In One Click</span>
</h2>
<p className="text-xl text-gray-300 mb-8">
Transform your images instantly with AI-powered background removal
</p>
<ul className="space-y-4">
{[
'100% Free & Private',
'Browser-based Processing',
'Advanced Background Editing'
].map((feature, index) => (
<li key={index} className="flex items-center gap-3 text-gray-300">
<svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
{feature}
</li>
))}
</ul>
</div>
</div>
</div>
)}

<div className={images.length === 0 ? '' : 'w-full'}>
<div
{...getRootProps()}
className={`dropzone p-8 mb-8 rounded-xl text-center cursor-pointer
${isDragAccept ? "border-green-500 bg-green-500/10" : ""}
${isDragReject ? "border-red-500 bg-red-500/10" : ""}
${isDragActive ? "border-accent bg-accent/10" : ""}
${isLoading || isModelSwitching ? "cursor-not-allowed opacity-50" : ""}
`}
>
<input {...getInputProps()} className="hidden" disabled={isLoading || isModelSwitching} />
<div className="flex flex-col items-center gap-2">
{isLoading || isModelSwitching ? (
<>
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600 mb-2"></div>
<p className="text-lg text-gray-600">
{isModelSwitching ? 'Switching models...' : 'Loading background removal model...'}
</p>
</>
) : error ? (
<>
<svg className="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p className="text-lg text-red-600 font-medium mb-2">{error.message}</p>
{currentModel === 'Xenova/modnet' && (
<button
onClick={(e) => {
e.stopPropagation();
handleModelChange({ target: { value: 'briaai/RMBG-1.4' }} as any);
}}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Switch to Cross-browser Version
</button>
)}

<div className={images.length === 0 ? '' : 'w-full'}>
<div className="flex flex-col gap-4 mb-8">
<div
{...getRootProps()}
className={`dropzone p-8 rounded-xl text-center cursor-pointer
${isDragAccept ? "border-green-500 bg-green-500/10" : ""}
${isDragReject ? "border-red-500 bg-red-500/10" : ""}
${isDragActive ? "border-accent bg-accent/10" : ""}
${isLoading || isModelSwitching ? "cursor-not-allowed opacity-50" : ""}
`}
>
<input {...getInputProps()} className="hidden" disabled={isLoading || isModelSwitching} />
<div className="flex flex-col items-center gap-2">
{isLoading || isModelSwitching ? (
<>
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600 mb-2"></div>
<p className="text-lg text-gray-600">
{isModelSwitching ? 'Switching models...' : 'Loading background removal model...'}
</p>
</>
) : error ? (
<>
<svg className="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p className="text-lg text-red-600 font-medium mb-2">{error.message}</p>
{currentModel === 'Xenova/modnet' && (
<button
onClick={(e) => {
e.stopPropagation();
handleModelChange({ target: { value: 'briaai/RMBG-1.4' }} as any);
}}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Switch to Cross-browser Version
</button>
)}
</>
) : (
<>
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-lg text-gray-600">
{isDragActive
? "Drop the images here..."
: "Drag and drop images here"}
</p>
<p className="text-sm text-gray-500">or click to select files</p>
</>
)}
</>
) : (
<>
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-lg text-gray-600">
{isDragActive
? "Drop the images here..."
: "Drag and drop images here"}
</p>
<p className="text-sm text-gray-500">or click to select files</p>
</>
)}
</div>
</div>

<button
onClick={() => setShowCamera(true)}
className="flex items-center justify-center gap-2 p-4 rounded-xl border-2 border-dashed border-gray-600 hover:border-accent transition-colors"
disabled={isLoading || isModelSwitching}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Take a Picture
</button>
</div>
</div>

{images.length === 0 && (
<div className="glass-card p-6">
<h3 className="text-xl font-semibold text-gray-300 mb-6">Try with sample images:</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{sampleImages.map((url, index) => (
<button
key={index}
onClick={() => handleSampleImageClick(url)}
className="gradient-border overflow-hidden rounded-xl hover:scale-105 transition-transform"
>
<img
src={url}
alt={`Sample ${index + 1}`}
className="w-full aspect-square object-cover"
/>
</button>
))}
{images.length === 0 && (
<div className="glass-card p-6">
<h3 className="text-xl font-semibold text-gray-300 mb-6">Try with sample images:</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{sampleImages.map((url, index) => (
<button
key={index}
onClick={() => handleSampleImageClick(url)}
className="gradient-border overflow-hidden rounded-xl hover:scale-105 transition-transform"
>
<img
src={url}
alt={`Sample ${index + 1}`}
className="w-full aspect-square object-cover"
/>
</button>
))}
</div>
<p className="text-sm text-gray-500 mt-4">
All images are processed locally on your device and are not uploaded to any server.
</p>
</div>
<p className="text-sm text-gray-500 mt-4">
All images are processed locally on your device and are not uploaded to any server.
</p>
</div>
)}
)}

<Images images={images} onDelete={(id) => setImages(prev => prev.filter(img => img.id !== id))} />
<Images images={images} onDelete={(id) => setImages(prev => prev.filter(img => img.id !== id))} />
</div>
</div>
</div>
)}
</main>

<footer className="mt-auto py-6 glass-nav">
Expand Down
Loading