113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect, useCallback } from 'react';
|
||
|
|
import { RetouchUpload } from '@/components/retouch/RetouchUpload';
|
||
|
|
import { BeforeAfterSlider } from '@/components/retouch/BeforeAfterSlider';
|
||
|
|
import { PriceComparison } from '@/components/retouch/PriceComparison';
|
||
|
|
import { getRetouchResult, type RetouchResultResponse } from '@/lib/retouch';
|
||
|
|
|
||
|
|
export default function RetouchPage() {
|
||
|
|
const [retouchId, setRetouchId] = useState<string | null>(null);
|
||
|
|
const [result, setResult] = useState<RetouchResultResponse | null>(null);
|
||
|
|
const [polling, setPolling] = useState(false);
|
||
|
|
|
||
|
|
const handleUploadComplete = useCallback((id: string) => {
|
||
|
|
setRetouchId(id);
|
||
|
|
setResult(null);
|
||
|
|
setPolling(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!retouchId || !polling) return;
|
||
|
|
|
||
|
|
let cancelled = false;
|
||
|
|
|
||
|
|
const poll = async () => {
|
||
|
|
try {
|
||
|
|
const res = await getRetouchResult(retouchId);
|
||
|
|
if (cancelled) return;
|
||
|
|
setResult(res);
|
||
|
|
if (res.status === 'completed' || res.status === 'failed') {
|
||
|
|
setPolling(false);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
if (cancelled) return;
|
||
|
|
// Keep polling on error
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
poll();
|
||
|
|
const interval = setInterval(poll, 3000);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
cancelled = true;
|
||
|
|
clearInterval(interval);
|
||
|
|
};
|
||
|
|
}, [retouchId, polling]);
|
||
|
|
|
||
|
|
const buildImageUrl = (path: string | null | undefined): string => {
|
||
|
|
if (!path) return '';
|
||
|
|
const filename = path.split('/').pop();
|
||
|
|
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||
|
|
return `${base.replace('/api/v1', '')}/uploads/${filename}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div data-testid="retouch-page" className="space-y-6">
|
||
|
|
<h1 className="text-2xl font-bold text-text">Bildretusche & Preisvergleich</h1>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2 className="text-lg font-semibold text-text mb-3">Image Upload</h2>
|
||
|
|
<RetouchUpload onUploadComplete={handleUploadComplete} />
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{result && (
|
||
|
|
<section>
|
||
|
|
<h2 className="text-lg font-semibold text-text mb-3">Retouch Result</h2>
|
||
|
|
<div data-testid="retouch-result" className="space-y-4">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<span className="text-sm text-text-muted">Status:</span>
|
||
|
|
<span
|
||
|
|
className={`px-2 py-1 rounded text-sm font-medium ${
|
||
|
|
result.status === 'completed'
|
||
|
|
? 'bg-green-100 text-green-700'
|
||
|
|
: result.status === 'failed'
|
||
|
|
? 'bg-error/10 text-error'
|
||
|
|
: 'bg-yellow-100 text-yellow-700'
|
||
|
|
}`}
|
||
|
|
data-testid="retouch-status"
|
||
|
|
>
|
||
|
|
{result.status}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{result.error_message && (
|
||
|
|
<div data-testid="retouch-error-message" className="p-4 bg-error/10 text-error rounded-lg">
|
||
|
|
{result.error_message}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{result.status === 'completed' && result.retouched_file_path && (
|
||
|
|
<BeforeAfterSlider
|
||
|
|
beforeSrc={buildImageUrl(result.original_file_path)}
|
||
|
|
afterSrc={buildImageUrl(result.retouched_file_path)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{result.status === 'processing' && (
|
||
|
|
<div data-testid="retouch-processing" className="text-center text-text-muted py-8">
|
||
|
|
Retouching in progress... This may take a few moments.
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2 className="text-lg font-semibold text-text mb-3">Price Comparison</h2>
|
||
|
|
<PriceComparison vehicleId={result?.vehicle_id || undefined} />
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|