diff --git a/MEMEAIGEN/STRIPE WEBHOOKS.bru b/MEMEAIGEN/STRIPE WEBHOOKS.bru index 45b6e56..842d9c7 100644 --- a/MEMEAIGEN/STRIPE WEBHOOKS.bru +++ b/MEMEAIGEN/STRIPE WEBHOOKS.bru @@ -12,136 +12,190 @@ post { body:json { { - "id": "evt_1RgRPrEEXQJo9EEOWggvNEHe", + "id": "evt_1RghW8EEXQJo9EEOI2W1LyN4", "object": "event", "api_version": "2025-05-28.basil", - "created": 1751465619, + "created": 1751527512, "data": { "object": { - "id": "cs_test_a1RMSyzQu5Mk2cZYoAYLOCV9hfoc5z9RuYdE4jLY2htoizGr1AJZeAcnDh", - "object": "checkout.session", - "adaptive_pricing": { - "enabled": true - }, - "after_expiration": null, - "allow_promotion_codes": null, - "amount_subtotal": 400, - "amount_total": 400, + "id": "sub_1RgU1iEEXQJo9EEOBmsO7ZIo", + "object": "subscription", + "application": null, + "application_fee_percent": null, "automatic_tax": { + "disabled_reason": null, "enabled": false, - "liability": null, - "provider": null, - "status": null + "liability": null }, - "billing_address_collection": null, - "cancel_url": "https://memeaigen.test/subscribe/cancelled?session_id={CHECKOUT_SESSION_ID}", - "client_reference_id": null, - "client_secret": null, - "collected_information": { - "shipping_details": null + "billing_cycle_anchor": 1751475654, + "billing_cycle_anchor_config": null, + "billing_mode": { + "type": "classic" }, - "consent": null, - "consent_collection": null, - "created": 1751465613, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": 1751527511, + "cancellation_details": { + "comment": null, + "feedback": null, + "reason": "cancellation_requested" + }, + "collection_method": "charge_automatically", + "created": 1751475654, "currency": "usd", - "currency_conversion": null, - "custom_fields": [], - "custom_text": { - "after_submit": null, - "shipping_address": null, - "submit": null, - "terms_of_service_acceptance": null - }, "customer": "cus_SbGYl34MpG4nv5", - "customer_creation": null, - "customer_details": { - "address": { - "city": null, - "country": "MY", - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": "memeaigen.com@gmail.com", - "name": "TEST NAME", - "phone": null, - "tax_exempt": "none", - "tax_ids": [] - }, - "customer_email": null, + "days_until_due": null, + "default_payment_method": "pm_1RgU1hEEXQJo9EEOBnHcyDTz", + "default_source": null, + "default_tax_rates": [], + "description": null, "discounts": [], - "expires_at": 1751552013, - "invoice": null, - "invoice_creation": { - "enabled": false, - "invoice_data": { - "account_tax_ids": null, - "custom_fields": null, - "description": null, - "footer": null, - "issuer": null, - "metadata": {}, - "rendering_options": null + "ended_at": 1751527511, + "invoice_settings": { + "account_tax_ids": null, + "issuer": { + "type": "self" } }, - "livemode": false, - "locale": null, - "metadata": {}, - "mode": "payment", - "payment_intent": "pi_3RgRPqEEXQJo9EEO1a43uB5g", - "payment_link": null, - "payment_method_collection": "if_required", - "payment_method_configuration_details": { - "id": "pmc_1RfN0QEEXQJo9EEOzYHrN3LV", - "parent": null - }, - "payment_method_options": { - "card": { - "request_three_d_secure": "automatic" - } - }, - "payment_method_types": [ - "card", - "link" - ], - "payment_status": "paid", - "permissions": null, - "phone_number_collection": { - "enabled": false - }, - "recovered_from": null, - "saved_payment_method_options": { - "allow_redisplay_filters": [ - "always" + "items": { + "object": "list", + "data": [ + { + "id": "si_SbhQOOEJ1xUQ7Y", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1751475654, + "current_period_end": 1754154054, + "current_period_start": 1751475654, + "discounts": [], + "metadata": {}, + "plan": { + "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", + "object": "plan", + "active": true, + "amount": 400, + "amount_decimal": "400", + "billing_scheme": "per_unit", + "created": 1751210467, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_SaY8TGjiPi5hWu", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1751210467, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_SaY8TGjiPi5hWu", + "recurring": { + "interval": "month", + "interval_count": 1, + "meter": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 400, + "unit_amount_decimal": "400" + }, + "quantity": 1, + "subscription": "sub_1RgU1iEEXQJo9EEOBmsO7ZIo", + "tax_rates": [] + } ], - "payment_method_remove": "disabled", - "payment_method_save": null + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_1RgU1iEEXQJo9EEOBmsO7ZIo" }, - "setup_intent": null, - "shipping_address_collection": null, - "shipping_cost": null, - "shipping_options": [], - "status": "complete", - "submit_type": null, - "subscription": null, - "success_url": "https://memeaigen.test/subscribe/success?session_id={CHECKOUT_SESSION_ID}", - "total_details": { - "amount_discount": 0, - "amount_shipping": 0, - "amount_tax": 0 + "latest_invoice": "in_1RgU1iEEXQJo9EEObpcu0isX", + "livemode": false, + "metadata": { + "is_on_session_checkout": "true" }, - "ui_mode": "hosted", - "url": null, - "wallet_options": null + "next_pending_invoice_item_invoice": null, + "on_behalf_of": null, + "pause_collection": null, + "payment_settings": { + "payment_method_options": { + "acss_debit": null, + "bancontact": null, + "card": { + "network": null, + "request_three_d_secure": "automatic" + }, + "customer_balance": null, + "konbini": null, + "sepa_debit": null, + "us_bank_account": null + }, + "payment_method_types": null, + "save_default_payment_method": "off" + }, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": { + "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", + "object": "plan", + "active": true, + "amount": 400, + "amount_decimal": "400", + "billing_scheme": "per_unit", + "created": 1751210467, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_SaY8TGjiPi5hWu", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start_date": 1751475654, + "status": "canceled", + "test_clock": null, + "transfer_data": null, + "trial_end": null, + "trial_settings": { + "end_behavior": { + "missing_payment_method": "create_invoice" + } + }, + "trial_start": null } }, "livemode": false, "pending_webhooks": 2, "request": { - "id": null, + "id": "req_cCXhmqNMHSgIwr", "idempotency_key": null }, - "type": "checkout.session.completed" + "type": "customer.subscription.deleted" } } diff --git a/app/Helpers/FirstParty/Meme/MemeGenerator.php b/app/Helpers/FirstParty/Meme/MemeGenerator.php index e63d9f2..7d50e3e 100644 --- a/app/Helpers/FirstParty/Meme/MemeGenerator.php +++ b/app/Helpers/FirstParty/Meme/MemeGenerator.php @@ -108,24 +108,58 @@ public static function generateMemeByCategory(Category $category) return $meme; } - public static function generateMemeOutputByCategory(Category $category) + public static function generateMemeByKeyword($keyword) { + $meme_output = self::generateMemeOutputByKeyword($keyword); + + $meme = null; + + if ($meme_output->success) { + $meme = Meme::create([ + 'type' => self::TYPE_SINGLE_CAPTION_MEME_BACKGROUND, + 'prompt' => $meme_output->prompt, + 'category_id' => null, + 'caption' => $meme_output->caption, + 'meme_keywords' => $meme_output->keywords, + 'background' => $meme_output->background, + 'keywords' => $meme_output->keywords, + 'is_system' => true, + 'status' => self::STATUS_PENDING, + 'primary_keyword_type' => $meme_output->primary_keyword_type, + 'action_keywords' => $meme_output->action_keywords, + 'emotion_keywords' => $meme_output->emotion_keywords, + 'misc_keywords' => $meme_output->misc_keywords, + ]); + + $meme->attachTags($meme_output->keywords, 'meme'); + } + + if (! is_null($meme) && $meme->status == self::STATUS_PENDING) { + // populate meme_media_id + $meme->meme_media_id = self::getSuitableMemeMedia($meme)->id; + $meme->background_media_id = self::generateBackgroundMediaWithRunware($meme_output->background)->id; + + if ( + // !is_null($meme->meme_media_id) && + ! is_null($meme->background_media_id) + ) { + $meme->status = self::STATUS_COMPLETED; + } + + $meme->save(); + } + + return $meme; + } + + + public static function generateMemeOutputByKeyword($keyword, $category = null) + { + $retries = 3; $attempt = 0; - $random_keyword = Str::lower($category->name); - - if (! is_null($category->parent_id)) { - $random_keyword = $category->parent->name.' - '.$random_keyword; - } - - if (! is_null($category->meme_angles)) { - $random_keyword .= ' - '.collect($category->meme_angles)->random(); - } elseif (! is_null($category->keywords)) { - $random_keyword .= ' - '.collect($category->keywords)->random(); - } - - $prompt = "Write me 1 meme about {$random_keyword}"; + $prompt = "Write me 1 meme about {$keyword}"; // RETRY MECHANISM START do { @@ -169,13 +203,31 @@ public static function generateMemeOutputByCategory(Category $category) $meme_output = (object) [ 'success' => false, 'attempts' => $attempt, // Optional: track how many attempts were made - 'error' => 'Failed to generate valid meme after '.$retries.' attempts', + 'error' => 'Failed to generate valid meme after ' . $retries . ' attempts', ]; } return $meme_output; } + public static function generateMemeOutputByCategory(Category $category) + { + + $random_keyword = Str::lower($category->name); + + if (! is_null($category->parent_id)) { + $random_keyword = $category->parent->name . ' - ' . $random_keyword; + } + + if (! is_null($category->meme_angles)) { + $random_keyword .= ' - ' . collect($category->meme_angles)->random(); + } elseif (! is_null($category->keywords)) { + $random_keyword .= ' - ' . collect($category->keywords)->random(); + } + + return self::generateMemeOutputByKeyword($random_keyword, $category); + } + public static function generateBackgroundMediaWithRunware($prompt) { $media_width = 1024; diff --git a/app/Http/Controllers/UserAIController.php b/app/Http/Controllers/UserAIController.php index 9058a71..1e06d6b 100644 --- a/app/Http/Controllers/UserAIController.php +++ b/app/Http/Controllers/UserAIController.php @@ -2,12 +2,74 @@ namespace App\Http\Controllers; +use App\Helpers\FirstParty\Credits\CreditsService; +use App\Helpers\FirstParty\Meme\MemeGenerator; use Illuminate\Http\Request; +use App\Models\Category; +use Auth; class UserAIController extends Controller { + public function generateMeme(Request $request) + { + $user = Auth::user(); + + + if (!CreditsService::canSpend($user->id, 2)) { + return response()->json([ + 'error' => [ + 'message' => 'You do not have enough credits to generate a meme. Please purchase credits from the Store.', + ], + ]); + } + + CreditsService::spend($user->id, 2); + + $meme = MemeGenerator::generateMemeByKeyword($request->prompt); + + $meme_media = MemeGenerator::getSuitableMemeMedia($meme, 2); + + return response()->json([ + 'success' => [ + 'data' => [ + 'generate' => [ + 'info' => $meme, + 'caption' => $meme->caption, + 'meme' => $meme_media, + 'background' => $meme->background_media, + ], + ], + ], + ]); + } + public function aiHints() { - // TODO: Take 5 Category where meme_angles is not null, and return a random one from the array + $categories = Category::whereNotNull('keywords') + ->inRandomOrder() + ->take(8) + ->get(); + + if ($categories->isEmpty()) { + return response()->json([ + 'success' => [ + 'data' => [ + 'keywords' => [] + ] + ] + ]); + } + + $keywords = $categories->map(function ($category) { + return collect($category->keywords)->random(); + })->toArray(); + + return response()->json([ + 'success' => [ + 'data' => [ + 'keywords' => $keywords + ] + ] + ]); } } diff --git a/resources/js/modules/editor/partials/editor-ai-sheet.jsx b/resources/js/modules/editor/partials/editor-ai-sheet.jsx index 0ddee16..4142b44 100644 --- a/resources/js/modules/editor/partials/editor-ai-sheet.jsx +++ b/resources/js/modules/editor/partials/editor-ai-sheet.jsx @@ -3,19 +3,28 @@ 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 } from 'react'; const EditorAISheet = () => { const [isOpen, setIsOpen] = useState(false); const [prompt, setPrompt] = useState(''); const emitter = useMitt(); + const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints } = useMediaStore(); + + const { user } = useUserStore(); + const { auth } = usePage().props; useEffect(() => { const openSheetListener = () => { setIsOpen(true); + fetchAIHints(); }; emitter.on('open-ai-editor-sheet', openSheetListener); @@ -23,7 +32,7 @@ const EditorAISheet = () => { return () => { emitter.off('open-ai-editor-sheet', openSheetListener); }; - }, [emitter]); + }, [emitter, fetchAIHints]); const handleOpenChange = (open) => { setIsOpen(open); @@ -31,39 +40,77 @@ const EditorAISheet = () => { const handleSend = () => { if (prompt.trim()) { - console.log('Sending prompt:', prompt); - setPrompt(''); + generateMeme(prompt); } }; return ( - - - What can I help with? + button]:hidden')} + onInteractOutside={(e) => isGeneratingMeme && e.preventDefault()} + onEscapeKeyDown={(e) => isGeneratingMeme && e.preventDefault()} + > + + + {isGeneratingMeme ? 'Creating...' : 'What meme would you like to create?'} +
setPrompt(e.target.value)} className="bg-muted/30 max-h-20 min-h-12 resize-none rounded-3xl border-0 p-4 text-base" /> - + ))}
- + )} +
+ +
+ A new meme costs 1 credit for AI captions & 1 credit for AI background.{' '} +
+
diff --git a/resources/js/stores/MediaStore.js b/resources/js/stores/MediaStore.js index d368b33..58087d7 100644 --- a/resources/js/stores/MediaStore.js +++ b/resources/js/stores/MediaStore.js @@ -20,6 +20,11 @@ const useMediaStore = create( currentCaption: 'I am chicken rice', watermarked: true, + keywords: [], + isLoadingAIHints: false, + + isGeneratingMeme: false, + setCurrentTab: (tab) => { set({ currentTab: tab }); }, @@ -42,6 +47,29 @@ const useMediaStore = create( set({ selectedBackground: null }); }, + // Fetch AI hints + fetchAIHints: async () => { + set({ isLoadingAIHints: true }); + try { + const response = await axiosInstance.post(route('api.ai_hints')); + set({ + keywords: response.data.success?.data?.keywords || [], + }); + return response.data; + } catch (error) { + console.error(route('api.ai_hints')); + console.error('Error fetching AI hints:', error); + toast.error('Failed to fetch AI hints'); + } finally { + set({ isLoadingAIHints: false }); + } + }, + + // Clear keywords + clearKeywords: () => { + set({ keywords: [] }); + }, + init: async () => { try { const response = await axiosInstance.post(route('api.app.init')); @@ -62,6 +90,31 @@ const useMediaStore = create( } }, + generateMeme: async (prompt) => { + set({ isGeneratingMeme: true }); + try { + const response = await axiosInstance.post(route('api.user.generate_meme', { prompt: prompt })); + + if (response?.data?.success?.data?.generate) { + set({ + currentCaption: response.data.success.data.generate.caption, + selectedMeme: response.data.success.data.generate.meme, + selectedBackground: response.data.success.data.generate.background, + }); + } else { + throw 'Invalid API response'; + } + + return response.data; + } catch (error) { + console.error(route('api.app.generate_meme')); + console.error('Error generating meme:', error); + toast.error('Failed to generate meme'); + } finally { + set({ isGeneratingMeme: false }); + } + }, + // Fetch memes (overlays) fetchMemes: async () => { set({ isFetchingMemes: true }); diff --git a/routes/api.php b/routes/api.php index c29f0ab..e131369 100644 --- a/routes/api.php +++ b/routes/api.php @@ -34,6 +34,8 @@ Route::post('/premium-export/request', [UserExportController::class, 'premiumExportRequest'])->name('api.user.premium_export.request'); Route::post('/premium-export/complete', [UserExportController::class, 'premiumExportComplete'])->name('api.user.premium_export.complete'); + + Route::post('generate_meme', [UserAIController::class, 'generateMeme'])->name('api.user.generate_meme'); }); });