Add tests for featured thumbnail generation; apply Pint formatting and related edits

This commit is contained in:
2026-05-06 18:55:40 +02:00
parent 7a8bc8e22a
commit 82f2b1f660
65 changed files with 11325 additions and 49545 deletions

View File

@@ -4,11 +4,14 @@ declare(strict_types=1);
namespace App\Services\Academy;
use App\Models\AcademyAiComparisonResult;
use App\Models\AcademyChallenge;
use App\Models\AcademyLesson;
use App\Models\AcademyLessonBlock;
use App\Models\AcademyPromptPack;
use App\Models\AcademyPromptTemplate;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
final class AcademyAccessService
@@ -66,6 +69,7 @@ final class AcademyAccessService
'access_level' => (string) $lesson->access_level,
'lesson_type' => (string) $lesson->lesson_type,
'cover_image' => $lesson->cover_image,
'cover_image_url' => $this->resolveLessonCoverImageUrl((string) ($lesson->cover_image ?? '')),
'video_url' => $authorized ? $lesson->video_url : null,
'reading_minutes' => (int) $lesson->reading_minutes,
'featured' => (bool) $lesson->featured,
@@ -76,6 +80,9 @@ final class AcademyAccessService
'name' => (string) $lesson->category->name,
'slug' => (string) $lesson->category->slug,
] : null,
'blocks' => ($authorized && $includeFull)
? $lesson->activeBlocks->map(fn (AcademyLessonBlock $block): ?array => $this->lessonBlockPayload($block))->filter()->values()->all()
: [],
'locked' => ! $authorized,
'can_access' => $authorized,
];
@@ -100,7 +107,7 @@ final class AcademyAccessService
'aspect_ratio' => $prompt->aspect_ratio,
'tags' => array_values((array) ($prompt->tags ?? [])),
'tool_notes' => $authorized ? (array) ($prompt->tool_notes ?? []) : [],
'preview_image' => $prompt->preview_image,
'preview_image' => $this->resolvePreviewImageUrl((string) ($prompt->preview_image ?? '')),
'featured' => (bool) $prompt->featured,
'prompt_of_week' => (bool) $prompt->prompt_of_week,
'published_at' => $prompt->published_at?->toISOString(),
@@ -204,6 +211,115 @@ final class AcademyAccessService
$previewLength = max(1, $length - 1);
}
return rtrim(mb_substr($plain, 0, $previewLength)) . '...';
return rtrim(mb_substr($plain, 0, $previewLength)).'...';
}
}
private function resolvePreviewImageUrl(string $previewImage): ?string
{
$previewImage = trim($previewImage);
if ($previewImage === '') {
return null;
}
if (str_starts_with($previewImage, 'http://') || str_starts_with($previewImage, 'https://') || str_starts_with($previewImage, '/')) {
return $previewImage;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($previewImage);
}
private function resolveLessonCoverImageUrl(string $coverImage): ?string
{
$coverImage = trim($coverImage);
if ($coverImage === '') {
return null;
}
if (str_starts_with($coverImage, 'http://') || str_starts_with($coverImage, 'https://') || str_starts_with($coverImage, '/')) {
return $coverImage;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($coverImage);
}
private function resolveLessonMediaUrl(string $path): ?string
{
$path = trim($path);
if ($path === '') {
return null;
}
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://') || str_starts_with($path, '/')) {
return $path;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($path);
}
/**
* @return array<string, mixed>|null
*/
private function lessonBlockPayload(AcademyLessonBlock $block): ?array
{
if ($block->type !== 'ai_comparison') {
return null;
}
$payload = is_array($block->payload) ? $block->payload : [];
$criteria = collect($payload['criteria'] ?? [])
->map(static fn ($criterion): string => trim((string) $criterion))
->filter(static fn (string $criterion): bool => $criterion !== '')
->values()
->all();
$results = $block->activeComparisonResults
->map(fn (AcademyAiComparisonResult $result): array => [
'id' => (int) $result->id,
'provider' => (string) ($result->provider ?? ''),
'model_name' => (string) ($result->model_name ?? ''),
'image_path' => (string) $result->image_path,
'image_url' => $this->resolveLessonMediaUrl((string) $result->image_path),
'thumb_path' => (string) ($result->thumb_path ?? ''),
'thumb_url' => $this->resolveLessonMediaUrl((string) ($result->thumb_path ?? '')),
'settings' => (string) ($result->settings ?? ''),
'strengths' => (string) ($result->strengths ?? ''),
'weaknesses' => (string) ($result->weaknesses ?? ''),
'best_for' => (string) ($result->best_for ?? ''),
'score' => $result->score,
'sort_order' => (int) $result->sort_order,
'active' => (bool) $result->active,
])
->values()
->all();
$hasPromptData = filled($payload['prompt'] ?? null)
|| filled($payload['negative_prompt'] ?? null)
|| filled($payload['intro'] ?? null)
|| filled($payload['title'] ?? null)
|| filled($payload['aspect_ratio'] ?? null)
|| ! empty($criteria);
if (! $hasPromptData && $results === []) {
return null;
}
return [
'id' => (int) $block->id,
'type' => (string) $block->type,
'title' => (string) ($block->title ?? ($payload['title'] ?? '')),
'payload' => [
'title' => (string) ($payload['title'] ?? ''),
'intro' => (string) ($payload['intro'] ?? ''),
'prompt' => (string) ($payload['prompt'] ?? ''),
'negative_prompt' => (string) ($payload['negative_prompt'] ?? ''),
'aspect_ratio' => (string) ($payload['aspect_ratio'] ?? ''),
'criteria' => $criteria,
],
'sort_order' => (int) $block->sort_order,
'active' => (bool) $block->active,
'comparison_results' => $results,
];
}
}