QuestionReviewWorkbench.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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(): void
  60. {
  61. $candidateId = $this->selectedId;
  62. if (!$candidateId) {
  63. return;
  64. }
  65. $service = app(QuestionReviewService::class);
  66. $service->promoteCandidateToQuestion($candidateId);
  67. }
  68. public function reject(): void
  69. {
  70. $candidateId = $this->selectedId;
  71. if (!$candidateId) {
  72. return;
  73. }
  74. PreQuestionCandidate::where('id', $candidateId)->update([
  75. 'status' => PreQuestionCandidate::STATUS_REJECTED,
  76. ]);
  77. }
  78. public function bulkApprove(): void
  79. {
  80. $service = app(QuestionReviewService::class);
  81. foreach ($this->selectedIds as $candidateId) {
  82. $service->promoteCandidateToQuestion((int) $candidateId);
  83. }
  84. }
  85. public function bulkReject(): void
  86. {
  87. PreQuestionCandidate::query()
  88. ->whereIn('id', $this->selectedIds)
  89. ->update(['status' => PreQuestionCandidate::STATUS_REJECTED]);
  90. }
  91. public function aiAssist(): void
  92. {
  93. $candidate = $this->currentCandidate();
  94. if (!$candidate) {
  95. return;
  96. }
  97. $sourceText = $candidate->stem ?: (string) $candidate->raw_markdown;
  98. $result = app(QuestionGenerationService::class)->generateFromSource($sourceText);
  99. if (!($result['success'] ?? false)) {
  100. return;
  101. }
  102. $question = $result['question'] ?? [];
  103. $meta = $candidate->meta ?? [];
  104. $meta['question_type'] = $question['question_type'] ?? $meta['question_type'] ?? null;
  105. $meta['difficulty'] = $question['difficulty'] ?? $meta['difficulty'] ?? null;
  106. $meta['kp_codes'] = $question['knowledge_points'] ?? $meta['kp_codes'] ?? [];
  107. $meta['solution'] = $question['solution'] ?? $meta['solution'] ?? null;
  108. $meta['solution_steps'] = $question['solution_steps'] ?? $meta['solution_steps'] ?? [];
  109. $candidate->update([
  110. 'stem' => $question['stem'] ?? $candidate->stem,
  111. 'options' => $question['options'] ?? $candidate->options,
  112. 'meta' => $meta,
  113. ]);
  114. }
  115. public function aiMatchKp(): void
  116. {
  117. $candidate = $this->currentCandidate();
  118. if (!$candidate) {
  119. return;
  120. }
  121. $text = $candidate->stem ?: (string) $candidate->raw_markdown;
  122. $matches = app(AiKnowledgeService::class)->matchKnowledgePointsByAi($text);
  123. $kpCodes = array_values(array_filter(array_map(fn ($item) => $item['kp_code'] ?? null, $matches)));
  124. $meta = $candidate->meta ?? [];
  125. $meta['kp_codes'] = $kpCodes;
  126. $candidate->update(['meta' => $meta]);
  127. }
  128. public function aiGenerateSolution(): void
  129. {
  130. $candidate = $this->currentCandidate();
  131. if (!$candidate) {
  132. return;
  133. }
  134. $result = app(AiSolutionService::class)->generateSolution($candidate->stem ?? '');
  135. $meta = $candidate->meta ?? [];
  136. $meta['solution'] = $result['solution'] ?? '';
  137. $meta['solution_steps'] = $result['steps'] ?? [];
  138. $candidate->update(['meta' => $meta]);
  139. }
  140. public function currentCandidate(): ?PreQuestionCandidate
  141. {
  142. return $this->selectedId ? PreQuestionCandidate::query()->find($this->selectedId) : null;
  143. }
  144. public function jumpToNextIssue(): void
  145. {
  146. $candidates = $this->candidates();
  147. $currentIndex = $candidates->search(fn ($item) => $item->id === $this->selectedId);
  148. $find = function ($start) use ($candidates) {
  149. for ($i = $start; $i < $candidates->count(); $i++) {
  150. $candidate = $candidates[$i];
  151. $meta = $candidate->meta ?? [];
  152. if (empty($meta['difficulty']) || empty($meta['kp_codes']) || empty($meta['answer']) || empty($meta['solution'])) {
  153. return $candidate->id;
  154. }
  155. }
  156. return null;
  157. };
  158. $nextId = $find(($currentIndex !== false ? $currentIndex + 1 : 0)) ?? $find(0);
  159. if ($nextId) {
  160. $this->selectedId = $nextId;
  161. }
  162. }
  163. }