|
|
@@ -1,10 +1,9 @@
|
|
|
"use client";
|
|
|
|
|
|
-import { useState, useRef, useEffect } from "react";
|
|
|
-import { Header } from "@/components/layout";
|
|
|
-import { AppLayout } from "@/components/layout";
|
|
|
+import { useState, useRef, useEffect, Suspense } from "react";
|
|
|
+import { useSearchParams } from "next/navigation";
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
-
|
|
|
+import { Input } from "@/components/ui/input";
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
import {
|
|
|
Card,
|
|
|
@@ -14,18 +13,14 @@ import {
|
|
|
apiClient,
|
|
|
type JobInfo,
|
|
|
type JobDetailsResponse,
|
|
|
+ type ModelInfo,
|
|
|
+ type EnhancedModelsResponse,
|
|
|
} from "@/lib/api";
|
|
|
import { Loader2, Download, X, Upload } from "lucide-react";
|
|
|
import {
|
|
|
- downloadImage,
|
|
|
downloadAuthenticatedImage,
|
|
|
fileToBase64,
|
|
|
} from "@/lib/utils";
|
|
|
-import { useLocalStorage, useGeneratedImages } from "@/lib/storage";
|
|
|
-import {
|
|
|
- useModelSelection,
|
|
|
- useModelTypeSelection,
|
|
|
-} from "@/contexts/model-selection-context";
|
|
|
import {
|
|
|
Select,
|
|
|
SelectContent,
|
|
|
@@ -33,7 +28,7 @@ import {
|
|
|
SelectTrigger,
|
|
|
SelectValue,
|
|
|
} from "@/components/ui/select";
|
|
|
-// import { AutoSelectionStatus } from '@/components/features/models';
|
|
|
+import { AppLayout, Header } from "@/components/layout";
|
|
|
|
|
|
type UpscalerFormData = {
|
|
|
upscale_factor: number;
|
|
|
@@ -45,20 +40,13 @@ const defaultFormData: UpscalerFormData = {
|
|
|
model: "",
|
|
|
};
|
|
|
|
|
|
+
|
|
|
+
|
|
|
function UpscalerForm() {
|
|
|
- const { actions } = useModelSelection();
|
|
|
-
|
|
|
- const {
|
|
|
- availableModels: upscalerModels,
|
|
|
- selectedModel: selectedUpscalerModel,
|
|
|
- setSelectedModel: setSelectedUpscalerModel,
|
|
|
- } = useModelTypeSelection("upscaler");
|
|
|
-
|
|
|
- const [formData, setFormData] = useLocalStorage<UpscalerFormData>(
|
|
|
- "upscaler-form-data",
|
|
|
- defaultFormData,
|
|
|
- { excludeLargeData: true, maxSize: 512 * 1024 },
|
|
|
- );
|
|
|
+ const searchParams = useSearchParams();
|
|
|
+
|
|
|
+ // Simple state management - no complex hooks initially
|
|
|
+ const [formData, setFormData] = useState<UpscalerFormData>(defaultFormData);
|
|
|
|
|
|
// Separate state for image data (not stored in localStorage)
|
|
|
const [uploadedImage, setUploadedImage] = useState<string>("");
|
|
|
@@ -67,11 +55,17 @@ function UpscalerForm() {
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [jobInfo, setJobInfo] = useState<JobInfo | null>(null);
|
|
|
- const { images: storedImages, addImages, getLatestImages } = useGeneratedImages('upscaler');
|
|
|
- const [generatedImages, setGeneratedImages] = useState<string[]>(() => getLatestImages());
|
|
|
+ const [generatedImages, setGeneratedImages] = useState<string[]>([]);
|
|
|
const [pollCleanup, setPollCleanup] = useState<(() => void) | null>(null);
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
+ // URL input state
|
|
|
+ const [urlInput, setUrlInput] = useState('');
|
|
|
+
|
|
|
+ // Local state for upscaler models - no global context to avoid performance issues
|
|
|
+ const [upscalerModels, setUpscalerModels] = useState<ModelInfo[]>([]);
|
|
|
+ const [modelsLoading, setModelsLoading] = useState(false);
|
|
|
+
|
|
|
// Cleanup polling on unmount
|
|
|
useEffect(() => {
|
|
|
return () => {
|
|
|
@@ -81,42 +75,86 @@ function UpscalerForm() {
|
|
|
};
|
|
|
}, [pollCleanup]);
|
|
|
|
|
|
+ // Load image from URL parameter on mount
|
|
|
+ useEffect(() => {
|
|
|
+ const imageUrl = searchParams.get('imageUrl');
|
|
|
+ if (imageUrl) {
|
|
|
+ loadImageFromUrl(imageUrl);
|
|
|
+ }
|
|
|
+ }, [searchParams]);
|
|
|
+
|
|
|
+ // Load upscaler models on mount
|
|
|
useEffect(() => {
|
|
|
+ let isComponentMounted = true;
|
|
|
+
|
|
|
const loadModels = async () => {
|
|
|
try {
|
|
|
- // Fetch all models with enhanced info
|
|
|
- const modelsData = await apiClient.getModels();
|
|
|
- // Filter for upscaler models
|
|
|
- const upscalerModels = modelsData.models.filter(
|
|
|
- (m) => m.type.toLowerCase() === "upscaler",
|
|
|
+ setModelsLoading(true);
|
|
|
+ setError(null);
|
|
|
+
|
|
|
+ // Set up timeout for API call
|
|
|
+ const timeoutPromise = new Promise((_, reject) =>
|
|
|
+ setTimeout(() => reject(new Error('API call timeout')), 5000)
|
|
|
);
|
|
|
- actions.setModels(modelsData.models);
|
|
|
|
|
|
- // Set first upscaler model as default if none selected
|
|
|
- if (upscalerModels.length > 0 && !formData.model) {
|
|
|
+ const apiPromise = apiClient.getModels("esrgan");
|
|
|
+ const modelsData = await Promise.race([apiPromise, timeoutPromise]) as EnhancedModelsResponse;
|
|
|
+
|
|
|
+ console.log("API call completed, models:", modelsData.models?.length || 0);
|
|
|
+
|
|
|
+ if (!isComponentMounted) return;
|
|
|
+
|
|
|
+ // Set models locally - no global state updates
|
|
|
+ setUpscalerModels(modelsData.models || []);
|
|
|
+
|
|
|
+ // Set first model as default if none selected
|
|
|
+ if (modelsData.models?.length > 0 && !formData.model) {
|
|
|
setFormData((prev) => ({
|
|
|
...prev,
|
|
|
- model: upscalerModels[0].name,
|
|
|
+ model: modelsData.models[0].name,
|
|
|
}));
|
|
|
}
|
|
|
} catch (err) {
|
|
|
console.error("Failed to load upscaler models:", err);
|
|
|
+ if (isComponentMounted) {
|
|
|
+ setError(`Failed to load upscaler models: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (isComponentMounted) {
|
|
|
+ setModelsLoading(false);
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
loadModels();
|
|
|
- }, [actions, setFormData]);
|
|
|
|
|
|
- // Update form data when upscaler model changes
|
|
|
- useEffect(() => {
|
|
|
- if (selectedUpscalerModel) {
|
|
|
- setFormData((prev) => ({
|
|
|
- ...prev,
|
|
|
- model: selectedUpscalerModel,
|
|
|
- }));
|
|
|
- }
|
|
|
- }, [selectedUpscalerModel, setFormData]);
|
|
|
+ return () => {
|
|
|
+ isComponentMounted = false;
|
|
|
+ };
|
|
|
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
-
|
|
|
+ const loadImageFromUrl = async (url: string) => {
|
|
|
+ try {
|
|
|
+ setError(null);
|
|
|
+ // Fetch the image and convert to base64
|
|
|
+ const response = await fetch(url);
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch image');
|
|
|
+ }
|
|
|
+ const blob = await response.blob();
|
|
|
+ const base64 = await new Promise<string>((resolve, reject) => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = () => resolve(reader.result as string);
|
|
|
+ reader.onerror = reject;
|
|
|
+ reader.readAsDataURL(blob);
|
|
|
+ });
|
|
|
+ setUploadedImage(base64);
|
|
|
+ setPreviewImage(base64);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load image from URL:', err);
|
|
|
+ setError('Failed to load image from gallery');
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
const file = e.target.files?.[0];
|
|
|
@@ -176,11 +214,10 @@ function UpscalerForm() {
|
|
|
|
|
|
// Create a new array to trigger React re-render
|
|
|
setGeneratedImages([...imageUrls]);
|
|
|
- addImages(imageUrls, jobId);
|
|
|
setLoading(false);
|
|
|
isPolling = false;
|
|
|
} else if (status.job.status === "failed") {
|
|
|
- setError(status.job.error || "Upscaling failed");
|
|
|
+ setError(status.job.error_message || status.job.error || "Upscaling failed");
|
|
|
setLoading(false);
|
|
|
isPolling = false;
|
|
|
} else if (status.job.status === "cancelled") {
|
|
|
@@ -230,15 +267,55 @@ function UpscalerForm() {
|
|
|
|
|
|
try {
|
|
|
// Validate model selection
|
|
|
- if (!selectedUpscalerModel) {
|
|
|
+ if (!formData.model) {
|
|
|
setError("Please select an upscaler model");
|
|
|
setLoading(false);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // Unload all currently loaded models and load the selected upscaler model
|
|
|
+ const selectedModel = upscalerModels.find(m => m.name === formData.model);
|
|
|
+ const modelId = selectedModel?.id || selectedModel?.sha256;
|
|
|
+ if (!selectedModel) {
|
|
|
+ setError("Selected upscaler model not found.");
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!modelId) {
|
|
|
+ setError("Selected upscaler model does not have a hash. Please compute the hash on the models page.");
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Get all loaded models
|
|
|
+ const loadedModels = await apiClient.getAllModels(undefined, true);
|
|
|
+
|
|
|
+ // Unload all loaded models
|
|
|
+ for (const model of loadedModels) {
|
|
|
+ const unloadId = model.id || model.sha256;
|
|
|
+ if (unloadId) {
|
|
|
+ try {
|
|
|
+ await apiClient.unloadModel(unloadId);
|
|
|
+ } catch (unloadErr) {
|
|
|
+ console.warn(`Failed to unload model ${model.name}:`, unloadErr);
|
|
|
+ // Continue with others
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load the selected upscaler model
|
|
|
+ await apiClient.loadModel(modelId);
|
|
|
+ } catch (modelErr) {
|
|
|
+ console.error("Failed to prepare upscaler model:", modelErr);
|
|
|
+ setError("Failed to prepare upscaler model. Please try again.");
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const job = await apiClient.upscale({
|
|
|
image: uploadedImage,
|
|
|
- model: selectedUpscalerModel,
|
|
|
+ model: formData.model,
|
|
|
upscale_factor: formData.upscale_factor,
|
|
|
});
|
|
|
setJobInfo(job);
|
|
|
@@ -282,185 +359,221 @@ function UpscalerForm() {
|
|
|
/>
|
|
|
<div className="container mx-auto p-6">
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
|
- {/* Left Panel - Form */}
|
|
|
- <Card>
|
|
|
- <CardContent className="pt-6">
|
|
|
- <form onSubmit={handleUpscale} className="space-y-4">
|
|
|
- <div className="space-y-2">
|
|
|
- <Label>Source Image *</Label>
|
|
|
- <div className="space-y-4">
|
|
|
- {previewImage && (
|
|
|
- <div className="relative">
|
|
|
- <img
|
|
|
- src={previewImage}
|
|
|
- alt="Preview"
|
|
|
- className="h-64 w-full rounded-lg object-cover"
|
|
|
- />
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="destructive"
|
|
|
- size="icon"
|
|
|
- className="absolute top-2 right-2 h-8 w-8"
|
|
|
- onClick={() => {
|
|
|
- setPreviewImage(null);
|
|
|
- setUploadedImage("");
|
|
|
- }}
|
|
|
- >
|
|
|
- <X className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
+ {/* Left Panel - Form Parameters */}
|
|
|
+ <div className="space-y-6">
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <form onSubmit={handleUpscale} className="space-y-4">
|
|
|
+ {/* Image Upload Section */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="image-upload">Image *</Label>
|
|
|
<div className="space-y-2">
|
|
|
- <div className="flex items-center justify-center">
|
|
|
- <input
|
|
|
- ref={fileInputRef}
|
|
|
- type="file"
|
|
|
- accept="image/*"
|
|
|
- onChange={handleImageUpload}
|
|
|
- className="hidden"
|
|
|
+ <input
|
|
|
+ id="image-upload"
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ onChange={handleImageUpload}
|
|
|
+ ref={fileInputRef}
|
|
|
+ className="hidden"
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => fileInputRef.current?.click()}
|
|
|
+ className="w-full"
|
|
|
+ >
|
|
|
+ <Upload className="mr-2 h-4 w-4" />
|
|
|
+ Choose Image File
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Input
|
|
|
+ type="url"
|
|
|
+ placeholder="Or paste image URL"
|
|
|
+ value={urlInput}
|
|
|
+ onChange={(e) => setUrlInput(e.target.value)}
|
|
|
+ className="flex-1"
|
|
|
/>
|
|
|
<Button
|
|
|
type="button"
|
|
|
variant="outline"
|
|
|
- onClick={() => fileInputRef.current?.click()}
|
|
|
+ onClick={() => loadImageFromUrl(urlInput)}
|
|
|
+ disabled={!urlInput}
|
|
|
>
|
|
|
- <Upload className="mr-2 h-4 w-4" />
|
|
|
- Choose Image
|
|
|
+ Load
|
|
|
</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <div className="space-y-2">
|
|
|
- <Label>Upscaling Factor</Label>
|
|
|
- <select
|
|
|
- value={formData.upscale_factor}
|
|
|
- onChange={(e) =>
|
|
|
- setFormData((prev) => ({
|
|
|
- ...prev,
|
|
|
- upscale_factor: Number(e.target.value),
|
|
|
- }))
|
|
|
- }
|
|
|
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
|
- >
|
|
|
- <option value={2}>2x (Double)</option>
|
|
|
- <option value={3}>3x (Triple)</option>
|
|
|
- <option value={4}>4x (Quadruple)</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
+ {/* Model Selection */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="model">Upscaler Model</Label>
|
|
|
+ <Select
|
|
|
+ value={formData.model}
|
|
|
+ onValueChange={(value) =>
|
|
|
+ setFormData((prev) => ({ ...prev, model: value }))
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Select upscaler model" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ {upscalerModels.map((model) => (
|
|
|
+ <SelectItem key={model.name} value={model.name}>
|
|
|
+ {model.name}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ {modelsLoading && (
|
|
|
+ <p className="text-sm text-muted-foreground">Loading models...</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
|
|
|
- <div className="space-y-2">
|
|
|
- <Label>Upscaler Model</Label>
|
|
|
- <Select
|
|
|
- value={formData.model}
|
|
|
- onValueChange={(value) => {
|
|
|
- setFormData((prev) => ({ ...prev, model: value }));
|
|
|
- setSelectedUpscalerModel(value);
|
|
|
- }}
|
|
|
- >
|
|
|
- <SelectTrigger>
|
|
|
- <SelectValue placeholder="Select an upscaler model" />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent>
|
|
|
- {upscalerModels.map((model) => (
|
|
|
- <SelectItem key={model.name} value={model.name}>
|
|
|
- {model.name}
|
|
|
- </SelectItem>
|
|
|
- ))}
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- </div>
|
|
|
+ {/* Upscale Factor */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="upscale_factor">
|
|
|
+ Upscale Factor *
|
|
|
+ <span className="text-xs text-muted-foreground ml-1">
|
|
|
+ ({formData.upscale_factor}x)
|
|
|
+ </span>
|
|
|
+ </Label>
|
|
|
+ <input
|
|
|
+ id="upscale_factor"
|
|
|
+ name="upscale_factor"
|
|
|
+ type="range"
|
|
|
+ min="2"
|
|
|
+ max="8"
|
|
|
+ step="1"
|
|
|
+ value={formData.upscale_factor}
|
|
|
+ onChange={(e) =>
|
|
|
+ setFormData((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ upscale_factor: Number(e.target.value),
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
|
|
+ />
|
|
|
+ <div className="flex justify-between text-xs text-muted-foreground">
|
|
|
+ <span>2x</span>
|
|
|
+ <span>8x</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div className="flex gap-2">
|
|
|
- <Button
|
|
|
- type="submit"
|
|
|
- disabled={loading || !uploadedImage}
|
|
|
- className="flex-1"
|
|
|
- >
|
|
|
+ {/* Generate Button */}
|
|
|
+ <Button type="submit" disabled={loading || !uploadedImage} className="w-full">
|
|
|
{loading ? (
|
|
|
<>
|
|
|
- <Loader2 className="h-4 w-4 animate-spin" />
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
Upscaling...
|
|
|
</>
|
|
|
) : (
|
|
|
- "Upscale"
|
|
|
+ "Upscale Image"
|
|
|
)}
|
|
|
</Button>
|
|
|
- {loading && (
|
|
|
+
|
|
|
+ {/* Cancel Button */}
|
|
|
+ {jobInfo && (
|
|
|
<Button
|
|
|
type="button"
|
|
|
- variant="destructive"
|
|
|
+ variant="outline"
|
|
|
onClick={handleCancel}
|
|
|
+ disabled={!loading}
|
|
|
+ className="w-full"
|
|
|
>
|
|
|
- <X className="h-4 w-4" />
|
|
|
+ <X className="h-4 w-4 mr-2" />
|
|
|
Cancel
|
|
|
</Button>
|
|
|
)}
|
|
|
- </div>
|
|
|
|
|
|
- {error && (
|
|
|
- <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
|
|
- {error}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </form>
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- <Card>
|
|
|
- <CardContent className="pt-6">
|
|
|
- <div className="space-y-4">
|
|
|
- <h3 className="text-lg font-semibold">Upscaled Image</h3>
|
|
|
- {generatedImages.length === 0 ? (
|
|
|
- <div className="flex h-96 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
|
|
- <p className="text-muted-foreground">
|
|
|
- {loading
|
|
|
- ? "Upscaling..."
|
|
|
- : "Upscaled image will appear here"}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="grid gap-4">
|
|
|
- {generatedImages.map((image, index) => (
|
|
|
- <div key={index} className="relative group">
|
|
|
- <img
|
|
|
- src={image}
|
|
|
- alt={`Upscaled ${index + 1}`}
|
|
|
- className="w-full rounded-lg border border-border"
|
|
|
- />
|
|
|
- <Button
|
|
|
- size="icon"
|
|
|
- variant="secondary"
|
|
|
- className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
|
- onClick={() => {
|
|
|
- const authToken =
|
|
|
- localStorage.getItem("auth_token");
|
|
|
- const unixUser = localStorage.getItem("unix_user");
|
|
|
- downloadAuthenticatedImage(
|
|
|
- image,
|
|
|
- `upscaled-${Date.now()}-${formData.upscale_factor}x.png`,
|
|
|
- authToken || undefined,
|
|
|
- unixUser || undefined,
|
|
|
- ).catch((err) => {
|
|
|
- console.error("Failed to download image:", err);
|
|
|
- // Fallback to regular download if authenticated download fails
|
|
|
- downloadImage(
|
|
|
+ {/* Error Display */}
|
|
|
+ {error && (
|
|
|
+ <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
|
|
+ {error}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </form>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Right Panel - Image Preview and Results */}
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Image Preview */}
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="space-y-4">
|
|
|
+ <h3 className="text-lg font-semibold">Image Preview</h3>
|
|
|
+ {previewImage ? (
|
|
|
+ <div className="relative">
|
|
|
+ <img
|
|
|
+ src={previewImage}
|
|
|
+ alt="Preview"
|
|
|
+ className="w-full rounded-lg border border-border"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="flex h-64 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
|
|
+ <p className="text-muted-foreground">
|
|
|
+ Upload an image to see preview
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Results */}
|
|
|
+ <Card>
|
|
|
+ <CardContent className="pt-6">
|
|
|
+ <div className="space-y-4">
|
|
|
+ <h3 className="text-lg font-semibold">Upscaled Images</h3>
|
|
|
+ {generatedImages.length === 0 ? (
|
|
|
+ <div className="flex h-64 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
|
|
+ <p className="text-muted-foreground">
|
|
|
+ {loading
|
|
|
+ ? "Upscaling in progress..."
|
|
|
+ : "Upscaled images will appear here"}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="grid gap-4">
|
|
|
+ {generatedImages.map((image, index) => (
|
|
|
+ <div key={index} className="relative group">
|
|
|
+ <img
|
|
|
+ src={image}
|
|
|
+ alt={`Upscaled ${index + 1}`}
|
|
|
+ className="w-full rounded-lg border border-border"
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ size="icon"
|
|
|
+ variant="secondary"
|
|
|
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
|
+ onClick={() => {
|
|
|
+ const authToken =
|
|
|
+ localStorage.getItem("auth_token");
|
|
|
+ const unixUser = localStorage.getItem("unix_user");
|
|
|
+ downloadAuthenticatedImage(
|
|
|
image,
|
|
|
- `upscaled-${Date.now()}-${formData.upscale_factor}x.png`,
|
|
|
- );
|
|
|
- });
|
|
|
- }}
|
|
|
- >
|
|
|
- <Download className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
+ `upscaled-${Date.now()}-${index}.png`,
|
|
|
+ authToken || undefined,
|
|
|
+ unixUser || undefined,
|
|
|
+ ).catch((err) => {
|
|
|
+ console.error("Failed to download image:", err);
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Download className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</AppLayout>
|
|
|
@@ -468,5 +581,9 @@ function UpscalerForm() {
|
|
|
}
|
|
|
|
|
|
export default function UpscalerPage() {
|
|
|
- return <UpscalerForm />;
|
|
|
+ return (
|
|
|
+ <Suspense fallback={<div>Loading...</div>}>
|
|
|
+ <UpscalerForm />
|
|
|
+ </Suspense>
|
|
|
+ );
|
|
|
}
|