Explorar o código

feat(qc-review): add weighted similarity workflow and quick duplicate marking

Persist review filters in URL, compute weighted composite similarity, and improve manual duplicate handling with one-click similar marking and linked right-panel navigation.

Made-with: Cursor
yemeishu hai 2 semanas
pai
achega
ef66ee9b05

+ 220 - 1
app/Filament/Pages/QuestionTemQualityReview.php

@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
 
 use App\Jobs\ImportTemToQuestionsJob;
 use App\Services\ExamPdfExportService;
+use App\Services\MathFormulaProcessor;
 use App\Services\QuestionQualityCheckService;
 use App\Services\QuestionsTemAssemblyService;
 use App\Services\QuestionTemReviewService;
@@ -17,6 +18,7 @@ use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Schema;
 use Livewire\Attributes\Computed;
 use Livewire\Attributes\Renderless;
+use Livewire\Attributes\Url;
 use UnitEnum;
 
 class QuestionTemQualityReview extends Page
@@ -38,13 +40,17 @@ class QuestionTemQualityReview extends Page
 
     protected string $view = 'filament.pages.question-tem-quality-review';
 
+    #[Url(as: 'kp', except: '')]
     public ?string $selectedKpCode = null;
 
     /** 左侧知识点列表搜索(匹配 kp_code、kp_name,不区分大小写) */
+    #[Url(as: 'kps', except: '')]
     public string $kpSearch = '';
     /** 左侧按年级筛选;空字符串=全部 */
+    #[Url(as: 'grade', except: '')]
     public string $gradeFilter = '';
     /** 左侧按学期筛选;空字符串=全部 */
+    #[Url(as: 'semester', except: '')]
     public string $semesterFilter = '';
     /** 中间卡片首屏渲染数量(性能优先,按需加载更多) */
     public int $cardRenderLimit = 20;
@@ -390,7 +396,7 @@ class QuestionTemQualityReview extends Page
         return Cache::remember($key, now()->addMinutes(2), function (): array {
             $q = DB::table('questions')
                 ->where('kp_code', $this->selectedKpCode)
-                ->select(['id', 'stem', 'question_type'])
+                ->select(['id', 'stem', 'question_type', 'options', 'answer', 'solution'])
                 ->orderByRaw("
                     CASE
                         WHEN LOWER(COALESCE(question_type, '')) LIKE '%choice%' OR question_type LIKE '%选择%' THEN 1
@@ -407,6 +413,89 @@ class QuestionTemQualityReview extends Page
         });
     }
 
