Kaynağa Gözat

增加添加题目的功能

yemeishu 1 ay önce
ebeveyn
işleme
ff394bd8af

+ 371 - 52
app/Filament/Pages/QuestionManagement.php

@@ -3,6 +3,9 @@
 namespace App\Filament\Pages;
 
 use App\Services\QuestionServiceApi;
+use App\Services\KnowledgeGraphService;
+use App\Services\QuestionBankService;
+use App\Services\PromptService;
 use BackedEnum;
 use Filament\Actions;
 use Filament\Notifications\Notification;
@@ -13,6 +16,8 @@ use Livewire\Attributes\On;
 
 class QuestionManagement extends Page
 {
+    protected static ?string $title = '题库管理';
+
     protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
 
     protected static string|UnitEnum|null $navigationGroup = '题库系统';
@@ -21,8 +26,6 @@ class QuestionManagement extends Page
 
     protected static ?int $navigationSort = 2;
 
-    protected ?string $heading = '题库管理';
-
     protected string $view = 'filament.pages.question-management';
 
     public ?string $search = null;
@@ -35,6 +38,21 @@ class QuestionManagement extends Page
 
     public int $perPage = 25;
 
+    // 生成题目相关属性
+    public ?string $generateKpCode = null;
+    public array $selectedSkills = [];
+    public int $questionCount = 100;
+    public ?string $promptTemplate = null;
+    public bool $showGenerateModal = false;
+    public bool $showPromptModal = false;
+
+    // 异步任务相关属性
+    public ?string $currentTaskId = null;
+    public ?string $currentTaskStatus = null;
+    public int $currentTaskProgress = 0;
+    public ?string $currentTaskMessage = null;
+    public bool $isGenerating = false;
+
     /**
      * 计算属性:从 API 获取题目列表
      */
@@ -98,6 +116,118 @@ class QuestionManagement extends Page
         return $service->getKnowledgePointOptions();
     }
 
+    /**
+     * 计算属性:根据知识点获取技能列表
+     */
+    #[Computed]
+    public function skillsOptions(): array
+    {
+        if (!$this->generateKpCode) {
+            return [];
+        }
+
+        $service = app(KnowledgeGraphService::class);
+        return $service->getSkillsByKnowledgePoint($this->generateKpCode);
+    }
+
+    /**
+     * 计算属性:提示词模板
+     */
+    #[Computed]
+    public function promptTemplateData(): array
+    {
+        $service = app(PromptService::class);
+        try {
+            $response = $service->listPrompts();
+            if (!empty($response)) {
+                return $response;
+            }
+        } catch (\Exception $e) {
+            // 使用默认提示词
+        }
+
+        // 返回默认提示词模板
+        return [[
+            'id' => 'default',
+            'template_name' => 'AI题目生成_增强版',
+            'template_content' => $service->getDefaultPromptTemplate(),
+            'version' => 2,
+            'is_active' => true,
+            'description' => '增强版AI题目生成模板,支持精确的难度和题型分布控制',
+            'tags' => 'AI生成,增强版,智能分布'
+        ]];
+    }
+
+    /**
+     * 打开生成题目模态框
+     */
+    public function openGenerateModal(): void
+    {
+        $this->showGenerateModal = true;
+    }
+
+    /**
+     * 关闭生成题目模态框
+     */
+    public function closeGenerateModal(): void
+    {
+        $this->showGenerateModal = false;
+        $this->reset(['generateKpCode', 'selectedSkills', 'questionCount']);
+    }
+
+    /**
+     * 打开提示词编辑模态框
+     */
+    public function openPromptModal(): void
+    {
+        $templates = $this->promptTemplateData;
+        if (!empty($templates)) {
+            $this->promptTemplate = $templates[0]['template_content'] ?? null;
+        }
+        $this->showPromptModal = true;
+    }
+
+    /**
+     * 关闭提示词编辑模态框
+     */
+    public function closePromptModal(): void
+    {
+        $this->showPromptModal = false;
+        $this->reset('promptTemplate');
+    }
+
+    /**
+     * 全选/取消全选技能
+     */
+    public function toggleAllSkills(): void
+    {
+        $skills = $this->skillsOptions;
+        if (count($this->selectedSkills) === count($skills)) {
+            $this->selectedSkills = [];
+        } else {
+            $this->selectedSkills = array_column($skills, 'code');
+        }
+    }
+
+    /**
+     * 监听知识点选择变化
+     */
+    public function updatedGenerateKpCode(): void
+    {
+        // 选择新知识点时重置技能选择
+        $this->selectedSkills = [];
+
+        // 重新计算skillsOptions会自动触发(通过#[Computed])
+    }
+
+    /**
+     * 监听技能选择变化
+     */
+    public function updatedSelectedSkills(): void
+    {
+        // 可选:在这里添加其他逻辑
+    }
+
     /**
      * 搜索更新处理
      */
@@ -145,53 +275,264 @@ class QuestionManagement extends Page
     }
 
     /**
-     * 重置缓存
+     * AI 生成题目
      */
-    private function resetCache(): void
+    #[On('ai-generate')]
+    public function aiGenerate(): void
     {
-        // 清除相关缓存
-        cache()->forget('question-list-' . md5(json_encode([
-            'page' => $this->currentPage,
-            'per_page' => $this->perPage,
-            'filters' => array_filter([
-                'kp_code' => $this->selectedKpCode,
-                'difficulty' => $this->selectedDifficulty,
-                'search' => $this->search,
-            ]),
-        ])));
-
-        cache()->forget('question-statistics');
+        // 打开生成模态框
+        $this->openGenerateModal();
     }
 
     /**
-     * AI 生成题目
+     * 执行生成题目(异步模式)
      */
