PromptManagement.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Services\QuestionServiceApi;
  4. use BackedEnum;
  5. use Filament\Actions;
  6. use Filament\Actions\Action;
  7. use Filament\Notifications\Notification;
  8. use Filament\Pages\Page;
  9. use UnitEnum;
  10. use Livewire\Attributes\Computed;
  11. use Livewire\Attributes\On;
  12. use Illuminate\Support\Facades\Http;
  13. class PromptManagement extends Page
  14. {
  15. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chat-bubble-left-right';
  16. protected static string|UnitEnum|null $navigationGroup = '管理';
  17. protected static ?string $navigationLabel = '提示词管理';
  18. protected static ?int $navigationSort = 12;
  19. protected ?string $heading = '提示词管理';
  20. protected string $view = 'filament.pages.prompt-management';
  21. public ?string $selectedType = null;
  22. public ?string $search = null;
  23. public int $currentPage = 1;
  24. public int $perPage = 10;
  25. public array $selectedPrompts = [];
  26. // 表单状态
  27. public bool $showPromptModal = false;
  28. public bool $isEditing = false;
  29. public ?string $editingName = null;
  30. public array $form = [
  31. 'template_name' => '',
  32. 'template_type' => 'question_generation',
  33. 'template_content' => '',
  34. 'variables' => '[]',
  35. 'description' => '',
  36. 'tags' => '',
  37. 'is_active' => 'yes',
  38. ];
  39. /**
  40. * 获取提示词列表
  41. */
  42. public function getPrompts(): array
  43. {
  44. $service = app(QuestionServiceApi::class);
  45. try {
  46. // 获取所有提示词
  47. $prompts = $service->listPrompts();
  48. // 筛选
  49. if ($this->selectedType) {
  50. $prompts = array_filter($prompts, fn($prompt) =>
  51. $prompt['template_type'] === $this->selectedType
  52. );
  53. }
  54. if ($this->search) {
  55. $searchTerm = strtolower($this->search);
  56. $prompts = array_filter($prompts, fn($prompt) =>
  57. str_contains(strtolower($prompt['template_name']), $searchTerm) ||
  58. str_contains(strtolower($prompt['description']), $searchTerm)
  59. );
  60. }
  61. // 分页
  62. $total = count($prompts);
  63. $offset = ($this->currentPage - 1) * $this->perPage;
  64. $paginated = array_slice($prompts, $offset, $this->perPage);
  65. return [
  66. 'data' => $paginated,
  67. 'meta' => [
  68. 'page' => $this->currentPage,
  69. 'per_page' => $this->perPage,
  70. 'total' => $total,
  71. 'total_pages' => (int) ceil($total / $this->perPage),
  72. ]
  73. ];
  74. } catch (\Exception $e) {
  75. \Log::error('Failed to fetch prompts: ' . $e->getMessage());
  76. return ['data' => [], 'meta' => ['total' => 0]];
  77. }
  78. }
  79. /**
  80. * 获取提示词类型统计
  81. */
  82. public function getTypeStats(): array
  83. {
  84. $service = app(QuestionServiceApi::class);
  85. try {
  86. $prompts = $service->listPrompts();
  87. $stats = [];
  88. foreach ($prompts as $prompt) {
  89. $type = $prompt['template_type'];
  90. if (!isset($stats[$type])) {
  91. $stats[$type] = 0;
  92. }
  93. $stats[$type]++;
  94. }
  95. return $stats;
  96. } catch (\Exception $e) {
  97. \Log::error('Failed to fetch prompt stats: ' . $e->getMessage());
  98. return [];
  99. }
  100. }
  101. /**
  102. * 获取所有类型选项
  103. */
  104. public function getTypeOptions(): array
  105. {
  106. return [
  107. '题目生成' => '题目生成',
  108. '掌握度评估' => '掌握度评估',
  109. '技能熟练度' => '技能熟练度',
  110. '质量审核' => '质量审核',
  111. ];
  112. }
  113. /**
  114. * 筛选更新
  115. */
  116. public function updatedSelectedType(): void
  117. {
  118. $this->currentPage = 1;
  119. }
  120. public function updatedSearch(): void
  121. {
  122. $this->currentPage = 1;
  123. }
  124. public function updatedPerPage(): void
  125. {
  126. $this->currentPage = 1;
  127. }
  128. /**
  129. * 跳转到指定页
  130. */
  131. public function gotoPage(int $page): void
  132. {
  133. $this->currentPage = $page;
  134. }
  135. /**
  136. * 上一页
  137. */
  138. public function previousPage(): void
  139. {
  140. if ($this->currentPage > 1) {
  141. $this->currentPage--;
  142. }
  143. }
  144. /**
  145. * 下一页
  146. */
  147. public function nextPage(): void
  148. {
  149. $totalPages = $this->prompts['meta']['total_pages'] ?? 1;
  150. if ($this->currentPage < $totalPages) {
  151. $this->currentPage++;
  152. }
  153. }
  154. /**
  155. * 创建新提示词
  156. */
  157. #[On('create-prompt')]
  158. public function createPrompt(): void
  159. {
  160. $this->resetPromptForm();
  161. $this->isEditing = false;
  162. $this->editingName = null;
  163. $this->showPromptModal = true;
  164. }
  165. /**
  166. * 编辑提示词
  167. */
  168. #[On('edit-prompt')]
  169. public function editPrompt(array $prompt): void
  170. {
  171. $this->form = [
  172. 'template_name' => $prompt['template_name'] ?? '',
  173. 'template_type' => $prompt['template_type'] ?? 'question_generation',
  174. 'template_content' => $this->fetchPromptContent($prompt['template_name'] ?? '') ?? '',
  175. 'variables' => $prompt['variables'] ?? '[]',
  176. 'description' => $prompt['description'] ?? '',
  177. 'tags' => $prompt['tags'] ?? '',
  178. 'is_active' => ($prompt['is_active'] === 'yes' || $prompt['is_active'] === true) ? 'yes' : 'no',
  179. ];
  180. $this->isEditing = true;
  181. $this->editingName = $prompt['template_name'] ?? null;
  182. $this->showPromptModal = true;
  183. }
  184. /**
  185. * 删除提示词
  186. */
  187. #[On('delete-prompt')]
  188. public function deletePrompt(string $promptName): void
  189. {
  190. try {
  191. $this->request('DELETE', "/prompts/{$promptName}");
  192. Notification::make()
  193. ->title('删除成功')
  194. ->body("提示词 {$promptName} 已删除")
  195. ->success()
  196. ->send();
  197. } catch (\Exception $e) {
  198. Notification::make()
  199. ->title('删除失败')
  200. ->body($e->getMessage())
  201. ->danger()
  202. ->send();
  203. }
  204. }
  205. /**
  206. * 启用/禁用提示词
  207. */
  208. #[On('toggle-prompt')]
  209. public function togglePrompt(?string $promptName = null, ?bool $isActive = null): void
  210. {
  211. if (!$promptName || $isActive === null) {
  212. return;
  213. }
  214. try {
  215. $this->request('PUT', "/prompts/{$promptName}", [
  216. 'is_active' => $isActive ? 'no' : 'yes',
  217. ]);
  218. Notification::make()
  219. ->title(($isActive ? '已禁用 ' : '已启用 ') . $promptName)
  220. ->success()
  221. ->send();
  222. } catch (\Exception $e) {
  223. Notification::make()
  224. ->title('更新状态失败')
  225. ->body($e->getMessage())
  226. ->danger()
  227. ->send();
  228. }
  229. }
  230. /**
  231. * 复制提示词
  232. */
  233. #[On('duplicate-prompt')]
  234. public function duplicatePrompt(array $prompt): void
  235. {
  236. $newName = ($prompt['template_name'] ?? 'template') . '_copy';
  237. try {
  238. $this->request('POST', '/prompts', [
  239. 'template_name' => $newName,
  240. 'template_type' => $prompt['template_type'] ?? 'question_generation',
  241. 'template_content' => $this->fetchPromptContent($prompt['template_name'] ?? '') ?? '',
  242. 'variables' => $prompt['variables'] ?? '[]',
  243. 'description' => $prompt['description'] ?? '',
  244. 'tags' => $prompt['tags'] ?? '',
  245. ]);
  246. Notification::make()
  247. ->title('复制成功')
  248. ->body("已创建副本:{$newName}")
  249. ->success()
  250. ->send();
  251. } catch (\Exception $e) {
  252. Notification::make()
  253. ->title('复制失败')
  254. ->body($e->getMessage())
  255. ->danger()
  256. ->send();
  257. }
  258. }
  259. /**
  260. * 刷新数据
  261. */
  262. #[On('refresh-prompts')]
  263. public function refreshPrompts(): void
  264. {
  265. Notification::make()
  266. ->title('数据已刷新')
  267. ->success()
  268. ->send();
  269. }
  270. /**
  271. * 头部操作
  272. */
  273. protected function getHeaderActions(): array
  274. {
  275. return [
  276. Actions\Action::make('create')
  277. ->label('新建提示词')
  278. ->icon('heroicon-m-plus')
  279. ->color('success')
  280. ->action('createPrompt'),
  281. Actions\Action::make('refresh')
  282. ->label('刷新')
  283. ->icon('heroicon-m-arrow-path')
  284. ->color('warning')
  285. ->action('refreshPrompts'),
  286. ];
  287. }
  288. public function savePrompt(): void
  289. {
  290. $data = $this->validate([
  291. 'form.template_name' => $this->isEditing ? 'nullable|string' : 'required|string',
  292. 'form.template_type' => 'required|string',
  293. 'form.template_content' => 'required|string',
  294. 'form.variables' => 'nullable|string',
  295. 'form.description' => 'nullable|string',
  296. 'form.tags' => 'nullable|string',
  297. 'form.is_active' => 'nullable|string',
  298. ])['form'];
  299. try {
  300. if ($this->isEditing && $this->editingName) {
  301. $payload = [
  302. 'template_content' => $data['template_content'],
  303. 'template_type' => $data['template_type'],
  304. 'variables' => $data['variables'] ?? '[]',
  305. 'description' => $data['description'] ?? '',
  306. 'tags' => $data['tags'] ?? '',
  307. 'is_active' => $data['is_active'] ?? 'yes',
  308. ];
  309. $this->request('PUT', "/prompts/{$this->editingName}", $payload);
  310. } else {
  311. $this->request('POST', '/prompts', [
  312. 'template_name' => $data['template_name'],
  313. 'template_type' => $data['template_type'],
  314. 'template_content' => $data['template_content'],
  315. 'variables' => $data['variables'] ?? '[]',
  316. 'description' => $data['description'] ?? '',
  317. 'tags' => $data['tags'] ?? '',
  318. ]);
  319. }
  320. $this->showPromptModal = false;
  321. $this->refreshPrompts();
  322. Notification::make()
  323. ->title('保存成功')
  324. ->success()
  325. ->send();
  326. } catch (\Exception $e) {
  327. Notification::make()
  328. ->title('保存失败')
  329. ->body($e->getMessage())
  330. ->danger()
  331. ->send();
  332. }
  333. }
  334. protected function request(string $method, string $path, array $payload = []): mixed
  335. {
  336. $baseUrl = rtrim(config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015')), '/');
  337. $url = $baseUrl . $path;
  338. $response = Http::timeout(10)->send($method, $url, [
  339. 'json' => $payload,
  340. ]);
  341. if (!$response->successful()) {
  342. throw new \Exception("API 请求失败: {$response->status()} - " . ($response->json('detail') ?? $response->body()));
  343. }
  344. return $response->json();
  345. }
  346. protected function fetchPromptContent(string $templateName): ?string
  347. {
  348. if (!$templateName) {
  349. return null;
  350. }
  351. try {
  352. $baseUrl = rtrim(config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015')), '/');
  353. $resp = Http::timeout(8)->get($baseUrl . "/prompts/{$templateName}");
  354. if ($resp->successful()) {
  355. return $resp->json('template_content');
  356. }
  357. } catch (\Exception $e) {
  358. \Log::warning('获取提示词内容失败: ' . $e->getMessage());
  359. }
  360. return null;
  361. }
  362. protected function resetPromptForm(): void
  363. {
  364. $this->form = [
  365. 'template_name' => '',
  366. 'template_type' => 'question_generation',
  367. 'template_content' => '',
  368. 'variables' => '[]',
  369. 'description' => '',
  370. 'tags' => '',
  371. 'is_active' => 'yes',
  372. ];
  373. }
  374. }