user()?->hasStaffAccess(); } protected function prepareForValidation(): void { $this->merge([ 'featured' => $this->boolean('featured'), 'prompt_of_week' => $this->boolean('prompt_of_week'), 'active' => $this->boolean('active', true), 'new_category_name' => trim((string) $this->input('new_category_name', '')), 'tags' => array_values(array_filter((array) $this->input('tags', []))), 'documentation' => $this->normalizeDocumentation($this->input('documentation')), 'placeholders' => $this->normalizePlaceholders($this->input('placeholders')), 'helper_prompts' => $this->normalizeHelperPrompts($this->input('helper_prompts')), 'prompt_variants' => $this->normalizePromptVariants($this->input('prompt_variants')), 'filled_examples' => $this->normalizeFilledExamples($this->input('filled_examples')), 'tool_notes' => collect($this->input('tool_notes', [])) ->filter(static fn ($note): bool => is_array($note) || is_string($note)) ->map(function ($note): array|string { if (is_string($note)) { return $note; } return [ 'display_type' => $note['display_type'] ?? null, 'provider' => $note['provider'] ?? null, 'model_name' => $note['model_name'] ?? null, 'notes' => $note['notes'] ?? null, 'strengths' => $note['strengths'] ?? null, 'weaknesses' => $note['weaknesses'] ?? null, 'best_for' => $note['best_for'] ?? null, 'image_path' => $note['image_path'] ?? null, 'thumb_path' => $note['thumb_path'] ?? null, 'settings' => $note['settings'] ?? null, 'score' => filled($note['score'] ?? null) ? (int) $note['score'] : null, 'active' => filter_var($note['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true, ]; }) ->values() ->all(), ]); } public function rules(): array { $promptId = $this->route('academyPromptTemplate')?->id; return [ // Require either an existing category selection or a new category name. 'category_id' => ['nullable', 'integer', 'exists:academy_categories,id', 'required_without:new_category_name'], 'new_category_name' => ['nullable', 'string', 'max:120', 'required_without:category_id'], 'title' => ['required', 'string', 'max:180'], 'slug' => ['required', 'string', 'max:180', Rule::unique('academy_prompt_templates', 'slug')->ignore($promptId)], 'excerpt' => ['nullable', 'string'], 'prompt' => ['required', 'string'], 'negative_prompt' => ['nullable', 'string'], 'usage_notes' => ['nullable', 'string'], 'workflow_notes' => ['nullable', 'string'], 'documentation' => ['nullable', 'array'], 'documentation.summary' => ['nullable', 'string'], 'documentation.best_for' => ['nullable', 'array'], 'documentation.best_for.*' => ['nullable', 'string'], 'documentation.how_to_use' => ['nullable', 'array'], 'documentation.how_to_use.*' => ['nullable', 'string'], 'documentation.required_inputs' => ['nullable', 'array'], 'documentation.required_inputs.*' => ['nullable', 'string'], 'documentation.workflow' => ['nullable', 'array'], 'documentation.workflow.*' => ['nullable', 'string'], 'documentation.tips' => ['nullable', 'array'], 'documentation.tips.*' => ['nullable', 'string'], 'documentation.common_mistakes' => ['nullable', 'array'], 'documentation.common_mistakes.*' => ['nullable', 'string'], 'documentation.data_accuracy_notes' => ['nullable', 'array'], 'documentation.data_accuracy_notes.*' => ['nullable', 'string'], 'documentation.display_notes' => ['nullable', 'string'], 'placeholders' => ['nullable', 'array'], 'placeholders.*.key' => ['nullable', 'string', 'max:120'], 'placeholders.*.label' => ['nullable', 'string', 'max:180'], 'placeholders.*.description' => ['nullable', 'string'], 'placeholders.*.required' => ['nullable', 'boolean'], 'placeholders.*.example' => ['nullable'], 'placeholders.*.default' => ['nullable'], 'placeholders.*.type' => ['nullable', 'string', 'max:120'], 'helper_prompts' => ['nullable', 'array'], 'helper_prompts.*.title' => ['required_with:helper_prompts', 'string', 'max:180'], 'helper_prompts.*.type' => ['nullable', 'string', Rule::in(['data_collection', 'prompt_preparation', 'refinement', 'validation', 'variation', 'translation', 'seo', 'other'])], 'helper_prompts.*.description' => ['nullable', 'string'], 'helper_prompts.*.prompt' => ['required_with:helper_prompts', 'string'], 'helper_prompts.*.expected_output' => ['nullable', 'string', Rule::in(['json', 'text', 'markdown', 'image_prompt'])], 'helper_prompts.*.active' => ['nullable', 'boolean'], 'prompt_variants' => ['nullable', 'array'], 'prompt_variants.*.title' => ['required_with:prompt_variants', 'string', 'max:180'], 'prompt_variants.*.slug' => ['nullable', 'string', 'max:180'], 'prompt_variants.*.description' => ['nullable', 'string'], 'prompt_variants.*.prompt' => ['required_with:prompt_variants', 'string'], 'prompt_variants.*.negative_prompt' => ['nullable', 'string'], 'prompt_variants.*.recommended' => ['nullable', 'boolean'], 'prompt_variants.*.recommended_for' => ['nullable', 'array'], 'prompt_variants.*.recommended_for.*' => ['nullable', 'string'], 'prompt_variants.*.risk_notes' => ['nullable', 'array'], 'prompt_variants.*.risk_notes.*' => ['nullable', 'string'], 'prompt_variants.*.active' => ['nullable', 'boolean'], 'filled_examples' => ['nullable', 'array', 'max:5'], 'filled_examples.*.title' => ['nullable', 'string', 'max:180'], 'filled_examples.*.description' => ['nullable', 'string'], 'filled_examples.*.placeholder_values' => ['nullable', 'array'], 'filled_examples.*.prompt' => ['required_with:filled_examples', 'string'], 'filled_examples.*.negative_prompt' => ['nullable', 'string'], 'difficulty' => ['required', 'string', Rule::in((array) config('academy.difficulty_levels', []))], 'access_level' => ['required', 'string', Rule::in(['free', 'creator', 'pro'])], 'aspect_ratio' => ['nullable', 'string', 'max:20'], 'tags' => ['nullable', 'array'], 'tags.*' => ['string', 'max:60'], 'tool_notes' => ['nullable', 'array'], 'tool_notes.*.display_type' => ['nullable', 'string', 'max:50'], 'tool_notes.*.provider' => ['nullable', 'string', 'max:100'], 'tool_notes.*.model_name' => ['nullable', 'string', 'max:150'], 'tool_notes.*.notes' => ['nullable', 'string'], 'tool_notes.*.strengths' => ['nullable', 'string'], 'tool_notes.*.weaknesses' => ['nullable', 'string'], 'tool_notes.*.best_for' => ['nullable', 'string'], 'tool_notes.*.image_path' => ['nullable', 'string', 'max:500'], 'tool_notes.*.thumb_path' => ['nullable', 'string', 'max:500'], 'tool_notes.*.settings' => ['nullable', 'string'], 'tool_notes.*.score' => ['nullable', 'integer', 'min:1', 'max:10'], 'tool_notes.*.active' => ['nullable', 'boolean'], 'preview_image' => ['nullable', 'string', 'max:2048'], 'preview_image_file' => ['nullable', 'file', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], 'featured' => ['required', 'boolean'], 'prompt_of_week' => ['required', 'boolean'], 'active' => ['required', 'boolean'], 'published_at' => ['nullable', 'date'], 'seo_title' => ['nullable', 'string', 'max:180'], 'seo_description' => ['nullable', 'string', 'max:255'], ]; } private function decodeStructuredInput(mixed $value): mixed { if (! is_string($value)) { return $value; } $trimmed = trim($value); if ($trimmed === '') { return null; } try { return json_decode($trimmed, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { return $value; } } private function normalizeDocumentation(mixed $value): mixed { $value = $this->decodeStructuredInput($value); if ($value === null) { return null; } if (! is_array($value)) { return $value; } $listFields = ['best_for', 'how_to_use', 'required_inputs', 'workflow', 'tips', 'common_mistakes', 'data_accuracy_notes']; $documentation = [ 'summary' => $this->normalizeOptionalString($value['summary'] ?? null), 'display_notes' => $this->normalizeOptionalString($value['display_notes'] ?? null), ]; foreach ($listFields as $field) { $documentation[$field] = $this->normalizeStringList($value[$field] ?? []); } $hasContent = $documentation['summary'] !== null || $documentation['display_notes'] !== null || collect($listFields)->contains(fn (string $field): bool => $documentation[$field] !== []); return $hasContent ? $documentation : null; } private function normalizePlaceholders(mixed $value): mixed { $value = $this->decodeStructuredInput($value); if ($value === null) { return []; } if (! is_array($value)) { return $value; } $value = $this->normalizeStructuredObjectList($value, ['key', 'label', 'description', 'required', 'example', 'default', 'type']); return collect($value) ->values() ->map(function ($placeholder): mixed { if (! is_array($placeholder)) { return $placeholder; } return [ 'key' => $this->normalizeOptionalString($placeholder['key'] ?? null), 'label' => $this->normalizeOptionalString($placeholder['label'] ?? null), 'description' => $this->normalizeOptionalString($placeholder['description'] ?? null), 'required' => filter_var($placeholder['required'] ?? false, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false, 'example' => $this->normalizeJsonScalar($placeholder['example'] ?? null), 'default' => $this->normalizeJsonScalar($placeholder['default'] ?? null), 'type' => $this->normalizeOptionalString($placeholder['type'] ?? null), ]; }) ->filter(function ($placeholder): bool { if (! is_array($placeholder)) { return true; } return collect([ $placeholder['key'] ?? null, $placeholder['label'] ?? null, $placeholder['description'] ?? null, $placeholder['example'] ?? null, $placeholder['default'] ?? null, $placeholder['type'] ?? null, ])->contains(fn ($item): bool => $item !== null && $item !== '' && $item !== []); }) ->values() ->all(); } private function normalizeHelperPrompts(mixed $value): mixed { $value = $this->decodeStructuredInput($value); if ($value === null) { return []; } if (! is_array($value)) { return $value; } $value = $this->normalizeStructuredObjectList($value, ['title', 'type', 'description', 'prompt', 'expected_output', 'active']); return collect($value) ->values() ->map(function ($helperPrompt): mixed { if (! is_array($helperPrompt)) { return $helperPrompt; } return [ 'title' => $this->normalizeOptionalString($helperPrompt['title'] ?? null), 'type' => $this->normalizeOptionalString($helperPrompt['type'] ?? null) ?? 'other', 'description' => $this->normalizeOptionalString($helperPrompt['description'] ?? null), 'prompt' => $this->normalizeOptionalString($helperPrompt['prompt'] ?? null), 'expected_output' => $this->normalizeOptionalString($helperPrompt['expected_output'] ?? null) ?? 'text', 'active' => filter_var($helperPrompt['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true, ]; }) ->filter(function ($helperPrompt): bool { if (! is_array($helperPrompt)) { return true; } return collect([ $helperPrompt['title'] ?? null, $helperPrompt['description'] ?? null, $helperPrompt['prompt'] ?? null, ])->contains(fn ($item): bool => $item !== null && $item !== ''); }) ->values() ->all(); } private function normalizeFilledExamples(mixed $value): mixed { $value = $this->decodeStructuredInput($value); if ($value === null) { return []; } if (! is_array($value)) { return $value; } $value = $this->normalizeStructuredObjectList($value, ['title', 'description', 'placeholder_values', 'prompt', 'negative_prompt']); return collect($value) ->values() ->map(function ($example): mixed { if (! is_array($example)) { return $example; } return [ 'title' => $this->normalizeOptionalString($example['title'] ?? null), 'description' => $this->normalizeOptionalString($example['description'] ?? null), 'placeholder_values' => is_array($example['placeholder_values'] ?? null) ? $example['placeholder_values'] : [], 'prompt' => $this->normalizeOptionalString($example['prompt'] ?? null), 'negative_prompt' => $this->normalizeOptionalString($example['negative_prompt'] ?? null), ]; }) ->filter(function ($example): bool { if (! is_array($example)) { return true; } return collect([ $example['title'] ?? null, $example['description'] ?? null, $example['prompt'] ?? null, $example['negative_prompt'] ?? null, $example['placeholder_values'] ?? null, ])->contains(fn ($item): bool => $item !== null && $item !== '' && $item !== []); }) ->take(5) ->values() ->all(); } private function normalizePromptVariants(mixed $value): mixed { $value = $this->decodeStructuredInput($value); if ($value === null) { return []; } if (! is_array($value)) { return $value; } $value = $this->normalizeStructuredObjectList($value, ['title', 'slug', 'description', 'prompt', 'negative_prompt', 'recommended', 'recommended_for', 'risk_notes', 'active']); return collect($value) ->values() ->map(function ($variant): mixed { if (! is_array($variant)) { return $variant; } return [ 'title' => $this->normalizeOptionalString($variant['title'] ?? null), 'slug' => $this->normalizeOptionalString($variant['slug'] ?? null), 'description' => $this->normalizeOptionalString($variant['description'] ?? null), 'prompt' => $this->normalizeOptionalString($variant['prompt'] ?? null), 'negative_prompt' => $this->normalizeOptionalString($variant['negative_prompt'] ?? null), 'recommended' => filter_var($variant['recommended'] ?? false, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false, 'recommended_for' => $this->normalizeStringList($variant['recommended_for'] ?? []), 'risk_notes' => $this->normalizeStringList($variant['risk_notes'] ?? []), 'active' => filter_var($variant['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true, ]; }) ->filter(function ($variant): bool { if (! is_array($variant)) { return true; } return collect([ $variant['title'] ?? null, $variant['description'] ?? null, $variant['prompt'] ?? null, $variant['negative_prompt'] ?? null, ])->contains(fn ($item): bool => $item !== null && $item !== ''); }) ->values() ->all(); } private function normalizeStringList(mixed $value): array { if (! is_array($value)) { $value = $value === null ? [] : [$value]; } return collect($value) ->map(fn ($item): string => trim((string) $item)) ->filter(static fn (string $item): bool => $item !== '') ->values() ->all(); } private function normalizeOptionalString(mixed $value): ?string { if ($value === null) { return null; } $normalized = trim((string) $value); return $normalized !== '' ? $normalized : null; } private function normalizeJsonScalar(mixed $value): mixed { if (! is_string($value)) { return $value; } $trimmed = trim($value); return $trimmed !== '' ? $trimmed : null; } /** * @param array $value * @param array $expectedKeys * @return array */ private function normalizeStructuredObjectList(array $value, array $expectedKeys): array { if (array_is_list($value)) { return $value; } $keys = array_keys($value); $normalizedKeys = array_map(static fn ($key): string => (string) $key, $keys); if ($normalizedKeys === [] || array_intersect($normalizedKeys, $expectedKeys) === []) { return $value; } return [$value]; } }