question-candidate-workbench.blade.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <x-filament::page>
  2. <div
  3. x-data
  4. x-on:keydown.window.prevent.arrow-right="$wire.nextCandidate()"
  5. x-on:keydown.window.prevent.arrow-left="$wire.previousCandidate()"
  6. class="space-y-4"
  7. >
  8. <div class="flex flex-wrap items-center gap-3">
  9. <x-filament::input.wrapper class="w-64">
  10. <x-filament::input wire:model.debounce.400ms="search" placeholder="搜索题干/Markdown" />
  11. </x-filament::input.wrapper>
  12. <x-filament::input.wrapper class="w-40">
  13. <x-filament::input.select wire:model="statusFilter">
  14. <option value="">全部状态</option>
  15. <option value="pending">待审核</option>
  16. <option value="reviewed">已审核</option>
  17. <option value="accepted">已接受</option>
  18. </x-filament::input.select>
  19. </x-filament::input.wrapper>
  20. <x-filament::input.wrapper class="w-56">
  21. <x-filament::input.select wire:model="sourcePaperFilter">
  22. <option value="">全部卷子</option>
  23. @foreach($this->sourcePaperOptions() as $id => $title)
  24. <option value="{{ $id }}">{{ $title }}</option>
  25. @endforeach
  26. </x-filament::input.select>
  27. </x-filament::input.wrapper>
  28. <x-filament::input.wrapper class="w-48">
  29. <x-filament::input.select wire:model="partFilter">
  30. <option value="">全部区块</option>
  31. @foreach($this->partOptions() as $id => $title)
  32. <option value="{{ $id }}">{{ $title }}</option>
  33. @endforeach
  34. </x-filament::input.select>
  35. </x-filament::input.wrapper>
  36. <x-filament::button color="gray" wire:click="previousCandidate">上一题 ←</x-filament::button>
  37. <x-filament::button color="gray" wire:click="nextCandidate">下一题 →</x-filament::button>
  38. <x-filament::button color="primary" wire:click="saveCandidate">保存当前</x-filament::button>
  39. <x-filament::button color="gray" wire:click="$set('dense', ! $wire.dense)">
  40. 密度切换
  41. </x-filament::button>
  42. <x-filament::button color="gray" wire:click="$set('viewMode', 'list')">表格视图</x-filament::button>
  43. <x-filament::button color="gray" wire:click="$set('viewMode', 'card')">题卡视图</x-filament::button>
  44. <x-filament::button color="gray" wire:click="$set('viewMode', 'review')">审核模式</x-filament::button>
  45. <div class="text-xs text-slate-500">快捷键:← → 切换题目</div>
  46. </div>
  47. <div class="grid grid-cols-12 gap-6">
  48. <div class="col-span-8 space-y-4">
  49. <x-filament::section>
  50. <div class="text-sm text-slate-500 mb-2">题目列表(勾选后批量编辑)</div>
  51. @if($viewMode === 'card')
  52. <div class="grid grid-cols-2 gap-3 max-h-64 overflow-y-auto">
  53. @foreach($this->candidates() as $candidate)
  54. <div class="border rounded-lg p-3 hover:border-primary-400">
  55. <label class="flex items-start gap-2">
  56. <input type="checkbox" wire:model="selectedIds" value="{{ $candidate->id }}" class="mt-1 rounded border-gray-300">
  57. <button type="button" wire:click="selectCandidate({{ $candidate->id }})" class="text-left">
  58. <div class="text-sm font-medium text-gray-900">{{ \Illuminate\Support\Str::limit($candidate->stem ?? $candidate->raw_markdown, 60) }}</div>
  59. <div class="text-xs text-gray-500">#{{ $candidate->index }} · {{ $candidate->part?->title }}</div>
  60. <div class="mt-1 flex flex-wrap gap-2 text-xs">
  61. @if(($candidate->ai_confidence ?? 0) < 0.6)
  62. <span class="ui-tag text-amber-700 border-amber-200 bg-amber-50">AI不确定</span>
  63. @endif
  64. @if(empty($candidate->meta['difficulty'] ?? null))
  65. <span class="ui-tag text-rose-700 border-rose-200 bg-rose-50">缺难度</span>
  66. @endif
  67. @if(empty($candidate->meta['kp_codes'] ?? []))
  68. <span class="ui-tag text-rose-700 border-rose-200 bg-rose-50">缺知识点</span>
  69. @endif
  70. </div>
  71. </button>
  72. </label>
  73. </div>
  74. @endforeach
  75. </div>
  76. @else
  77. <div class="{{ $dense ? 'max-h-64' : 'max-h-64' }} overflow-y-auto divide-y divide-gray-100">
  78. @foreach($this->candidates() as $candidate)
  79. <label class="flex items-start gap-3 {{ $dense ? 'py-1' : 'py-2' }}">
  80. <input type="checkbox" wire:model="selectedIds" value="{{ $candidate->id }}" class="mt-1 rounded border-gray-300">
  81. <button type="button" wire:click="selectCandidate({{ $candidate->id }})" class="text-left flex-1">
  82. <div class="text-sm font-medium text-gray-900">{{ \Illuminate\Support\Str::limit($candidate->stem ?? $candidate->raw_markdown, 80) }}</div>
  83. <div class="text-xs text-gray-500">#{{ $candidate->index }} · {{ $candidate->sourcePaper?->title }} · {{ $candidate->part?->title }}</div>
  84. <div class="mt-1 flex flex-wrap gap-2 text-xs">
  85. @if(($candidate->ai_confidence ?? 0) < 0.6)
  86. <span class="ui-tag text-amber-700 border-amber-200 bg-amber-50">AI不确定</span>
  87. @endif
  88. @if(empty($candidate->meta['difficulty'] ?? null))
  89. <span class="ui-tag text-rose-700 border-rose-200 bg-rose-50">缺难度</span>
  90. @endif
  91. @if(empty($candidate->meta['kp_codes'] ?? []))
  92. <span class="ui-tag text-rose-700 border-rose-200 bg-rose-50">缺知识点</span>
  93. @endif
  94. @if(empty($candidate->meta['answer'] ?? null))
  95. <span class="ui-tag text-rose-700 border-rose-200 bg-rose-50">缺答案</span>
  96. @endif
  97. </div>
  98. </button>
  99. </label>
  100. @endforeach
  101. @if($this->candidates()->isEmpty())
  102. <div class="py-6 text-center text-sm text-gray-500">暂无候选题目</div>
  103. @endif
  104. </div>
  105. @endif
  106. </x-filament::section>
  107. <x-filament::section>
  108. <div class="text-sm text-slate-500 mb-2">题目预览</div>
  109. <div class="prose prose-sm max-w-none bg-gray-50 p-4 rounded-lg min-h-[240px]">
  110. @if($this->currentCandidate())
  111. {!! \App\Services\MathFormulaProcessor::processFormulas($this->currentCandidate()?->stem ?? $this->currentCandidate()?->raw_markdown ?? '') !!}
  112. @else
  113. <div class="text-sm text-gray-400">暂无选中题目</div>
  114. @endif
  115. </div>
  116. @if($this->currentCandidate()?->images)
  117. <div class="mt-3 grid grid-cols-3 gap-3">
  118. @foreach(($this->currentCandidate()?->images ?? []) as $img)
  119. <div class="rounded-lg border border-slate-200 bg-white p-2">
  120. <img src="{{ $img }}" alt="题目图片" class="w-full h-24 object-contain" />
  121. </div>
  122. @endforeach
  123. </div>
  124. @endif
  125. </x-filament::section>
  126. </div>
  127. <div class="col-span-4 space-y-4">
  128. <x-filament::section heading="题目属性">
  129. <div class="grid grid-cols-2 gap-3">
  130. <x-filament::input.wrapper>
  131. <x-filament::input.select wire:model="form.question_type">
  132. <option value="">题型</option>
  133. <option value="choice">选择题</option>
  134. <option value="fill">填空题</option>
  135. <option value="short">简答题</option>
  136. <option value="calc">计算题</option>
  137. </x-filament::input.select>
  138. </x-filament::input.wrapper>
  139. <x-filament::input.wrapper>
  140. <x-filament::input.select wire:model="form.difficulty">
  141. <option value="">难度</option>
  142. <option value="1">★</option>
  143. <option value="2">★★</option>
  144. <option value="3">★★★</option>
  145. <option value="4">★★★★</option>
  146. <option value="5">★★★★★</option>
  147. </x-filament::input.select>
  148. </x-filament::input.wrapper>
  149. <x-filament::input.wrapper>
  150. <x-filament::input wire:model="form.score" placeholder="分值" />
  151. </x-filament::input.wrapper>
  152. <x-filament::input.wrapper>
  153. <x-filament::input.select wire:model="form.part_id">
  154. <option value="">题型区块</option>
  155. @foreach($this->partOptions() as $id => $title)
  156. <option value="{{ $id }}">{{ $title }}</option>
  157. @endforeach
  158. </x-filament::input.select>
  159. </x-filament::input.wrapper>
  160. <x-filament::input.wrapper class="col-span-2">
  161. <x-filament::input.select wire:model="form.source_paper_id">
  162. <option value="">来源卷子</option>
  163. @foreach($this->sourcePaperOptions() as $id => $title)
  164. <option value="{{ $id }}">{{ $title }}</option>
  165. @endforeach
  166. </x-filament::input.select>
  167. </x-filament::input.wrapper>
  168. </div>
  169. <div class="mt-3 space-y-2">
  170. <x-filament::input.wrapper>
  171. <x-filament::input wire:model.debounce.300ms="kpSearch" placeholder="搜索知识点(编码/名称)" />
  172. </x-filament::input.wrapper>
  173. <x-filament::input.wrapper>
  174. <x-filament::input.select wire:model="form.kp_codes" multiple>
  175. @foreach($this->filteredKnowledgePointOptions() as $code => $name)
  176. <option value="{{ $code }}">{{ $name }} ({{ $code }})</option>
  177. @endforeach
  178. </x-filament::input.select>
  179. </x-filament::input.wrapper>
  180. </div>
  181. <div class="mt-3">
  182. <x-filament::input.wrapper>
  183. <x-filament::input wire:model="form.tags" placeholder="标签(逗号分隔)" />
  184. </x-filament::input.wrapper>
  185. </div>
  186. <div class="mt-3">
  187. <x-filament::input.wrapper>
  188. <textarea wire:model="form.stem" rows="5" placeholder="题干编辑" class="w-full rounded-lg border-gray-300 focus:border-primary-500 focus:ring-primary-500"></textarea>
  189. </x-filament::input.wrapper>
  190. </div>
  191. <div class="mt-3">
  192. <x-filament::input.wrapper>
  193. <textarea wire:model="form.options" rows="4" placeholder="选项 JSON" class="w-full rounded-lg border-gray-300 focus:border-primary-500 focus:ring-primary-500"></textarea>
  194. </x-filament::input.wrapper>
  195. </div>
  196. <div class="mt-3">
  197. <x-filament::input.wrapper>
  198. <x-filament::input wire:model="form.order_index" placeholder="题目顺序" />
  199. </x-filament::input.wrapper>
  200. </div>
  201. <div class="mt-3">
  202. <x-filament::input.wrapper>
  203. <x-filament::input wire:model="form.images" placeholder="SVG/图片 URL(逗号分隔)" />
  204. </x-filament::input.wrapper>
  205. </div>
  206. </x-filament::section>
  207. <x-filament::section heading="AI 辅助">
  208. <div class="flex flex-wrap gap-2">
  209. <x-filament::button color="gray" wire:click="aiMatchKnowledge">推荐知识点</x-filament::button>
  210. <x-filament::button color="gray" wire:click="aiGenerateSolution">生成解析</x-filament::button>
  211. <x-filament::button color="primary" wire:click="aiAutoFill">智能补全</x-filament::button>
  212. </div>
  213. <div class="mt-3">
  214. <x-filament::input.wrapper>
  215. <x-filament::input wire:model="form.answer" placeholder="答案" />
  216. </x-filament::input.wrapper>
  217. </div>
  218. <div class="mt-3">
  219. <x-filament::input.wrapper>
  220. <textarea wire:model="form.solution" rows="3" placeholder="AI 解析文本" class="w-full rounded-lg border-gray-300 focus:border-primary-500 focus:ring-primary-500"></textarea>
  221. </x-filament::input.wrapper>
  222. </div>
  223. <div class="mt-3">
  224. <x-filament::input.wrapper>
  225. <textarea wire:model="form.solution_steps" rows="5" placeholder="分步 JSON" class="w-full rounded-lg border-gray-300 focus:border-primary-500 focus:ring-primary-500"></textarea>
  226. </x-filament::input.wrapper>
  227. </div>
  228. </x-filament::section>
  229. <x-filament::section heading="批量设置">
  230. <div class="grid grid-cols-2 gap-2">
  231. <x-filament::input.wrapper>
  232. <x-filament::input.select wire:model="batch.question_type">
  233. <option value="">题型</option>
  234. <option value="choice">选择题</option>
  235. <option value="fill">填空题</option>
  236. <option value="short">简答题</option>
  237. <option value="calc">计算题</option>
  238. </x-filament::input.select>
  239. </x-filament::input.wrapper>
  240. <x-filament::input.wrapper>
  241. <x-filament::input wire:model="batch.difficulty" placeholder="难度" />
  242. </x-filament::input.wrapper>
  243. <x-filament::input.wrapper>
  244. <x-filament::input wire:model="batch.score" placeholder="分值" />
  245. </x-filament::input.wrapper>
  246. <x-filament::input.wrapper>
  247. <x-filament::input.select wire:model="batch.part_id">
  248. <option value="">区块</option>
  249. @foreach($this->partOptions() as $id => $title)
  250. <option value="{{ $id }}">{{ $title }}</option>
  251. @endforeach
  252. </x-filament::input.select>
  253. </x-filament::input.wrapper>
  254. </div>
  255. <div class="mt-2">
  256. <x-filament::input.wrapper>
  257. <x-filament::input wire:model="batch.tags" placeholder="标签(逗号分隔)" />
  258. </x-filament::input.wrapper>
  259. </div>
  260. <div class="mt-2">
  261. <x-filament::input.wrapper>
  262. <x-filament::input.select wire:model="batch.source_paper_id">
  263. <option value="">来源卷子</option>
  264. @foreach($this->sourcePaperOptions() as $id => $title)
  265. <option value="{{ $id }}">{{ $title }}</option>
  266. @endforeach
  267. </x-filament::input.select>
  268. </x-filament::input.wrapper>
  269. </div>
  270. <div class="mt-2">
  271. <x-filament::input.wrapper>
  272. <x-filament::input.select wire:model="aiBatchMode">
  273. <option value="missing">AI 只补空字段</option>
  274. <option value="overwrite">AI 覆盖全部字段</option>
  275. </x-filament::input.select>
  276. </x-filament::input.wrapper>
  277. </div>
  278. <div class="mt-2 text-xs text-slate-500">按题序难度仅基于选中题目;批量 AI 会逐题执行,题量较大时可能耗时。</div>
  279. <div class="mt-3">
  280. <x-filament::button color="warning" x-on:click.prevent="if(confirm('确认批量覆盖选中题目?')) { $wire.applyBatch() }">批量应用</x-filament::button>
  281. <x-filament::button color="gray" wire:click="seedBatchFromCurrent">以当前题为默认</x-filament::button>
  282. <x-filament::button color="gray" x-on:click.prevent="if(confirm('确认对选中题按题序自动设置难度?')) { $wire.applyDifficultyByOrder() }">按题序自动难度</x-filament::button>
  283. <x-filament::button color="primary" x-on:click.prevent="if(confirm('确认对选中题进行 AI 批量补全?')) { $wire.aiBatchAutoFill() }">AI 批量补全</x-filament::button>
  284. </div>
  285. </x-filament::section>
  286. </div>
  287. </div>
  288. </div>
  289. </x-filament::page>