390 lines
17 KiB
PHP
390 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Requests\Academy;
|
|
|
|
use JsonException;
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class UpsertAcademyPromptTemplateRequest extends FormRequest
|
|
{
|
|
public function authorize(): bool
|
|
{
|
|
return (bool) $this->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')),
|
|
'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 [
|
|
'category_id' => ['nullable', 'integer', 'exists:academy_categories,id'],
|
|
'new_category_name' => ['nullable', 'string', 'max:120'],
|
|
'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'],
|
|
'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 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<int|string, mixed> $value
|
|
* @param array<int, string> $expectedKeys
|
|
* @return array<int|string, mixed>
|
|
*/
|
|
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];
|
|
}
|
|
} |