-    #[On('ai-generate')]
-    public function aiGenerate(): void
+    #[On('execute-generate')]
+    public function executeGenerate(): void
     {
-        // 调用智能题目生成API
+        if (!$this->generateKpCode) {
+            Notification::make()
+                ->title('请选择知识点')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        if (empty($this->selectedSkills)) {
+            Notification::make()
+                ->title('请至少选择一个技能')
+                ->danger()
+                ->send();
+            return;
+        }
+
         try {
-            $response = Http::timeout(60)->post('http://localhost:5015/generate-intelligent-questions', [
-                'knowledge_points' => ['KP1001'],
-                'max_questions_per_skill' => 5
+            $service = app(QuestionBankService::class);
+
+            // 构建回调 URL
+            $callbackUrl = route('api.questions.callback');
+
+            $result = $service->generateIntelligentQuestions([
+                'kp_code' => $this->generateKpCode,
+                'skills' => $this->selectedSkills,
+                'count' => $this->questionCount,
+                'prompt_template' => $this->promptTemplate
+            ], $callbackUrl);
+
+            if ($result['success'] ?? false) {
+                // 获取task_id
+                $this->currentTaskId = $result['task_id'] ?? null;
+
+                // 关闭模态框
+                $this->showGenerateModal = false;
+
+                // 显示提示消息
+                Notification::make()
+                    ->title('任务已创建')
+                    ->body("任务 ID: {$this->currentTaskId}\n题目生成完成后将自动刷新列表")
+                    ->info()
+                    ->send();
+
+                // 不再轮询,等待回调
+            } else {
+                $error = $result['message'] ?? '未知错误';
+                Notification::make()
+                    ->title('创建任务失败')
+                    ->body($error)
+                    ->danger()
+                    ->send();
+            }
+        } catch (\Exception $e) {
+            Notification::make()
+                ->title('生成异常')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
+
+    /**
+     * 轮询任务状态
+     */
+    public function pollTaskStatus(): void
+    {
+        if (!$this->currentTaskId) {
+            return;
+        }
+
+        try {
+            $service = app(QuestionBankService::class);
+            $taskStatus = $service->getTaskStatus($this->currentTaskId);
+
+            if ($taskStatus) {
+                $this->currentTaskStatus = $taskStatus['status'] ?? 'unknown';
+                $this->currentTaskProgress = $taskStatus['progress'] ?? 0;
+                $this->currentTaskMessage = $this->getTaskStatusMessage($taskStatus);
+
+                // 如果任务完成或失败,停止轮询
+                if (in_array($this->currentTaskStatus, ['completed', 'failed'])) {
+                    $this->isGenerating = false;
+
+                    if ($this->currentTaskStatus === 'completed') {
+                        $result = $taskStatus['result'] ?? null;
+                        $total = $result['total'] ?? $this->questionCount;
+
+                        Notification::make()
+                            ->title('生成完成')
+                            ->body("已为知识点 {$this->generateKpCode} 成功生成 {$total} 道题目")
+                            ->success()
+                            ->send();
+
+                        // 刷新数据
+                        $this->refreshData();
+                    } else {
+                        $error = $taskStatus['error_message'] ?? '未知错误';
+                        Notification::make()
+                            ->title('生成失败')
+                            ->body($error)
+                            ->danger()
+                            ->send();
+                    }
+
+                    // 重置任务状态
+                    $this->currentTaskId = null;
+                    $this->currentTaskStatus = null;
+                    $this->currentTaskProgress = 0;
+                    $this->currentTaskMessage = null;
+                } else {
+                    // 继续轮询
+                    $this->dispatch('$refresh');
+                    $this->dispatch('poll-task');
+                }
+            }
+        } catch (\Exception $e) {
+            $this->isGenerating = false;
+            Notification::make()
+                ->title('查询任务状态失败')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
+
+    /**
+     * 获取任务状态消息
+     */
+    private function getTaskStatusMessage(array $taskStatus): string
+    {
+        $status = $taskStatus['status'] ?? 'unknown';
+        $progress = $taskStatus['progress'] ?? 0;
+
+        return match ($status) {
+            'pending' => '任务已创建,等待开始...',
+            'processing' => "正在生成题目... {$progress}%",
+            'completed' => '生成完成',
+            'failed' => '生成失败',
+            default => '未知状态',
+        };
+    }
+
+    /**
+     * 取消生成任务
+     */
+    #[On('cancel-generate')]
+    public function cancelGenerate(): void
+    {
+        $this->isGenerating = false;
+        $this->currentTaskId = null;
+        $this->currentTaskStatus = null;
+        $this->currentTaskProgress = 0;
+        $this->currentTaskMessage = null;
+
+        Notification::make()
+            ->title('已取消生成任务')
+            ->warning()
+            ->send();
+    }
+
+    /**
+     * 保存提示词
+     */
+    #[On('save-prompt')]
+    public function savePrompt(): void
+    {
+        if (!$this->promptTemplate) {
+            Notification::make()
+                ->title('提示词不能为空')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        try {
+            $service = app(PromptService::class);
+            $result = $service->savePrompt([
+                'template_name' => 'AI题目生成_增强版',
+                'template_type' => '题目生成',
+                'template_content' => $this->promptTemplate,
+                'version' => 2,
+                'is_active' => true,
+                'description' => '增强版AI题目生成模板,支持精确的难度和题型分布控制',
+                'tags' => 'AI生成,增强版,智能分布'
             ]);
 
-            if ($response->successful()) {
+            if ($result['success'] ?? false) {
                 Notification::make()
-                    ->title('AI 生成成功')
-                    ->body('因式分解题目生成任务已启动,请稍后查看结果')
+                    ->title('提示词保存成功')
                     ->success()
                     ->send();
             } else {
                 Notification::make()
-                    ->title('AI 生成失败')
-                    ->body('请检查API服务状态')
+                    ->title('保存失败')
+                    ->body($result['message'] ?? '未知错误')
                     ->danger()
                     ->send();
             }
+            $this->closePromptModal();
         } catch (\Exception $e) {
             Notification::make()
-                ->title('AI 生成异常')
+                ->title('保存异常')
+                ->body($e->getMessage())
+                ->danger()
+                ->send();
+        }
+    }
+
+    /**
+     * 删除题目
+     */
+    public function deleteQuestion(string $questionCode): void
+    {
+        try {
+            $service = app(QuestionBankService::class);
+            $result = $service->deleteQuestion($questionCode);
+
+            if ($result) {
+                // 显示成功消息
+                Notification::make()
+                    ->title('删除成功')
+                    ->body("题目 {$questionCode} 已删除")
+                    ->success()
+                    ->send();
+
+                // 延迟1秒后刷新页面,确保用户体验好
+                $this->dispatch('show-message', [
+                    'type' => 'success',
+                    'message' => "题目 {$questionCode} 已删除"
+                ]);
+
+                // 强制刷新页面
+                $this->dispatch('refresh-page');
+            } else {
+                Notification::make()
+                    ->title('删除失败')
+                    ->body("题目 {$questionCode} 不存在或已被删除")
+                    ->warning()
+                    ->send();
+            }
+        } catch (\Exception $e) {
+            Notification::make()
+                ->title('删除异常')
                 ->body($e->getMessage())
                 ->danger()
                 ->send();
@@ -259,28 +600,6 @@ class QuestionManagement extends Page
     }
 
     /**
-     * 头部操作按钮
+     * 头部操作按钮已在视图中直接添加
      */
-    protected function getHeaderActions(): array
-    {
-        return [
-            Actions\Action::make('ai_generate')
-                ->label('AI 生成题目')
-                ->icon('heroicon-m-sparkles')
-                ->color('success')
-                ->action('aiGenerate'),
-
-            Actions\Action::make('smart_search')
-                ->label('智能搜索')
-                ->icon('heroicon-m-magnifying-glass')
-                ->color('info')
-                ->action('smartSearch'),
-
-            Actions\Action::make('refresh')
-                ->label('刷新')
-                ->icon('heroicon-m-arrow-path')
-                ->color('warning')
-                ->action('refreshData'),
-        ];
-    }
 }

+ 22 - 0
app/Services/HttpClientService.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+
+class HttpClientService
+{
+    /**
+     * 创建带标准头的HTTP客户端
+     */
+    public static function make(string $timeout = '10'): \Illuminate\Http\Client\PendingRequest
+    {
+        return Http::timeout((int) $timeout)
+            ->withHeaders([
+                'Accept' => 'application/json',
+                'Content-Type' => 'application/json',
+                'User-Agent' => 'Laravel/' . (app()->version()),
+            ])
+            ->retry(2, 200);
+    }
+}

+ 129 - 303
app/Services/KnowledgeGraphService.php

@@ -2,367 +2,193 @@
 
 namespace App\Services;
 
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
 class KnowledgeGraphService
 {
-    protected MathRecSysService $mathRecSys;
+    protected string $baseUrl;
 
-    public function __construct(MathRecSysService $mathRecSys)
+    public function __construct()
     {
-        $this->mathRecSys = $mathRecSys;
-    }
-
-    /**
-     * 获取知识图谱数据(格式化后)
-     *
-     * @param string|null $focus 焦点知识点
-     * @return array
-     */
-    public function getGraphData(?string $focus = null): array
-    {
-        try {
-            $graphData = $this->mathRecSys->getKnowledgeGraph($focus);
-
-            return [
-                'success' => true,
-                'nodes' => $this->formatNodes($graphData['nodes'] ?? []),
-                'edges' => $this->formatEdges($graphData['edges'] ?? []),
-                'categories' => $this->extractCategories($graphData['nodes'] ?? []),
-                'metadata' => [
-                    'total_nodes' => count($graphData['nodes'] ?? []),
-                    'total_edges' => count($graphData['edges'] ?? []),
-                    'focus' => $focus
-                ]
-            ];
-
-        } catch (\Exception $e) {
-            \Log::error('获取知识图谱数据失败', ['error' => $e->getMessage()]);
-            return [
-                'success' => false,
-                'nodes' => [],
-                'edges' => [],
-                'categories' => [],
-                'error' => $e->getMessage()
-            ];
-        }
+        // 从配置文件读取base_url,而不是写死
+        $this->baseUrl = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
+        $this->baseUrl = rtrim($this->baseUrl, '/');
     }
 
     /**
-     * 获取知识点详情
-     *
-     * @param string $kpId 知识点ID
-     * @return array
+     * 获取知识点列表
      */
-    public function getKnowledgePointDetail(string $kpId): array
+    public function listKnowledgePoints(int $page = 1, int $perPage = 100): array
     {
         try {
-            // 从MathRecSys获取
-            $detail = $this->mathRecSys->getKnowledgePoint($kpId);
-
-            // 从本地获取相关题目(这里需要实现)
-            $relatedQuestions = $this->getRelatedQuestions($kpId);
-
-            return [
-                'success' => true,
-                'data' => array_merge($detail['data'] ?? [], [
-                    'related_questions' => $relatedQuestions
-                ])
-            ];
+            $response = Http::timeout(10)
+                ->withHeaders(['Accept' => 'application/json'])
+                ->get($this->baseUrl . '/knowledge-points/', [
+                    'page' => $page,
+                    'per_page' => $perPage
+                ]);
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $points = $data['data'] ?? $data ?? [];
+
+                // 格式化知识点数据
+                return array_map(function($kp) {
+                    return [
+                        'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
+                        'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
+                        'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
+                        'subject' => $kp['category'] ?? '数学',
+                        'phase' => $kp['phase'] ?? '',
+                        'importance' => $kp['importance'] ?? 0,
+                    ];
+                }, $points);
+            }
 
+            Log::warning('知识图谱API调用失败', [
+                'status' => $response->status(),
+                'url' => $this->baseUrl . '/knowledge-points',
+                'response' => $response->json()
+            ]);
         } catch (\Exception $e) {
-            \Log::error('获取知识点详情失败', [
-                'kp_id' => $kpId,
+            Log::error('获取知识点列表失败', [
                 'error' => $e->getMessage()
             ]);
-            return [
-                'success' => false,
-                'error' => $e->getMessage()
-            ];
         }
+
+        // 返回备用数据
+        return $this->getFallbackKnowledgePoints();
     }
 
     /**
-     * 获取知识点依赖关系
-     *
-     * @param string $kpId 知识点ID
-     * @return array
+     * 根据知识点代码获取技能列表
      */
-    public function getKnowledgePointDependencies(string $kpId): array
+    public function getSkillsByKnowledgePoint(string $kpCode): array
     {
         try {
-            $graphData = $this->mathRecSys->getKnowledgeGraph($kpId);
-
-            $dependencies = [
-                'prerequisites' => [], // 前置知识点
-                'advances' => [],      // 后续知识点
-                'contrasts' => []      // 对比知识点
-            ];
-
-            foreach ($graphData['edges'] ?? [] as $edge) {
-                if ($edge['start_uuid'] === $kpId) {
-                    // 该知识点指向其他知识点
-                    if ($edge['type'] === 'AdvancesTo') {
-                        $dependencies['advances'][] = [
-                            'target' => $edge['end_uuid'],
-                            'description' => $edge['properties']['description'] ?? ''
-                        ];
-                    } elseif ($edge['type'] === 'ContrastsWith') {
-                        $dependencies['contrasts'][] = [
-                            'target' => $edge['end_uuid'],
-                            'description' => $edge['properties']['description'] ?? ''
-                        ];
-                    }
-                } elseif ($edge['end_uuid'] === $kpId) {
-                    // 其他知识点指向该知识点
-                    if ($edge['type'] === 'Prerequisite') {
-                        $dependencies['prerequisites'][] = [
-                            'source' => $edge['start_uuid'],
-                            'description' => $edge['properties']['description'] ?? ''
-                        ];
-                    }
-                }
+            $response = Http::timeout(10)
+                ->withHeaders(['Accept' => 'application/json'])
+                ->get($this->baseUrl . "/graph/node/{$kpCode}");
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $skills = $data['skills'] ?? [];
+
+                return array_map(function($skill) {
+                    return [
+                        'code' => $skill['skill_code'] ?? '',
+                        'name' => $skill['skill_name'] ?? '',
+                        'weight' => $skill['weight'] ?? 0,
+                    ];
+                }, $skills);
             }
 
-            return [
-                'success' => true,
-                'data' => $dependencies
-            ];
-
+            Log::warning('获取技能列表失败', [
+                'status' => $response->status(),
+                'kp_code' => $kpCode
+            ]);
         } catch (\Exception $e) {
-            \Log::error('获取知识点依赖关系失败', [
-                'kp_id' => $kpId,
+            Log::error('获取技能列表异常', [
+                'kp_code' => $kpCode,
                 'error' => $e->getMessage()
             ]);
-            return [
-                'success' => false,
-                'error' => $e->getMessage()
-            ];
         }
+
+        return [];
     }
 
     /**
-     * 获取学习路径建议
-     *
-     * @param string $studentId 学生ID
-     * @param string $targetKp 目标知识点
-     * @return array
+     * 获取所有技能列表
      */
-    public function getLearningPath(string $studentId, string $targetKp): array
+    public function listSkills(int $page = 1, int $perPage = 50): array
     {
         try {
-            // 获取学生画像
-            $profile = $this->mathRecSys->getStudentProfile($studentId);
-
-            // 获取知识图谱
-            $graphData = $this->mathRecSys->getKnowledgeGraph($targetKp);
-
-            // 分析前置知识点掌握情况
-            $prerequisites = [];
-            foreach ($graphData['edges'] ?? [] as $edge) {
-                if ($edge['end_uuid'] === $targetKp && $edge['type'] === 'Prerequisite') {
-                    $prereqId = $edge['start_uuid'];
-                    $mastery = $this->getMasteryFromProfile($profile, $prereqId);
-
-                    $prerequisites[] = [
-                        'kp_id' => $prereqId,
-                        'description' => $edge['properties']['description'] ?? '',
-                        'current_mastery' => $mastery,
-                        'is_ready' => $mastery >= 0.6,
-                        'priority' => $this->calculatePriority($mastery)
-                    ];
-                }
-            }
-
-            // 按优先级排序
-            usort($prerequisites, function ($a, $b) {
-                return $b['priority'] <=> $a['priority'];
-            });
-
-            // 生成学习路径
-            $learningPath = [];
-            foreach ($prerequisites as $prereq) {
-                if (!$prereq['is_ready']) {
-                    $learningPath[] = [
-                        'type' => 'review',
-                        'kp_id' => $prereq['kp_id'],
-                        'reason' => '需要先掌握前置知识点',
-                        'priority' => $prereq['priority']
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/skills/', [
+                    'page' => $page,
+                    'per_page' => $perPage
+                ]);
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $skills = $data['data'] ?? $data ?? [];
+
+                // 格式化技能数据
+                return array_map(function($skill) {
+                    return [
+                        'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
+                        'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
+                        'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
+                        'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
                     ];
-                }
+                }, $skills);
             }
 
-            // 添加目标知识点
-            $learningPath[] = [
-                'type' => 'learn',
-                'kp_id' => $targetKp,
-                'reason' => '学习目标知识点',
-                'priority' => 100
-            ];
-
-            return [
-                'success' => true,
-                'data' => [
-                    'student_id' => $studentId,
-                    'target_kp' => $targetKp,
-                    'learning_path' => $learningPath,
-                    'total_steps' => count($learningPath),
-                    'estimated_hours' => count($learningPath) * 2 // 假设每个知识点需要2小时
-                ]
-            ];
-
+            Log::warning('技能API调用失败', [
+                'status' => $response->status()
+            ]);
         } catch (\Exception $e) {
-            \Log::error('获取学习路径失败', [
-                'student_id' => $studentId,
-                'target_kp' => $targetKp,
+            Log::error('获取技能列表失败', [
                 'error' => $e->getMessage()
             ]);
-            return [
-                'success' => false,
-                'error' => $e->getMessage()
-            ];
         }
-    }
 
-    /**
-     * 格式化节点数据
-     *
-     * @param array $nodes 原始节点数据
-     * @return array
-     */
-    private function formatNodes(array $nodes): array
-    {
-        return array_map(function ($node) {
-            $props = $node['properties'] ?? [];
-
-            return [
-                'id' => $props['uuid'] ?? $props['kp_id'] ?? uniqid(),
-                'name' => $props['node_name'] ?? $props['kp_name'] ?? '未知知识点',
-                'category' => $props['grade'] ?? $props['grade_label'] ?? '未分类',
-                'description' => $props['description'] ?? '',
-                'value' => $props['mastery'] ?? 0.6,
-                'difficulty' => $props['difficulty'] ?? 0.5,
-                'book' => $props['book'] ?? '',
-                'chapter' => $props['chapter'] ?? '',
-                'keywords' => $props['keywords'] ?? '',
-                'uuid' => $props['uuid'] ?? null,
-                'kp_id' => $props['kp_id'] ?? null
-            ];
-        }, $nodes);
+        // 返回备用数据
+        return $this->getFallbackSkills();
     }
 
     /**
-     * 格式化边数据
-     *
-     * @param array $edges 原始边数据
-     * @return array
+     * 检查服务健康状态
      */
-    private function formatEdges(array $edges): array
+    public function checkHealth(): bool
     {
-        return array_map(function ($edge) {
-            return [
-                'source' => $edge['start_uuid'] ?? $edge['parent_kp'] ?? '',
-                'target' => $edge['end_uuid'] ?? $edge['child_kp'] ?? '',
-                'type' => $edge['type'] ?? $edge['relation_type'] ?? 'Prerequisite',
-                'description' => $edge['properties']['description'] ?? '',
-                'strength' => $edge['properties']['strength'] ?? 0.8
-            ];
-        }, $edges);
-    }
+        try {
+            $response = Http::timeout(5)
+                ->get($this->baseUrl . '/health');
 
-    /**
-     * 提取分类
-     *
-     * @param array $nodes 节点数据
-     * @return array
-     */
-    private function extractCategories(array $nodes): array
-    {
-        $categories = [];
-        foreach ($nodes as $node) {
-            $props = $node['properties'] ?? [];
-            $category = $props['grade'] ?? $props['grade_label'] ?? '未分类';
-            if (!in_array($category, $categories)) {
-                $categories[] = $category;
-            }
+            return $response->successful();
+        } catch (\Exception $e) {
+            Log::error('知识图谱服务健康检查失败', [
+                'error' => $e->getMessage()
+            ]);
+            return false;
         }
-
-        // 为每个分类分配颜色
-        return array_map(function ($category, $index) {
-            return [
-                'name' => $category,
-                'color' => $this->getCategoryColor($index)
-            ];
-        }, $categories, array_keys($categories));
     }
 
     /**
-     * 获取分类颜色
-     *
-     * @param int $index 分类索引
-     * @return string
+     * 获取备用知识点数据
      */
-    private function getCategoryColor(int $index): string
+    private function getFallbackKnowledgePoints(): array
     {
-        $colors = [
-            '#3b82f6', // 蓝色
-            '#10b981', // 绿色
-            '#f59e0b', // 黄色
-            '#ef4444', // 红色
-            '#8b5cf6', // 紫色
-            '#ec4899', // 粉色
-            '#06b6d4', // 青色
-            '#84cc16', // 青柠
+        return [
+            ['id' => 'kp_1', 'code' => 'KP1001', 'name' => '因式分解基础', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_2', 'code' => 'KP1002', 'name' => '提取公因式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_3', 'code' => 'KP1003', 'name' => '平方差公式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_4', 'code' => 'KP1004', 'name' => '完全平方公式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_5', 'code' => 'KP1005', 'name' => '分组分解法', 'subject' => '数学', 'phase' => '初中', 'importance' => 4],
+            ['id' => 'kp_6', 'code' => 'KP1006', 'name' => '十字相乘法', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_7', 'code' => 'KP1007', 'name' => '有理数运算', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_8', 'code' => 'KP1008', 'name' => '一元二次方程', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
+            ['id' => 'kp_9', 'code' => 'KP1009', 'name' => '不等式', 'subject' => '数学', 'phase' => '初中', 'importance' => 4],
+            ['id' => 'kp_10', 'code' => 'KP1010', 'name' => '函数基础', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
         ];
-
-        return $colors[$index % count($colors)];
-    }
-
-    /**
-     * 从画像中获取掌握度
-     *
-     * @param array $profile 学生画像
-     * @param string $kpId 知识点ID
-     * @return float
-     */
-    private function getMasteryFromProfile(array $profile, string $kpId): float
-    {
-        $masteryData = $profile['data']['mastery'] ?? $profile['mastery'] ?? [];
-        foreach ($masteryData as $mastery) {
-            if (($mastery['kp'] ?? $mastery['kp_id'] ?? '') === $kpId) {
-                return floatval($mastery['level'] ?? $mastery['mastery_level'] ?? 0);
-            }
-        }
-        return 0.0;
     }
 
     /**
-     * 计算优先级
-     *
-     * @param float $mastery 掌握度
-     * @return int
+     * 获取备用技能数据
      */
-    private function calculatePriority(float $mastery): int
+    private function getFallbackSkills(): array
     {
-        if ($mastery < 0.3) {
-            return 90; // 急需掌握
-        } elseif ($mastery < 0.6) {
-            return 70; // 需要复习
-        } elseif ($mastery < 0.8) {
-            return 50; // 适当巩固
-        } else {
-            return 10; // 已经掌握
-        }
-    }
-
-    /**
-     * 获取相关题目(需要根据实际题库实现)
-     *
-     * @param string $kpId 知识点ID
-     * @return array
-     */
-    private function getRelatedQuestions(string $kpId): array
-    {
-        // 这里需要实现从本地题库查询相关题目的逻辑
-        // 暂时返回空数组
-        return [];
+        return [
+            ['id' => 'sk_1', 'code' => 'SK001', 'name' => '计算能力', 'category' => '基础技能'],
+            ['id' => 'sk_2', 'code' => 'SK002', 'name' => '逻辑推理', 'category' => '思维技能'],
+            ['id' => 'sk_3', 'code' => 'SK003', 'name' => '模式识别', 'category' => '认知技能'],
+            ['id' => 'sk_4', 'code' => 'SK004', 'name' => '代数运算', 'category' => '专业技能'],
+            ['id' => 'sk_5', 'code' => 'SK005', 'name' => '解题能力', 'category' => '专业技能'],
+            ['id' => 'sk_6', 'code' => 'SK006', 'name' => '分析能力', 'category' => '思维技能'],
+            ['id' => 'sk_7', 'code' => 'SK007', 'name' => '抽象思维', 'category' => '高级技能'],
+            ['id' => 'sk_8', 'code' => 'SK008', 'name' => '创新思维', 'category' => '高级技能'],
+        ];
     }
 }

+ 135 - 0
app/Services/PromptService.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class PromptService
+{
+    protected string $baseUrl;
+
+    public function __construct()
+    {
+        // 从配置文件读取base_url
+        $this->baseUrl = config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015'));
+        $this->baseUrl = rtrim($this->baseUrl, '/');
+    }
+
+    /**
+     * 获取提示词列表
+     */
+    public function listPrompts(?string $type = null, ?string $active = null): array
+    {
+        try {
+            $query = array_filter([
+                'type' => $type,
+                'active' => $active,
+            ], fn($value) => filled($value));
+
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/prompts', $query);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('获取提示词列表失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('获取提示词列表异常', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return [];
+    }
+
+    /**
+     * 保存提示词
+     */
+    public function savePrompt(array $data): array
+    {
+        try {
+            // 先尝试更新
+            $response = Http::timeout(10)
+                ->put($this->baseUrl . '/prompts/default', $data);
+
+            // 如果更新失败(可能不存在),则尝试创建
+            if (!isset($response->json()['success']) || !$response->json()['success']) {
+                $response = Http::timeout(10)
+                    ->post($this->baseUrl . '/prompts', $data);
+            }
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('保存提示词失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('保存提示词异常', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return ['success' => false, 'message' => '保存失败'];
+    }
+
+    /**
+     * 获取默认提示词模板
+     */
+    public function getDefaultPromptTemplate(): string
+    {
+        return '你是资深的中学数学命题专家,请为{knowledge_point}知识点生成高质量题目。
+
+【核心要求】
+1. 题目必须符合{grade_level}年级水平
+2. 难度分布:基础({basic_ratio}%) + 中等({intermediate_ratio}%) + 拔高({advanced_ratio}%)
+3. 题型分配:选择题({choice}道) + 填空题({fill}道) + 解答题({solution}道)
+
+【技能覆盖】
+{skill_coverage}
+
+【质量标准】
+- 准确性:100%正确
+- 多样性:避免重复
+- 梯度性:难度递进合理
+- 实用性:贴近实际应用
+
+【输出格式】
+{
+  "total": {count},
+  "questions": [
+    {
+      "id": "唯一标识",
+      "stem": "题干",
+      "answer": "标准答案",
+      "solution": "详细解答",
+      "difficulty": 难度值(0.3/0.6/0.85),
+      "skill": "关联技能"
+    }
+  ]
+}';
+    }
+
+    /**
+     * 检查服务健康状态
+     */
+    public function checkHealth(): bool
+    {
+        try {
+            $response = Http::timeout(5)
+                ->get($this->baseUrl . '/health');
+
+            return $response->successful();
+        } catch (\Exception $e) {
+            Log::error('提示词服务健康检查失败', [
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+}

+ 240 - 0
app/Services/QuestionBankService.php

@@ -0,0 +1,240 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class QuestionBankService
+{
+    protected string $baseUrl;
+
+    public function __construct()
+    {
+        // 从配置文件读取base_url
+        $this->baseUrl = config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://fa.test/api.questions.callback'));
+        $this->baseUrl = rtrim($this->baseUrl, '/');
+    }
+
+    /**
+     * 获取题目列表
+     */
+    public function listQuestions(int $page = 1, int $perPage = 50, array $filters = []): array
+    {
+        try {
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/questions', [
+                    'page' => $page,
+                    'per_page' => $perPage,
+                    ...$filters
+                ]);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('题库API调用失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('获取题目列表失败', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return ['data' => [], 'meta' => ['total' => 0]];
+    }
+
+    /**
+     * 智能生成题目(异步模式)
+     */
+    public function generateIntelligentQuestions(array $params, ?string $callbackUrl = null): array
+    {
+        try {
+            // 添加回调 URL
+            if ($callbackUrl) {
+                $params['callback_url'] = $callbackUrl;
+            }
+
+            $response = Http::timeout(10)
+                ->post($this->baseUrl . '/generate-intelligent-questions', $params);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('题目生成API调用失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('题目生成异常', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return ['success' => false, 'message' => '生成失败'];
+    }
+
+    /**
+     * 获取任务状态
+     */
+    public function getTaskStatus(string $taskId): ?array
+    {
+        try {
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/tasks/' . $taskId);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('获取任务状态失败', [
+                'task_id' => $taskId,
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('获取任务状态异常', [
+                'task_id' => $taskId,
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取任务列表
+     */
+    public function listTasks(?string $status = null, int $page = 1, int $perPage = 10): array
+    {
+        try {
+            $params = [
+                'page' => $page,
+                'per_page' => $perPage
+            ];
+
+            if ($status) {
+                $params['status'] = $status;
+            }
+
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/tasks', $params);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('获取任务列表失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('获取任务列表异常', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return ['data' => [], 'meta' => ['total' => 0]];
+    }
+
+    /**
+     * 获取题目统计信息
+     */
+    public function getStatistics(): array
+    {
+        try {
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/questions/statistics');
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+
+            Log::warning('获取题目统计失败', [
+                'status' => $response->status()
+            ]);
+        } catch (\Exception $e) {
+            Log::error('获取题目统计异常', [
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return [
+            'total' => 0,
+            'by_difficulty' => [],
+            'by_kp' => [],
+            'by_source' => []
+        ];
+    }
+
+    /**
+     * 根据知识点获取题目
+     */
+    public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array
+    {
+        try {
+            $response = Http::timeout(10)
+                ->get($this->baseUrl . '/questions', [
+                    'kp_code' => $kpCode,
+                    'limit' => $limit
+                ]);
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+        } catch (\Exception $e) {
+            Log::error('根据知识点获取题目失败', [
+                'kp_code' => $kpCode,
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        return [];
+    }
+
+    /**
+     * 删除题目
+     */
+    public function deleteQuestion(string $questionCode): bool
+    {
+        try {
+            $response = Http::timeout(10)
+                ->delete($this->baseUrl . "/questions/{$questionCode}");
+
+            // 只有返回204(删除成功)才返回true,404(不存在)返回false
+            if ($response->status() === 204) {
+                return true;
+            }
+
+            if ($response->status() === 404) {
+                Log::warning('尝试删除不存在的题目', ['question_code' => $questionCode]);
+                return false;
+            }
+
+            return false;
+        } catch (\Exception $e) {
+            Log::error('删除题目失败', [
+                'question_code' => $questionCode,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 检查服务健康状态
+     */
+    public function checkHealth(): bool
+    {
+        try {
+            $response = Http::timeout(5)
+                ->get($this->baseUrl . '/health');
+
+            return $response->successful();
+        } catch (\Exception $e) {
+            Log::error('题库服务健康检查失败', [
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+}

+ 44 - 5
app/Services/QuestionServiceApi.php

@@ -206,12 +206,21 @@ class QuestionServiceApi
     public function getKnowledgePointOptions(): array
     {
         try {
-            $knowledgeService = app(KnowledgeServiceApi::class);
-            $points = $knowledgeService->listKnowledgePoints(limit: 1000);
+            $knowledgeService = app(KnowledgeGraphService::class);
+            $points = $knowledgeService->listKnowledgePoints(1, 100);
 
-            return $points->pluck('cn_name', 'kp_code')
-                ->sort()
-                ->all();
+            // 转换为键值对格式
+            $options = [];
+            foreach ($points as $point) {
+                $code = $point['code'];
+                $name = $point['name'];
+                $options[$code] = $name;
+            }
+
+            // 按名称排序
+            asort($options);
+
+            return $options;
         } catch (\Exception $e) {
             \Log::error('Failed to get knowledge points: ' . $e->getMessage());
             return [];
@@ -249,6 +258,36 @@ class QuestionServiceApi
         );
     }
 
+    /**
+     * 保存提示词模板
+     */
+    public function savePrompt(array $data): array
+    {
+        try {
+            // 先尝试更新
+            $response = $this->request('PUT', '/prompts/default', $data);
+
+            // 如果更新失败(可能不存在),则尝试创建
+            if (!isset($response['success']) || !$response['success']) {
+                $response = $this->request('POST', '/prompts', $data);
+            }
+
+            // 清除提示词缓存
+            Cache::forget('prompts-list-all-all');
+
+            return $response ?? [
+                'success' => true,
+                'message' => '提示词保存成功',
+            ];
+        } catch (\Exception $e) {
+            \Log::error('Failed to save prompt: ' . $e->getMessage());
+            return [
+                'success' => false,
+                'message' => '保存失败:' . $e->getMessage(),
+            ];
+        }
+    }
+
     /**
      * @throws RequestException
      * @return mixed

+ 4 - 0
config/services.php

@@ -39,6 +39,10 @@ return [
         'base_url' => env('KNOWLEDGE_API_BASE_URL', 'http://localhost:5011'),
     ],
 
+    'question_bank' => [
+        'base_url' => env('QUESTION_BANK_API_BASE', 'http://localhost:5015'),
+    ],
+
     'learning_analytics' => [
         'url' => env('LEARNING_ANALYTICS_URL', 'http://localhost:5016'),
         'timeout' => env('LEARNING_ANALYTICS_TIMEOUT', 30),

+ 0 - 27
resources/views/filament/pages/prompt-management.blade.php

@@ -1,32 +1,5 @@
 <x-filament-panels::page>
     <div class="flex flex-col gap-y-6">
-        {{-- 头部操作 --}}
-        <div class="flex items-center justify-between">
-            <div>
-                <h2 class="text-2xl font-bold tracking-tight">提示词管理</h2>
-                <p class="text-sm text-gray-500">管理系统提示词模板,支持分类管理和搜索</p>
-            </div>
-            <div class="flex gap-2">
-                <button
-                    wire:click="refreshPrompts"
-                    class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
-                >
-                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
-                    </svg>
-                    刷新
-                </button>
-                <button
-                    wire:click="createPrompt"
-                    class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-lg hover:bg-green-700"
-                >
-                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
-                    </svg>
-                    新建提示词
-                </button>
-            </div>
-        </div>
 
         {{-- 统计信息 --}}
         <div class="grid grid-cols-1 md:grid-cols-4 gap-4">

+ 272 - 46
resources/views/filament/pages/question-management.blade.php

@@ -5,49 +5,56 @@
     $statisticsData = $this->statistics;
 @endphp
 
-<div class="filament-page">
-    <div class="filament-page-header">
-        <div class="filament-page-header-actions">
-            {{-- 头部操作按钮将在此处 --}}
-        </div>
-    </div>
-
-    <div class="filament-page-content">
-        {{-- 页面内容 --}}
-        <x-filament::section>
-        <div class="flex items-center justify-between mb-6">
-            <div>
-                <h2 class="text-xl font-bold tracking-tight">题库管理</h2>
-                <p class="mt-1 text-sm text-gray-500">
-                    管理和浏览题库中的所有题目
-                </p>
-            </div>
-            <div class="flex gap-3">
-                <button
-                    type="button"
-                    class="filament-button filament-button-size-sm filament-button-color-success filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
-                    wire:click="$dispatch('ai-generate')"
-                >
-                    <span class="filament-button-icon mr-2">
-                        <!-- heroicon -->
-                    </span>
-                    AI 生成题目
-                </button>
-                <button
-                    type="button"
-                    class="filament-button filament-button-size-sm filament-button-color-warning filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
-                    wire:click="$dispatch('refresh-data')"
-                >
-                    <span class="filament-button-icon mr-2">
-                        <!-- heroicon -->
-                    </span>
-                    刷新
-                </button>
-            </div>
+<x-filament-panels::page>
+    <div class="space-y-6">
+        {{-- 操作按钮栏 --}}
+        <div class="flex justify-end gap-3">
+            <button
+                type="button"
+                class="filament-button filament-button-size-sm filament-button-color-primary filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                wire:click="$dispatch('open-prompt-modal')"
+            >
+                <span class="filament-button-icon mr-2">
+                    @svg('heroicon-m-document-text', 'h-4 w-4')
+                </span>
+                管理提示词
+            </button>
+            <button
+                type="button"
+                class="filament-button filament-button-size-sm filament-button-color-success filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                wire:click="$dispatch('ai-generate')"
+            >
+                <span class="filament-button-icon mr-2">
+                    @svg('heroicon-m-sparkles', 'h-4 w-4')
+                </span>
+                生成题目
+            </button>
+            <button
+                type="button"
+                class="filament-button filament-button-size-sm filament-button-color-warning filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                wire:click="$dispatch('refresh-data')"
+                wire:loading.attr="disabled"
+            >
+                <span class="filament-button-icon mr-2" wire:loading.remove>
+                    @svg('heroicon-m-arrow-path', 'h-4 w-4')
+                </span>
+                <span wire:loading>刷新中...</span>
+                <span wire:loading.remove>刷新</span>
+            </button>
+            <button
+                type="button"
+                class="filament-button filament-button-size-sm filament-button-color-danger filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                onclick="window.location.reload()"
+            >
+                <span class="filament-button-icon mr-2">
+                    @svg('heroicon-m-arrow-path', 'h-4 w-4')
+                </span>
+                强制刷新
+            </button>
         </div>
 
         {{-- 统计信息卡片 --}}
-        <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
+        <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
             <div class="bg-white p-4 rounded-lg border">
                 <div class="text-sm text-gray-500">题目总数</div>
                 <div class="text-2xl font-bold text-primary-600">
@@ -74,8 +81,42 @@
             </div>
         </div>
 
+        {{-- 任务进度显示 --}}
+        @if($isGenerating && $currentTaskId)
+        <div class="bg-white p-6 rounded-lg border border-primary-200 bg-primary-50">
+            <div class="flex items-center justify-between mb-4">
+                <div class="flex items-center gap-3">
+                    <div class="animate-spin rounded-full h-5 w-5 border-b-2 border-primary-600"></div>
+                    <h3 class="text-lg font-semibold text-primary-900">AI正在生成题目</h3>
+                </div>
+                <button
+                    type="button"
+                    class="text-sm text-gray-500 hover:text-gray-700"
+                    wire:click="$dispatch('cancel-generate')"
+                >
+                    取消
+                </button>
+            </div>
+            <div class="space-y-3">
+                <div class="flex justify-between text-sm">
+                    <span class="text-gray-600">{{ $currentTaskMessage }}</span>
+                    <span class="font-medium text-gray-900">{{ $currentTaskProgress }}%</span>
+                </div>
+                <div class="w-full bg-gray-200 rounded-full h-2">
+                    <div
+                        class="bg-primary-600 h-2 rounded-full transition-all duration-500"
+                        style="width: {{ $currentTaskProgress }}%"
+                    ></div>
+                </div>
+                <div class="text-xs text-gray-500">
+                    任务ID: {{ $currentTaskId }}
+                </div>
+            </div>
+        </div>
+        @endif
+
         {{-- 搜索和筛选 --}}
-        <div class="bg-white p-4 rounded-lg border mb-6">
+        <div class="bg-white p-4 rounded-lg border">
             <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
                 <div>
                     <label class="block text-sm font-medium text-gray-700 mb-2">
@@ -173,7 +214,7 @@
                                 </td>
                                 <td class="px-6 py-4">
                                     <div class="text-sm text-gray-900 max-w-xs">
-                                        {{ \Illuminate\Support\Str::limit($question['stem'] ?? 'N/A', 80) }}
+                                                                {{ \Illuminate\Support\Str::limit($question['stem'] ?? 'N/A', 80) }}
                                     </div>
                                 </td>
                                 <td class="px-6 py-4">
@@ -235,7 +276,14 @@
                                     <button type="button" class="text-indigo-600 hover:text-indigo-900 mr-3">
                                         查看
                                     </button>
-                                    <button type="button" class="text-red-600 hover:text-red-900">
+                                    <button
+                                        type="button"
+                                        class="text-red-600 hover:text-red-900"
+                                        wire:click="deleteQuestion('{{ $question['question_code'] }}')"
+                                        wire:confirm="确定要删除题目 {{ $question['question_code'] }} 吗?此操作不可撤销!"
+                                        wire:loading.attr="disabled"
+                                        wire:loading.text="删除中..."
+                                    >
                                         删除
                                     </button>
                                 </td>
@@ -244,7 +292,7 @@
                             <tr>
                                 <td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500">
                                     <div class="flex flex-col items-center">
-                                        <x-heroicon-m-document-magnifying-glass class="w-12 h-12 text-gray-400 mb-3" />
+                                        @svg('heroicon-m-document-magnifying-glass', 'w-12 h-12 text-gray-400')
                                         暂无题目数据
                                         <p class="mt-2 text-xs text-gray-400">
                                             请尝试调整搜索条件或生成新题目
@@ -305,5 +353,183 @@
             <div class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
             <span>加载中...</span>
         </div>
-    </x-filament::section>
-</div>
+
+        {{-- 生成题目模态框 --}}
+        @if($showGenerateModal)
+            <div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" x-data>
+                <div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
+                    <div class="p-6 border-b">
+                        <h3 class="text-lg font-semibold">生成题目</h3>
+                    </div>
+
+                    <div class="p-6 space-y-4">
+                        {{-- 选择知识点 --}}
+                        <div>
+                            <label class="block text-sm font-medium text-gray-700 mb-2">
+                                选择知识点 <span class="text-red-500">*</span>
+                            </label>
+                            <select
+                                wire:model.live="generateKpCode"
+                                class="filament-forms-input block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
+                            >
+                                <option value="">请选择知识点</option>
+                                @foreach($this->knowledgePointOptions as $code => $name)
+                                    <option value="{{ $code }}">{{ $code }} - {{ $name }}</option>
+                                @endforeach
+                            </select>
+                        </div>
+
+                        {{-- 选择技能 --}}
+                        @if(!empty($this->skillsOptions))
+                            <div>
+                                <div class="flex items-center justify-between mb-2">
+                                    <label class="block text-sm font-medium text-gray-700">
+                                        选择技能 <span class="text-red-500">*</span>
+                                        <span class="text-xs text-gray-500 ml-2">(已选择 {{ count($selectedSkills) }} / {{ count($this->skillsOptions) }})</span>
+                                    </label>
+                                    <button
+                                        type="button"
+                                        class="text-sm text-primary-600 hover:text-primary-700"
+                                        wire:click="toggleAllSkills"
+                                    >
+                                        {{ count($selectedSkills) === count($this->skillsOptions) ? '取消全选' : '全选' }}
+                                    </button>
+                                </div>
+                                <div class="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-48 overflow-y-auto border rounded-lg p-3">
+                                    @foreach($this->skillsOptions as $skill)
+                                        <label class="flex items-center space-x-2 cursor-pointer">
+                                            <input
+                                                type="checkbox"
+                                                value="{{ $skill['code'] }}"
+                                                wire:model="selectedSkills"
+                                                class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
+                                            />
+                                            <span class="text-sm">
+                                                <span class="font-medium">{{ $skill['code'] }}</span>
+                                                <span class="text-gray-600"> - {{ $skill['name'] }}</span>
+                                                <span class="text-xs text-gray-400">(权重: {{ $skill['weight'] }})</span>
+                                            </span>
+                                        </label>
+                                    @endforeach
+                                </div>
+                            </div>
+                        @else
+                            <div class="text-sm text-gray-500 italic">
+                                请先选择知识点以加载技能列表
+                            </div>
+                        @endif
+
+                        {{-- 题目数量 --}}
+                        <div>
+                            <label class="block text-sm font-medium text-gray-700 mb-2">
+                                题目数量
+                            </label>
+                            <input
+                                type="number"
+                                wire:model="questionCount"
+                                min="1"
+                                max="500"
+                                class="filament-forms-input block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
+                            />
+                            <p class="mt-1 text-xs text-gray-500">建议单次生成不超过200道题</p>
+                        </div>
+                    </div>
+
+                    <div class="p-6 border-t bg-gray-50 flex justify-end gap-3">
+                        <button
+                            type="button"
+                            class="filament-button filament-button-size-sm filament-button-color-gray filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                            wire:click="closeGenerateModal"
+                        >
+                            取消
+                        </button>
+                        <button
+                            type="button"
+                            class="filament-button filament-button-size-sm filament-button-color-success filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                            wire:click="executeGenerate"
+                            wire:loading.attr="disabled"
+                        >
+                            <span wire:loading.remove>开始生成</span>
+                            <span wire:loading>生成中...</span>
+                        </button>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 提示词编辑模态框 --}}
+        @if($showPromptModal)
+            <div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" x-data>
+                <div class="bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden flex flex-col">
+                    <div class="p-6 border-b">
+                        <h3 class="text-lg font-semibold">管理提示词模板</h3>
+                        <p class="mt-1 text-sm text-gray-500">
+                            自定义AI题目生成的提示词模板,支持变量替换
+                        </p>
+                    </div>
+
+                    <div class="flex-1 overflow-y-auto p-6">
+                        <div class="space-y-4">
+                            <div>
+                                <label class="block text-sm font-medium text-gray-700 mb-2">
+                                    提示词模板内容
+                                </label>
+                                <textarea
+                                    wire:model="promptTemplate"
+                                    rows="20"
+                                    class="filament-forms-input block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm font-mono"
+                                    placeholder="输入提示词模板..."
+                                ></textarea>
+                                <p class="mt-2 text-xs text-gray-500">
+                                    可用变量:{knowledge_point}, {grade_level}, {basic_ratio}, {intermediate_ratio}, {advanced_ratio}, {choice}, {fill}, {solution}, {count}, {skill_coverage}
+                                </p>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="p-6 border-t bg-gray-50 flex justify-between items-center">
+                        <div class="text-sm text-gray-500">
+                            <span class="font-medium">{{ strlen($promptTemplate ?? '') }}</span> 个字符
+                        </div>
+                        <div class="flex gap-3">
+                            <button
+                                type="button"
+                                class="filament-button filament-button-size-sm filament-button-color-gray filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                                wire:click="closePromptModal"
+                            >
+                                取消
+                            </button>
+                            <button
+                                type="button"
+                                class="filament-button filament-button-size-sm filament-button-color-primary filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                                wire:click="savePrompt"
+                                wire:loading.attr="disabled"
+                            >
+                                <span wire:loading.remove>保存</span>
+                                <span wire:loading>保存中...</span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        @endif
+    </div>
+
+    {{-- 任务状态轮询 --}}
+    <script>
+        document.addEventListener('livewire:init', () => {
+            Livewire.on('poll-task', () => {
+                setTimeout(() => {
+                    @this.call('pollTaskStatus');
+                }, 2000); // 每2秒轮询一次
+            });
+
+            // 监听页面刷新事件
+            Livewire.on('refresh-page', () => {
+                setTimeout(() => {
+                    window.location.reload();
+                }, 500); // 延迟500ms刷新,给用户时间看到成功消息
+            });
+        });
+    </script>
+</x-filament-panels::page>

+ 33 - 0
routes/api.php

@@ -2,6 +2,7 @@
 
 use App\Services\QuestionServiceApi;
 use Illuminate\Support\Facades\Route;
+use Illuminate\Support\Facades\Log;
 
 /*
 |--------------------------------------------------------------------------
@@ -9,6 +10,38 @@ use Illuminate\Support\Facades\Route;
 |--------------------------------------------------------------------------
 */
 
+// 接收题目生成回调
+Route::post('/questions/callback', function () {
+    try {
+        $data = request()->all();
+        Log::info('Received question generation callback', $data);
+
+        // 验证回调数据
+        if (!isset($data['task_id']) || !isset($data['status'])) {
+            return response()->json(['error' => 'Invalid callback data'], 400);
+        }
+
+        // 存储回调结果到 session 或缓存中,供前端查询
+        session(['question_gen_callback_' . $data['task_id'] => $data]);
+
+        return response()->json(['success' => true, 'message' => 'Callback received']);
+    } catch (\Exception $e) {
+        Log::error('Callback processing failed: ' . $e->getMessage());
+        return response()->json(['error' => $e->getMessage()], 500);
+    }
+})->name('api.questions.callback');
+
+// 获取题目生成回调结果
+Route::get('/questions/callback/{taskId}', function (string $taskId) {
+    $callbackData = session('question_gen_callback_' . $taskId);
+    if ($callbackData) {
+        // 清除已读取的回调数据
+        session()->forget('question_gen_callback_' . $taskId);
+        return response()->json($callbackData);
+    }
+    return response()->json(['status' => 'pending'], 202);
+})->name('api.questions.callback.get');
+
 // 题目相关 API
 Route::get('/questions', function (QuestionServiceApi $service) {
     try {