isAdmin()) { return true; } return $this->rankForUser($user) >= $this->rankForLevel($accessLevel); } public function canAccessLesson(?User $user, AcademyLesson $lesson): bool { return $this->canAccessContent($user, (string) $lesson->access_level); } public function canAccessPrompt(?User $user, AcademyPromptTemplate $prompt): bool { return $this->canAccessContent($user, (string) $prompt->access_level); } public function canAccessPack(?User $user, AcademyPromptPack $pack): bool { return $this->canAccessContent($user, (string) $pack->access_level); } public function canAccessChallenge(?User $user, AcademyChallenge $challenge): bool { return $this->canAccessContent($user, (string) $challenge->access_level); } public function lessonPayload(AcademyLesson $lesson, ?User $viewer, bool $includeFull = false): array { $authorized = $this->canAccessLesson($viewer, $lesson); $fullContent = (string) ($lesson->content ?? ''); return [ 'id' => (int) $lesson->id, 'title' => (string) $lesson->title, 'slug' => (string) $lesson->slug, 'excerpt' => (string) ($lesson->excerpt ?? ''), 'content' => ($authorized && $includeFull) ? $fullContent : null, 'content_preview' => $authorized ? null : $this->previewText($fullContent, 360), 'difficulty' => (string) $lesson->difficulty, '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, 'active' => (bool) $lesson->active, 'published_at' => $lesson->published_at?->toISOString(), 'category' => $lesson->category ? [ 'id' => (int) $lesson->category->id, '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, ]; } public function promptPayload(AcademyPromptTemplate $prompt, ?User $viewer, bool $includeFull = false): array { $authorized = $this->canAccessPrompt($viewer, $prompt); return [ 'id' => (int) $prompt->id, 'title' => (string) $prompt->title, 'slug' => (string) $prompt->slug, 'excerpt' => (string) ($prompt->excerpt ?? ''), 'prompt' => ($authorized && $includeFull) ? (string) $prompt->prompt : null, 'negative_prompt' => ($authorized && $includeFull) ? (string) ($prompt->negative_prompt ?? '') : null, 'usage_notes' => ($authorized && $includeFull) ? (string) ($prompt->usage_notes ?? '') : null, 'workflow_notes' => ($authorized && $includeFull) ? (string) ($prompt->workflow_notes ?? '') : null, 'prompt_preview' => $authorized ? null : $this->previewText((string) $prompt->prompt, 220), 'difficulty' => (string) $prompt->difficulty, 'access_level' => (string) $prompt->access_level, 'aspect_ratio' => $prompt->aspect_ratio, 'tags' => array_values((array) ($prompt->tags ?? [])), 'tool_notes' => $authorized ? (array) ($prompt->tool_notes ?? []) : [], '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(), 'category' => $prompt->category ? [ 'id' => (int) $prompt->category->id, 'name' => (string) $prompt->category->name, 'slug' => (string) $prompt->category->slug, ] : null, 'locked' => ! $authorized, 'can_access' => $authorized, ]; } public function packPayload(AcademyPromptPack $pack, ?User $viewer, bool $includePrompts = false): array { $authorized = $this->canAccessPack($viewer, $pack); return [ 'id' => (int) $pack->id, 'title' => (string) $pack->title, 'slug' => (string) $pack->slug, 'excerpt' => (string) ($pack->excerpt ?? ''), 'description' => (string) ($pack->description ?? ''), 'access_level' => (string) $pack->access_level, 'cover_image' => $pack->cover_image, 'tags' => array_values((array) ($pack->tags ?? [])), 'featured' => (bool) $pack->featured, 'published_at' => $pack->published_at?->toISOString(), 'locked' => ! $authorized, 'can_access' => $authorized, 'prompts' => $includePrompts ? $pack->prompts->map(fn (AcademyPromptTemplate $prompt): array => $this->promptPayload($prompt, $viewer, $authorized))->values()->all() : [], ]; } public function challengePayload(AcademyChallenge $challenge, ?User $viewer, bool $includeSubmissions = false): array { $authorized = $this->canAccessChallenge($viewer, $challenge); return [ 'id' => (int) $challenge->id, 'title' => (string) $challenge->title, 'slug' => (string) $challenge->slug, 'excerpt' => (string) ($challenge->excerpt ?? ''), 'description' => (string) ($challenge->description ?? ''), 'brief' => (string) ($challenge->brief ?? ''), 'rules' => (string) ($challenge->rules ?? ''), 'access_level' => (string) $challenge->access_level, 'status' => (string) $challenge->status, 'starts_at' => $challenge->starts_at?->toISOString(), 'ends_at' => $challenge->ends_at?->toISOString(), 'voting_starts_at' => $challenge->voting_starts_at?->toISOString(), 'voting_ends_at' => $challenge->voting_ends_at?->toISOString(), 'cover_image' => $challenge->cover_image, 'prize_text' => $challenge->prize_text, 'required_tags' => array_values((array) ($challenge->required_tags ?? [])), 'allowed_categories' => array_values((array) ($challenge->allowed_categories ?? [])), 'featured' => (bool) $challenge->featured, 'locked' => ! $authorized, 'can_access' => $authorized, 'submission_count' => $includeSubmissions ? (int) $challenge->submissions()->approved()->count() : null, ]; } private function rankForUser(User $user): int { if (method_exists($user, 'hasAcademyProAccess') && $user->hasAcademyProAccess()) { return $this->rankForLevel('pro'); } if (method_exists($user, 'hasAcademyCreatorAccess') && $user->hasAcademyCreatorAccess()) { return $this->rankForLevel('creator'); } return $this->rankForLevel('free'); } private function rankForLevel(string $accessLevel): int { return match (Str::lower(trim($accessLevel))) { 'admin' => 99, 'pro' => 30, 'creator' => 20, default => 10, }; } private function previewText(string $value, int $limit): string { $plain = trim(strip_tags($value)); if ($plain === '') { return ''; } $length = mb_strlen($plain); $previewLength = min($limit, max(12, (int) ceil($length * 0.55))); if ($previewLength >= $length) { $previewLength = max(1, $length - 1); } 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|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, ]; } }