diff --git a/app/Http/Requests/App/Ai/GeneratePostContentRequest.php b/app/Http/Requests/App/Ai/GeneratePostContentRequest.php index c8e1c854..a94dc534 100644 --- a/app/Http/Requests/App/Ai/GeneratePostContentRequest.php +++ b/app/Http/Requests/App/Ai/GeneratePostContentRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests\App\Ai; +use App\Support\AiPromptRules; use Illuminate\Foundation\Http\FormRequest; class GeneratePostContentRequest extends FormRequest @@ -19,7 +20,7 @@ public function authorize(): bool public function rules(): array { return [ - 'prompt' => ['required', 'string', 'max:2000'], + 'prompt' => ['required', 'string', 'max:'.AiPromptRules::PROMPT_MAX_LENGTH], 'current_content' => ['nullable', 'string', 'max:10000'], ]; } diff --git a/app/Http/Requests/App/Ai/StartPostCreationRequest.php b/app/Http/Requests/App/Ai/StartPostCreationRequest.php index 2d24a4fa..34ffb1bc 100644 --- a/app/Http/Requests/App/Ai/StartPostCreationRequest.php +++ b/app/Http/Requests/App/Ai/StartPostCreationRequest.php @@ -6,6 +6,7 @@ use App\Enums\Ai\ContentStyle; use App\Enums\PostPlatform\ContentType; +use App\Support\AiPromptRules; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; @@ -33,7 +34,7 @@ public function rules(): array ], 'social_account_id' => ['nullable', 'uuid'], 'image_count' => ['nullable', 'integer', 'min:0', 'max:10'], - 'prompt' => ['required', 'string', 'max:2000'], + 'prompt' => AiPromptRules::promptRule(), 'date' => ['nullable', 'date_format:Y-m-d'], 'template' => ['sometimes', 'string', Rule::enum(ContentStyle::class)], ]; diff --git a/app/Support/AiPromptRules.php b/app/Support/AiPromptRules.php new file mode 100644 index 00000000..9803cb90 --- /dev/null +++ b/app/Support/AiPromptRules.php @@ -0,0 +1,34 @@ + + */ + public static function promptRule(): array + { + return ['required', 'string', 'min:'.self::PROMPT_MIN_LENGTH, 'max:'.self::PROMPT_MAX_LENGTH]; + } +} diff --git a/resources/js/components/posts/create/AiPostWizard.vue b/resources/js/components/posts/create/AiPostWizard.vue index 797f91f4..4e304921 100644 --- a/resources/js/components/posts/create/AiPostWizard.vue +++ b/resources/js/components/posts/create/AiPostWizard.vue @@ -63,7 +63,7 @@ const selectedAccountId = ref(null); const includeImages = ref(true); const imageCount = ref(2); const promptText = ref(''); -// Mirrors the backend validation (`prompt` max:2000) so the limit is visible in the UI. +const PROMPT_MIN = 3; const PROMPT_MAX = 2000; const submitting = ref(false); @@ -159,11 +159,13 @@ const submittedImageCount = computed(() => { return 0; }); +const promptLength = computed(() => [...promptText.value.trim()].length); + const canSubmit = computed(() => selectedFormat.value !== null && selectedAccountId.value !== null && - promptText.value.trim().length >= 3 && - promptText.value.length <= PROMPT_MAX, + promptLength.value >= PROMPT_MIN && + promptLength.value <= PROMPT_MAX, ); // Auto-pick the only account when format has exactly one match. @@ -363,9 +365,9 @@ const startGeneration = async () => { />

- {{ promptText.length }}/{{ PROMPT_MAX }} + {{ promptLength }}/{{ PROMPT_MAX }}

diff --git a/tests/Feature/Ai/PostAiCreateTest.php b/tests/Feature/Ai/PostAiCreateTest.php index 8f588452..91e27642 100644 --- a/tests/Feature/Ai/PostAiCreateTest.php +++ b/tests/Feature/Ai/PostAiCreateTest.php @@ -8,6 +8,7 @@ use App\Models\SocialAccount; use App\Models\User; use App\Models\Workspace; +use App\Support\AiPromptRules; use Illuminate\Support\Facades\Bus; use Symfony\Component\HttpFoundation\Response; @@ -32,6 +33,44 @@ ->assertJsonValidationErrors(['prompt']); }); +test('start rejects a prompt shorter than the minimum length', function () { + Bus::fake(); + + $this->actingAs($this->user) + ->postJson(route('app.posts.ai.create'), ['prompt' => 'hi', 'format' => 'x_post']) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonValidationErrors(['prompt']); + + Bus::assertNotDispatched(StreamPostCreation::class); +}); + +test('start rejects a prompt longer than the maximum length', function () { + Bus::fake(); + + $this->actingAs($this->user) + ->postJson(route('app.posts.ai.create'), [ + 'prompt' => str_repeat('a', AiPromptRules::PROMPT_MAX_LENGTH + 1), + 'format' => 'x_post', + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonValidationErrors(['prompt']); + + Bus::assertNotDispatched(StreamPostCreation::class); +}); + +test('start accepts a prompt at the maximum length', function () { + Bus::fake(); + + $this->actingAs($this->user) + ->postJson(route('app.posts.ai.create'), [ + 'prompt' => str_repeat('a', AiPromptRules::PROMPT_MAX_LENGTH), + 'format' => 'x_post', + ]) + ->assertStatus(Response::HTTP_ACCEPTED); + + Bus::assertDispatched(StreamPostCreation::class); +}); + test('start validates format is required', function () { Bus::fake();