diff --git a/app/Ai/Middleware/DebugGeminiRequest.php b/app/Ai/Middleware/DebugGeminiRequest.php deleted file mode 100644 index d8d0212b..00000000 --- a/app/Ai/Middleware/DebugGeminiRequest.php +++ /dev/null @@ -1,37 +0,0 @@ - get_class($prompt->agent), - 'model' => $prompt->model, - 'has_structured_output' => $prompt->agent instanceof HasStructuredOutput, - 'has_tools' => $prompt->agent instanceof HasTools, - 'tools_count' => $prompt->agent instanceof HasTools ? count(iterator_to_array($prompt->agent->tools())) : 0, - ]); - - return $next($prompt)->then(function (AgentResponse $response) { - Log::debug('Agent response debug', [ - 'response_class' => get_class($response), - 'text_length' => strlen($response->text ?? ''), - 'text_preview' => substr($response->text ?? '', 0, 200), - 'is_structured' => $response instanceof StructuredAgentResponse, - 'structured_data' => $response instanceof StructuredAgentResponse ? $response->structured : null, - ]); - }); - } -} diff --git a/app/Ai/PlatformRules/BlueskyRules.php b/app/Ai/PlatformRules/BlueskyRules.php deleted file mode 100644 index 22f5df4a..00000000 --- a/app/Ai/PlatformRules/BlueskyRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 300, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Bluesky: 300 chars max. Up to 4 images or 1 video. Similar to X but more concise.'; - } -} diff --git a/app/Ai/PlatformRules/Contract.php b/app/Ai/PlatformRules/Contract.php deleted file mode 100644 index 119047d1..00000000 --- a/app/Ai/PlatformRules/Contract.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - public function specs(): array; - - public function summary(): string; -} diff --git a/app/Ai/PlatformRules/FacebookRules.php b/app/Ai/PlatformRules/FacebookRules.php deleted file mode 100644 index 8e6bf1d0..00000000 --- a/app/Ai/PlatformRules/FacebookRules.php +++ /dev/null @@ -1,39 +0,0 @@ - 10000, - 'max_images' => 10, - 'text_only_allowed' => true, - 'formats' => [ - 'post' => ['max_media' => 10, 'text_only_allowed' => true], - 'reel' => ['aspect_ratio' => '9:16', 'video_only' => true, 'max_duration_seconds' => 90], - 'story' => ['aspect_ratio' => '9:16', 'ephemeral' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Facebook: caption up to 10k chars but short posts perform better. Text-only allowed. -- Post: flexible aspect ratio, up to 10 images/videos. -- Reel: vertical 9:16 video only, up to 90s. -- Story: vertical 9:16, single image/video, ephemeral. -TXT; - } -} diff --git a/app/Ai/PlatformRules/InstagramRules.php b/app/Ai/PlatformRules/InstagramRules.php deleted file mode 100644 index dc844352..00000000 --- a/app/Ai/PlatformRules/InstagramRules.php +++ /dev/null @@ -1,42 +0,0 @@ - 2200, - 'max_images' => 10, - 'max_videos' => 10, - 'text_only_allowed' => false, - 'formats' => [ - 'feed' => ['aspect_ratio' => '1:1 or 4:5', 'media_limit' => 10], - 'carousel' => ['aspect_ratio' => '1:1 or 4:5', 'media_limit' => 10], - 'reel' => ['aspect_ratio' => '9:16', 'video_only' => true, 'max_duration_seconds' => 90], - 'story' => ['aspect_ratio' => '9:16', 'media_limit' => 1, 'ephemeral' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Instagram: caption max 2200 chars. -- Feed: square (1:1) or portrait (4:5). Up to 10 images/videos. -- Carousel: 2-10 slides, all same aspect ratio. First slide is the hook. -- Reel: vertical 9:16 video only, up to 90s (30-60s performs best). Hook in first 1-3s. -- Story: vertical 9:16, single image/video, ephemeral (24h). -TXT; - } -} diff --git a/app/Ai/PlatformRules/LinkedInRules.php b/app/Ai/PlatformRules/LinkedInRules.php deleted file mode 100644 index 4c0c7f35..00000000 --- a/app/Ai/PlatformRules/LinkedInRules.php +++ /dev/null @@ -1,39 +0,0 @@ - 3000, - 'max_images' => 1, - 'max_videos' => 1, - 'text_only_allowed' => true, - 'formats' => [ - 'post' => ['max_media' => 1], - 'carousel' => ['max_slides' => 20, 'optimal_slides' => '8-12', 'images_only' => true, 'document_based' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -LinkedIn: 3000 chars max. First 2-3 lines visible before "see more" — make them compelling. -Professional tone. Educational/how-to content performs well. -- Post: 1 image or 1 video. -- Carousel: PDF-based document, up to 20 slides (8-12 optimal), images only. -TXT; - } -} diff --git a/app/Ai/PlatformRules/MastodonRules.php b/app/Ai/PlatformRules/MastodonRules.php deleted file mode 100644 index 99dd770d..00000000 --- a/app/Ai/PlatformRules/MastodonRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 500, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Mastodon: 500 chars max. Up to 4 images or 1 video. Community-focused, avoid commercial tone.'; - } -} diff --git a/app/Ai/PlatformRules/PinterestRules.php b/app/Ai/PlatformRules/PinterestRules.php deleted file mode 100644 index 11e60f43..00000000 --- a/app/Ai/PlatformRules/PinterestRules.php +++ /dev/null @@ -1,40 +0,0 @@ - 800, - 'max_images' => 5, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'formats' => [ - 'pin' => ['aspect_ratio' => '2:3', 'media_limit' => 1, 'images_only' => true], - 'video_pin' => ['aspect_ratio' => '9:16 or 2:3', 'video_only' => true, 'min_duration_seconds' => 4, 'max_duration_seconds' => 900], - 'carousel' => ['aspect_ratio' => '2:3', 'max_slides' => 5, 'images_only' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Pinterest: title max 100 chars, description max 500 chars. Text overlay on images helps discovery. -- Pin: portrait 2:3 (1000x1500px), single image. -- Video Pin: vertical 9:16 or portrait 2:3, 4s to 15 minutes. -- Carousel: portrait 2:3, up to 5 images. -TXT; - } -} diff --git a/app/Ai/PlatformRules/Registry.php b/app/Ai/PlatformRules/Registry.php deleted file mode 100644 index 11eac343..00000000 --- a/app/Ai/PlatformRules/Registry.php +++ /dev/null @@ -1,53 +0,0 @@ -> - */ - private static array $map = []; - - public static function for(Platform $platform): ?Contract - { - $class = self::$map[$platform->value] ?? null; - - return $class ? new $class : null; - } - - /** - * @param array $platforms - * @return array - */ - public static function forMany(array $platforms): array - { - $rules = []; - - foreach ($platforms as $platform) { - $rule = self::for($platform); - if ($rule) { - $rules[] = $rule; - } - } - - return $rules; - } - - /** - * @param class-string $ruleClass - */ - public static function register(Platform $platform, string $ruleClass): void - { - self::$map[$platform->value] = $ruleClass; - } - - public static function clear(): void - { - self::$map = []; - } -} diff --git a/app/Ai/PlatformRules/ThreadsRules.php b/app/Ai/PlatformRules/ThreadsRules.php deleted file mode 100644 index 9743a473..00000000 --- a/app/Ai/PlatformRules/ThreadsRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 500, - 'max_images' => 10, - 'max_videos' => 10, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Threads: 500 chars max. Up to 10 images/videos. Conversational tone. Ask questions to drive replies.'; - } -} diff --git a/app/Ai/PlatformRules/TikTokRules.php b/app/Ai/PlatformRules/TikTokRules.php deleted file mode 100644 index f694171b..00000000 --- a/app/Ai/PlatformRules/TikTokRules.php +++ /dev/null @@ -1,31 +0,0 @@ - 2200, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'aspect_ratio' => '9:16', - 'video_only' => true, - ]; - } - - public function summary(): string - { - return 'TikTok: vertical 9:16 video only. 15-60s performs best. Hook in first 1-2s. Trendy, authentic style. Caption max 2200 chars.'; - } -} diff --git a/app/Ai/PlatformRules/XRules.php b/app/Ai/PlatformRules/XRules.php deleted file mode 100644 index 380d5fee..00000000 --- a/app/Ai/PlatformRules/XRules.php +++ /dev/null @@ -1,31 +0,0 @@ - 280, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - 'best_aspect_ratio' => '16:9', - ]; - } - - public function summary(): string - { - return 'X (Twitter): 280 chars max. Up to 4 images OR 1 video. 16:9 landscape performs best. Concise, punchy copy.'; - } -} diff --git a/app/Ai/PlatformRules/YouTubeRules.php b/app/Ai/PlatformRules/YouTubeRules.php deleted file mode 100644 index 17d0ec34..00000000 --- a/app/Ai/PlatformRules/YouTubeRules.php +++ /dev/null @@ -1,33 +0,0 @@ - 5000, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'aspect_ratio' => '9:16', - 'video_only' => true, - 'max_duration_seconds' => 60, - 'requires_title' => true, - ]; - } - - public function summary(): string - { - return 'YouTube Shorts: vertical 9:16 video only, up to 60s. Hook immediately. Loop-friendly content helps. Description max 5000 chars. Title required.'; - } -} diff --git a/app/Ai/Providers/ExtendedGeminiProvider.php b/app/Ai/Providers/ExtendedGeminiProvider.php deleted file mode 100644 index afbc5958..00000000 --- a/app/Ai/Providers/ExtendedGeminiProvider.php +++ /dev/null @@ -1,46 +0,0 @@ - match ($quality) { - 'low', '1K' => '1K', - 'medium', '2K' => '2K', - 'high', '4K' => '4K', - default => '1K', - }, - 'aspect_ratio' => match ($size) { - '1:1' => '1:1', - '2:3' => '2:3', - '3:2' => '3:2', - '9:16' => '9:16', - '16:9' => '16:9', - '4:3' => '4:3', - '3:4' => '3:4', - '4:5' => '4:5', - '5:4' => '5:4', - '21:9' => '21:9', - default => null, - }, - ]); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dbe4bcea..3888f6ee 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,19 +4,6 @@ namespace App\Providers; -use App\Ai\PlatformRules\BlueskyRules; -use App\Ai\PlatformRules\FacebookRules; -use App\Ai\PlatformRules\InstagramRules; -use App\Ai\PlatformRules\LinkedInRules; -use App\Ai\PlatformRules\MastodonRules; -use App\Ai\PlatformRules\PinterestRules; -use App\Ai\PlatformRules\Registry; -use App\Ai\PlatformRules\ThreadsRules; -use App\Ai\PlatformRules\TikTokRules; -use App\Ai\PlatformRules\XRules; -use App\Ai\PlatformRules\YouTubeRules; -use App\Ai\Providers\ExtendedGeminiProvider; -use App\Enums\SocialAccount\Platform; use App\Listeners\StripeEventListener; use App\Models\AccessToken; use App\Models\Account; @@ -45,7 +32,6 @@ use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Cache\RateLimiting\Limit; -use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; @@ -57,8 +43,6 @@ use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; -use Laravel\Ai\Ai; -use Laravel\Ai\Gateway\Gemini\GeminiGateway; use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\WebhookReceived; use Laravel\Nightwatch\Facades\Nightwatch; @@ -96,8 +80,6 @@ public function boot(): void { $this->configureDefaults(); $this->configureMorphMap(); - $this->configureAi(); - $this->configurePlatformRules(); $this->configurePostHog(); $this->configureRateLimiting(); $this->configureSocialite(); @@ -125,42 +107,6 @@ protected function configurePassport(): void Passport::authorizationView('mcp.authorize'); } - protected function configureAi(): void - { - Ai::extend('gemini', function ($app, array $config) { - return new ExtendedGeminiProvider( - new GeminiGateway($app['events']), - $config, - $app->make(Dispatcher::class), - ); - }); - } - - protected function configurePlatformRules(): void - { - $map = [ - Platform::Instagram->value => InstagramRules::class, - Platform::InstagramFacebook->value => InstagramRules::class, - Platform::Facebook->value => FacebookRules::class, - Platform::X->value => XRules::class, - Platform::TikTok->value => TikTokRules::class, - Platform::YouTube->value => YouTubeRules::class, - Platform::LinkedIn->value => LinkedInRules::class, - Platform::LinkedInPage->value => LinkedInRules::class, - Platform::Threads->value => ThreadsRules::class, - Platform::Pinterest->value => PinterestRules::class, - Platform::Bluesky->value => BlueskyRules::class, - Platform::Mastodon->value => MastodonRules::class, - ]; - - foreach ($map as $value => $class) { - Registry::register( - Platform::from($value), - $class, - ); - } - } - protected function configureMorphMap(): void { Relation::enforceMorphMap([ diff --git a/resources/js/components/posts/ai/AiGenerateDialog.vue b/resources/js/components/posts/ai/AiGenerateDialog.vue index 788190c1..bd3a1f6a 100644 --- a/resources/js/components/posts/ai/AiGenerateDialog.vue +++ b/resources/js/components/posts/ai/AiGenerateDialog.vue @@ -45,8 +45,24 @@ const startGeneration = async () => { } }; +// The streamer agent shares the structured-output prompt template with +// PostContentGenerator, so what comes through the stream is a JSON object +// like {"content": "...", "image_title": "...", ...}. We only want the +// content field — and only once the JSON is complete (mid-stream the parse +// fails and we show nothing, avoiding the typewriter-of-JSON effect). +const previewText = computed(() => { + if (! text.value) return ''; + try { + const parsed = JSON.parse(text.value); + if (parsed && typeof parsed.content === 'string') return parsed.content; + } catch { + // Mid-stream the JSON is incomplete — leave preview empty. + } + return ''; +}); + const apply = () => { - emit('apply', text.value); + emit('apply', previewText.value); open.value = false; }; @@ -56,7 +72,7 @@ const retry = () => { startGeneration(); }; -const canApply = computed(() => status.value === 'completed' && text.value.trim().length > 0); +const canApply = computed(() => status.value === 'completed' && previewText.value.trim().length > 0); const canRetry = computed(() => status.value === 'completed' || status.value === 'failed'); watch(open, () => { @@ -88,15 +104,16 @@ watch(open, () => {
-
{{ text || '...' }}
+
{{ previewText || '...' }}

{{ errorMessage }}