Implement academy analytics, billing, and web stories updates
This commit is contained in:
@@ -111,7 +111,7 @@ class UpsertAcademyLessonRequest extends FormRequest
|
||||
'cover_image' => ['nullable', 'string', 'max:2048'],
|
||||
'article_cover_image' => ['nullable', 'string', 'max:2048'],
|
||||
'tags' => ['nullable', 'array'],
|
||||
'tags.*' => ['string', 'max:60'],
|
||||
'tags.*' => ['string', 'max:100'],
|
||||
'video_url' => ['nullable', 'string', 'max:2048'],
|
||||
'reading_minutes' => ['required', 'integer', 'min:1', 'max:999'],
|
||||
'featured' => ['required', 'boolean'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Academy;
|
||||
|
||||
use JsonException;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -22,6 +23,10 @@ class UpsertAcademyPromptTemplateRequest extends FormRequest
|
||||
'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 {
|
||||
@@ -30,6 +35,7 @@ class UpsertAcademyPromptTemplateRequest extends FormRequest
|
||||
}
|
||||
|
||||
return [
|
||||
'display_type' => $note['display_type'] ?? null,
|
||||
'provider' => $note['provider'] ?? null,
|
||||
'model_name' => $note['model_name'] ?? null,
|
||||
'notes' => $note['notes'] ?? null,
|
||||
@@ -62,12 +68,57 @@ class UpsertAcademyPromptTemplateRequest extends FormRequest
|
||||
'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'],
|
||||
@@ -89,4 +140,251 @@ class UpsertAcademyPromptTemplateRequest extends FormRequest
|
||||
'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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user