Update
This commit is contained in:
271
resources/js/pages/admin/background-gen.tsx
Normal file
271
resources/js/pages/admin/background-gen.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import { useAxios } from '@/plugins/AxiosContext';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Admin',
|
||||
href: route('admin.dashboard'),
|
||||
},
|
||||
{
|
||||
title: 'Background Generation',
|
||||
href: route('admin.background-generation'),
|
||||
},
|
||||
];
|
||||
|
||||
export default function BackgroundGeneration({ pendingMedia }) {
|
||||
const [prompt, setPrompt] = useState(pendingMedia?.prompt || '');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generatedImageUrl, setGeneratedImageUrl] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const axios = useAxios();
|
||||
|
||||
// Auto-assemble prompt when pendingMedia is available
|
||||
useEffect(() => {
|
||||
if (pendingMedia && pendingMedia.location_name && pendingMedia.list_type && pendingMedia.area) {
|
||||
const assembledPrompt =
|
||||
`${pendingMedia.location_name}, ${pendingMedia.list_type} ${pendingMedia.area}, wide angle shot, low angle view, sharp focus, cinematic composition`.toLowerCase();
|
||||
setPrompt(assembledPrompt);
|
||||
}
|
||||
}, [pendingMedia]);
|
||||
|
||||
// If no pending media, show completed state
|
||||
if (!pendingMedia) {
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Background Generation Complete" />
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center gap-6 rounded-xl p-6">
|
||||
<div className="max-w-md rounded-lg border bg-white p-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
|
||||
<svg className="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">All Done!</h2>
|
||||
<p className="mb-6 text-gray-600">
|
||||
There are no pending media records to process. All background generation tasks have been completed.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
alert('Please enter a prompt');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
setGeneratedImageUrl(null);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.generate'), {
|
||||
prompt: prompt,
|
||||
media_id: pendingMedia.id,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setGeneratedImageUrl(response.data.image_url);
|
||||
} else {
|
||||
alert(response.data.error || 'Generation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Generation error:', error);
|
||||
alert('Failed to generate image');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!generatedImageUrl) {
|
||||
alert('No generated image to save');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.save'), {
|
||||
media_id: pendingMedia.id,
|
||||
image_url: generatedImageUrl,
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
// Reload the page to start the cycle again
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.data.error || 'Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
alert('Failed to save image');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Are you sure you want to delete this pending media? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(route('admin.background-generation.delete', pendingMedia.id));
|
||||
|
||||
if (response.data.success) {
|
||||
// Reload the page to start the cycle again or show completed state
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.data.error || 'Failed to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
alert('Failed to delete media');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Background Generation" />
|
||||
<div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-2 sm:p-4 lg:flex-row">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="flex w-full flex-col gap-4 lg:w-1/2">
|
||||
{/* Pending Media Info */}
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Pending Media Information</h2>
|
||||
<div className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm sm:grid-cols-2">
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">ID:</span>
|
||||
<span className="ml-2 break-all text-gray-900">{pendingMedia.id}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">List Type:</span>
|
||||
<span className="ml-2 text-gray-900">{pendingMedia.list_type}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Area:</span>
|
||||
<span className="ml-2 text-gray-900">{pendingMedia.area}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Location:</span>
|
||||
<span className="ml-2 break-words text-gray-900">{pendingMedia.location_name}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center">
|
||||
<span className="font-medium text-gray-700">Status:</span>
|
||||
<span className="ml-2 inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
{pendingMedia.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
<span className="font-medium text-gray-700">Created:</span>
|
||||
<span className="ml-2 text-gray-900">{new Date(pendingMedia.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-gray-200 pt-3">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting || isGenerating || isSaving}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-red-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:bg-gray-400 sm:w-auto"
|
||||
>
|
||||
{isDeleting && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isDeleting ? 'Deleting...' : 'Delete Media'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Section */}
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Generate Background Image</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="prompt" className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="prompt"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="min-h-[80px] w-full rounded-md border border-gray-300 px-3 py-3 text-sm shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
rows={4}
|
||||
placeholder="Enter your prompt for image generation..."
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating || !prompt.trim()}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-blue-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{isGenerating && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isGenerating ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
{generatedImageUrl && (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex min-h-[44px] w-full items-center justify-center gap-1 rounded-md bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:bg-gray-400"
|
||||
>
|
||||
{isSaving && <Spinner className="h-4 w-4 text-white" />}
|
||||
{isSaving ? 'Saving...' : 'Save & Complete'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Image Preview */}
|
||||
<div className="order-first w-full lg:order-last lg:w-1/2">
|
||||
<div className="rounded-lg border bg-white p-3 sm:p-4">
|
||||
<h2 className="mb-3 text-base font-semibold sm:text-lg">Image Preview</h2>
|
||||
{/* Square aspect ratio container */}
|
||||
<div className="aspect-square w-full">
|
||||
<div className="flex h-full w-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
{isGenerating ? (
|
||||
<div className="p-4 text-center">
|
||||
<Spinner className="h-8 w-8" />
|
||||
<p className="text-sm text-gray-600">Generating image...</p>
|
||||
</div>
|
||||
) : generatedImageUrl ? (
|
||||
<img src={generatedImageUrl} alt="Generated background" className="h-full w-full rounded-lg object-contain" />
|
||||
) : (
|
||||
<div className="p-4 text-center">
|
||||
<svg className="mx-auto h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 text-sm text-gray-500">No image generated yet</p>
|
||||
<p className="text-xs text-gray-400">Click Generate to create an image</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user