|
|
@@ -1,28 +1,26 @@
|
|
|
'use client';
|
|
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
-import { Header } from '@/components/header';
|
|
|
+import { Header } from '@/components/layout';
|
|
|
import { AppLayout } from '@/components/layout';
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
-import { Card, CardContent } from '@/components/ui/card';
|
|
|
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
|
import { apiClient, type JobInfo, type ModelInfo } from '@/lib/api';
|
|
|
import { Loader2, Download, X, Upload } from 'lucide-react';
|
|
|
import { downloadImage, downloadAuthenticatedImage, fileToBase64 } from '@/lib/utils';
|
|
|
-import { useLocalStorage } from '@/lib/hooks';
|
|
|
+import { useLocalStorage } from '@/lib/storage';
|
|
|
import { ModelSelectionProvider, useModelSelection, useModelTypeSelection } from '@/contexts/model-selection-context';
|
|
|
-import { EnhancedModelSelect, EnhancedModelSelectGroup } from '@/components/enhanced-model-select';
|
|
|
-import { ModelSelectionWarning, AutoSelectionStatus } from '@/components/model-selection-indicator';
|
|
|
+import { EnhancedModelSelect } from '@/components/features/models';
|
|
|
+// import { AutoSelectionStatus } from '@/components/features/models';
|
|
|
|
|
|
type UpscalerFormData = {
|
|
|
- image: string;
|
|
|
upscale_factor: number;
|
|
|
model: string;
|
|
|
};
|
|
|
|
|
|
const defaultFormData: UpscalerFormData = {
|
|
|
- image: '',
|
|
|
upscale_factor: 2,
|
|
|
model: '',
|
|
|
};
|
|
|
@@ -42,16 +40,30 @@ function UpscalerForm() {
|
|
|
|
|
|
const [formData, setFormData] = useLocalStorage<UpscalerFormData>(
|
|
|
'upscaler-form-data',
|
|
|
- defaultFormData
|
|
|
+ defaultFormData,
|
|
|
+ { excludeLargeData: true, maxSize: 512 * 1024 }
|
|
|
);
|
|
|
|
|
|
+ // Separate state for image data (not stored in localStorage)
|
|
|
+ const [uploadedImage, setUploadedImage] = useState<string>('');
|
|
|
+ const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
+
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [jobInfo, setJobInfo] = useState<JobInfo | null>(null);
|
|
|
const [generatedImages, setGeneratedImages] = useState<string[]>([]);
|
|
|
- const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
+ const [pollCleanup, setPollCleanup] = useState<(() => void) | null>(null);
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
+ // Cleanup polling on unmount
|
|
|
+ useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ if (pollCleanup) {
|
|
|
+ pollCleanup();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, [pollCleanup]);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
const loadModels = async () => {
|
|
|
try {
|
|
|
@@ -101,7 +113,7 @@ function UpscalerForm() {
|
|
|
|
|
|
try {
|
|
|
const base64 = await fileToBase64(file);
|
|
|
- setFormData((prev) => ({ ...prev, image: base64 }));
|
|
|
+ setUploadedImage(base64);
|
|
|
setPreviewImage(base64);
|
|
|
setError(null);
|
|
|
} catch (err) {
|
|
|
@@ -112,8 +124,11 @@ function UpscalerForm() {
|
|
|
const pollJobStatus = async (jobId: string) => {
|
|
|
const maxAttempts = 300;
|
|
|
let attempts = 0;
|
|
|
+ let isPolling = true;
|
|
|
|
|
|
const poll = async () => {
|
|
|
+ if (!isPolling) return;
|
|
|
+
|
|
|
try {
|
|
|
const status = await apiClient.getJobStatus(jobId);
|
|
|
setJobInfo(status);
|
|
|
@@ -147,32 +162,44 @@ function UpscalerForm() {
|
|
|
// Create a new array to trigger React re-render
|
|
|
setGeneratedImages([...imageUrls]);
|
|
|
setLoading(false);
|
|
|
+ isPolling = false;
|
|
|
} else if (status.status === 'failed') {
|
|
|
setError(status.error || 'Upscaling failed');
|
|
|
setLoading(false);
|
|
|
+ isPolling = false;
|
|
|
} else if (status.status === 'cancelled') {
|
|
|
setError('Upscaling was cancelled');
|
|
|
setLoading(false);
|
|
|
+ isPolling = false;
|
|
|
} else if (attempts < maxAttempts) {
|
|
|
attempts++;
|
|
|
setTimeout(poll, 2000);
|
|
|
} else {
|
|
|
setError('Job polling timeout');
|
|
|
setLoading(false);
|
|
|
+ isPolling = false;
|
|
|
}
|
|
|
} catch (err) {
|
|
|
- setError(err instanceof Error ? err.message : 'Failed to check job status');
|
|
|
- setLoading(false);
|
|
|
+ if (isPolling) {
|
|
|
+ setError(err instanceof Error ? err.message : 'Failed to check job status');
|
|
|
+ setLoading(false);
|
|
|
+ isPolling = false;
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
poll();
|
|
|
+
|
|
|
+ // Return cleanup function
|
|
|
+ return () => {
|
|
|
+ isPolling = false;
|
|
|
+ };
|
|
|
};
|
|
|
|
|
|
const handleUpscale = async (e: React.FormEvent) => {
|
|
|
e.preventDefault();
|
|
|
|
|
|
- if (!formData.image) {
|
|
|
+ if (!uploadedImage) {
|
|
|
setError('Please upload an image first');
|
|
|
return;
|
|
|
}
|
|
|
@@ -194,12 +221,14 @@ function UpscalerForm() {
|
|
|
const job = await apiClient.generateImage({
|
|
|
prompt: `upscale ${formData.upscale_factor}x`,
|
|
|
model: selectedUpscalerModel,
|
|
|
+ image: uploadedImage,
|
|
|
// Add upscale-specific parameters here based on your API
|
|
|
} as any);
|
|
|
setJobInfo(job);
|
|
|
const jobId = job.request_id || job.id;
|
|
|
if (jobId) {
|
|
|
- await pollJobStatus(jobId);
|
|
|
+ const cleanup = pollJobStatus(jobId);
|
|
|
+ setPollCleanup(() => cleanup);
|
|
|
} else {
|
|
|
setError('No job ID returned from server');
|
|
|
setLoading(false);
|
|
|
@@ -217,6 +246,11 @@ function UpscalerForm() {
|
|
|
await apiClient.cancelJob(jobId);
|
|
|
setLoading(false);
|
|
|
setError('Upscaling cancelled');
|
|
|
+ // Cleanup polling
|
|
|
+ if (pollCleanup) {
|
|
|
+ pollCleanup();
|
|
|
+ setPollCleanup(null);
|
|
|
+ }
|
|
|
} catch (err) {
|
|
|
console.error('Failed to cancel job:', err);
|
|
|
}
|
|
|
@@ -237,88 +271,79 @@ function UpscalerForm() {
|
|
|
<div className="space-y-4">
|
|
|
{previewImage && (
|
|
|
<div className="relative">
|
|
|
- <img
|
|
|
- src={previewImage}
|
|
|
- alt="Source"
|
|
|
- className="w-full rounded-lg border border-border"
|
|
|
+ <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>
|
|
|
)}
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="outline"
|
|
|
- onClick={() => fileInputRef.current?.click()}
|
|
|
- className="w-full"
|
|
|
- >
|
|
|
- <Upload className="h-4 w-4" />
|
|
|
- {previewImage ? 'Change Image' : 'Upload Image'}
|
|
|
- </Button>
|
|
|
- <input
|
|
|
- ref={fileInputRef}
|
|
|
- type="file"
|
|
|
- accept="image/*"
|
|
|
- onChange={handleImageUpload}
|
|
|
- className="hidden"
|
|
|
- />
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <div className="flex items-center justify-center">
|
|
|
+ <input
|
|
|
+ ref={fileInputRef}
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ onChange={handleImageUpload}
|
|
|
+ className="hidden"
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => fileInputRef.current?.click()}
|
|
|
+ >
|
|
|
+ <Upload className="mr-2 h-4 w-4" />
|
|
|
+ Choose Image
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
- <Label htmlFor="upscale_factor">Upscale Factor</Label>
|
|
|
+ <Label>Upscaling Factor</Label>
|
|
|
<select
|
|
|
- id="upscale_factor"
|
|
|
- name="upscale_factor"
|
|
|
value={formData.upscale_factor}
|
|
|
- onChange={handleInputChange}
|
|
|
- className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
|
+ 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>
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
- Higher factors take longer to process
|
|
|
- </p>
|
|
|
</div>
|
|
|
|
|
|
- {/* Model Selection Section */}
|
|
|
- <EnhancedModelSelectGroup
|
|
|
- title="Model Selection"
|
|
|
- description="Select the upscaler model for image enhancement"
|
|
|
- >
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Upscaler Model</Label>
|
|
|
<EnhancedModelSelect
|
|
|
modelType="upscaler"
|
|
|
label="Upscaling Model"
|
|
|
- description="Model to use for upscaling the image"
|
|
|
- value={selectedUpscalerModel}
|
|
|
+ value={formData.model}
|
|
|
+ onValueChange={(value) => setFormData(prev => ({ ...prev, model: value }))}
|
|
|
+ onSetUserOverride={(value) => {/* User override logic */}}
|
|
|
+ onClearOverride={clearUpscalerUserOverride}
|
|
|
availableModels={upscalerModels}
|
|
|
isAutoSelected={isUpscalerAutoSelected}
|
|
|
isUserOverride={isUpscalerUserOverride}
|
|
|
- isLoading={state.isLoading}
|
|
|
- onValueChange={setSelectedUpscalerModel}
|
|
|
- onSetUserOverride={setUpscalerUserOverride}
|
|
|
- onClearOverride={clearUpscalerUserOverride}
|
|
|
placeholder="Select an upscaler model"
|
|
|
/>
|
|
|
-
|
|
|
- {/* Auto-selection Status */}
|
|
|
- <div className="pt-2">
|
|
|
- <AutoSelectionStatus
|
|
|
- isAutoSelecting={state.isAutoSelecting}
|
|
|
- hasAutoSelection={Object.keys(state.autoSelectedModels).length > 0}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Warnings and Errors */}
|
|
|
- <ModelSelectionWarning
|
|
|
- warnings={state.warnings}
|
|
|
- errors={error ? [error] : []}
|
|
|
- onClearWarnings={actions.clearWarnings}
|
|
|
- />
|
|
|
- </EnhancedModelSelectGroup>
|
|
|
+ </div>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
- <Button type="submit" disabled={loading || !formData.image} className="flex-1">
|
|
|
+ <Button type="submit" disabled={loading || !uploadedImage} className="flex-1">
|
|
|
{loading ? (
|
|
|
<>
|
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
|
@@ -341,28 +366,9 @@ function UpscalerForm() {
|
|
|
{error}
|
|
|
</div>
|
|
|
)}
|
|
|
-
|
|
|
- {loading && jobInfo && (
|
|
|
- <div className="rounded-md bg-muted p-3 text-sm">
|
|
|
- <p>Job ID: {jobInfo.id || jobInfo.request_id || 'N/A'}</p>
|
|
|
- <p>Status: {jobInfo.status}</p>
|
|
|
- {jobInfo.progress !== undefined && (
|
|
|
- <p>Progress: {Math.round(jobInfo.progress * 100)}%</p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- <div className="rounded-md bg-blue-500/10 p-3 text-sm text-blue-600 dark:text-blue-400">
|
|
|
- <p className="font-medium">Note</p>
|
|
|
- <p className="mt-1">
|
|
|
- Upscaling functionality depends on your backend configuration and available upscaler models.
|
|
|
- </p>
|
|
|
- </div>
|
|
|
</form>
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
-
|
|
|
- {/* Right Panel - Upscaled Images */}
|
|
|
<Card>
|
|
|
<CardContent className="pt-6">
|
|
|
<div className="space-y-4">
|