+    /**
+     * 中间卡片:给每道 tem 题计算「当前知识点下最相似的正式库题目」综合相似度。
+     *
+     * @return array<int, array{question_id:int, score:float, score_text:string}>
+     */
+    #[Computed]
+    public function temSimilarityHints(): array
+    {
+        if (! $this->selectedKpCode) {
+            return [];
+        }
+
+        $temRows = $this->visibleTemQuestionCards;
+        $questionRows = $this->currentKpQuestionStemRows;
+        if ($temRows === [] || $questionRows === []) {
+            return [];
+        }
+
+        $normalizedQuestions = [];
+        foreach ($questionRows as $qr) {
+            $qid = (int) ($qr->id ?? 0);
+            if ($qid <= 0) {
+                continue;
+            }
+            $normalizedQuestions[$qid] = [
+                'type' => $this->normalizeSimilarityQuestionType((string) ($qr->question_type ?? '')),
+                'stem' => $this->normalizeSimilarityText((string) ($qr->stem ?? '')),
+                'options' => $this->normalizeSimilarityOptions($qr->options ?? null),
+                'answer' => $this->normalizeSimilarityText((string) ($qr->answer ?? '')),
+                'solution' => $this->normalizeSimilarityText((string) ($qr->solution ?? '')),
+            ];
+        }
+
+        $out = [];
+        foreach ($temRows as $card) {
+            $tid = (int) ($card['id'] ?? 0);
+            if ($tid <= 0) {
+                continue;
+            }
+
+            $grouped = $card['grouped_questions'] ?? ['choice' => [], 'fill' => [], 'answer' => []];
+            $q = $grouped['choice'][0] ?? $grouped['fill'][0] ?? $grouped['answer'][0] ?? null;
+            $temPayload = [
+                'type' => $this->normalizeSimilarityQuestionType((string) ($q->question_type ?? '')),
+                'stem' => $this->normalizeSimilarityText((string) ($q->stem ?? $q->content ?? $card['stem_preview'] ?? '')),
+                'options' => $this->normalizeSimilarityOptions($q->options ?? null),
+                'answer' => $this->normalizeSimilarityText((string) ($q->answer ?? '')),
+                'solution' => $this->normalizeSimilarityText((string) ($q->solution ?? '')),
+            ];
+
+            if ($temPayload['stem'] === '') {
+                continue;
+            }
+
+            $bestQid = 0;
+            $bestScore = 0.0;
+            foreach ($normalizedQuestions as $qid => $questionPayload) {
+                if (($questionPayload['stem'] ?? '') === '') {
+                    continue;
+                }
+                if ($temPayload['type'] !== '' && $questionPayload['type'] !== '' && $temPayload['type'] !== $questionPayload['type']) {
+                    continue;
+                }
+
+                $score = $this->calculateCompositeSimilarityScore($temPayload, $questionPayload);
+                if ($score > $bestScore) {
+                    $bestScore = $score;
+                    $bestQid = (int) $qid;
+                }
+            }
+
+            if ($bestQid > 0) {
+                $out[$tid] = [
+                    'question_id' => $bestQid,
+                    'score' => round($bestScore, 1),
+                    'score_text' => number_format($bestScore, 1).'%',
+                ];
+            }
+        }
+
+        return $out;
+    }
+
     public function updatedSelectedTemId(mixed $value): void
     {
         if ($this->selectedTemId) {
@@ -491,6 +580,42 @@ class QuestionTemQualityReview extends Page
         ImportTemToQuestionsJob::dispatch($id, Auth::id() ? (int) Auth::id() : null, $this->selectedKpCode);
     }
 
+    public function markAsSimilarQuestion(int $temId, int $questionId): void
+    {
+        if ($temId <= 0 || $questionId <= 0) {
+            Notification::make()->title('参数无效')->warning()->send();
+            return;
+        }
+
+        if (! Schema::hasTable('questions_tem')) {
+            Notification::make()->title('questions_tem 表不存在')->danger()->send();
+            return;
+        }
+
+        $updates = [
+            'audit_reason' => "相似题{$questionId}",
+            'updated_at' => now(),
+        ];
+        if (Schema::hasColumn('questions_tem', 'audit_status')) {
+            $updates['audit_status'] = -1;
+        }
+
+        DB::table('questions_tem')
+            ->where('id', $temId)
+            ->update($updates);
+
+        $this->pendingImportTemIds = array_values(array_filter(
+            $this->pendingImportTemIds,
+            static fn ($x) => (int) $x !== $temId
+        ));
+
+        $this->forgetCurrentKpCaches();
+        Notification::make()
+            ->title("已标记为相似题 #{$questionId}")
+            ->success()
+            ->send();
+    }
+
     public function removeFromPendingImport(int $id): void
     {
         $this->pendingImportTemIds = array_values(array_filter(
@@ -987,6 +1112,100 @@ class QuestionTemQualityReview extends Page
         return trim($text);
     }
 
+    private function normalizeSimilarityText(string $text): string
+    {
+        if ($text === '') {
+            return '';
+        }
+
+        $text = MathFormulaProcessor::processFormulas($text);
+        $text = preg_replace('/<img\b[^>]*>/iu', ' ', $text) ?? $text;
+        $text = strip_tags($text);
+        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+        $text = mb_strtolower($text, 'UTF-8');
+        $text = preg_replace('/[[:punct:]\p{P}\p{S}]+/u', ' ', $text) ?? $text;
+        $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
+
+        return trim($text);
+    }
+
+    private function normalizeSimilarityOptions(mixed $raw): string
+    {
+        if (is_string($raw)) {
+            $trimmed = trim($raw);
+            if ($trimmed === '') {
+                return '';
+            }
+            $decoded = json_decode($trimmed, true);
+            if (is_array($decoded)) {
+                $raw = $decoded;
+            } else {
+                return $this->normalizeSimilarityText($trimmed);
+            }
+        }
+
+        if (! is_array($raw) || $raw === []) {
+            return '';
+        }
+
+        $parts = [];
+        foreach ($raw as $item) {
+            if (is_array($item)) {
+                $parts[] = (string) ($item['content'] ?? $item['text'] ?? $item['value'] ?? reset($item) ?? '');
+            } else {
+                $parts[] = (string) $item;
+            }
+        }
+
+        return $this->normalizeSimilarityText(implode(' | ', $parts));
+    }
+
+    private function normalizeSimilarityQuestionType(string $raw): string
+    {
+        $type = mb_strtolower(trim($raw), 'UTF-8');
+        return match (true) {
+            str_contains($type, 'choice'), str_contains($type, '选择') => 'choice',
+            str_contains($type, 'fill'), str_contains($type, 'blank'), str_contains($type, '填空') => 'fill',
+            str_contains($type, 'answer'), str_contains($type, '解答') => 'answer',
+            default => '',
+        };
+    }
+
+    /**
+     * @param  array{type:string,stem:string,options:string,answer:string,solution:string}  $tem
+     * @param  array{type:string,stem:string,options:string,answer:string,solution:string}  $question
+     */
+    private function calculateCompositeSimilarityScore(array $tem, array $question): float
+    {
+        $weights = [
+            'stem' => 0.45,
+            'options' => 0.20,
+            'answer' => 0.20,
+            'solution' => 0.15,
+        ];
+
+        $weightedSum = 0.0;
+        $effectiveWeight = 0.0;
+        foreach ($weights as $field => $weight) {
+            $a = (string) ($tem[$field] ?? '');
+            $b = (string) ($question[$field] ?? '');
+            if ($a === '' || $b === '') {
+                continue;
+            }
+
+            similar_text($a, $b, $pct);
+            $score = max(0.0, min(100.0, (float) $pct));
+            $weightedSum += $score * $weight;
+            $effectiveWeight += $weight;
+        }
+
+        if ($effectiveWeight <= 0.0) {
+            return 0.0;
+        }
+
+        return round($weightedSum / $effectiveWeight, 1);
+    }
+
     private function formatMetaDatetime(mixed $value): ?string
     {
         if ($value === null) {

+ 148 - 58
resources/views/filament/pages/question-tem-quality-review.blade.php

@@ -242,6 +242,14 @@
                                     $tid = (int) $card['id'];
                                     $checks = $card['checks'];
                                     $meta = $card['meta'] ?? [];
+                                    $similar = $this->temSimilarityHints[$tid] ?? null;
+                                    $similarScore = (float) ($similar['score'] ?? 0);
+                                    $similarStyle = 'background:#f0fdf4;color:#166534;border:1px solid #86efac;';
+                                    if ($similarScore >= 60.0 && $similarScore <= 80.0) {
+                                        $similarStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fdba74;';
+                                    } elseif ($similarScore > 80.0) {
+                                        $similarStyle = 'background:#fee2e2;color:#b91c1c;border:1px solid #fca5a5;';
+                                    }
                                     $auditReason = trim((string) ($meta['audit_reason'] ?? ''));
                                     $isQualified = $auditReason === '合格';
                                     $updatedDate = '';
@@ -274,6 +282,32 @@
                                             </span>
                                         @endif
                                     </div>
+                                    @if ($similar)
+                                        <div class="mt-1 text-[11px] text-gray-600 dark:text-gray-300">
+                                            <button
+                                                type="button"
+                                                class="inline-flex items-center rounded-full px-2 py-0.5 hover:opacity-90"
+                                                style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"
+                                                x-on:click.prevent="
+                                                    const qid = {{ (int) ($similar['question_id'] ?? 0) }};
+                                                    window.dispatchEvent(new CustomEvent('qtr-scroll-to-question', { detail: { qid } }));
+                                                "
+                                            >
+                                                最相似 question #{{ (int) ($similar['question_id'] ?? 0) }}
+                                            </button>
+                                            <span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5" style="{{ $similarStyle }}">
+                                                综合相似度 {{ (string) ($similar['score_text'] ?? '-') }}
+                                            </span>
+                                            <x-filament::button
+                                                size="xs"
+                                                color="danger"
+                                                class="ml-1"
+                                                wire:click="markAsSimilarQuestion({{ $tid }}, {{ (int) ($similar['question_id'] ?? 0) }})"
+                                            >
+                                                判定相似题
+                                            </x-filament::button>
+                                        </div>
+                                    @endif
                                     <div class="qtr-full-preview-wrap">
                                         @if ($q)
                                             @include('filament.pages.partials.question-tem-question-check-preview', [
@@ -341,6 +375,11 @@
                                                 <span x-show="operated || {{ $opLocked ? 'true' : 'false' }}">已操作</span>
                                             </button>
                                             <span class="text-xs text-success-600" x-show="operated">已入库</span>
+                                            @if ($auditReason !== '' && $auditReason !== '-')
+                                                <span class="ml-auto qtr-badge {{ $isQualified ? 'qtr-badge-ok' : 'qtr-badge-no' }}">
+                                                    {{ $auditReason }}
+                                                </span>
+                                            @endif
                                         </div>
                                     </div>
                                 </div>
@@ -358,73 +397,38 @@
             @endif
         </x-filament::section>
 
-        {{-- 右:人工判重 + 待入库 --}}
+        {{-- 右:待入库 + 人工判重 --}}
         <div
             class="qtr-sticky-side"
             wire:key="qtr-right-stack"
             wire:loading.class=""
             wire:target="addToPendingImport,removeFromPendingImport,queueImportTem,importPendingTem,importPendingAll,clearPendingImport,selectKp,updatedSelectedKpCode,updatedGradeFilter,updatedSemesterFilter"
-        >
-            <x-filament::section
-                heading="同知识点正式库题目(人工判重)"
-                description="当前筛选 + 当前知识点下,列出 questions 中已有题目(编号+题干)用于人工核对是否重复。"
-                :compact="true"
-            >
-                @if (! $this->selectedKpCode)
-                    <p class="text-sm text-gray-600 dark:text-gray-400">先在左侧选择知识点。</p>
-                @elseif ($this->currentKpQuestionStemRows === [])
-                    <p class="text-sm text-gray-600 dark:text-gray-400">正式库当前知识点暂无题目。</p>
-                @else
-                    <div class="max-h-64 overflow-y-auto rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
-                        @foreach ($this->currentKpQuestionStemRows as $qr)
-                            @php
-                                $qid = (int) ($qr->id ?? 0);
-                                $qStem = (string) ($qr->stem ?? '');
-                                $qTypeRaw = mb_strtolower((string) ($qr->question_type ?? ''));
+            x-data="{}"
+            x-on:qtr-scroll-to-question.window="
+                const qid = Number($event.detail?.qid || 0);
+                if (!qid) return;
+                const target = document.getElementById('kp-question-' + qid);
+                if (!target) return;
 
-                                // 这里用内联样式,避免 Tailwind 动态 class 被清理后“看起来没变化”。
-                                $noPalette = [
-                                    'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;',
-                                    'background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;',
-                                    'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;',
-                                    'background:#fffbeb;color:#b45309;border:1px solid #fde68a;',
-                                    'background:#fff1f2;color:#be123c;border:1px solid #fecdd3;',
-                                ];
-                                $noStyle = $noPalette[$qid % count($noPalette)];
-
-                                $typeLabel = '简答';
-                                $typeStyle = 'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;';
-                                if (str_contains($qTypeRaw, 'choice') || str_contains($qTypeRaw, '选择')) {
-                                    $typeLabel = '选择';
-                                    $typeStyle = 'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;';
-                                } elseif (str_contains($qTypeRaw, 'fill') || str_contains($qTypeRaw, 'blank') || str_contains($qTypeRaw, '填空')) {
-                                    $typeLabel = '填空';
-                                    $typeStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fed7aa;';
-                                }
-                            @endphp
-                            <div class="rounded border border-gray-100 p-2 dark:border-white/10" wire:key="kp-q-{{ (int) ($qr->id ?? 0) }}">
-                                <div class="mb-1 flex items-center gap-1.5">
-                                    <span class="inline-flex items-center rounded-full px-1.5 py-0.5 font-mono text-[10px]" style="{{ $noStyle }}">
-                                        question #{{ $qid }}
-                                    </span>
-                                    <span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px]" style="{{ $typeStyle }}">
-                                        {{ $typeLabel }}
-                                    </span>
-                                </div>
-                                <div style="font-size:11px;line-height:1.3;color:#4b5563;">
-                                    {{ $qStem }}
-                                </div>
-                            </div>
-                        @endforeach
-                    </div>
-                @endif
-            </x-filament::section>
+                // 优先滚动右侧容器本身(sticky side),避免只滚动主页面
+                const targetTop = target.offsetTop;
+                const targetHeight = target.offsetHeight || 0;
+                const viewHeight = $el.clientHeight || 0;
+                const nextTop = Math.max(0, targetTop - (viewHeight / 2) + (targetHeight / 2));
+                $el.scrollTo({ top: nextTop, behavior: 'smooth' });
 
+                // 兜底:再触发一次元素就位
+                setTimeout(() => {
+                    target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                    target.classList.add('ring-2', 'ring-green-400');
+                    setTimeout(() => target.classList.remove('ring-2', 'ring-green-400'), 1200);
+                }, 180);
+            "
+        >
             <x-filament::section
                 heading="待入库"
-                description="仅显示待入库题目 id。"
+                description="优先处理当前待入库列表,可直接入库、回到中间审核或移除。"
                 :compact="true"
-                class="mt-4"
             >
                 @if ($this->pendingImportRows === [])
                     <p class="text-sm text-gray-600 dark:text-gray-400">先在中间卡片点击「加入待入库」。</p>
@@ -437,7 +441,7 @@
                     <div class="mb-2 text-xs text-gray-500">
                         待入库 <span class="font-semibold text-gray-800 dark:text-gray-200">{{ count($this->pendingImportRows) }}</span> 道
                     </div>
-                    <div class="mb-3 max-h-44 overflow-y-auto rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
+                    <div class="mb-3 rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
                         @foreach ($this->pendingImportRows as $pr)
                             <div class="rounded border border-gray-100 p-2 dark:border-white/10" wire:key="pending-{{ (int) ($pr->id ?? 0) }}">
                                 <div class="mb-1 font-mono">tem #{{ (int) ($pr->id ?? 0) }}</div>
@@ -489,6 +493,92 @@
                     </x-filament::button>
                 </div>
             </x-filament::section>
+
+            <x-filament::section
+                heading="同知识点正式库题目(人工判重)"
+                description="当前筛选 + 当前知识点下,列出 questions 中已有题目(含题干/答案)用于人工核对是否重复。"
+                :compact="true"
+                class="mt-4"
+            >
+                @if (! $this->selectedKpCode)
+                    <p class="text-sm text-gray-600 dark:text-gray-400">先在左侧选择知识点。</p>
+                @elseif ($this->currentKpQuestionStemRows === [])
+                    <p class="text-sm text-gray-600 dark:text-gray-400">正式库当前知识点暂无题目。</p>
+                @else
+                    <div class="rounded-lg border border-gray-100 p-2 text-xs dark:border-white/10 space-y-2">
+                        @foreach ($this->currentKpQuestionStemRows as $qr)
+                            @php
+                                $qid = (int) ($qr->id ?? 0);
+                                $qStem = (string) ($qr->stem ?? '');
+                                $qAnswer = trim((string) ($qr->answer ?? ''));
+                                $qTypeRaw = mb_strtolower((string) ($qr->question_type ?? ''));
+
+                                $noPalette = [
+                                    'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;',
+                                    'background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;',
+                                    'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;',
+                                    'background:#fffbeb;color:#b45309;border:1px solid #fde68a;',
+                                    'background:#fff1f2;color:#be123c;border:1px solid #fecdd3;',
+                                ];
+                                $noStyle = $noPalette[$qid % count($noPalette)];
+
+                                $typeLabel = '简答';
+                                $typeStyle = 'background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;';
+                                if (str_contains($qTypeRaw, 'choice') || str_contains($qTypeRaw, '选择')) {
+                                    $typeLabel = '选择';
+                                    $typeStyle = 'background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;';
+                                } elseif (str_contains($qTypeRaw, 'fill') || str_contains($qTypeRaw, 'blank') || str_contains($qTypeRaw, '填空')) {
+                                    $typeLabel = '填空';
+                                    $typeStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fed7aa;';
+                                }
+
+                                $bestForSelected = null;
+                                if ((int) ($this->selectedTemId ?? 0) > 0) {
+                                    $bestForSelected = $this->temSimilarityHints[(int) $this->selectedTemId] ?? null;
+                                }
+                                $isTopSimilar = $bestForSelected && (int) ($bestForSelected['question_id'] ?? 0) === $qid;
+                                $bestScore = (float) ($bestForSelected['score'] ?? 0);
+                                // 右侧用“建议阈值”:>=90 绿,80-90 橙,<80 灰
+                                $bestScoreStyle = 'background:#f3f4f6;color:#4b5563;border:1px solid #d1d5db;';
+                                if ($bestScore >= 90.0) {
+                                    $bestScoreStyle = 'background:#f0fdf4;color:#166534;border:1px solid #86efac;';
+                                } elseif ($bestScore >= 80.0) {
+                                    $bestScoreStyle = 'background:#fff7ed;color:#c2410c;border:1px solid #fdba74;';
+                                }
+                            @endphp
+                            <div
+                                class="rounded border p-2 dark:border-white/10"
+                                style="{{ $isTopSimilar ? 'border-color:#22c55e;background:#f0fdf4;' : 'border-color:rgb(243 244 246);' }}"
+                                wire:key="kp-q-{{ (int) ($qr->id ?? 0) }}"
+                                id="kp-question-{{ $qid }}"
+                                data-kp-question-id="{{ $qid }}"
+                            >
+                                <div class="mb-1 flex items-center gap-1.5">
+                                    <span class="inline-flex items-center rounded-full px-1.5 py-0.5 font-mono text-[10px]" style="{{ $noStyle }}">
+                                        question #{{ $qid }}
+                                    </span>
+                                    <span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px]" style="{{ $typeStyle }}">
+                                        {{ $typeLabel }}
+                                    </span>
+                                    @if ($isTopSimilar && $bestForSelected)
+                                        <span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px]" style="{{ $bestScoreStyle }}">
+                                            综合相似 {{ number_format((float) ($bestForSelected['score'] ?? 0), 1) }}%
+                                        </span>
+                                    @endif
+                                </div>
+                                <div style="font-size:11px;line-height:1.35;color:#4b5563;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">
+                                    {{ $qStem }}
+                                </div>
+                                @if ($qAnswer !== '')
+                                    <div class="mt-1 text-[11px] text-gray-500" style="display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;">
+                                        答案:{{ $qAnswer }}
+                                    </div>
+                                @endif
+                            </div>
+                        @endforeach
+                    </div>
+                @endif
+            </x-filament::section>
         </div>
     </div>
 </x-filament::page>