mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): migrate AI tool settings to ProgressCard
Replace AIProgressBar and inline progress indicators with the new ProgressCard component across all 5 AI tool settings. The three useToolProcessor-based tools (remove-bg, blur-faces, upscale) now destructure `progress` from the hook. The two custom-fetch tools (erase-object, ocr) gain inline XHR upload tracking + SSE processing progress with elapsed timer.
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFileStore } from "@/stores/file-store";
|
import { useFileStore } from "@/stores/file-store";
|
||||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { ProgressCard } from "@/components/common/progress-card";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
|
||||||
export function BlurFacesSettings() {
|
export function BlurFacesSettings() {
|
||||||
const { files } = useFileStore();
|
const { files } = useFileStore();
|
||||||
const { processFiles, processing, error, downloadUrl, originalSize, processedSize } =
|
const { processFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
|
||||||
useToolProcessor("blur-faces");
|
useToolProcessor("blur-faces");
|
||||||
|
|
||||||
const [blurRadius, setBlurRadius] = useState(30);
|
const [blurRadius, setBlurRadius] = useState(30);
|
||||||
@@ -79,25 +80,23 @@ export function BlurFacesSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
{processing ? (
|
||||||
onClick={handleProcess}
|
<ProgressCard
|
||||||
disabled={!hasFile || processing}
|
active={processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||||
>
|
label="Blurring faces"
|
||||||
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
|
stage={progress.stage}
|
||||||
{processing ? "Detecting Faces..." : "Blur Faces"}
|
percent={progress.percent}
|
||||||
</button>
|
elapsed={progress.elapsed}
|
||||||
|
/>
|
||||||
{/* Progress indicator */}
|
) : (
|
||||||
{processing && (
|
<button
|
||||||
<div className="space-y-2">
|
onClick={handleProcess}
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
disabled={!hasFile || processing}
|
||||||
<div className="h-full bg-primary rounded-full animate-pulse" style={{ width: '100%' }} />
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
</div>
|
>
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
Blur Faces
|
||||||
AI processing may take 10-30 seconds...
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useFileStore } from "@/stores/file-store";
|
import { useFileStore } from "@/stores/file-store";
|
||||||
import { Download, Loader2, Upload } from "lucide-react";
|
import { ProgressCard } from "@/components/common/progress-card";
|
||||||
|
import { Download, Upload } from "lucide-react";
|
||||||
|
|
||||||
function getToken(): string {
|
function getToken(): string {
|
||||||
return localStorage.getItem("stirling-token") || "";
|
return localStorage.getItem("stirling-token") || "";
|
||||||
@@ -13,6 +14,11 @@ export function EraseObjectSettings() {
|
|||||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
const [originalSize, setOriginalSize] = useState<number | null>(null);
|
const [originalSize, setOriginalSize] = useState<number | null>(null);
|
||||||
const [processedSize, setProcessedSize] = useState<number | null>(null);
|
const [processedSize, setProcessedSize] = useState<number | null>(null);
|
||||||
|
const [progressPhase, setProgressPhase] = useState<"idle" | "uploading" | "processing">("idle");
|
||||||
|
const [progressPercent, setProgressPercent] = useState(0);
|
||||||
|
const [progressStage, setProgressStage] = useState<string | undefined>();
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const elapsedRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
const handleMaskSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleMaskSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selected = e.target.files?.[0];
|
const selected = e.target.files?.[0];
|
||||||
@@ -25,32 +31,81 @@ export function EraseObjectSettings() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setDownloadUrl(null);
|
setDownloadUrl(null);
|
||||||
|
setProgressPhase("uploading");
|
||||||
|
setProgressPercent(0);
|
||||||
|
setProgressStage(undefined);
|
||||||
|
setElapsed(0);
|
||||||
|
|
||||||
try {
|
const startTime = Date.now();
|
||||||
const formData = new FormData();
|
elapsedRef.current = setInterval(() => {
|
||||||
formData.append("file", files[0]);
|
setElapsed(Math.floor((Date.now() - startTime) / 1000));
|
||||||
formData.append("mask", maskFile);
|
}, 1000);
|
||||||
|
|
||||||
const res = await fetch("/api/v1/tools/erase-object", {
|
const clientJobId = crypto.randomUUID();
|
||||||
method: "POST",
|
|
||||||
headers: { Authorization: `Bearer ${getToken()}` },
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
// Open SSE for server-side progress
|
||||||
const body = await res.json().catch(() => ({}));
|
const es = new EventSource(`/api/v1/jobs/${clientJobId}/progress`);
|
||||||
throw new Error(body.error || body.details || `Failed: ${res.status}`);
|
es.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === "single" && typeof data.percent === "number") {
|
||||||
|
setProgressPhase("processing");
|
||||||
|
setProgressPercent(data.percent);
|
||||||
|
setProgressStage(data.stage);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
es.onerror = () => es.close();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", files[0]);
|
||||||
|
formData.append("mask", maskFile);
|
||||||
|
formData.append("clientJobId", clientJobId);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
setProgressPercent((e.loaded / e.total) * 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.upload.onload = () => {
|
||||||
|
setProgressPhase("processing");
|
||||||
|
setProgressPercent(0);
|
||||||
|
setProgressStage("Starting...");
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||||
|
es.close();
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
setDownloadUrl(data.downloadUrl);
|
||||||
|
setOriginalSize(data.originalSize);
|
||||||
|
setProcessedSize(data.processedSize);
|
||||||
|
} catch {
|
||||||
|
setError("Invalid response");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(xhr.responseText);
|
||||||
|
setError(body.error || body.details || `Failed: ${xhr.status}`);
|
||||||
|
} catch {
|
||||||
|
setError(`Processing failed: ${xhr.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
setDownloadUrl(data.downloadUrl);
|
|
||||||
setOriginalSize(data.originalSize);
|
|
||||||
setProcessedSize(data.processedSize);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Object erasing failed");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
setProgressPhase("idle");
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||||
|
es.close();
|
||||||
|
setError("Network error");
|
||||||
|
setProcessing(false);
|
||||||
|
setProgressPhase("idle");
|
||||||
|
};
|
||||||
|
xhr.open("POST", "/api/v1/tools/erase-object");
|
||||||
|
xhr.setRequestHeader("Authorization", `Bearer ${getToken()}`);
|
||||||
|
xhr.send(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
@@ -100,25 +155,23 @@ export function EraseObjectSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
{processing ? (
|
||||||
onClick={handleProcess}
|
<ProgressCard
|
||||||
disabled={!hasFile || !maskFile || processing}
|
active={processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
phase={progressPhase === "idle" ? "uploading" : progressPhase}
|
||||||
>
|
label="Erasing object"
|
||||||
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
|
stage={progressStage}
|
||||||
{processing ? "Erasing..." : "Erase Object"}
|
percent={progressPercent}
|
||||||
</button>
|
elapsed={elapsed}
|
||||||
|
/>
|
||||||
{/* Progress indicator */}
|
) : (
|
||||||
{processing && (
|
<button
|
||||||
<div className="space-y-2">
|
onClick={handleProcess}
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
disabled={!hasFile || !maskFile || processing}
|
||||||
<div className="h-full bg-primary rounded-full animate-pulse" style={{ width: '100%' }} />
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
</div>
|
>
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
Erase Object
|
||||||
AI processing may take 10-30 seconds...
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useFileStore } from "@/stores/file-store";
|
import { useFileStore } from "@/stores/file-store";
|
||||||
import { Loader2, Copy, Check } from "lucide-react";
|
import { ProgressCard } from "@/components/common/progress-card";
|
||||||
|
import { Copy, Check } from "lucide-react";
|
||||||
|
|
||||||
function getToken(): string {
|
function getToken(): string {
|
||||||
return localStorage.getItem("stirling-token") || "";
|
return localStorage.getItem("stirling-token") || "";
|
||||||
@@ -26,6 +27,11 @@ export function OcrSettings() {
|
|||||||
const [text, setText] = useState<string | null>(null);
|
const [text, setText] = useState<string | null>(null);
|
||||||
const [detectedEngine, setDetectedEngine] = useState<string>("");
|
const [detectedEngine, setDetectedEngine] = useState<string>("");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [progressPhase, setProgressPhase] = useState<"idle" | "uploading" | "processing">("idle");
|
||||||
|
const [progressPercent, setProgressPercent] = useState(0);
|
||||||
|
const [progressStage, setProgressStage] = useState<string | undefined>();
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
const elapsedRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
const handleProcess = async () => {
|
const handleProcess = async () => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
@@ -33,31 +39,80 @@ export function OcrSettings() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setText(null);
|
setText(null);
|
||||||
|
setProgressPhase("uploading");
|
||||||
|
setProgressPercent(0);
|
||||||
|
setProgressStage(undefined);
|
||||||
|
setElapsed(0);
|
||||||
|
|
||||||
try {
|
const startTime = Date.now();
|
||||||
const formData = new FormData();
|
elapsedRef.current = setInterval(() => {
|
||||||
formData.append("file", files[0]);
|
setElapsed(Math.floor((Date.now() - startTime) / 1000));
|
||||||
formData.append("settings", JSON.stringify({ engine, language }));
|
}, 1000);
|
||||||
|
|
||||||
const res = await fetch("/api/v1/tools/ocr", {
|
const clientJobId = crypto.randomUUID();
|
||||||
method: "POST",
|
|
||||||
headers: { Authorization: `Bearer ${getToken()}` },
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
// Open SSE for server-side progress
|
||||||
const body = await res.json().catch(() => ({}));
|
const es = new EventSource(`/api/v1/jobs/${clientJobId}/progress`);
|
||||||
throw new Error(body.error || body.details || `Failed: ${res.status}`);
|
es.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === "single" && typeof data.percent === "number") {
|
||||||
|
setProgressPhase("processing");
|
||||||
|
setProgressPercent(data.percent);
|
||||||
|
setProgressStage(data.stage);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
es.onerror = () => es.close();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", files[0]);
|
||||||
|
formData.append("settings", JSON.stringify({ engine, language }));
|
||||||
|
formData.append("clientJobId", clientJobId);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
setProgressPercent((e.loaded / e.total) * 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.upload.onload = () => {
|
||||||
|
setProgressPhase("processing");
|
||||||
|
setProgressPercent(0);
|
||||||
|
setProgressStage("Starting...");
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||||
|
es.close();
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
setText(data.text || "");
|
||||||
|
setDetectedEngine(data.engine || engine);
|
||||||
|
} catch {
|
||||||
|
setError("Invalid response");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(xhr.responseText);
|
||||||
|
setError(body.error || body.details || `Failed: ${xhr.status}`);
|
||||||
|
} catch {
|
||||||
|
setError(`Processing failed: ${xhr.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
setText(data.text || "");
|
|
||||||
setDetectedEngine(data.engine || engine);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "OCR failed");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
setProgressPhase("idle");
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||||
|
es.close();
|
||||||
|
setError("Network error");
|
||||||
|
setProcessing(false);
|
||||||
|
setProgressPhase("idle");
|
||||||
|
};
|
||||||
|
xhr.open("POST", "/api/v1/tools/ocr");
|
||||||
|
xhr.setRequestHeader("Authorization", `Bearer ${getToken()}`);
|
||||||
|
xhr.send(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
@@ -119,25 +174,23 @@ export function OcrSettings() {
|
|||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
{processing ? (
|
||||||
onClick={handleProcess}
|
<ProgressCard
|
||||||
disabled={!hasFile || processing}
|
active={processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
phase={progressPhase === "idle" ? "uploading" : progressPhase}
|
||||||
>
|
label="Extracting text"
|
||||||
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
|
stage={progressStage}
|
||||||
{processing ? "Extracting Text..." : "Extract Text"}
|
percent={progressPercent}
|
||||||
</button>
|
elapsed={elapsed}
|
||||||
|
/>
|
||||||
{/* Progress indicator */}
|
) : (
|
||||||
{processing && (
|
<button
|
||||||
<div className="space-y-2">
|
onClick={handleProcess}
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
disabled={!hasFile || processing}
|
||||||
<div className="h-full bg-primary rounded-full animate-pulse" style={{ width: '100%' }} />
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
</div>
|
>
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
Extract Text
|
||||||
AI processing may take 10-30 seconds...
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Result */}
|
{/* Result */}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState } from "react";
|
||||||
import { useFileStore } from "@/stores/file-store";
|
import { useFileStore } from "@/stores/file-store";
|
||||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { ProgressCard } from "@/components/common/progress-card";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
|
||||||
type BgModel =
|
type BgModel =
|
||||||
| "birefnet-general"
|
| "birefnet-general"
|
||||||
@@ -31,24 +32,11 @@ const BG_PRESETS = [
|
|||||||
|
|
||||||
export function RemoveBgSettings() {
|
export function RemoveBgSettings() {
|
||||||
const { files } = useFileStore();
|
const { files } = useFileStore();
|
||||||
const { processFiles, processing, error, downloadUrl, originalSize, processedSize } =
|
const { processFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
|
||||||
useToolProcessor("remove-background");
|
useToolProcessor("remove-background");
|
||||||
|
|
||||||
const [model, setModel] = useState<BgModel>("u2net");
|
const [model, setModel] = useState<BgModel>("u2net");
|
||||||
const [bgColor, setBgColor] = useState("");
|
const [bgColor, setBgColor] = useState("");
|
||||||
const [elapsed, setElapsed] = useState(0);
|
|
||||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
|
|
||||||
// Progress timer
|
|
||||||
useEffect(() => {
|
|
||||||
if (processing) {
|
|
||||||
setElapsed(0);
|
|
||||||
timerRef.current = setInterval(() => setElapsed((e) => e + 1), 1000);
|
|
||||||
} else {
|
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
return () => { if (timerRef.current) clearInterval(timerRef.current); };
|
|
||||||
}, [processing]);
|
|
||||||
|
|
||||||
const handleProcess = () => {
|
const handleProcess = () => {
|
||||||
const settings: Record<string, unknown> = { model };
|
const settings: Record<string, unknown> = { model };
|
||||||
@@ -58,15 +46,6 @@ export function RemoveBgSettings() {
|
|||||||
|
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
|
|
||||||
const progressStage =
|
|
||||||
elapsed < 3 ? "Loading AI model..." :
|
|
||||||
elapsed < 8 ? "Analyzing image..." :
|
|
||||||
elapsed < 15 ? "Removing background..." :
|
|
||||||
elapsed < 25 ? "Refining edges..." :
|
|
||||||
"Almost done...";
|
|
||||||
|
|
||||||
const progressPercent = Math.min(95, elapsed * 4);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Model selector */}
|
{/* Model selector */}
|
||||||
@@ -149,32 +128,23 @@ export function RemoveBgSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
{processing ? (
|
||||||
onClick={handleProcess}
|
<ProgressCard
|
||||||
disabled={!hasFile || processing}
|
active={processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||||
>
|
label="Removing background"
|
||||||
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
|
stage={progress.stage}
|
||||||
{processing ? "Removing Background..." : "Remove Background"}
|
percent={progress.percent}
|
||||||
</button>
|
elapsed={progress.elapsed}
|
||||||
|
/>
|
||||||
{/* Animated progress bar with stages */}
|
) : (
|
||||||
{processing && (
|
<button
|
||||||
<div className="space-y-2 p-3 rounded-lg bg-muted/50 border border-border">
|
onClick={handleProcess}
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
disabled={!hasFile || processing}
|
||||||
<span>{progressStage}</span>
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
<span>{elapsed}s</span>
|
>
|
||||||
</div>
|
Remove Background
|
||||||
<div className="w-full bg-muted rounded-full h-2.5 overflow-hidden">
|
</button>
|
||||||
<div
|
|
||||||
className="h-full bg-primary rounded-full transition-all duration-1000 ease-out"
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-muted-foreground text-center">
|
|
||||||
First run may take longer as the model loads into memory
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFileStore } from "@/stores/file-store";
|
import { useFileStore } from "@/stores/file-store";
|
||||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { ProgressCard } from "@/components/common/progress-card";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
|
||||||
export function UpscaleSettings() {
|
export function UpscaleSettings() {
|
||||||
const { files } = useFileStore();
|
const { files } = useFileStore();
|
||||||
const { processFiles, processing, error, downloadUrl, originalSize, processedSize } =
|
const { processFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
|
||||||
useToolProcessor("upscale");
|
useToolProcessor("upscale");
|
||||||
|
|
||||||
const [scale, setScale] = useState(2);
|
const [scale, setScale] = useState(2);
|
||||||
@@ -55,25 +56,23 @@ export function UpscaleSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
<button
|
{processing ? (
|
||||||
onClick={handleProcess}
|
<ProgressCard
|
||||||
disabled={!hasFile || processing}
|
active={processing}
|
||||||
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||||
>
|
label="Upscaling image"
|
||||||
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
|
stage={progress.stage}
|
||||||
{processing ? "Upscaling..." : `Upscale ${scale}x`}
|
percent={progress.percent}
|
||||||
</button>
|
elapsed={progress.elapsed}
|
||||||
|
/>
|
||||||
{/* Progress indicator */}
|
) : (
|
||||||
{processing && (
|
<button
|
||||||
<div className="space-y-2">
|
onClick={handleProcess}
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
disabled={!hasFile || processing}
|
||||||
<div className="h-full bg-primary rounded-full animate-pulse" style={{ width: '100%' }} />
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
</div>
|
>
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
{`Upscale ${scale}x`}
|
||||||
AI processing may take 10-30 seconds...
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
|
|||||||
Reference in New Issue
Block a user