Procházet zdrojové kódy

周末大部分功能开发提交

yemeishu před 1 měsícem
rodič
revize
70be0f795f

+ 117 - 86
app/Filament/Pages/IntelligentExamGeneration.php

@@ -729,6 +729,16 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
         float $totalScore,
         array $questionTypeRatio
     ): array {
+        // 去重:确保输入题目列表没有重复ID
+        $uniqueQuestions = [];
+        foreach ($questions as $q) {
+            $id = $q['id'] ?? $q['question_id'] ?? null;
+            if ($id && !isset($uniqueQuestions[$id])) {
+                $uniqueQuestions[$id] = $q;
+            }
+        }
+        $questions = array_values($uniqueQuestions);
+
         if (count($questions) <= $targetCount) {
             return $questions;
         }
@@ -760,92 +770,106 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
         $difficultyFilteredQuestions = $this->filterByDifficulty($categorizedQuestions, $difficultyCategory);
 
         // 3. 根据题型配比计算每种题型应选择的题目数量
-        // 先确保每种题型至少有1题(如果题目数量>=3)
         $selectedQuestions = [];
-        $totalSelected = 0;
-
+        $selectedIds = []; // 用于追踪已选题目ID
+        
         // 优先保证每种题型至少一题(适用于总题目数>=3的情况)
         if ($targetCount >= 3) {
             foreach (['choice', 'fill', 'answer'] as $typeKey) {
                 if (!empty($difficultyFilteredQuestions[$typeKey])) {
                     // 随机选择1道该题型的题目
                     $randomIndex = array_rand($difficultyFilteredQuestions[$typeKey]);
-                    $selectedQuestions[] = $difficultyFilteredQuestions[$typeKey][$randomIndex];
-                    $totalSelected++;
+                    $q = $difficultyFilteredQuestions[$typeKey][$randomIndex];
+                    $id = $q['id'] ?? $q['question_id'];
+                    
+                    if (!in_array($id, $selectedIds)) {
+                        $selectedQuestions[] = $q;
+                        $selectedIds[] = $id;
+                    }
 
                     \Illuminate\Support\Facades\Log::info("保证题型最少题目: {$typeKey}", [
                         'selected_index' => $randomIndex
                     ]);
                 } else {
-                    // 如果某题型没有题目,标记为缺失
                     \Illuminate\Support\Facades\Log::warning("题型缺失: {$typeKey},需要从其他题型补充");
                 }
             }
         }
 
-        // 如果题目数量不足3题,则跳过最少保证
         // 根据题型配比计算每种题型应选择的题目数量
         foreach ($questionTypeRatio as $type => $ratio) {
             $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer');
-            $countForType = floor($targetCount * $ratio / 100);
-
-            // 如果总题目数>=3,已经为每种题型分配了1题,需要从剩余数量中扣除
-            if ($targetCount >= 3 && $totalSelected > 0) {
-                // 重新计算:确保至少1题 + 按比例分配的额外题目
-                $baseCount = 1; // 最少1题
-                $extraCount = floor(($targetCount - 3) * $ratio / 100); // 剩余题目按比例分配
-                $countForType = $baseCount + $extraCount;
-            }
-
-            if ($countForType > 0 && !empty($difficultyFilteredQuestions[$typeKey])) {
-                // 按难度排序后选择该题型的一部分题目
-                $availableCount = count($difficultyFilteredQuestions[$typeKey]);
-                $takeCount = min($countForType, $availableCount, $targetCount - $totalSelected);
-
-                // 如果该题型已经在最少保证中分配过,需要排除已分配的题目
-                if ($targetCount >= 3 && isset($selectedQuestions)) {
-                    // 重新获取题目,排除已选择的
-                    $availableQuestions = $difficultyFilteredQuestions[$typeKey];
-                    $takeCount = min($takeCount, $availableCount, $targetCount - $totalSelected);
-                    if ($takeCount > 0) {
-                        $selectedFromType = array_rand(array_flip(array_keys($availableQuestions)), $takeCount);
-                        if (!is_array($selectedFromType)) {
-                            $selectedFromType = [$selectedFromType];
-                        }
-                        foreach ($selectedFromType as $index) {
-                            $selectedQuestions[] = $availableQuestions[$index];
-                        }
-                        $totalSelected += $takeCount;
+            
+            // 计算该类型目标数量
+            $targetTypeCount = floor($targetCount * $ratio / 100);
+            
+            // 调整目标数量:如果总数>=3,需要考虑已经选了的题目
+            // 简单起见,我们计算总共需要的数量,然后减去已经选了的数量
+            // 但这里为了保证比例,我们还是尽量多选
+            
+            if ($targetTypeCount <= 0) continue;
+
+            if (!empty($difficultyFilteredQuestions[$typeKey])) {
+                $availableQuestions = $difficultyFilteredQuestions[$typeKey];
+                // 过滤掉已选的
+                $availableQuestions = array_filter($availableQuestions, function($q) use ($selectedIds) {
+                    $id = $q['id'] ?? $q['question_id'];
+                    return !in_array($id, $selectedIds);
+                });
+                
+                // 如果没有可用题目了,跳过
+                if (empty($availableQuestions)) continue;
+                
+                $availableCount = count($availableQuestions);
+                // 还需要选多少:目标数量 - 已选该类型的数量
+                $currentTypeCount = 0;
+                foreach ($selectedQuestions as $sq) {
+                    if ($this->determineQuestionType($sq) === $typeKey) {
+                        $currentTypeCount++;
                     }
-                } else {
-                    // 正常分配
+                }
+                
+                $needToSelect = $targetTypeCount - $currentTypeCount;
+                
+                if ($needToSelect > 0) {
+                    $takeCount = min($needToSelect, $availableCount, $targetCount - count($selectedQuestions));
+                    
                     if ($takeCount > 0) {
-                        $selectedFromType = array_rand(array_flip(array_keys($difficultyFilteredQuestions[$typeKey])), $takeCount);
-                        if (!is_array($selectedFromType)) {
-                            $selectedFromType = [$selectedFromType];
+                        $randomKeys = array_rand($availableQuestions, $takeCount);
+                        if (!is_array($randomKeys)) {
+                            $randomKeys = [$randomKeys];
                         }
-                        foreach ($selectedFromType as $index) {
-                            $selectedQuestions[] = $difficultyFilteredQuestions[$typeKey][$index];
+                        
+                        foreach ($randomKeys as $key) {
+                            $q = $availableQuestions[$key];
+                            $selectedQuestions[] = $q;
+                            $selectedIds[] = $q['id'] ?? $q['question_id'];
                         }
-                        $totalSelected += $takeCount;
                     }
                 }
-
-                \Illuminate\Support\Facades\Log::info("{$type}题型筛选结果", [
-                    'available' => $availableCount,
-                    'take' => $takeCount,
-                    'ratio' => $ratio,
-                    'type' => $typeKey
-                ]);
             }
         }
 
         // 4. 如果还有空缺,随机补充其他题型
-        while ($totalSelected < $targetCount && count($selectedQuestions) < count($questions)) {
-            $randomQuestion = $questions[array_rand($questions)];
-            if (!in_array($randomQuestion, $selectedQuestions)) {
-                $selectedQuestions[] = $randomQuestion;
-                $totalSelected++;
+        if (count($selectedQuestions) < $targetCount) {
+            // 从所有题目中过滤掉已选的
+            $remainingQuestions = array_filter($questions, function($q) use ($selectedIds) {
+                $id = $q['id'] ?? $q['question_id'];
+                return !in_array($id, $selectedIds);
+            });
+            
+            if (!empty($remainingQuestions)) {
+                $needed = $targetCount - count($selectedQuestions);
+                $take = min($needed, count($remainingQuestions));
+                
+                $randomKeys = array_rand($remainingQuestions, $take);
+                if (!is_array($randomKeys)) {
+                    $randomKeys = [$randomKeys];
+                }
+                
+                foreach ($randomKeys as $key) {
+                    $selectedQuestions[] = $remainingQuestions[$key];
+                }
             }
         }
 
@@ -1011,8 +1035,15 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
      */
     protected function determineQuestionType(array $question): string
     {
+        // 0. 如果题目已有明确类型,直接返回
+        if (!empty($question['type'])) {
+            if ($question['type'] === 'choice' || $question['type'] === '选择题') return 'choice';
+            if ($question['type'] === 'fill' || $question['type'] === '填空题') return 'fill';
+            if ($question['type'] === 'answer' || $question['type'] === '解答题') return 'answer';
+        }
+
         $tags = $question['tags'] ?? '';
-        $stem = $question['stem'] ?? '';
+        $stem = $question['stem'] ?? $question['content'] ?? '';
 
         // 1. 根据标签判断
         if (is_string($tags)) {
@@ -1027,45 +1058,45 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
             }
         }
 
-        // 2. 根据题干内容判断 - 选择题(有括号的或包含选项A.B.C.D.)
+        // 2. 根据题干内容判断 - 填空题优先(有下划线)
+        // 填空题特征:连续的下划线,或者括号中明显是填空的(通常不会有选项)
         if (is_string($stem)) {
-            // 检查全角括号
-            if (strpos($stem, '()') !== false) {
-                return 'choice';
-            }
-            // 检查半角括号
-            if (strpos($stem, '()') !== false) {
-                return 'choice';
-            }
-            // 检查选项格式 A. B. C. D.(支持跨行匹配)
-            if (preg_match('/[A-D]\.\s/m', $stem)) {
-                return 'choice';
+            // 检查填空题特征:连续下划线
+            if (strpos($stem, '____') !== false || strpos($stem, '______') !== false) {
+                return 'fill';
             }
         }
 
-        // 3. 根据题干内容判断 - 填空题(有下划线)
-        if (is_string($stem) && (strpos($stem, '____') !== false || strpos($stem, '______') !== false)) {
-            return 'fill';
-        }
-
-        // 4. 根据题干长度和内容判断(启发式)
+        // 3. 根据题干内容判断 - 选择题
+        // 选择题特征:必须包含选项 A. B. C. D.
         if (is_string($stem)) {
-            $shortQuestions = ['下列', '判断', '选择', '计算', '求'];
-            $isShort = false;
-            foreach ($shortQuestions as $keyword) {
-                if (strpos($stem, $keyword) !== false) {
-                    $isShort = true;
-                    break;
+            // 检查选项格式 A. B. C. D.(支持跨行匹配)
+            // 更严格的正则:A. 后面跟内容,或者 (A) 后面跟内容
+            if (preg_match('/[A-D]\s*\./', $stem) || preg_match('/\([A-D]\)/', $stem)) {
+                // 再次确认是否包含多个选项,防止误判
+                if (preg_match('/A\./', $stem) && preg_match('/B\./', $stem)) {
+                    return 'choice';
                 }
             }
+            
+            // 如果只有括号但没有选项,可能是填空题
+            // 比如 "计算:(1) ... (2) ..." 这种是解答题
+            // "若 x > 0,则 x + 1 (  )" 这种可能是填空也可能是选择,取决于是否有选项
+            // 这里我们保守一点,如果没有选项特征,就不认为是选择题
+        }
 
-            // 短题目通常是选择题或填空题
-            if ($isShort && mb_strlen($stem) < 100) {
-                return 'choice';
+        // 4. 再次检查填空题特征(括号填空)
+        if (is_string($stem)) {
+            // 只有括号且没有选项,通常是填空
+            if ((strpos($stem, '()') !== false || strpos($stem, '()') !== false) && !preg_match('/[A-D]\./', $stem)) {
+                return 'fill';
             }
+        }
 
-            // 有证明、解答等关键词的是解答题
-            if (strpos($stem, '证明') !== false || strpos($stem, '分析') !== false || strpos($stem, '求证') !== false) {
+        // 5. 根据题干长度和内容判断(启发式)
+        if (is_string($stem)) {
+            // 有证明、解答、计算、求证等关键词的是解答题
+            if (strpos($stem, '证明') !== false || strpos($stem, '求证') !== false || strpos($stem, '解方程') !== false || strpos($stem, '计算:') !== false) {
                 return 'answer';
             }
         }

+ 25 - 2
app/Filament/Pages/OCRRecordView.php

@@ -34,7 +34,20 @@ class OCRRecordView extends Page
     {
         $this->recordId = $recordId;
         $record = $this->record();
+        
         if ($record) {
+            // Fix stuck processing status: if status is processing but we have questions, it's actually completed
+            if ($record->status === 'processing' && $record->questions()->count() > 0) {
+                $record->update([
+                    'status' => 'completed',
+                    'processed_at' => $record->processed_at ?? now(),
+                    'total_questions' => $record->questions()->count(),
+                    'processed_questions' => $record->questions()->count(),
+                ]);
+                // Refresh record to get updated status
+                $record = $this->record();
+            }
+
             foreach ($record->questions as $question) {
                 if ($question->manual_answer) {
                     $this->manualAnswers[$question->id] = $question->manual_answer;
@@ -51,9 +64,9 @@ class OCRRecordView extends Page
      */
     private function checkAnalysisResults(OCRRecord $record): void
     {
-        $this->hasAnalysisResults = $record->questions()
+        // Only consider analyzed if ai_analyzed_at is set AND we have scores
+        $this->hasAnalysisResults = $record->ai_analyzed_at && $record->questions()
             ->whereNotNull('ai_score')
-            ->orWhereNotNull('ai_feedback')
             ->exists();
     }
 
@@ -87,6 +100,16 @@ class OCRRecordView extends Page
         // Call LearningAnalytics API
         $client = new \App\Services\LearningAnalyticsClient();
         $results = $client->analyze($record);
+
+        if ($results === null) {
+            Notification::make()
+                ->title('分析失败')
+                ->body('无法连接到分析服务或服务返回错误,请检查日志。')
+                ->danger()
+                ->send();
+            return;
+        }
+
         $aiUpdated = 0;
 
         // LearningAnalyticsClient已经处理了数据更新,这里只需要更新记录状态

+ 1 - 1
app/Filament/Pages/StudentKnowledgeGraphPage.php

@@ -21,7 +21,7 @@ class StudentKnowledgeGraphPage extends Page
 
     public function mount(): void
     {
-        static::authorizeResourceAccess();
+        // Page 类不需要 authorizeResourceAccess
     }
 
     public function getBreadcrumbs(): array

+ 36 - 15
app/Filament/Pages/UploadExamPaper.php

@@ -11,6 +11,7 @@ use Filament\Notifications\Notification;
 use Filament\Pages\Page;
 use Livewire\WithFileUploads;
 use Livewire\Attributes\Computed;
+use Livewire\Attributes\On;
 use Illuminate\Support\Facades\Storage;
 use UnitEnum;
 
@@ -26,11 +27,18 @@ class UploadExamPaper extends Page
     protected static ?string $slug = 'upload-exam-paper';
     protected string $view = 'filament.pages.upload-exam-paper';
 
-    public ?string $selectedTeacherId = null;
-    public ?string $selectedStudentId = null;
+    public ?string $teacherId = null;
+    public ?string $studentId = null;
     public $uploadedImage = null;
     public bool $isUploading = false;
 
+    public function mount()
+    {
+        $this->teacherId = null;
+        $this->studentId = null;
+        $this->uploadedImage = null;
+    }
+
     #[Computed]
     public function teachers(): array
     {
@@ -85,14 +93,14 @@ class UploadExamPaper extends Page
     #[Computed]
     public function students(): array
     {
-        if (empty($this->selectedTeacherId)) {
+        if (empty($this->teacherId)) {
             return [];
         }
 
         try {
             return Student::query()
                 ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
-                ->where('students.teacher_id', $this->selectedTeacherId)
+                ->where('students.teacher_id', $this->teacherId)
                 ->select(
                     'students.student_id',
                     'students.name',
@@ -108,7 +116,7 @@ class UploadExamPaper extends Page
                 ->all();
         } catch (\Exception $e) {
             \Illuminate\Support\Facades\Log::error('加载学生列表失败', [
-                'teacher_id' => $this->selectedTeacherId,
+                'teacher_id' => $this->teacherId,
                 'error' => $e->getMessage()
             ]);
             return [];
@@ -125,15 +133,15 @@ class UploadExamPaper extends Page
             ->toArray();
     }
 
-    public function updatedSelectedTeacherId($value): void
+    public function updatedTeacherId($value): void
     {
         // 当教师选择变化时,清空之前选择的学生
-        $this->selectedStudentId = null;
+        $this->studentId = null;
     }
 
     public function submitUpload(): void
     {
-        if (!$this->selectedTeacherId) {
+        if (!$this->teacherId) {
             Notification::make()
                 ->title('请选择老师')
                 ->danger()
@@ -141,7 +149,7 @@ class UploadExamPaper extends Page
             return;
         }
 
-        if (!$this->selectedStudentId) {
+        if (!$this->studentId) {
             Notification::make()
                 ->title('请选择学生')
                 ->danger()
@@ -166,7 +174,7 @@ class UploadExamPaper extends Page
 
             // 创建OCR记录
             $record = OCRRecord::create([
-                'student_id' => $this->selectedStudentId,
+                'student_id' => $this->studentId,
                 'image_path' => $path,
                 'image_filename' => $filename,
                 'status' => 'pending',
@@ -174,15 +182,15 @@ class UploadExamPaper extends Page
                 'processed_questions' => 0,
             ]);
 
-            // 自动触发OCR处理
-            ProcessOCRRecord::dispatch($record->id);
-
             // 立即更新状态为处理中,提供更好的用户体验
             $record->update(['status' => 'processing']);
 
+            // 自动触发OCR处理
+            ProcessOCRRecord::dispatch($record->id);
+
             // 重置表单
-            $this->selectedTeacherId = null;
-            $this->selectedStudentId = null;
+            $this->teacherId = null;
+            $this->studentId = null;
             $this->uploadedImage = null;
 
             Notification::make()
@@ -205,6 +213,19 @@ class UploadExamPaper extends Page
         }
     }
 
+    #[On('teacherChanged')]
+    public function updateTeacherId($teacherId)
+    {
+        $this->teacherId = $teacherId;
+        $this->studentId = null;
+    }
+
+    #[On('studentChanged')]
+    public function updateStudentId($teacherId, $studentId)
+    {
+        $this->studentId = $studentId;
+    }
+
     public function removeImage(): void
     {
         $this->uploadedImage = null;

+ 1 - 0
app/Filament/Resources/OCRRecordResource.php

@@ -203,6 +203,7 @@ class OCRRecordResource extends Resource
         return [
             'index' => Pages\ListOCRRecords::route('/'),
             'create' => Pages\CreateOCRRecord::route('/create'),
+            'view' => Pages\ViewOCRRecord::route('/{record}'),
         ];
     }
 

+ 11 - 0
app/Filament/Resources/OCRRecordResource/Pages/ViewOCRRecord.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Filament\Resources\OCRRecordResource\Pages;
+
+use App\Filament\Resources\OCRRecordResource;
+use Filament\Resources\Pages\ViewRecord;
+
+class ViewOCRRecord extends ViewRecord
+{
+    protected static string $resource = OCRRecordResource::class;
+}

+ 75 - 67
app/Livewire/TeacherStudentSelector.php

@@ -8,8 +8,8 @@ use Livewire\Component;
 
 class TeacherStudentSelector extends Component
 {
-    public ?string $selectedTeacherId = null;
-    public ?string $selectedStudentId = null;
+    public ?string $teacherId = null;
+    public ?string $studentId = null;
     public bool $required = false;
     public string $teacherLabel = '选择老师';
     public string $studentLabel = '选择学生';
@@ -34,8 +34,8 @@ class TeacherStudentSelector extends Component
         ?string $teacherHelperText = null,
         ?string $studentHelperText = null
     ): void {
-        $this->selectedTeacherId = $initialTeacherId;
-        $this->selectedStudentId = $initialStudentId;
+        $this->teacherId = $initialTeacherId;
+        $this->studentId = $initialStudentId;
         $this->required = $required;
         $this->teacherLabel = $teacherLabel;
         $this->studentLabel = $studentLabel;
@@ -52,11 +52,65 @@ class TeacherStudentSelector extends Component
         ]);
 
         $this->loadTeacherOptions();
-        if ($this->selectedTeacherId) {
+        
+        // 验证 teacherId 是否在选项中
+        if ($this->teacherId && !array_key_exists($this->teacherId, $this->teacherOptions)) {
+            $this->teacherId = null;
+        }
+
+        if ($this->teacherId) {
             $this->loadStudentOptions();
         }
     }
 
+    public function updatedTeacherId($value): void
+    {
+        \Illuminate\Support\Facades\Log::info('教师选择已更新', [
+            'old_value' => $this->teacherId,
+            'new_value' => $value,
+            'is_empty' => empty($value)
+        ]);
+
+        // 当教师选择变化时,清空之前选择的学生
+        $this->studentId = null;
+        $this->loadStudentOptions();
+
+        // 发送事件到父组件
+        $this->dispatch('teacherChanged', teacherId: $value);
+
+        // 强制刷新组件视图
+        $this->dispatch('$refresh');
+    }
+
+    public function updatedStudentId($value): void
+    {
+        \Illuminate\Support\Facades\Log::info('学生选择已更新', [
+            'teacher_id' => $this->teacherId,
+            'student_id' => $value,
+            'is_empty' => empty($value),
+            'previous_value' => $this->studentId
+        ]);
+
+        // 发送事件到父组件
+        $this->dispatch('studentChanged',
+            teacherId: $this->teacherId,
+            studentId: $value
+        );
+
+        // 同时分发到浏览器窗口,确保父组件能接收到
+        $this->dispatch('window-student-changed',
+            teacherId: $this->teacherId,
+            studentId: $value
+        );
+
+        // 强制刷新组件视图
+        $this->dispatch('$refresh');
+
+        \Illuminate\Support\Facades\Log::info('学生选择事件已分发', [
+            'dispatched' => true
+        ]);
+    }
+
     public function loadTeacherOptions(): void
     {
         try {
@@ -124,7 +178,7 @@ class TeacherStudentSelector extends Component
 
     public function loadStudentOptions(): void
     {
-        if (empty($this->selectedTeacherId)) {
+        if (empty($this->teacherId)) {
             $this->studentOptions = [];
             return;
         }
@@ -132,7 +186,7 @@ class TeacherStudentSelector extends Component
         try {
             $students = Student::query()
                 ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
-                ->where('students.teacher_id', $this->selectedTeacherId)
+                ->where('students.teacher_id', $this->teacherId)
                 ->select(
                     'students.student_id',
                     'students.name',
@@ -147,6 +201,13 @@ class TeacherStudentSelector extends Component
                 ->orderBy('students.name')
                 ->get();
 
+            \Illuminate\Support\Facades\Log::info('已加载学生列表', [
+                'teacher_id' => $this->teacherId,
+                'student_count' => count($students)
+            ]);
+
+            // 如果没有找到学生,可能是因为 teacher_id 不在 teachers 表中
+            // 但学生表中确实有该 teacher_id 的记录,所以直接返回查询结果
             $this->studentOptions = $students->mapWithKeys(function ($student) {
                 // 构建详细的显示文本,包含所有可用字段
                 $displayName = trim($student->name ?? $student->student_id);
@@ -159,86 +220,33 @@ class TeacherStudentSelector extends Component
                 ];
             })->toArray();
 
-            \Illuminate\Support\Facades\Log::info('已加载学生列表', [
-                'teacher_id' => $this->selectedTeacherId,
-                'student_count' => count($this->studentOptions)
-            ]);
-
         } catch (\Exception $e) {
             \Illuminate\Support\Facades\Log::error('加载学生列表失败', [
-                'teacher_id' => $this->selectedTeacherId,
+                'teacher_id' => $this->teacherId,
                 'error' => $e->getMessage()
             ]);
             $this->studentOptions = [];
         }
     }
 
-    public function updatedSelectedTeacherId($value): void
-    {
-        \Illuminate\Support\Facades\Log::info('教师选择已更新', [
-            'old_value' => $this->selectedTeacherId,
-            'new_value' => $value,
-            'is_empty' => empty($value)
-        ]);
-
-        // 当教师选择变化时,清空之前选择的学生
-        $this->selectedStudentId = null;
-        $this->loadStudentOptions();
-
-        // 发送事件到父组件
-        $this->dispatch('teacherChanged', teacherId: $value);
-
-        // 强制刷新组件视图
-        $this->dispatch('$refresh');
-    }
-
-    public function updatedSelectedStudentId($value): void
-    {
-        \Illuminate\Support\Facades\Log::info('学生选择已更新', [
-            'teacher_id' => $this->selectedTeacherId,
-            'student_id' => $value,
-            'is_empty' => empty($value),
-            'previous_value' => $this->selectedStudentId
-        ]);
-
-        // 发送事件到父组件
-        $this->dispatch('studentChanged',
-            teacherId: $this->selectedTeacherId,
-            studentId: $value
-        );
-
-        // 同时分发到浏览器窗口,确保父组件能接收到
-        $this->dispatch('window-student-changed',
-            teacherId: $this->selectedTeacherId,
-            studentId: $value
-        );
-
-        // 强制刷新组件视图
-        $this->dispatch('$refresh');
-
-        \Illuminate\Support\Facades\Log::info('学生选择事件已分发', [
-            'dispatched' => true
-        ]);
-    }
-
     public function getSelectedTeacherName(): string
     {
-        return $this->teacherOptions[$this->selectedTeacherId] ?? '未选择';
+        return $this->teacherOptions[$this->teacherId] ?? '未选择';
     }
 
     public function getSelectedStudentName(): string
     {
-        return $this->studentOptions[$this->selectedStudentId] ?? '未选择';
+        return $this->studentOptions[$this->studentId] ?? '未选择';
     }
 
     public function hasSelections(): bool
     {
-        return !empty($this->selectedTeacherId) && !empty($this->selectedStudentId);
+        return !empty($this->teacherId) && !empty($this->studentId);
     }
 
     public function isStudentDropdownDisabled(): bool
     {
-        return empty($this->selectedTeacherId);
+        return empty($this->teacherId);
     }
 
     public function hasStudents(): bool
@@ -251,7 +259,7 @@ class TeacherStudentSelector extends Component
      */
     public function getStudentDetails(string $studentId): ?array
     {
-        if (empty($studentId) || empty($this->selectedTeacherId)) {
+        if (empty($studentId) || empty($this->teacherId)) {
             return null;
         }
 
@@ -259,7 +267,7 @@ class TeacherStudentSelector extends Component
             $student = Student::query()
                 ->leftJoin('users as u', 'students.student_id', '=', 'u.user_id')
                 ->where('students.student_id', $studentId)
-                ->where('students.teacher_id', $this->selectedTeacherId)
+                ->where('students.teacher_id', $this->teacherId)
                 ->select(
                     'students.student_id',
                     'students.name',

+ 9 - 7
app/Livewire/UploadExamPaper.php

@@ -50,15 +50,17 @@ class UploadExamPaper extends Component
 
     public function loadTeachers()
     {
-        $this->teachers = DB::table('teachers')
-            ->join('users', 'teachers.user_id', '=', 'users.user_id')
-            ->select('teachers.teacher_id', 'users.name')
+        // Load teachers using the Teacher model (assumes a Teacher model exists with a relation to User)
+        $this->teachers = \App\Models\Teacher::with('user')
             ->orderBy('users.name')
             ->get()
-            ->map(fn ($teacher) => [
-                'id' => $teacher->teacher_id,
-                'name' => $teacher->name,
-            ])
+            ->map(function ($teacher) {
+                return [
+                    'id' => $teacher->id,
+                    'teacher_id' => $teacher->id,
+                    'name' => $teacher->user->name,
+                ];
+            })
             ->toArray();
     }
 

+ 17 - 12
app/Services/QuestionBankService.php

@@ -358,20 +358,25 @@ class QuestionBankService
                         // 如果没有类型,根据内容推断
                         $content = $question['stem'] ?? $question['content'] ?? '';
                         if (is_string($content)) {
-                            // 检查全角括号
-                            if (strpos($content, '()') !== false) {
-                                $questionType = 'choice';
-                            }
-                            // 检查半角括号
-                            elseif (strpos($content, '()') !== false) {
-                                $questionType = 'choice';
+                            // 1. 优先检查填空题(下划线)
+                            if (strpos($content, '____') !== false || strpos($content, '______') !== false) {
+                                $questionType = 'fill';
                             }
-                            // 检查选项格式 A. B. C. D.(支持跨行匹配)
-                            elseif (preg_match('/[A-D]\.\s/m', $content)) {
-                                $questionType = 'choice';
+                            // 2. 检查选择题(必须有选项 A. B. C. D.)
+                            elseif (preg_match('/[A-D]\s*\./', $content) || preg_match('/\([A-D]\)/', $content)) {
+                                if (preg_match('/A\./', $content) && preg_match('/B\./', $content)) {
+                                    $questionType = 'choice';
+                                } else {
+                                    // 只有括号没有选项,可能是填空
+                                    if (strpos($content, '()') !== false || strpos($content, '()') !== false) {
+                                        $questionType = 'fill';
+                                    } else {
+                                        $questionType = 'answer';
+                                    }
+                                }
                             }
-                            // 检查填空题
-                            elseif (strpos($content, '____') !== false || strpos($content, '______') !== false) {
+                            // 3. 检查纯括号填空
+                            elseif (strpos($content, '()') !== false || strpos($content, '()') !== false) {
                                 $questionType = 'fill';
                             }
                             else {

+ 10 - 8
resources/views/filament/pages/knowledge-graph-management.blade.php

@@ -175,17 +175,19 @@
                             <div class="flex items-center justify-end gap-1">
                                 <div class="tooltip" data-tip="编辑">
                                     <button wire:click="edit('{{ $point['kp_code'] ?? '' }}')" class="btn btn-ghost btn-xs">
-                                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4">
-                                          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
-                                        </svg>
-                                    </button>
+                                          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
+                                          </svg>
+                                          Edit
+                                        </button>
                                 </div>
                                 <div class="tooltip" data-tip="删除">
                                     <button wire:click="delete('{{ $point['kp_code'] ?? '' }}')" class="btn btn-ghost btn-xs text-error hover:bg-error/10">
-                                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4">
-                                          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
-                                        </svg>
-                                    </button>
+                                          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
+                                          </svg>
+                                          Delete
+                                        </button>
                                 </div>
                             </div>
                         </td>

+ 9 - 4
resources/views/filament/pages/ocr-record-view-new.blade.php

@@ -392,11 +392,16 @@
                                     <button
                                         wire:click="submitForAnalysis"
                                         class="btn btn-primary btn-lg"
+                                        wire:loading.attr="disabled"
                                     >
-                                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
-                                        </svg>
-                                        提交分析
+                                        <span wire:loading.remove>
+                                            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
+                                            </svg>
+                                        </span>
+                                        <span wire:loading class="loading loading-spinner loading-sm"></span>
+                                        <span wire:loading.remove>提交分析</span>
+                                        <span wire:loading>分析中...</span>
                                     </button>
                                 </div>
                             </div>

+ 42 - 9
resources/views/filament/pages/student-dashboard.blade.php

@@ -18,15 +18,48 @@
 
             {{-- 选择器区域 --}}
             <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
-                <livewire:teacher-student-selector
-                    wire:model.selectedTeacherId="teacherId"
-                    wire:model.selectedStudentId="studentId"
-                    :required="true"
-                    teacher-label="选择老师"
-                    student-label="选择学生"
-                    teacher-helper-text="选择要查看的老师"
-                    student-helper-text="选择要查看的学生"
-                />
+                <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
+                    <div>
+                        <label for="teacher" class="block text-sm font-medium text-gray-700 mb-1">选择老师</label>
+                        <select
+                            id="teacher"
+                            wire:model.live="teacherId"
+                            class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
+                        >
+                            <option value="">请选择老师...</option>
+                            @foreach($this->teachers as $teacher)
+                                <option value="{{ $teacher->teacher_id }}">
+                                    {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
+                                </option>
+                            @endforeach
+                        </select>
+                        <p class="mt-1 text-xs text-gray-500">选择要查看的老师</p>
+                    </div>
+
+                    <div>
+                        <label for="student" class="block text-sm font-medium text-gray-700 mb-1">选择学生</label>
+                        <select
+                            id="student"
+                            wire:model.live="studentId"
+                            class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
+                            @if(empty($teacherId)) disabled @endif
+                        >
+                            <option value="">
+                                @if(empty($teacherId))
+                                    请先选择老师
+                                @else
+                                    请选择学生...
+                                @endif
+                            </option>
+                            @foreach($this->students as $student)
+                                <option value="{{ $student->student_id }}">
+                                    {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
+                                </option>
+                            @endforeach
+                        </select>
+                        <p class="mt-1 text-xs text-gray-500">选择要查看的学生</p>
+                    </div>
+                </div>
 
                 <div class="mt-4 pb-0.5 flex space-x-2">
                     <button

+ 1 - 8
resources/views/filament/pages/student-knowledge-graph-page.blade.php

@@ -1,10 +1,3 @@
 <x-filament-panels::page>
-    <x-filament-panels::header
-        :heading="$this->getHeading()"
-        :subheading="$this->getSubHeading()"
-    />
-
-    <x-filament-panels::content>
-        @livewire('student-knowledge-graph')
-    </x-filament-panels::content>
+    @livewire('student-knowledge-graph')
 </x-filament-panels::page>

+ 46 - 12
resources/views/filament/pages/upload-exam-paper.blade.php

@@ -14,17 +14,48 @@
             <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                 {{-- 左侧:选择老师和学生 --}}
                 <div class="space-y-4">
-                    <livewire:teacher-student-selector
-                        wire:model.selectedTeacherId="selectedTeacherId"
-                        wire:model.selectedStudentId="selectedStudentId"
-                        :required="true"
-                        teacher-label="选择老师"
-                        student-label="选择学生"
-                        teacher-placeholder="请选择老师..."
-                        student-placeholder="请选择学生..."
-                        teacher-helper-text="选择要上传给的老师"
-                        student-helper-text="选择要上传给的学生"
-                    />
+                    {{-- 选择老师 --}}
+                    <div class="form-control w-full">
+                        <label class="label">
+                            <span class="label-text font-medium">选择老师 <span class="text-error">*</span></span>
+                        </label>
+                        <select
+                            wire:model.live="teacherId"
+                            class="select select-bordered w-full"
+                        >
+                            <option value="">请选择老师...</option>
+                            @foreach($this->teachers as $teacher)
+                                <option value="{{ $teacher->teacher_id }}">
+                                    {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    {{-- 选择学生 --}}
+                    <div class="form-control w-full">
+                        <label class="label">
+                            <span class="label-text font-medium">选择学生 <span class="text-error">*</span></span>
+                        </label>
+                        <select
+                            wire:model.live="studentId"
+                            class="select select-bordered w-full"
+                            @if(empty($teacherId)) disabled @endif
+                        >
+                            <option value="">
+                                @if(empty($teacherId))
+                                    请先选择老师
+                                @else
+                                    请选择学生...
+                                @endif
+                            </option>
+                            @foreach($this->students as $student)
+                                <option value="{{ $student->student_id }}">
+                                    {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
                 </div>
 
                 {{-- 右侧:上传图片 --}}
@@ -151,7 +182,10 @@
                         </thead>
                         <tbody>
                             @foreach($this->recentRecords as $record)
-                                <tr>
+                                <tr
+                                    class="hover:bg-base-200 cursor-pointer transition-colors"
+                                    onclick="window.location.href='{{ route('filament.admin.resources.ocr-records-legacy.view', ['record' => $record['id']]) }}'"
+                                >
                                     <td>{{ $record['student']['name'] ?? '未知' }}</td>
                                     <td class="max-w-xs truncate">{{ $record['image_filename'] }}</td>
                                     <td>

+ 9 - 9
resources/views/livewire/teacher-student-selector.blade.php

@@ -10,7 +10,7 @@
             </span>
         </label>
         <select
-            wire:model.live="selectedTeacherId"
+            wire:model.live="teacherId"
             class="select select-bordered w-full"
             @if($required) required @endif
         >
@@ -24,13 +24,13 @@
                 <span class="label-text-alt text-info">{{ $teacherHelperText }}</span>
             </label>
         @endif
-        @if($selectedTeacherId)
+        @if($teacherId)
             <label class="label">
                 <span class="label-text-alt text-success">
                     <svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
                     </svg>
-                    已选择:{{ $selectedTeacherId ? $teacherOptions[$selectedTeacherId] ?? '未选择' : '未选择' }}
+                    已选择:{{ $teacherId ? $teacherOptions[$teacherId] ?? '未选择' : '未选择' }}
                 </span>
             </label>
         @endif
@@ -47,14 +47,14 @@
             </span>
         </label>
         <select
-            wire:model.live="selectedStudentId"
+            wire:model.live="studentId"
             wire:change="$refresh"
             class="select select-bordered w-full"
-            @if(empty($selectedTeacherId)) disabled @endif
+            @if(empty($teacherId)) disabled @endif
             @if($required) required @endif
         >
             <option value="">
-                @if(empty($selectedTeacherId))
+                @if(empty($teacherId))
                     请先选择老师
                 @else
                     {{ $studentPlaceholder }}
@@ -69,16 +69,16 @@
                 <span class="label-text-alt text-info">{{ $studentHelperText }}</span>
             </label>
         @endif
-        @if($selectedStudentId)
+        @if($studentId)
             <label class="label">
                 <span class="label-text-alt text-success">
                     <svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
                     </svg>
-                    已选择:{{ $selectedStudentId ? $studentOptions[$selectedStudentId] ?? '未选择' : '未选择' }}
+                    已选择:{{ $studentId ? $studentOptions[$studentId] ?? '未选择' : '未选择' }}
                 </span>
             </label>
-        @elseif($selectedTeacherId && empty($studentOptions))
+        @elseif($teacherId && empty($studentOptions))
             <label class="label">
                 <span class="label-text-alt text-warning">
                     <svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">

+ 3 - 0
resources/views/livewire/test-teacher-student.blade.php

@@ -0,0 +1,3 @@
+<div>
+    {{-- Success is as dangerous as failure. --}}
+</div>