Optimize academy
This commit is contained in:
@@ -17,6 +17,7 @@ use App\Models\AcademyBadge;
|
||||
use App\Models\AcademyCategory;
|
||||
use App\Models\AcademyChallenge;
|
||||
use App\Models\AcademyChallengeSubmission;
|
||||
use App\Models\AcademyContentMetricDaily;
|
||||
use App\Models\AcademyCourse;
|
||||
use App\Models\AcademyCourseLesson;
|
||||
use App\Models\AcademyCourseSection;
|
||||
@@ -31,6 +32,7 @@ use App\Services\Academy\AcademyAdminBillingOverviewService;
|
||||
use App\Services\Academy\AcademyCacheService;
|
||||
use App\Services\Academy\AcademyCourseLessonOrderingService;
|
||||
use App\Services\Academy\AcademyLessonMarkdownRenderer;
|
||||
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -604,34 +606,48 @@ final class AcademyAdminController extends Controller
|
||||
$meta = $this->resourceMeta($resource);
|
||||
$search = trim((string) request()->query('search', ''));
|
||||
$query = $meta['model']::query();
|
||||
$filters = [
|
||||
'search' => $search,
|
||||
];
|
||||
$summary = null;
|
||||
|
||||
if ($resource === 'courses') {
|
||||
$query->withCount('courseLessons');
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($builder) use ($search): void {
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$builder->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('subtitle', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('description', 'like', $like);
|
||||
});
|
||||
$this->applyCourseAdminSearch($query, $search);
|
||||
}
|
||||
|
||||
$query->orderByDesc('is_featured')
|
||||
->orderBy('order_num')
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id');
|
||||
} elseif ($resource === 'prompts') {
|
||||
$query->with('category');
|
||||
$query->withSum(['metrics as total_views' => function ($builder): void {
|
||||
$builder->where('content_type', AcademyAnalyticsContentType::PROMPT);
|
||||
}], 'views');
|
||||
$promptFilters = [
|
||||
'category' => (string) request()->query('category', 'all'),
|
||||
'featured' => (string) request()->query('featured', 'all'),
|
||||
'prompt_of_week' => (string) request()->query('prompt_of_week', 'all'),
|
||||
'active' => (string) request()->query('active', 'all'),
|
||||
'access_level' => (string) request()->query('access_level', 'all'),
|
||||
'difficulty' => (string) request()->query('difficulty', 'all'),
|
||||
'order' => (string) request()->query('order', 'updated_desc'),
|
||||
];
|
||||
|
||||
$filters = array_merge($filters, $promptFilters);
|
||||
|
||||
$this->applyPromptAdminSearch($query, $search);
|
||||
$this->applyPromptAdminFilters($query, $promptFilters);
|
||||
$this->applyPromptAdminOrdering($query, $promptFilters['order']);
|
||||
|
||||
$summary = $this->promptAdminSummary($search, $promptFilters);
|
||||
} else {
|
||||
$query->latest('updated_at');
|
||||
}
|
||||
|
||||
if ($resource === 'prompts') {
|
||||
$query->with('category');
|
||||
}
|
||||
|
||||
if ($resource === 'lessons') {
|
||||
$query->with('courses:id,title');
|
||||
}
|
||||
@@ -646,48 +662,191 @@ final class AcademyAdminController extends Controller
|
||||
'items' => $items,
|
||||
'columns' => $meta['columns'],
|
||||
'createUrl' => route($meta['route_base'].'.create'),
|
||||
'filters' => [
|
||||
'search' => $search,
|
||||
],
|
||||
'filters' => $filters,
|
||||
'filterOptions' => $resource === 'prompts' ? [
|
||||
'categories' => $this->promptAdminCategoryFilterOptions(),
|
||||
'difficulty' => $this->filterOptionsWithAll($this->difficultyOptions(), 'All difficulties'),
|
||||
'access' => $this->filterOptionsWithAll($this->accessOptions(), 'All access levels'),
|
||||
'featured' => [
|
||||
['value' => 'all', 'label' => 'Any featured state'],
|
||||
['value' => 'yes', 'label' => 'Featured only'],
|
||||
['value' => 'no', 'label' => 'Not featured'],
|
||||
],
|
||||
'promptOfWeek' => [
|
||||
['value' => 'all', 'label' => 'Any weekly state'],
|
||||
['value' => 'yes', 'label' => 'Prompt of the week'],
|
||||
['value' => 'no', 'label' => 'Not prompt of the week'],
|
||||
],
|
||||
'active' => [
|
||||
['value' => 'all', 'label' => 'Any visibility state'],
|
||||
['value' => 'active', 'label' => 'Active only'],
|
||||
['value' => 'inactive', 'label' => 'Inactive only'],
|
||||
],
|
||||
'order' => [
|
||||
['value' => 'updated_desc', 'label' => 'Updated newest'],
|
||||
['value' => 'updated_asc', 'label' => 'Updated oldest'],
|
||||
['value' => 'views_desc', 'label' => 'Most viewed'],
|
||||
['value' => 'title_asc', 'label' => 'Title A-Z'],
|
||||
['value' => 'title_desc', 'label' => 'Title Z-A'],
|
||||
['value' => 'access_asc', 'label' => 'Access'],
|
||||
['value' => 'difficulty_asc', 'label' => 'Difficulty'],
|
||||
['value' => 'featured_desc', 'label' => 'Featured first'],
|
||||
],
|
||||
] : null,
|
||||
'summary' => $resource === 'courses' ? [
|
||||
'total' => (int) $items->total(),
|
||||
'published' => (int) (clone $meta['model']::query())->when($search !== '', function ($builder) use ($search): void {
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$builder->where(function ($inner) use ($like): void {
|
||||
$inner->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('subtitle', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('description', 'like', $like);
|
||||
});
|
||||
})->where('status', AcademyCourse::STATUS_PUBLISHED)->count(),
|
||||
'featured' => (int) (clone $meta['model']::query())->when($search !== '', function ($builder) use ($search): void {
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$builder->where(function ($inner) use ($like): void {
|
||||
$inner->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('subtitle', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('description', 'like', $like);
|
||||
});
|
||||
})->where('is_featured', true)->count(),
|
||||
'drafts' => (int) (clone $meta['model']::query())->when($search !== '', function ($builder) use ($search): void {
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$builder->where(function ($inner) use ($like): void {
|
||||
$inner->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('subtitle', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('description', 'like', $like);
|
||||
});
|
||||
})->where('status', AcademyCourse::STATUS_DRAFT)->count(),
|
||||
] : null,
|
||||
'published' => (int) (clone $meta['model']::query())->tap(fn ($builder) => $this->applyCourseAdminSearch($builder, $search))->where('status', AcademyCourse::STATUS_PUBLISHED)->count(),
|
||||
'featured' => (int) (clone $meta['model']::query())->tap(fn ($builder) => $this->applyCourseAdminSearch($builder, $search))->where('is_featured', true)->count(),
|
||||
'drafts' => (int) (clone $meta['model']::query())->tap(fn ($builder) => $this->applyCourseAdminSearch($builder, $search))->where('status', AcademyCourse::STATUS_DRAFT)->count(),
|
||||
] : $summary,
|
||||
]);
|
||||
}
|
||||
|
||||
private function applyCourseAdminSearch($query, string $search): void
|
||||
{
|
||||
if ($search === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$query->where(function ($builder) use ($like): void {
|
||||
$builder->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('subtitle', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('description', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
private function applyPromptAdminSearch($query, string $search): void
|
||||
{
|
||||
if ($search === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%';
|
||||
|
||||
$query->where(function ($builder) use ($like): void {
|
||||
$builder->where('title', 'like', $like)
|
||||
->orWhere('slug', 'like', $like)
|
||||
->orWhere('excerpt', 'like', $like)
|
||||
->orWhere('prompt', 'like', $like)
|
||||
->orWhere('negative_prompt', 'like', $like)
|
||||
->orWhere('usage_notes', 'like', $like)
|
||||
->orWhere('workflow_notes', 'like', $like)
|
||||
->orWhereHas('category', function ($categoryQuery) use ($like): void {
|
||||
$categoryQuery->where('name', 'like', $like);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function applyPromptAdminFilters($query, array $filters, bool $includeAccessFilter = true): void
|
||||
{
|
||||
$category = (string) ($filters['category'] ?? 'all');
|
||||
$featured = (string) ($filters['featured'] ?? 'all');
|
||||
$promptOfWeek = (string) ($filters['prompt_of_week'] ?? 'all');
|
||||
$active = (string) ($filters['active'] ?? 'all');
|
||||
$accessLevel = (string) ($filters['access_level'] ?? 'all');
|
||||
$difficulty = (string) ($filters['difficulty'] ?? 'all');
|
||||
|
||||
if ($category === 'uncategorized') {
|
||||
$query->whereNull('category_id');
|
||||
} elseif ($category !== '' && $category !== 'all' && ctype_digit($category)) {
|
||||
$query->where('category_id', (int) $category);
|
||||
}
|
||||
|
||||
if ($featured === 'yes') {
|
||||
$query->where('featured', true);
|
||||
} elseif ($featured === 'no') {
|
||||
$query->where('featured', false);
|
||||
}
|
||||
|
||||
if ($promptOfWeek === 'yes') {
|
||||
$query->where('prompt_of_week', true);
|
||||
} elseif ($promptOfWeek === 'no') {
|
||||
$query->where('prompt_of_week', false);
|
||||
}
|
||||
|
||||
if ($active === 'active') {
|
||||
$query->where('active', true);
|
||||
} elseif ($active === 'inactive') {
|
||||
$query->where('active', false);
|
||||
}
|
||||
|
||||
if ($includeAccessFilter && in_array($accessLevel, ['free', 'creator', 'pro'], true)) {
|
||||
$query->where('access_level', $accessLevel);
|
||||
}
|
||||
|
||||
if (in_array($difficulty, array_column($this->difficultyOptions(), 'value'), true)) {
|
||||
$query->where('difficulty', $difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPromptAdminOrdering($query, string $order): void
|
||||
{
|
||||
match ($order) {
|
||||
'updated_asc' => $query->orderBy('updated_at')->orderBy('id'),
|
||||
'views_desc' => $query->orderByDesc('total_views')->orderByDesc('updated_at')->orderByDesc('id'),
|
||||
'title_asc' => $query->orderBy('title')->orderByDesc('updated_at'),
|
||||
'title_desc' => $query->orderByDesc('title')->orderByDesc('updated_at'),
|
||||
'access_asc' => $query->orderByRaw("FIELD(access_level, 'free', 'creator', 'pro')")->orderBy('title'),
|
||||
'difficulty_asc' => $query->orderBy('difficulty')->orderBy('title'),
|
||||
'featured_desc' => $query->orderByDesc('featured')->orderByDesc('prompt_of_week')->orderBy('title'),
|
||||
default => $query->orderByDesc('updated_at')->orderByDesc('id'),
|
||||
};
|
||||
}
|
||||
|
||||
private function promptAdminSummary(string $search, array $filters): array
|
||||
{
|
||||
$summaryQuery = AcademyPromptTemplate::query();
|
||||
$accessSummaryQuery = AcademyPromptTemplate::query();
|
||||
|
||||
$this->applyPromptAdminSearch($summaryQuery, $search);
|
||||
$this->applyPromptAdminFilters($summaryQuery, $filters);
|
||||
|
||||
$this->applyPromptAdminSearch($accessSummaryQuery, $search);
|
||||
$this->applyPromptAdminFilters($accessSummaryQuery, $filters, false);
|
||||
|
||||
return [
|
||||
'total' => (int) $summaryQuery->count(),
|
||||
'active' => (int) (clone $summaryQuery)->where('active', true)->count(),
|
||||
'featured' => (int) (clone $summaryQuery)->where('featured', true)->count(),
|
||||
'promptOfWeek' => (int) (clone $summaryQuery)->where('prompt_of_week', true)->count(),
|
||||
'access' => [
|
||||
'free' => (int) (clone $accessSummaryQuery)->where('access_level', 'free')->count(),
|
||||
'creator' => (int) (clone $accessSummaryQuery)->where('access_level', 'creator')->count(),
|
||||
'pro' => (int) (clone $accessSummaryQuery)->where('access_level', 'pro')->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function promptAdminCategoryFilterOptions(): array
|
||||
{
|
||||
return AcademyCategory::query()
|
||||
->where('type', 'prompt')
|
||||
->orderBy('order_num')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn (AcademyCategory $category): array => ['value' => (string) $category->id, 'label' => $category->name])
|
||||
->prepend(['value' => 'uncategorized', 'label' => 'Uncategorized'])
|
||||
->prepend(['value' => 'all', 'label' => 'All categories'])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function filterOptionsWithAll(array $options, string $allLabel): array
|
||||
{
|
||||
return collect($options)
|
||||
->map(fn (array $option): array => [
|
||||
'value' => (string) ($option['value'] ?? ''),
|
||||
'label' => (string) ($option['label'] ?? $option['value'] ?? ''),
|
||||
])
|
||||
->prepend(['value' => 'all', 'label' => $allLabel])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function renderForm(string $resource, Model $record): Response
|
||||
{
|
||||
$meta = $this->resourceMeta($resource);
|
||||
@@ -893,7 +1052,7 @@ final class AcademyAdminController extends Controller
|
||||
'singular' => 'prompt template',
|
||||
'subtitle' => 'Manage prompt previews, premium prompts, and prompt of the week.',
|
||||
'route_base' => 'admin.academy.prompts',
|
||||
'columns' => ['title', 'difficulty', 'access_level', 'prompt_of_week', 'active'],
|
||||
'columns' => ['title', 'category_name', 'difficulty', 'access_level', 'prompt_of_week', 'active'],
|
||||
'fields' => [
|
||||
['name' => 'category_id', 'label' => 'Category', 'type' => 'select', 'options' => $this->categoryOptions('prompt')],
|
||||
['name' => 'title', 'label' => 'Title', 'type' => 'text'],
|
||||
@@ -907,6 +1066,7 @@ final class AcademyAdminController extends Controller
|
||||
['name' => 'placeholders', 'label' => 'Placeholders JSON', 'type' => 'json'],
|
||||
['name' => 'helper_prompts', 'label' => 'Helper Prompts JSON', 'type' => 'json'],
|
||||
['name' => 'prompt_variants', 'label' => 'Prompt Variants JSON', 'type' => 'json'],
|
||||
['name' => 'filled_examples', 'label' => 'Filled Examples JSON', 'type' => 'json'],
|
||||
['name' => 'difficulty', 'label' => 'Difficulty', 'type' => 'select', 'options' => $this->difficultyOptions()],
|
||||
['name' => 'access_level', 'label' => 'Access', 'type' => 'select', 'options' => $this->accessOptions()],
|
||||
['name' => 'aspect_ratio', 'label' => 'Aspect Ratio', 'type' => 'text'],
|
||||
@@ -1048,6 +1208,7 @@ final class AcademyAdminController extends Controller
|
||||
'active' => (bool) $model->active,
|
||||
'preview_image_url' => $this->resolvePromptPreviewImageUrl((string) ($model->preview_image ?? '')),
|
||||
'comparisons_count' => count($this->serializePromptToolNotes((array) ($model->tool_notes ?? []))),
|
||||
'views_count' => (int) ($model->total_views ?? 0),
|
||||
'tags' => array_values(array_filter(array_map(static fn ($tag): string => trim((string) $tag), (array) ($model->tags ?? [])))),
|
||||
'updated_at' => optional($model->updated_at)->toIso8601String(),
|
||||
'preview_url' => route('academy.prompts.show', ['slug' => $model->slug]),
|
||||
@@ -1167,6 +1328,7 @@ final class AcademyAdminController extends Controller
|
||||
'placeholders' => $this->encodePrettyJsonForForm($record->placeholders),
|
||||
'helper_prompts' => $this->encodePrettyJsonForForm($record->helper_prompts),
|
||||
'prompt_variants' => $this->encodePrettyJsonForForm($record->prompt_variants),
|
||||
'filled_examples' => $this->encodePrettyJsonForForm($record->filled_examples),
|
||||
'difficulty' => (string) ($record->difficulty ?? 'beginner'),
|
||||
'access_level' => (string) ($record->access_level ?? 'free'),
|
||||
'aspect_ratio' => (string) ($record->aspect_ratio ?? ''),
|
||||
@@ -2174,6 +2336,7 @@ final class AcademyAdminController extends Controller
|
||||
$validated['placeholders'] = $this->normalizePromptPlaceholders($validated['placeholders'] ?? null);
|
||||
$validated['helper_prompts'] = $this->normalizePromptHelperPrompts($validated['helper_prompts'] ?? null);
|
||||
$validated['prompt_variants'] = $this->normalizePromptVariants($validated['prompt_variants'] ?? null);
|
||||
$validated['filled_examples'] = $this->normalizePromptFilledExamples($validated['filled_examples'] ?? null);
|
||||
$validated['tool_notes'] = $this->normalizePromptToolNotes((array) ($validated['tool_notes'] ?? []));
|
||||
$previousToolNotes = $this->normalizePromptToolNotes((array) ($prompt?->tool_notes ?? []));
|
||||
|
||||
@@ -2382,6 +2545,56 @@ final class AcademyAdminController extends Controller
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function normalizePromptFilledExamples(mixed $filledExamples): array
|
||||
{
|
||||
if (! is_array($filledExamples)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($filledExamples)
|
||||
->filter(static fn ($example): bool => is_array($example))
|
||||
->map(function (array $example): array {
|
||||
return [
|
||||
'title' => $this->nullableTrimmedString($example['title'] ?? null),
|
||||
'description' => $this->nullableTrimmedString($example['description'] ?? null),
|
||||
'placeholder_values' => collect(is_array($example['placeholder_values'] ?? null) ? $example['placeholder_values'] : [])
|
||||
->mapWithKeys(function ($value, $key): array {
|
||||
$normalizedKey = trim((string) $key);
|
||||
|
||||
if ($normalizedKey === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalizedValue = $this->normalizePromptJsonValue($value);
|
||||
|
||||
if ($normalizedValue === null || $normalizedValue === '' || $normalizedValue === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$normalizedKey => $normalizedValue];
|
||||
})
|
||||
->all(),
|
||||
'prompt' => $this->nullableTrimmedString($example['prompt'] ?? null),
|
||||
'negative_prompt' => $this->nullableTrimmedString($example['negative_prompt'] ?? null),
|
||||
];
|
||||
})
|
||||
->filter(function (array $example): bool {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user