QuestionReviewWorkbench.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Models\PreQuestionCandidate;
  4. use App\Services\QuestionReviewService;
  5. use App\Services\QuestionGenerationService;
  6. use App\Services\AiKnowledgeService;
  7. use App\Services\AiSolutionService;
  8. use Filament\Pages\Page;
  9. use UnitEnum;
  10. class QuestionReviewWorkbench extends Page
  11. {
  12. protected static bool $shouldRegisterNavigation = true;
  13. protected static ?string $navigationLabel = '题目审核工作台';
  14. protected static UnitEnum|string|null $navigationGroup = '卷子导入流程';
  15. protected static ?int $navigationSort = 5;
  16. protected string $view = 'filament.pages.question-review-workbench';
  17. public ?int $selectedId = null;
  18. public array $selectedIds = [];
  19. public ?string $groupBy = 'paper';
  20. public function candidates()
  21. {
  22. return PreQuestionCandidate::query()
  23. ->with(['sourcePaper', 'part'])
  24. ->orderBy('sequence')
  25. ->limit(200)
  26. ->get();
  27. }
  28. public function groupedCandidates()
  29. {
  30. $candidates = $this->candidates();
  31. if ($this->groupBy === 'part') {
  32. return $candidates->groupBy(fn ($candidate) => $candidate->part?->title ?? '未分区块');
  33. }
  34. if ($this->groupBy === 'type') {
  35. return $candidates->groupBy(fn ($candidate) => $candidate->meta['question_type'] ?? '未标注题型');
  36. }
  37. return $candidates->groupBy(fn ($candidate) => $candidate->sourcePaper?->title ?? '未分卷');
  38. }
  39. public function stats(): array
  40. {
  41. $all = PreQuestionCandidate::query()->count();
  42. $pending = PreQuestionCandidate::query()->where('status', PreQuestionCandidate::STATUS_PENDING)->count();
  43. $reviewed = PreQuestionCandidate::query()->where('status', PreQuestionCandidate::STATUS_REVIEWED)->count();
  44. $accepted = PreQuestionCandidate::query()->where('status', PreQuestionCandidate::STATUS_ACCEPTED)->count();
  45. return compact('all', 'pending', 'reviewed', 'accepted');
  46. }
  47. public function selectCandidate(int $candidateId): void
  48. {
  49. $this->selectedId = $candidateId;
  50. }
  51. public function selectAllVisible(): void
  52. {
  53. $this->selectedIds = $this->candidates()->pluck('id')->toArray();
  54. }
  55. public function clearSelection(): void
  56. {
  57. $this->selectedIds = [];
  58. }
  59. public function approve(int $candidateId, QuestionReviewService $service): void
  60. {
  61. $service->promoteCandidateToQuestion($candidateId);
  62. }
  63. public function reject(int $candidateId): void
  64. {
  65. PreQuestionCandidate::where('id', $candidateId)->update([
  66. 'status' => PreQuestionCandidate::STATUS_REJECTED,
  67. ]);
  68. }
  69. public function bulkApprove(QuestionReviewService $service): void
  70. {
  71. foreach ($this->selectedIds as $candidateId) {
  72. $service->promoteCandidateToQuestion((int) $candidateId);
  73. }
  74. }
  75. public function bulkReject(): void
  76. {
  77. PreQuestionCandidate::query()
  78. ->whereIn('id', $this->selectedIds)
  79. ->update(['status' => PreQuestionCandidate::STATUS_REJECTED]);
  80. }
  81. public function aiAssist(): void
  82. {
  83. $candidate = $this->currentCandidate();
  84. if (!$candidate) {
  85. return;
  86. }
  87. $sourceText = $candidate->stem ?: (string) $candidate->raw_markdown;
  88. $result = app(QuestionGenerationService::class)->generateFromSource($sourceText);
  89. if (!($result['success'] ?? false)) {
  90. return;
  91. }
  92. $question = $result['question'] ?? [];
  93. $meta = $candidate->meta ?? [];
  94. $meta['question_type'] = $question['question_type'] ?? $meta['question_type'] ?? null;
  95. $meta['difficulty'] = $question['difficulty'] ?? $meta['difficulty'] ?? null;
  96. $meta['kp_codes'] = $question['knowledge_points'] ?? $meta['kp_codes'] ?? [];
  97. $meta['solution'] = $question['solution'] ?? $meta['solution'] ?? null;
  98. $meta['solution_steps'] = $question['solution_steps'] ?? $meta['solution_steps'] ?? [];
  99. $candidate->update([
  100. 'stem' => $question['stem'] ?? $candidate->stem,
  101. 'options' => $question['options'] ?? $candidate->options,
  102. 'meta' => $meta,
  103. ]);
  104. }
  105. public function aiMatchKp(): void
  106. {
  107. $candidate = $this->currentCandidate();
  108. if (!$candidate) {
  109. return;
  110. }
  111. $text = $candidate->stem ?: (string) $candidate->raw_markdown;
  112. $matches = app(AiKnowledgeService::class)->matchKnowledgePointsByAi($text);
  113. $kpCodes = array_values(array_filter(array_map(fn ($item) => $item['kp_code'] ?? null, $matches)));
  114. $meta = $candidate->meta ?? [];
  115. $meta['kp_codes'] = $kpCodes;
  116. $candidate->update(['meta' => $meta]);
  117. }
  118. public function aiGenerateSolution(): void
  119. {
  120. $candidate = $this->currentCandidate();
  121. if (!$candidate) {
  122. return;
  123. }
  124. $result = app(AiSolutionService::class)->generateSolution($candidate->stem ?? '');
  125. $meta = $candidate->meta ?? [];
  126. $meta['solution'] = $result['solution'] ?? '';
  127. $meta['solution_steps'] = $result['steps'] ?? [];
  128. $candidate->update(['meta' => $meta]);
  129. }
  130. public function currentCandidate(): ?PreQuestionCandidate
  131. {
  132. return $this->selectedId ? PreQuestionCandidate::query()->find($this->selectedId) : null;
  133. }
  134. public function jumpToNextIssue(): void
  135. {
  136. $candidates = $this->candidates();
  137. $currentIndex = $candidates->search(fn ($item) => $item->id === $this->selectedId);
  138. $find = function ($start) use ($candidates) {
  139. for ($i = $start; $i < $candidates->count(); $i++) {
  140. $candidate = $candidates[$i];
  141. $meta = $candidate->meta ?? [];
  142. if (empty($meta['difficulty']) || empty($meta['kp_codes']) || empty($meta['answer']) || empty($meta['solution'])) {
  143. return $candidate->id;
  144. }
  145. }
  146. return null;
  147. };
  148. $nextId = $find(($currentIndex !== false ? $currentIndex + 1 : 0)) ?? $find(0);
  149. if ($nextId) {
  150. $this->selectedId = $nextId;
  151. }
  152. }
  153. }