routeIs('admin.cp.ai-biography.*'); $routePrefix = $isAdminSurface ? 'admin.cp.ai-biography.' : 'cp.ai-biography.'; $filters = $this->filters($request); $records = $this->recordsQuery($filters) ->paginate(self::PER_PAGE) ->withQueryString() ->through(fn (CreatorAiBiography $record): array => $this->mapRecord($record)); return Inertia::render($isAdminSurface ? 'Admin/AiBiography' : 'Moderation/AiBiographyAdmin', [ 'title' => 'AI Biography Review', 'records' => $records, 'filters' => $filters, 'stats' => $this->stats(), 'filterOptions' => [ 'status' => [ ['value' => 'all', 'label' => 'All statuses'], ['value' => CreatorAiBiography::STATUS_GENERATED, 'label' => 'Generated'], ['value' => CreatorAiBiography::STATUS_APPROVED, 'label' => 'Approved'], ['value' => CreatorAiBiography::STATUS_EDITED, 'label' => 'Edited'], ['value' => CreatorAiBiography::STATUS_NEEDS_REVIEW, 'label' => 'Needs review'], ['value' => CreatorAiBiography::STATUS_FAILED, 'label' => 'Failed'], ['value' => CreatorAiBiography::STATUS_SUPPRESSED, 'label' => 'Suppressed'], ], 'scope' => [ ['value' => 'all', 'label' => 'All records'], ['value' => 'active', 'label' => 'Active only'], ['value' => 'inactive', 'label' => 'Inactive only'], ], 'tier' => [ ['value' => 'all', 'label' => 'All tiers'], ['value' => CreatorAiBiography::TIER_RICH, 'label' => 'Rich'], ['value' => CreatorAiBiography::TIER_MEDIUM, 'label' => 'Medium'], ['value' => CreatorAiBiography::TIER_SPARSE, 'label' => 'Sparse'], ], 'visibility' => [ ['value' => 'all', 'label' => 'All visibility'], ['value' => 'visible', 'label' => 'Visible'], ['value' => 'hidden', 'label' => 'Hidden'], ], 'review' => [ ['value' => 'all', 'label' => 'All review states'], ['value' => 'needs_review', 'label' => 'Needs review'], ['value' => 'failed', 'label' => 'Failed / errored'], ['value' => 'user_edited', 'label' => 'User edited'], ], ], 'endpoints' => [ 'index' => route($routePrefix . 'index'), 'rebuildPattern' => route($routePrefix . 'rebuild', ['user' => '__USER__']), 'approvePattern' => route($routePrefix . 'approve', ['biography' => '__BIOGRAPHY__']), 'flagPattern' => route($routePrefix . 'flag', ['biography' => '__BIOGRAPHY__']), 'hidePattern' => route($routePrefix . 'hide', ['biography' => '__BIOGRAPHY__']), 'showPattern' => route($routePrefix . 'show', ['biography' => '__BIOGRAPHY__']), ], ])->rootView($isAdminSurface ? 'admin' : 'moderation'); } public function rebuild(User $user): JsonResponse { if (! (bool) config('ai_biography.enabled', true)) { return response()->json([ 'success' => false, 'message' => 'AI Biography generation is currently disabled.', ], 409); } $active = $this->activeRecordForUser($user); if ($active !== null && $active->is_user_edited) { $result = $this->biographies->generate($user, CreatorAiBiography::REASON_ADMIN_BATCH); } elseif ($active !== null) { $result = $this->biographies->regenerate($user, true, CreatorAiBiography::REASON_ADMIN_BATCH); } else { $result = $this->biographies->generate($user, CreatorAiBiography::REASON_ADMIN_BATCH); } return response()->json([ 'success' => (bool) $result['success'], 'message' => $this->rebuildMessage($result), 'result' => $result, ], $result['success'] ? 200 : 422); } public function approve(CreatorAiBiography $biography): JsonResponse { $biography->update([ 'needs_review' => false, 'status' => $biography->is_user_edited ? CreatorAiBiography::STATUS_EDITED : CreatorAiBiography::STATUS_APPROVED, ]); return response()->json([ 'success' => true, 'message' => 'Biography marked as reviewed.', ]); } public function flag(CreatorAiBiography $biography): JsonResponse { $biography->update([ 'needs_review' => true, 'status' => $biography->is_user_edited ? CreatorAiBiography::STATUS_EDITED : CreatorAiBiography::STATUS_NEEDS_REVIEW, ]); return response()->json([ 'success' => true, 'message' => 'Biography flagged for review.', ]); } public function hide(CreatorAiBiography $biography): JsonResponse { if (! $biography->is_active) { return response()->json([ 'success' => false, 'message' => 'Only active biographies can be hidden.', ], 422); } $this->biographies->hide($biography->user); return response()->json([ 'success' => true, 'message' => 'Biography hidden from public view.', ]); } public function show(CreatorAiBiography $biography): JsonResponse { if (! $biography->is_active) { return response()->json([ 'success' => false, 'message' => 'Only active biographies can be made visible.', ], 422); } $this->biographies->show($biography->user); return response()->json([ 'success' => true, 'message' => 'Biography is public again.', ]); } /** * @return array */ private function filters(Request $request): array { return [ 'q' => trim((string) $request->query('q', '')), 'status' => $this->enumFilter( (string) $request->query('status', 'all'), [ 'all', CreatorAiBiography::STATUS_GENERATED, CreatorAiBiography::STATUS_APPROVED, CreatorAiBiography::STATUS_EDITED, CreatorAiBiography::STATUS_NEEDS_REVIEW, CreatorAiBiography::STATUS_FAILED, CreatorAiBiography::STATUS_SUPPRESSED, ], 'all', ), 'scope' => $this->enumFilter((string) $request->query('scope', 'all'), ['all', 'active', 'inactive'], 'all'), 'tier' => $this->enumFilter( (string) $request->query('tier', 'all'), ['all', CreatorAiBiography::TIER_RICH, CreatorAiBiography::TIER_MEDIUM, CreatorAiBiography::TIER_SPARSE], 'all', ), 'visibility' => $this->enumFilter((string) $request->query('visibility', 'all'), ['all', 'visible', 'hidden'], 'all'), 'review' => $this->enumFilter((string) $request->query('review', 'all'), ['all', 'needs_review', 'failed', 'user_edited'], 'all'), ]; } /** * @param array $filters */ private function recordsQuery(array $filters): Builder { $query = CreatorAiBiography::query() ->with('user:id,username,name,email,created_at') ->latest('created_at') ->latest('id'); if ($filters['q'] !== '') { $search = '%' . Str::lower($filters['q']) . '%'; $query->whereHas('user', function (Builder $userQuery) use ($search): void { $userQuery->where(function (Builder $matchQuery) use ($search): void { $matchQuery->whereRaw('LOWER(username) LIKE ?', [$search]) ->orWhereRaw('LOWER(COALESCE(name, "")) LIKE ?', [$search]) ->orWhereRaw('LOWER(COALESCE(email, "")) LIKE ?', [$search]); }); }); } if ($filters['status'] !== 'all') { $query->where('status', $filters['status']); } if ($filters['scope'] === 'active') { $query->where('is_active', true); } elseif ($filters['scope'] === 'inactive') { $query->where('is_active', false); } if ($filters['tier'] !== 'all') { $query->where('input_quality_tier', $filters['tier']); } if ($filters['visibility'] === 'visible') { $query->where('is_hidden', false); } elseif ($filters['visibility'] === 'hidden') { $query->where('is_hidden', true); } if ($filters['review'] === 'needs_review') { $query->where('needs_review', true); } elseif ($filters['review'] === 'failed') { $query->where(function (Builder $failedQuery): void { $failedQuery->where('status', CreatorAiBiography::STATUS_FAILED) ->orWhereNotNull('last_error_code'); }); } elseif ($filters['review'] === 'user_edited') { $query->where('is_user_edited', true); } return $query; } /** * @return array */ private function stats(): array { if (! Schema::hasTable('creator_ai_biographies')) { return [ 'total_records' => 0, 'active_records' => 0, 'needs_review' => 0, 'hidden_active' => 0, 'failed' => 0, 'user_edited_active' => 0, ]; } return [ 'total_records' => (int) CreatorAiBiography::query()->count(), 'active_records' => (int) CreatorAiBiography::query()->where('is_active', true)->count(), 'needs_review' => (int) CreatorAiBiography::query()->where('needs_review', true)->count(), 'hidden_active' => (int) CreatorAiBiography::query()->where('is_active', true)->where('is_hidden', true)->count(), 'failed' => (int) CreatorAiBiography::query()->where(function (Builder $query): void { $query->where('status', CreatorAiBiography::STATUS_FAILED) ->orWhereNotNull('last_error_code'); })->count(), 'user_edited_active' => (int) CreatorAiBiography::query()->where('is_active', true)->where('is_user_edited', true)->count(), ]; } /** * @return array */ private function mapRecord(CreatorAiBiography $record): array { $user = $record->user; $username = (string) ($user?->username ?? ''); $isStale = $record->is_active && $user !== null ? $this->biographies->isStale($user) : false; return [ 'id' => (int) $record->id, 'user_id' => (int) $record->user_id, 'user' => [ 'id' => $user?->id, 'username' => $username, 'display_name' => $user?->name ?: ($username !== '' ? '@' . $username : 'Unknown creator'), 'email' => $user?->email, 'profile_url' => $username !== '' ? route('profile.show', ['username' => Str::lower($username)]) : null, 'gallery_url' => $username !== '' ? route('profile.gallery', ['username' => Str::lower($username)]) : null, ], 'text' => $record->text, 'excerpt' => Str::limit((string) $record->text, 220), 'status' => (string) $record->status, 'is_active' => (bool) $record->is_active, 'is_hidden' => (bool) $record->is_hidden, 'is_user_edited' => (bool) $record->is_user_edited, 'needs_review' => (bool) $record->needs_review, 'is_stale' => $isStale, 'source_hash' => $record->source_hash, 'model' => $record->model, 'prompt_version' => $record->prompt_version, 'input_quality_tier' => $record->input_quality_tier, 'generation_reason' => $record->generation_reason, 'generated_at' => $record->generated_at?->toIso8601String(), 'approved_at' => $record->approved_at?->toIso8601String(), 'last_attempted_at' => $record->last_attempted_at?->toIso8601String(), 'last_error_code' => $record->last_error_code, 'last_error_reason' => $record->last_error_reason, 'created_at' => $record->created_at?->toIso8601String(), 'updated_at' => $record->updated_at?->toIso8601String(), ]; } private function activeRecordForUser(User $user): ?CreatorAiBiography { return CreatorAiBiography::query() ->where('user_id', (int) $user->id) ->where('is_active', true) ->latest('id') ->first(); } /** * @param array{success: bool, action: string, errors: list} $result */ private function rebuildMessage(array $result): string { if ($result['success']) { return match ($result['action']) { 'draft_stored' => 'New AI draft stored while preserving the active user-edited biography.', default => 'Biography rebuild completed.', }; } return $result['errors'][0] ?? 'Biography rebuild failed.'; } /** * @param list $allowed */ private function enumFilter(string $value, array $allowed, string $fallback): string { $normalized = trim(Str::lower($value)); return in_array($normalized, $allowed, true) ? $normalized : $fallback; } }