null, 'difficulty' => null, 'score' => null, 'kp_codes' => [], 'tags' => '', 'stem' => null, 'options' => '', 'images' => [], 'source_paper_id' => null, 'part_id' => null, 'order_index' => null, 'answer' => null, 'solution' => null, 'solution_steps' => '', ]; public array $batch = [ 'question_type' => null, 'difficulty' => null, 'score' => null, 'kp_codes' => [], 'tags' => '', 'part_id' => null, 'source_paper_id' => null, ]; public function mount(): void { $first = $this->candidates()->first(); if ($first) { $this->selectCandidate($first->id); } } public function candidates() { $query = PreQuestionCandidate::query()->with(['sourcePaper', 'part']); if ($this->search !== '') { $query->where(function ($q) { $q->where('stem', 'like', '%' . $this->search . '%') ->orWhere('raw_markdown', 'like', '%' . $this->search . '%'); }); } if ($this->statusFilter) { $query->where('status', $this->statusFilter); } if ($this->viewMode === 'review') { $query->where('status', 'pending'); } if ($this->sourcePaperFilter) { $query->where('source_paper_id', $this->sourcePaperFilter); } if ($this->partFilter) { $query->where('part_id', $this->partFilter); } return $query->orderBy('sequence')->limit(120)->get(); } public function selectCandidate(int $candidateId): void { $candidate = PreQuestionCandidate::query()->find($candidateId); if (!$candidate) { return; } $this->currentId = $candidateId; $meta = $candidate->meta ?? []; $this->form = [ 'question_type' => Arr::get($meta, 'question_type'), 'difficulty' => Arr::get($meta, 'difficulty'), 'score' => Arr::get($meta, 'score'), 'kp_codes' => Arr::get($meta, 'kp_codes', []), 'tags' => implode(',', Arr::get($meta, 'tags', [])), 'stem' => $candidate->stem, 'options' => $candidate->options ? json_encode($candidate->options, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '', 'images' => is_array($candidate->images) ? implode(',', $candidate->images) : (string) ($candidate->images ?? ''), 'source_paper_id' => $candidate->source_paper_id, 'part_id' => $candidate->part_id, 'order_index' => $candidate->order_index, 'answer' => Arr::get($meta, 'answer'), 'solution' => Arr::get($meta, 'solution'), 'solution_steps' => json_encode(Arr::get($meta, 'solution_steps', []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]; } public function saveCandidate(): void { $candidate = $this->currentCandidate(); if (!$candidate) { return; } $options = null; if ($this->form['options']) { $decoded = json_decode((string) $this->form['options'], true); if (json_last_error() === JSON_ERROR_NONE) { $options = $decoded; } } $steps = []; if ($this->form['solution_steps']) { $decoded = json_decode((string) $this->form['solution_steps'], true); if (json_last_error() === JSON_ERROR_NONE) { $steps = $decoded; } } $meta = $candidate->meta ?? []; $meta['question_type'] = $this->form['question_type'] ?? null; $meta['difficulty'] = $this->form['difficulty'] ?? null; $meta['score'] = $this->form['score'] ?? null; $meta['kp_codes'] = $this->form['kp_codes'] ?? []; $meta['tags'] = $this->explodeTags($this->form['tags'] ?? ''); $meta['answer'] = $this->form['answer'] ?? null; $meta['solution'] = $this->form['solution'] ?? null; $meta['solution_steps'] = $steps; $candidate->update([ 'stem' => $this->form['stem'], 'options' => $options, 'images' => $this->explodeTags((string) ($this->form['images'] ?? '')), 'source_paper_id' => $this->form['source_paper_id'], 'part_id' => $this->form['part_id'], 'order_index' => $this->form['order_index'], 'meta' => $meta, 'status' => 'reviewed', ]); } public function applyBatch(): void { if (empty($this->selectedIds)) { return; } foreach (PreQuestionCandidate::query()->whereIn('id', $this->selectedIds)->get() as $candidate) { $meta = $candidate->meta ?? []; if (!empty($this->batch['question_type'])) { $meta['question_type'] = $this->batch['question_type']; } if (!empty($this->batch['difficulty'])) { $meta['difficulty'] = $this->batch['difficulty']; } if (!empty($this->batch['score'])) { $meta['score'] = $this->batch['score']; } if (!empty($this->batch['kp_codes'])) { $meta['kp_codes'] = $this->batch['kp_codes']; } if (!empty($this->batch['tags'])) { $meta['tags'] = $this->explodeTags($this->batch['tags']); } $candidate->update([ 'part_id' => $this->batch['part_id'] ?: $candidate->part_id, 'source_paper_id' => $this->batch['source_paper_id'] ?: $candidate->source_paper_id, 'meta' => $meta, 'status' => 'reviewed', ]); } } public function seedBatchFromCurrent(): void { $candidate = $this->currentCandidate(); if (!$candidate) { return; } $meta = $candidate->meta ?? []; $this->batch = [ 'question_type' => Arr::get($meta, 'question_type'), 'difficulty' => Arr::get($meta, 'difficulty'), 'score' => Arr::get($meta, 'score'), 'kp_codes' => Arr::get($meta, 'kp_codes', []), 'tags' => implode(',', Arr::get($meta, 'tags', [])), 'part_id' => $candidate->part_id, 'source_paper_id' => $candidate->source_paper_id, ]; } public function aiAutoFill(): void { $candidate = $this->currentCandidate(); if (!$candidate) { return; } $sourceText = $candidate->stem ?: (string) $candidate->raw_markdown; $result = app(QuestionGenerationService::class)->generateFromSource($sourceText); if (!($result['success'] ?? false)) { return; } $question = $result['question'] ?? []; $meta = $candidate->meta ?? []; $meta['question_type'] = $question['question_type'] ?? $meta['question_type'] ?? null; $meta['difficulty'] = $question['difficulty'] ?? $meta['difficulty'] ?? null; $meta['kp_codes'] = $question['knowledge_points'] ?? $meta['kp_codes'] ?? []; $meta['answer'] = $question['answer'] ?? $meta['answer'] ?? null; $meta['solution'] = $question['solution'] ?? $meta['solution'] ?? null; $meta['solution_steps'] = $question['solution_steps'] ?? $meta['solution_steps'] ?? []; $candidate->update([ 'stem' => $question['stem'] ?? $candidate->stem, 'options' => $question['options'] ?? $candidate->options, 'meta' => $meta, ]); $this->selectCandidate($candidate->id); } public function aiBatchAutoFill(): void { if (empty($this->selectedIds)) { return; } $mode = $this->aiBatchMode; $candidates = PreQuestionCandidate::query()->whereIn('id', $this->selectedIds)->get(); foreach ($candidates as $candidate) { $sourceText = $candidate->stem ?: (string) $candidate->raw_markdown; $result = app(QuestionGenerationService::class)->generateFromSource($sourceText); if (!($result['success'] ?? false)) { continue; } $question = $result['question'] ?? []; $meta = $candidate->meta ?? []; $meta['question_type'] = $this->fillValue($meta['question_type'] ?? null, $question['question_type'] ?? null, $mode); $meta['difficulty'] = $this->fillValue($meta['difficulty'] ?? null, $question['difficulty'] ?? null, $mode); $meta['kp_codes'] = $this->fillArray($meta['kp_codes'] ?? [], $question['knowledge_points'] ?? [], $mode); $meta['answer'] = $this->fillValue($meta['answer'] ?? null, $question['answer'] ?? null, $mode); $meta['solution'] = $this->fillValue($meta['solution'] ?? null, $question['solution'] ?? null, $mode); $meta['solution_steps'] = $this->fillArray($meta['solution_steps'] ?? [], $question['solution_steps'] ?? [], $mode); $updates = ['meta' => $meta]; if ($mode === 'overwrite' || empty($candidate->stem)) { if (!empty($question['stem'])) { $updates['stem'] = $question['stem']; } } if ($mode === 'overwrite' || empty($candidate->options)) { if (!empty($question['options'])) { $updates['options'] = $question['options']; } } $candidate->update($updates); } } public function applyDifficultyByOrder(): void { if (empty($this->selectedIds)) { return; } $candidates = PreQuestionCandidate::query() ->whereIn('id', $this->selectedIds) ->get() ->groupBy(fn ($candidate) => $candidate->part_id ?: $candidate->source_paper_id); foreach ($candidates as $group) { $sorted = $group->sortBy(function ($candidate) { return $candidate->order_index ?? $candidate->sequence ?? $candidate->index ?? $candidate->id; })->values(); $total = max(1, $sorted->count()); foreach ($sorted as $index => $candidate) { $difficulty = (int) ceil((($index + 1) / $total) * 5); $difficulty = max(1, min(5, $difficulty)); $meta = $candidate->meta ?? []; $meta['difficulty'] = $difficulty; $candidate->update(['meta' => $meta]); } } } public function aiMatchKnowledge(): void { $candidate = $this->currentCandidate(); if (!$candidate) { return; } $text = $candidate->stem ?: (string) $candidate->raw_markdown; $matches = app(AiKnowledgeService::class)->matchKnowledgePointsByAi($text); $kpCodes = array_values(array_filter(array_map(fn ($item) => $item['kp_code'] ?? null, $matches))); $meta = $candidate->meta ?? []; $meta['kp_codes'] = $kpCodes; $candidate->update(['meta' => $meta]); $this->selectCandidate($candidate->id); } public function aiGenerateSolution(): void { $candidate = $this->currentCandidate(); if (!$candidate) { return; } $result = app(AiSolutionService::class)->generateSolution($candidate->stem ?? ''); $meta = $candidate->meta ?? []; $meta['solution'] = $result['solution'] ?? ''; $meta['solution_steps'] = $result['steps'] ?? []; $candidate->update(['meta' => $meta]); $this->selectCandidate($candidate->id); } public function nextCandidate(): void { $ids = $this->candidates()->pluck('id')->values(); $index = $ids->search($this->currentId); if ($index !== false && isset($ids[$index + 1])) { $this->selectCandidate($ids[$index + 1]); } } public function previousCandidate(): void { $ids = $this->candidates()->pluck('id')->values(); $index = $ids->search($this->currentId); if ($index !== false && $index > 0) { $this->selectCandidate($ids[$index - 1]); } } public function knowledgePointOptions(): array { return KnowledgePoint::query()->orderBy('kp_code')->pluck('name', 'kp_code')->toArray(); } public function knowledgePointTreeOptions(): array { $points = KnowledgePoint::query() ->orderBy('kp_code') ->get(['kp_code', 'name', 'parent_kp_code']) ->groupBy('parent_kp_code'); $walk = function ($parent, int $depth) use (&$walk, $points): array { $items = []; foreach ($points->get($parent, collect()) as $point) { $indent = str_repeat('—', $depth); $label = trim($indent . ' ' . $point->name); $items[$point->kp_code] = $label; $items += $walk($point->kp_code, $depth + 1); } return $items; }; return $walk(null, 0); } public function filteredKnowledgePointOptions(): array { $options = $this->knowledgePointTreeOptions(); if ($this->kpSearch === '') { return $options; } $needle = mb_strtolower($this->kpSearch); return array_filter($options, fn ($label, $code) => str_contains(mb_strtolower($label), $needle) || str_contains(mb_strtolower($code), $needle), ARRAY_FILTER_USE_BOTH); } public function sourcePaperOptions(): array { return SourcePaper::query()->orderByDesc('id')->limit(200)->pluck('title', 'id')->toArray(); } public function partOptions(): array { return PaperPart::query()->orderByDesc('id')->limit(200)->pluck('title', 'id')->toArray(); } public function currentCandidate(): ?PreQuestionCandidate { return $this->currentId ? PreQuestionCandidate::query()->find($this->currentId) : null; } private function explodeTags(string $tags): array { return array_values(array_filter(array_map('trim', explode(',', $tags)))); } private function fillValue($current, $incoming, string $mode) { if ($mode === 'overwrite') { return $incoming ?? $current; } return $current !== null && $current !== '' ? $current : $incoming; } private function fillArray(array $current, array $incoming, string $mode): array { if ($mode === 'overwrite') { return !empty($incoming) ? $incoming : $current; } return !empty($current) ? $current : $incoming; } }