From b3ffc261a31cd897357b94f509515fdd137ccf19 Mon Sep 17 00:00:00 2001 From: ct Date: Fri, 4 Jul 2025 14:55:56 +0800 Subject: [PATCH] Update --- app/Http/Controllers/UserAIController.php | 128 ++++++++++++++++++ app/Jobs/GenerateMemeJob.php | 67 ++++++++- app/Models/UserMemeGeneration.php | 34 +++++ ...813_create_user_meme_generations_table.php | 37 +++++ .../editor/partials/editor-ai-sheet.jsx | 32 ++++- resources/js/pages/home/home.tsx | 4 +- resources/js/stores/MediaStore.js | 20 +++ routes/api.php | 2 + 8 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 app/Models/UserMemeGeneration.php create mode 100644 database/migrations/2025_07_04_054813_create_user_meme_generations_table.php diff --git a/app/Http/Controllers/UserAIController.php b/app/Http/Controllers/UserAIController.php index 1256f8d..acb9b0c 100644 --- a/app/Http/Controllers/UserAIController.php +++ b/app/Http/Controllers/UserAIController.php @@ -4,6 +4,7 @@ use App\Helpers\FirstParty\Credits\CreditsService; use App\Jobs\GenerateMemeJob; +use App\Models\UserMemeGeneration; use Illuminate\Http\Request; use App\Models\Category; use Auth; @@ -16,6 +17,23 @@ public function generateMeme(Request $request) { $user = Auth::user(); + // Check if user has an active job + $activeJobId = Cache::get("user_active_job_{$user->id}"); + if ($activeJobId) { + $activeGeneration = UserMemeGeneration::where('job_id', $activeJobId) + ->where('user_id', $user->id) + ->whereIn('status', ['pending', 'processing']) + ->first(); + + if ($activeGeneration) { + return response()->json([ + 'error' => [ + 'message' => 'You already have a meme generation in progress. Please wait for it to complete.', + ], + ], 400); + } + } + if (!CreditsService::canSpend($user->id, 2)) { return response()->json([ 'error' => [ @@ -28,6 +46,18 @@ public function generateMeme(Request $request) $jobId = Str::uuid()->toString(); + // Create database record + $generation = UserMemeGeneration::create([ + 'user_id' => $user->id, + 'job_id' => $jobId, + 'prompt' => $request->prompt, + 'status' => 'pending', + 'credits_to_be_charged' => 2, + 'credits_are_processed' => false, + ]); + + // Set active job in cache + Cache::put("user_active_job_{$user->id}", $jobId, 300); Cache::put("meme_job_status_{$jobId}", 'pending', 300); $job = new GenerateMemeJob($user->id, $request->prompt, $jobId); @@ -89,6 +119,104 @@ public function checkMemeJobStatus(Request $request) return response()->json($response); } + public function getActiveJob() + { + $user = Auth::user(); + + $activeJobId = Cache::get("user_active_job_{$user->id}"); + + if (!$activeJobId) { + return response()->json([ + 'success' => [ + 'data' => null, + ], + ]); + } + + $generation = UserMemeGeneration::where('job_id', $activeJobId) + ->where('user_id', $user->id) + ->with('meme.meme_media', 'meme.background_media') + ->first(); + + if (!$generation) { + // Clean up stale cache + Cache::forget("user_active_job_{$user->id}"); + return response()->json([ + 'success' => [ + 'data' => null, + ], + ]); + } + + $response = [ + 'success' => [ + 'data' => [ + 'job_id' => $generation->job_id, + 'status' => $generation->status, + 'prompt' => $generation->prompt, + 'created_at' => $generation->created_at, + ], + ], + ]; + + // If completed, include the meme result + if ($generation->status === 'completed' && $generation->meme) { + $meme = $generation->meme; + $meme_media = $generation->meme->meme_media; + + $response['success']['data']['result'] = [ + 'generate' => [ + 'info' => $meme, + 'caption' => $meme->caption, + 'meme' => $meme_media, + 'background' => $meme->background_media, + ], + ]; + } + + return response()->json($response); + } + + public function getMemeHistory() + { + $user = Auth::user(); + + $generations = UserMemeGeneration::where('user_id', $user->id) + ->with('meme.meme_media', 'meme.background_media') + ->orderBy('created_at', 'desc') + ->limit(20) + ->get(); + + $history = $generations->map(function ($generation) { + $data = [ + 'job_id' => $generation->job_id, + 'prompt' => $generation->prompt, + 'status' => $generation->status, + 'created_at' => $generation->created_at, + ]; + + if ($generation->status === 'completed' && $generation->meme) { + $meme = $generation->meme; + $data['meme'] = [ + 'info' => $meme, + 'caption' => $meme->caption, + 'meme_media' => $meme->meme_media, + 'background_media' => $meme->background_media, + ]; + } + + return $data; + }); + + return response()->json([ + 'success' => [ + 'data' => [ + 'history' => $history, + ], + ], + ]); + } + public function aiHints() { $categories = Category::whereNotNull('keywords') diff --git a/app/Jobs/GenerateMemeJob.php b/app/Jobs/GenerateMemeJob.php index 61e0608..b6674bf 100644 --- a/app/Jobs/GenerateMemeJob.php +++ b/app/Jobs/GenerateMemeJob.php @@ -2,8 +2,10 @@ namespace App\Jobs; +use App\Helpers\FirstParty\Credits\CreditsService; use App\Helpers\FirstParty\Meme\MemeGenerator; use App\Models\User; +use App\Models\UserMemeGeneration; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -23,7 +25,7 @@ class GenerateMemeJob implements ShouldQueue protected $prompt; protected $jobId; - public function __construct(int $userId, string $prompt, string $jobId = null) + public function __construct(int $userId, string $prompt, ?string $jobId) { $this->userId = $userId; $this->prompt = $prompt; @@ -33,12 +35,27 @@ public function __construct(int $userId, string $prompt, string $jobId = null) public function handle(): void { + $userGeneration = UserMemeGeneration::where('job_id', $this->jobId)->first(); + + if (!$userGeneration) { + throw new \Exception("User generation record not found for job {$this->jobId}"); + } + try { + // Update status to processing + $userGeneration->update(['status' => 'processing']); Cache::put("meme_job_status_{$this->jobId}", 'processing', 300); $meme = MemeGenerator::generateMemeByKeyword($this->prompt); $meme_media = MemeGenerator::getSuitableMemeMedia($meme, 2); + // Update the generation record with the meme + $userGeneration->update([ + 'meme_id' => $meme->id, + 'status' => 'completed', + 'credits_are_processed' => true, + ]); + $result = [ 'generate' => [ 'info' => $meme, @@ -50,15 +67,61 @@ public function handle(): void Cache::put("meme_job_result_{$this->jobId}", $result, 300); Cache::put("meme_job_status_{$this->jobId}", 'completed', 300); + + // Clear active job from cache + Cache::forget("user_active_job_{$this->userId}"); } catch (\Exception $e) { + // Handle failure with credit refund + if (!$userGeneration->credits_are_processed) { + if ($userGeneration->credits_to_be_charged > 0) { + CreditsService::depositAlacarte( + $userGeneration->user_id, + $userGeneration->credits_to_be_charged, + 'Refunded credits for failed generation due to provider issue.' + ); + } + + $userGeneration->update([ + 'credits_are_processed' => true, + 'status' => 'failed', + ]); + } + Cache::put("meme_job_status_{$this->jobId}", 'failed', 300); Cache::put("meme_job_error_{$this->jobId}", $e->getMessage(), 300); + + // Clear active job from cache + Cache::forget("user_active_job_{$this->userId}"); + throw $e; } } + public function failed(\Throwable $exception): void + { + $userGeneration = UserMemeGeneration::where('job_id', $this->jobId)->first(); + + if ($userGeneration && !$userGeneration->credits_are_processed) { + if ($userGeneration->credits_to_be_charged > 0) { + CreditsService::depositAlacarte( + $userGeneration->user_id, + $userGeneration->credits_to_be_charged, + 'Refunded credits for failed generation due to system error.' + ); + } + + $userGeneration->update([ + 'credits_are_processed' => true, + 'status' => 'failed', + ]); + } + + Cache::put("meme_job_status_{$this->jobId}", 'failed', 300); + Cache::forget("user_active_job_{$this->userId}"); + } + public function getJobId(): string { return $this->jobId; } -} \ No newline at end of file +} diff --git a/app/Models/UserMemeGeneration.php b/app/Models/UserMemeGeneration.php new file mode 100644 index 0000000..eaa7016 --- /dev/null +++ b/app/Models/UserMemeGeneration.php @@ -0,0 +1,34 @@ + 'boolean', + 'credits_to_be_charged' => 'integer', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function meme(): BelongsTo + { + return $this->belongsTo(Meme::class); + } +} \ No newline at end of file diff --git a/database/migrations/2025_07_04_054813_create_user_meme_generations_table.php b/database/migrations/2025_07_04_054813_create_user_meme_generations_table.php new file mode 100644 index 0000000..f7eb5d4 --- /dev/null +++ b/database/migrations/2025_07_04_054813_create_user_meme_generations_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('meme_id')->nullable()->constrained()->onDelete('set null'); + $table->uuid('job_id')->unique(); + $table->text('prompt'); + $table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending'); + $table->integer('credits_to_be_charged'); + $table->boolean('credits_are_processed')->default(false); + $table->timestamps(); + + $table->index(['user_id', 'job_id']); + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_meme_generations'); + } +}; \ No newline at end of file diff --git a/resources/js/modules/editor/partials/editor-ai-sheet.jsx b/resources/js/modules/editor/partials/editor-ai-sheet.jsx index d28361b..1a403d0 100644 --- a/resources/js/modules/editor/partials/editor-ai-sheet.jsx +++ b/resources/js/modules/editor/partials/editor-ai-sheet.jsx @@ -17,7 +17,7 @@ const EditorAISheet = () => { const [isOpen, setIsOpen] = useState(false); const [prompt, setPrompt] = useState(''); const emitter = useMitt(); - const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme } = useMediaStore(); + const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme, checkActiveJob } = useMediaStore(); const pollingIntervalRef = useRef(null); const currentJobIdRef = useRef(null); @@ -38,6 +38,36 @@ const EditorAISheet = () => { }; }, [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); diff --git a/resources/js/pages/home/home.tsx b/resources/js/pages/home/home.tsx index f1f5658..3b1b08e 100644 --- a/resources/js/pages/home/home.tsx +++ b/resources/js/pages/home/home.tsx @@ -6,7 +6,9 @@ const Home = () => { return (
- {/*
What is MEMEAIGEN?
*/} +
MEMEAIGEN HERO
+
MEMEAIGEN features
+
MEMEAIGEN FAQ
diff --git a/resources/js/stores/MediaStore.js b/resources/js/stores/MediaStore.js index a5d8833..4c653a5 100644 --- a/resources/js/stores/MediaStore.js +++ b/resources/js/stores/MediaStore.js @@ -118,6 +118,26 @@ const useMediaStore = create( } }, + checkActiveJob: async () => { + try { + const response = await axiosInstance.post(route('api.user.get_active_job')); + return response.data; + } catch (error) { + console.error('Error checking active job:', error); + throw error; + } + }, + + getMemeHistory: async () => { + try { + const response = await axiosInstance.post(route('api.user.get_meme_history')); + return response.data; + } catch (error) { + console.error('Error getting meme history:', error); + throw error; + } + }, + updateMemeResult: (result) => { set({ currentCaption: result.generate.caption, diff --git a/routes/api.php b/routes/api.php index 1b86ef3..2720246 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,6 +38,8 @@ Route::group(['prefix' => 'generate_meme'], function () { Route::post('/', [UserAIController::class, 'generateMeme'])->name('api.user.generate_meme'); Route::post('/status', [UserAIController::class, 'checkMemeJobStatus'])->name('api.user.check_meme_job_status'); + Route::post('/active', [UserAIController::class, 'getActiveJob'])->name('api.user.get_active_job'); + Route::post('/history', [UserAIController::class, 'getMemeHistory'])->name('api.user.get_meme_history'); }); }); });