Files
memefast/resources/js/modules/editor/partials/editor-ai-sheet.jsx
2025-07-04 14:55:56 +08:00

269 lines
11 KiB
JavaScript

'use client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { useMitt } from '@/plugins/MittContext';
import CoinIcon from '@/reusables/coin-icon';
import useMediaStore from '@/stores/MediaStore';
import useUserStore from '@/stores/UserStore';
import { usePage } from '@inertiajs/react';
import { useEffect, useState, useRef } from 'react';
import { toast } from 'sonner';
const EditorAISheet = () => {
const [isOpen, setIsOpen] = useState(false);
const [prompt, setPrompt] = useState('');
const emitter = useMitt();
const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme, checkActiveJob } = useMediaStore();
const pollingIntervalRef = useRef(null);
const currentJobIdRef = useRef(null);
const { credits } = useUserStore();
const { auth } = usePage().props;
useEffect(() => {
const openSheetListener = () => {
setIsOpen(true);
fetchAIHints();
};
emitter.on('open-ai-editor-sheet', openSheetListener);
return () => {
emitter.off('open-ai-editor-sheet', openSheetListener);
};
}, [emitter, fetchAIHints]);
// Check for active job on component mount
useEffect(() => {
if (auth.user) {
checkForActiveJob();
}
}, [auth.user]);
const checkForActiveJob = async () => {
try {
const response = await checkActiveJob();
if (response?.success?.data) {
const { job_id, status, result } = response.success.data;
if (status === 'pending' || status === 'processing') {
// Resume polling for active job
setGeneratingMeme(true);
currentJobIdRef.current = job_id;
startPolling(job_id);
} else if (status === 'completed' && result) {
// Show completed result
updateMemeResult(result);
toast.success('Your previous meme generation completed!');
}
}
} catch (error) {
// No active job or error - continue normally
console.log('No active job found or error checking:', error);
}
};
const handleOpenChange = (open) => {
setIsOpen(open);
// If sheet is being closed while generating, stop polling
if (!open && isGeneratingMeme) {
stopPolling();
}
};
const startPolling = (jobId) => {
currentJobIdRef.current = jobId;
// Clear existing interval if any
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
const checkJobStatus = async () => {
try {
const response = await checkMemeJobStatus(jobId);
if (response?.success?.data) {
const { status, result, error } = response.success.data;
if (status === 'completed') {
// Job completed successfully
if (result?.generate) {
updateMemeResult(result);
stopPolling();
toast.success('Meme generated successfully!');
}
} else if (status === 'failed') {
// Job failed
stopPolling();
setGeneratingMeme(false);
toast.error(error || 'Failed to generate meme');
}
// If status is 'pending' or 'processing', continue polling
}
} catch (error) {
console.error('Error checking job status:', error);
stopPolling();
setGeneratingMeme(false);
toast.error('Error checking meme generation status');
}
};
// Start polling every 5 seconds
pollingIntervalRef.current = setInterval(checkJobStatus, 5000);
// Also check immediately
checkJobStatus();
};
const stopPolling = () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
currentJobIdRef.current = null;
};
const handleSend = async () => {
if (prompt.trim()) {
try {
const response = await generateMeme(prompt);
if (response?.success?.data?.job_id) {
startPolling(response.success.data.job_id);
}
} catch (error) {
// Error already handled in store
}
}
};
// Close sheet when generation is complete
useEffect(() => {
if (!isGeneratingMeme && isOpen && currentJobIdRef.current) {
// Small delay to let user see the success message
const timer = setTimeout(() => {
setIsOpen(false);
setPrompt('');
}, 1000);
return () => clearTimeout(timer);
}
}, [isGeneratingMeme, isOpen]);
// Cleanup polling on unmount
useEffect(() => {
return () => {
stopPolling();
};
}, []);
return (
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<SheetContent
side="bottom"
className={cn('gap-0! rounded-t-4xl pb-1', isGeneratingMeme && '[&>button]:hidden')}
onInteractOutside={(e) => isGeneratingMeme && e.preventDefault()}
onEscapeKeyDown={(e) => isGeneratingMeme && e.preventDefault()}
>
<SheetHeader className="mb-2 px-5">
<SheetTitle className="text-center text-xl font-semibold text-balance">
{isGeneratingMeme ? 'Creating...' : 'What meme would you like to create?'}
</SheetTitle>
<SheetDescription className="hidden"></SheetDescription>
</SheetHeader>
<div className="mx-auto w-full max-w-[600px] space-y-4 px-4 pb-4">
<div className="space-y-3">
<Input
disabled={isGeneratingMeme}
placeholder="Make a meme for e.g. work life stress"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="bg-muted/30 max-h-20 min-h-12 resize-none rounded-3xl border-0 p-4 text-center text-base"
/>
{/* AI Keywords */}
{isLoadingAIHints && <div className="text-muted-foreground text-center text-sm">Loading AI hints...</div>}
{keywords.length > 0 && !isLoadingAIHints && (
<div className="flex flex-wrap justify-center gap-2">
{keywords.map((keyword, index) => (
<Button
disabled={isGeneratingMeme}
key={index}
variant="secondary"
size="sm"
className="h-auto rounded-full px-3 py-1 text-xs"
onClick={() => setPrompt(keyword)}
>
{keyword}
</Button>
))}
</div>
)}
<div className={cn('space-y-2', !prompt.trim() && 'invisible')}>
{auth.user ? (
<>
<Button
onClick={handleSend}
className={cn('w-full rounded-full')}
size="lg"
variant="outline"
disabled={!prompt.trim() || isGeneratingMeme}
>
{isGeneratingMeme ? (
<Spinner className="text-primary h-4 w-4" />
) : (
<>
Generate Meme
<div className="flex items-center gap-1">
<CoinIcon></CoinIcon> 2
</div>
</>
)}
</Button>
<div className="text-muted-foreground text-center text-xs">
A new meme costs 1 credit for AI captions & 1 credit for AI background.{' '}
</div>
<div className="text-muted-foreground text-center text-xs">You have {credits} credits remaining.</div>
</>
) : (
<>
<div className="text-muted-foreground text-center text-xs flex items-center justify-center gap-1">
<Button
onClick={() => emitter.emit('login')}
variant="link"
size="sm"
className="text-primary h-auto p-0 text-xs"
>
Login
</Button>
<span>/</span>
<Button
onClick={() => emitter.emit('join')}
variant="link"
size="sm"
className="text-primary h-auto p-0 text-xs"
>
Signup
</Button>
<span>to use AI features.</span>
</div>
</>
)}
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
};
export default EditorAISheet;