outlines-editor.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. 'use client';
  2. import { Button } from '@/components/ui/button';
  3. import { Input } from '@/components/ui/input';
  4. import { Textarea } from '@/components/ui/textarea';
  5. import { Label } from '@/components/ui/label';
  6. import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
  7. import {
  8. Select,
  9. SelectContent,
  10. SelectItem,
  11. SelectTrigger,
  12. SelectValue,
  13. } from '@/components/ui/select';
  14. import { Plus, Trash2, ChevronUp, ChevronDown } from 'lucide-react';
  15. import { nanoid } from 'nanoid';
  16. import type { SceneOutline } from '@/lib/types/generation';
  17. interface OutlinesEditorProps {
  18. outlines: SceneOutline[];
  19. onChange: (outlines: SceneOutline[]) => void;
  20. onConfirm: () => void;
  21. onBack: () => void;
  22. isLoading?: boolean;
  23. }
  24. export function OutlinesEditor({
  25. outlines,
  26. onChange,
  27. onConfirm,
  28. onBack,
  29. isLoading = false,
  30. }: OutlinesEditorProps) {
  31. const addOutline = () => {
  32. const newOutline: SceneOutline = {
  33. id: nanoid(8),
  34. type: 'slide',
  35. title: '',
  36. description: '',
  37. keyPoints: [],
  38. order: outlines.length + 1,
  39. };
  40. onChange([...outlines, newOutline]);
  41. };
  42. const updateOutline = (index: number, updates: Partial<SceneOutline>) => {
  43. const newOutlines = [...outlines];
  44. newOutlines[index] = { ...newOutlines[index], ...updates };
  45. onChange(newOutlines);
  46. };
  47. const removeOutline = (index: number) => {
  48. const newOutlines = outlines.filter((_, i) => i !== index);
  49. // Update order
  50. newOutlines.forEach((outline, i) => {
  51. outline.order = i + 1;
  52. });
  53. onChange(newOutlines);
  54. };
  55. const moveOutline = (index: number, direction: 'up' | 'down') => {
  56. const newIndex = direction === 'up' ? index - 1 : index + 1;
  57. if (newIndex < 0 || newIndex >= outlines.length) return;
  58. const newOutlines = [...outlines];
  59. [newOutlines[index], newOutlines[newIndex]] = [newOutlines[newIndex], newOutlines[index]];
  60. // Update order
  61. newOutlines.forEach((outline, i) => {
  62. outline.order = i + 1;
  63. });
  64. onChange(newOutlines);
  65. };
  66. const updateKeyPoints = (index: number, keyPointsText: string) => {
  67. const keyPoints = keyPointsText
  68. .split('\n')
  69. .map((p) => p.trim())
  70. .filter(Boolean);
  71. updateOutline(index, { keyPoints });
  72. };
  73. return (
  74. <div className="space-y-6">
  75. <div className="flex justify-between items-center">
  76. <div>
  77. <h2 className="text-lg font-semibold">场景大纲</h2>
  78. <p className="text-sm text-muted-foreground">
  79. 共 {outlines.length} 个场景,可编辑、添加、删除或重排序
  80. </p>
  81. </div>
  82. <Button variant="outline" onClick={addOutline} disabled={isLoading}>
  83. <Plus className="size-4 mr-1" />
  84. 添加场景
  85. </Button>
  86. </div>
  87. <div className="space-y-4">
  88. {outlines.map((outline, index) => (
  89. <Card key={outline.id} className="relative">
  90. <CardHeader className="pb-3">
  91. <div className="flex items-center gap-3">
  92. <div className="flex flex-col gap-1">
  93. <Button
  94. variant="ghost"
  95. size="icon"
  96. onClick={() => moveOutline(index, 'up')}
  97. disabled={index === 0 || isLoading}
  98. className="size-6"
  99. >
  100. <ChevronUp className="size-4" />
  101. </Button>
  102. <Button
  103. variant="ghost"
  104. size="icon"
  105. onClick={() => moveOutline(index, 'down')}
  106. disabled={index === outlines.length - 1 || isLoading}
  107. className="size-6"
  108. >
  109. <ChevronDown className="size-4" />
  110. </Button>
  111. </div>
  112. <div className="flex-1">
  113. <CardTitle className="text-base flex items-center gap-2">
  114. <span className="bg-primary text-primary-foreground size-6 rounded-full flex items-center justify-center text-sm">
  115. {index + 1}
  116. </span>
  117. <Input
  118. value={outline.title}
  119. onChange={(e) => updateOutline(index, { title: e.target.value })}
  120. placeholder="场景标题"
  121. className="flex-1"
  122. disabled={isLoading}
  123. />
  124. </CardTitle>
  125. </div>
  126. <Select
  127. value={outline.type}
  128. onValueChange={(value) =>
  129. updateOutline(index, {
  130. type: value as SceneOutline['type'],
  131. })
  132. }
  133. disabled={isLoading}
  134. >
  135. <SelectTrigger className="w-28">
  136. <SelectValue />
  137. </SelectTrigger>
  138. <SelectContent>
  139. <SelectItem value="slide">幻灯片</SelectItem>
  140. <SelectItem value="quiz">测验</SelectItem>
  141. </SelectContent>
  142. </Select>
  143. <Button
  144. variant="ghost"
  145. size="icon"
  146. onClick={() => removeOutline(index)}
  147. disabled={isLoading}
  148. >
  149. <Trash2 className="size-4 text-destructive" />
  150. </Button>
  151. </div>
  152. </CardHeader>
  153. <CardContent className="space-y-4">
  154. <div className="space-y-2">
  155. <Label>场景描述</Label>
  156. <Textarea
  157. value={outline.description}
  158. onChange={(e) => updateOutline(index, { description: e.target.value })}
  159. placeholder="简短描述这个场景的目的和内容"
  160. rows={2}
  161. disabled={isLoading}
  162. />
  163. </div>
  164. <div className="space-y-2">
  165. <Label>关键要点(每行一个)</Label>
  166. <Textarea
  167. value={outline.keyPoints?.join('\n') || ''}
  168. onChange={(e) => updateKeyPoints(index, e.target.value)}
  169. placeholder="输入关键要点,每行一个"
  170. rows={3}
  171. disabled={isLoading}
  172. />
  173. </div>
  174. {outline.type === 'quiz' && (
  175. <div className="p-3 bg-muted/50 rounded-lg space-y-3">
  176. <Label className="text-sm font-medium">测验配置</Label>
  177. <div className="grid grid-cols-3 gap-3">
  178. <div className="space-y-1">
  179. <Label className="text-xs">题目数量</Label>
  180. <Input
  181. type="number"
  182. value={outline.quizConfig?.questionCount || 3}
  183. onChange={(e) =>
  184. updateOutline(index, {
  185. quizConfig: {
  186. ...outline.quizConfig,
  187. questionCount: parseInt(e.target.value) || 3,
  188. difficulty: outline.quizConfig?.difficulty || 'medium',
  189. questionTypes: outline.quizConfig?.questionTypes || ['single'],
  190. },
  191. })
  192. }
  193. min={1}
  194. max={10}
  195. disabled={isLoading}
  196. />
  197. </div>
  198. <div className="space-y-1">
  199. <Label className="text-xs">难度</Label>
  200. <Select
  201. value={outline.quizConfig?.difficulty || 'medium'}
  202. onValueChange={(value) =>
  203. updateOutline(index, {
  204. quizConfig: {
  205. ...outline.quizConfig,
  206. difficulty: value as 'easy' | 'medium' | 'hard',
  207. questionCount: outline.quizConfig?.questionCount || 3,
  208. questionTypes: outline.quizConfig?.questionTypes || ['single'],
  209. },
  210. })
  211. }
  212. disabled={isLoading}
  213. >
  214. <SelectTrigger>
  215. <SelectValue />
  216. </SelectTrigger>
  217. <SelectContent>
  218. <SelectItem value="easy">简单</SelectItem>
  219. <SelectItem value="medium">中等</SelectItem>
  220. <SelectItem value="hard">困难</SelectItem>
  221. </SelectContent>
  222. </Select>
  223. </div>
  224. <div className="space-y-1">
  225. <Label className="text-xs">题型</Label>
  226. <Select
  227. value={outline.quizConfig?.questionTypes?.[0] || 'single'}
  228. onValueChange={(value) =>
  229. updateOutline(index, {
  230. quizConfig: {
  231. ...outline.quizConfig,
  232. questionTypes: [value as 'single' | 'multiple' | 'text'],
  233. questionCount: outline.quizConfig?.questionCount || 3,
  234. difficulty: outline.quizConfig?.difficulty || 'medium',
  235. },
  236. })
  237. }
  238. disabled={isLoading}
  239. >
  240. <SelectTrigger>
  241. <SelectValue />
  242. </SelectTrigger>
  243. <SelectContent>
  244. <SelectItem value="single">单选</SelectItem>
  245. <SelectItem value="multiple">多选</SelectItem>
  246. <SelectItem value="text">简答</SelectItem>
  247. </SelectContent>
  248. </Select>
  249. </div>
  250. </div>
  251. </div>
  252. )}
  253. </CardContent>
  254. </Card>
  255. ))}
  256. </div>
  257. {outlines.length === 0 && (
  258. <Card className="p-8 text-center">
  259. <p className="text-muted-foreground mb-4">暂无场景大纲</p>
  260. <Button variant="outline" onClick={addOutline} disabled={isLoading}>
  261. <Plus className="size-4 mr-1" />
  262. 添加第一个场景
  263. </Button>
  264. </Card>
  265. )}
  266. {/* Actions */}
  267. <div className="flex justify-between pt-4">
  268. <Button variant="outline" onClick={onBack} disabled={isLoading}>
  269. 返回修改需求
  270. </Button>
  271. <Button onClick={onConfirm} disabled={isLoading || outlines.length === 0}>
  272. {isLoading ? '生成中...' : '确认并生成课程'}
  273. </Button>
  274. </div>
  275. </div>
  276. );
  277. }