279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
'use client';
|
|
|
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
|
import { cn } from '@/lib/utils';
|
|
import { useMitt } from '@/plugins/MittContext';
|
|
import useMediaStore from '@/stores/MediaStore';
|
|
import useUserStore from '@/stores/UserStore';
|
|
import { usePage } from '@inertiajs/react';
|
|
import { useEffect, useRef, useState } 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">AI features coming soon!</SheetTitle>
|
|
<SheetDescription className="hidden"></SheetDescription>
|
|
</SheetHeader>
|
|
|
|
{/* <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"
|
|
/>
|
|
|
|
{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;
|