From 67fb40d1777219e275d6a3fac7f00f9bada127b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20S=C3=A9rgio=20Dantas?= Date: Wed, 1 Jul 2026 01:47:57 -0300 Subject: [PATCH] feat(ai-create): let users choose brand colors or free AI colors for images The image pipeline already threads `applyBrandVisuals` through `TemplateContext` -> `PostImagePipeline` -> `TemplateImageGenerator`, but it was hardcoded to `true` at the dispatch site, so generated images always used the workspace brand palette with no way to opt out. Expose the choice in the create wizard: a "Brand colors" / "Let AI decide" toggle (shown only when images are generated). The flag flows front -> `StartPostCreationRequest` (`apply_brand_visuals`) -> `PostAiCreateController@start` -> `StreamPostCreation` -> `TemplateContext`, defaulting to `true` so existing behavior is unchanged when the field is absent. --- .../App/PostAiCreateController.php | 1 + .../App/Ai/StartPostCreationRequest.php | 1 + app/Jobs/Ai/StreamPostCreation.php | 2 ++ lang/en/posts.php | 3 +++ lang/es/posts.php | 3 +++ lang/pt-BR/posts.php | 3 +++ .../components/posts/create/AiPostWizard.vue | 27 ++++++++++++++++++- tests/Feature/Ai/PostAiCreateTest.php | 27 +++++++++++++++++++ 8 files changed, 66 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/App/PostAiCreateController.php b/app/Http/Controllers/App/PostAiCreateController.php index 1d40db64..d9410758 100644 --- a/app/Http/Controllers/App/PostAiCreateController.php +++ b/app/Http/Controllers/App/PostAiCreateController.php @@ -52,6 +52,7 @@ public function start(StartPostCreationRequest $request): JsonResponse prompt: $request->string('prompt')->toString(), date: $request->input('date'), template: $request->input('template', 'image_card'), + applyBrandVisuals: $request->boolean('apply_brand_visuals', true), ); return response()->json([ diff --git a/app/Http/Requests/App/Ai/StartPostCreationRequest.php b/app/Http/Requests/App/Ai/StartPostCreationRequest.php index 2d24a4fa..42bb6607 100644 --- a/app/Http/Requests/App/Ai/StartPostCreationRequest.php +++ b/app/Http/Requests/App/Ai/StartPostCreationRequest.php @@ -36,6 +36,7 @@ public function rules(): array 'prompt' => ['required', 'string', 'max:2000'], 'date' => ['nullable', 'date_format:Y-m-d'], 'template' => ['sometimes', 'string', Rule::enum(ContentStyle::class)], + 'apply_brand_visuals' => ['sometimes', 'boolean'], ]; } diff --git a/app/Jobs/Ai/StreamPostCreation.php b/app/Jobs/Ai/StreamPostCreation.php index 78bfdcad..f8c9eb14 100644 --- a/app/Jobs/Ai/StreamPostCreation.php +++ b/app/Jobs/Ai/StreamPostCreation.php @@ -43,6 +43,7 @@ public function __construct( public string $prompt, public ?string $date = null, public string $template = 'image_card', + public bool $applyBrandVisuals = true, ) { $this->onQueue('ai'); } @@ -64,6 +65,7 @@ public function handle(): void format: $this->format, imageCount: $this->imageCount, isCarousel: $isCarousel, + applyBrandVisuals: $this->applyBrandVisuals, ); $agent = new PostContentGenerator( diff --git a/lang/en/posts.php b/lang/en/posts.php index e4f52fed..62ae115f 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -597,6 +597,9 @@ 'media_optional_label' => 'How many images?', 'media_none' => 'None', 'media_count_label' => 'Number of images', + 'brand_colors_label' => 'Image colors', + 'brand_colors_on' => 'Brand colors', + 'brand_colors_off' => 'Let AI decide', 'prompt_title' => 'Describe your post', 'prompt_label' => 'What is this post about?', 'prompt_placeholder' => 'e.g. Announce our new carousel feature for Instagram', diff --git a/lang/es/posts.php b/lang/es/posts.php index 61509fd8..6a94828b 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -598,6 +598,9 @@ 'media_optional_label' => '¿Cuántas imágenes?', 'media_none' => 'Ninguna', 'media_count_label' => 'Número de imágenes', + 'brand_colors_label' => 'Colores de la imagen', + 'brand_colors_on' => 'Colores de marca', + 'brand_colors_off' => 'La IA decide', 'prompt_title' => 'Describe tu post', 'prompt_label' => '¿De qué trata este post?', 'prompt_placeholder' => 'Ej. Anuncia nuestra nueva función de carrusel para Instagram', diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index 72a8a361..3f529ec5 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -597,6 +597,9 @@ 'media_optional_label' => 'Quantas imagens?', 'media_none' => 'Nenhuma', 'media_count_label' => 'Número de imagens', + 'brand_colors_label' => 'Cores da imagem', + 'brand_colors_on' => 'Cores da marca', + 'brand_colors_off' => 'IA decide', 'prompt_title' => 'Descreva seu post', 'prompt_label' => 'Sobre o que é este post?', 'prompt_placeholder' => 'Ex. Anunciar nossa nova função de carrossel para o Instagram', diff --git a/resources/js/components/posts/create/AiPostWizard.vue b/resources/js/components/posts/create/AiPostWizard.vue index f02572d0..ebd74731 100644 --- a/resources/js/components/posts/create/AiPostWizard.vue +++ b/resources/js/components/posts/create/AiPostWizard.vue @@ -63,6 +63,8 @@ const selectedAccountId = ref(null); const includeImages = ref(true); const imageCount = ref(2); const promptText = ref(''); +// true = images use the workspace brand palette; false = the AI picks colors freely. +const useBrandColors = ref(true); const submitting = ref(false); @@ -73,7 +75,8 @@ const httpStart = useHttp<{ prompt: string; date: string | null; template: string; -}>({ format: null, social_account_id: null, image_count: 0, prompt: '', date: null, template: 'image_card' }); + apply_brand_visuals: boolean; +}>({ format: null, social_account_id: null, image_count: 0, prompt: '', date: null, template: 'image_card', apply_brand_visuals: true }); const AI_FORMATS: Array<{ value: AiFormat; platforms: string[] }> = [ { value: ContentType.InstagramFeed, platforms: ['instagram', 'instagram-facebook'] }, @@ -207,6 +210,7 @@ const startGeneration = async () => { httpStart.prompt = promptText.value.trim(); httpStart.date = props.date; httpStart.template = resolvedTemplate.value; + httpStart.apply_brand_visuals = useBrandColors.value; try { const data = await httpStart.post(startRoute.url()) as { creation_id: string; channel: string }; @@ -349,6 +353,27 @@ const startGeneration = async () => { + +
+ +
+ + +
+
+
diff --git a/tests/Feature/Ai/PostAiCreateTest.php b/tests/Feature/Ai/PostAiCreateTest.php index 8f588452..1d5d324a 100644 --- a/tests/Feature/Ai/PostAiCreateTest.php +++ b/tests/Feature/Ai/PostAiCreateTest.php @@ -148,6 +148,33 @@ Bus::assertDispatched(StreamPostCreation::class, fn ($job) => $job->date === '2026-06-15'); }); +test('start carries apply_brand_visuals=false when the user lets the AI decide colors', function () { + Bus::fake(); + + $this->actingAs($this->user) + ->postJson(route('app.posts.ai.create'), [ + 'prompt' => 'hello', + 'format' => 'x_post', + 'apply_brand_visuals' => false, + ]) + ->assertAccepted(); + + Bus::assertDispatched(StreamPostCreation::class, fn ($job) => $job->applyBrandVisuals === false); +}); + +test('start defaults apply_brand_visuals to true when omitted', function () { + Bus::fake(); + + $this->actingAs($this->user) + ->postJson(route('app.posts.ai.create'), [ + 'prompt' => 'hello', + 'format' => 'x_post', + ]) + ->assertAccepted(); + + Bus::assertDispatched(StreamPostCreation::class, fn ($job) => $job->applyBrandVisuals === true); +}); + test('start rejects invalid date format', function () { Bus::fake();